Full Code of Kamik423/cutie for AI

main e9beac3435bc cached
15 files
67.9 KB
16.7k tokens
99 symbols
1 requests
Download .txt
Repository: Kamik423/cutie
Branch: main
Commit: e9beac3435bc
Files: 15
Total size: 67.9 KB

Directory structure:
gitextract_er58z1a9/

├── .circleci/
│   └── config.yml
├── .gitignore
├── Makefile
├── cutie.py
├── example.py
├── license.md
├── readme.md
├── requirements.txt
├── setup.py
└── test/
    ├── __init__.py
    ├── test_get_number.py
    ├── test_prompt_yes_or_no.py
    ├── test_secure_input.py
    ├── test_select.py
    └── test_select_multiple.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .circleci/config.yml
================================================
version: 2.1

orbs:
  python: circleci/python@2.1.1

workflows:
  build_and_test:
    jobs:
      - build_and_test

jobs:
  build_and_test:
    executor: python/default
    steps:
      - checkout
      - python/install-packages:
          pkg-manager: pip
      - run:
          name: Run tests
          command: make tests
      - persist_to_workspace:
          root: ~/project
          paths:
            - .

================================================
FILE: .gitignore
================================================
._*

__pycache__/
*.pyc
.mypy_cache/
htmlcov/
.coverage


.DS_Store
.Trashes

build
cutie.egg-info
dist


================================================
FILE: Makefile
================================================
.PHONY: tests, coverage, lint, release

tests:
	python -m unittest

coverage:
	python -m coverage erase
	python -m coverage run --source=cutie -m unittest
	python -m coverage report
	python -m coverage html
	coveralls

black:
	black *.py

release: tests, black
	python setup.py sdist bdist_wheel
	twine upload dist/*


================================================
FILE: cutie.py
================================================
#! /usr/bin/env python3
"""
Commandline User Tools for Input Easification
"""

__version__ = "0.3.2"
__author__ = "Hans / Kamik423"
__license__ = "MIT"


import getpass
from typing import List, Optional

import readchar
from colorama import init

init()


class DefaultKeys:
    """List of default keybindings.

    Attributes:
        interrupt(List[str]): Keys that cause a keyboard interrupt.
        select(List[str]): Keys that trigger list element selection.
        confirm(List[str]): Keys that trigger list confirmation.
        delete(List[str]): Keys that trigger character deletion.
        down(List[str]): Keys that select the element below.
        up(List[str]): Keys that select the element above.
    """

    interrupt: List[str] = [readchar.key.CTRL_C, readchar.key.CTRL_D]
    select: List[str] = [readchar.key.SPACE]
    confirm: List[str] = [readchar.key.ENTER]
    delete: List[str] = [readchar.key.BACKSPACE]
    down: List[str] = [readchar.key.DOWN, "j"]
    up: List[str] = [readchar.key.UP, "k"]


def get_number(
    prompt: str,
    min_value: Optional[float] = None,
    max_value: Optional[float] = None,
    allow_float: bool = True,
) -> float:
    """Get a number from user input.
    If an invalid number is entered the user will be prompted again.

    Args:
        prompt (str): The prompt asking the user to input.
        min_value (float, optional): The [inclusive] minimum value.
        max_value (float, optional): The [inclusive] maximum value.
        allow_float (bool, optional): Allow floats or force integers.

    Returns:
        float: The number input by the user.
    """
    return_value: Optional[float] = None
    while return_value is None:
        input_value = input(prompt + " ")
        try:
            return_value = float(input_value)
        except ValueError:
            print("Not a valid number.\033[K\033[1A\r\033[K", end="")
        if not allow_float and return_value is not None:
            if return_value != int(return_value):
                print("Has to be an integer.\033[K\033[1A\r\033[K", end="")
                return_value = None
        if min_value is not None and return_value is not None:
            if return_value < min_value:
                print(f"Has to be at least {min_value}.\033[K\033[1A\r\033[K", end="")
                return_value = None
        if max_value is not None and return_value is not None:
            if return_value > max_value:
                print(f"Has to be at most {max_value}.\033[1A\r\033[K", end="")
                return_value = None
        if return_value is not None:
            break
    print("\033[K", end="")
    if allow_float:
        return return_value
    return int(return_value)


def secure_input(prompt: str) -> str:
    """Get secure input without showing it in the command line.

    Args:
        prompt (str): The prompt asking the user to input.

    Returns:
        str: The secure input.
    """
    return getpass.getpass(prompt + " ")


def select(
    options: List[str],
    caption_indices: Optional[List[int]] = None,
    deselected_prefix: str = "\033[1m[ ]\033[0m ",
    selected_prefix: str = "\033[1m[\033[32;1mx\033[0;1m]\033[0m ",
    caption_prefix: str = "",
    selected_index: int = 0,
    confirm_on_select: bool = True,
) -> int:
    """Select an option from a list.

    Args:
        options (List[str]): The options to select from.
        caption_indices (List[int], optional): Non-selectable indices.
        deselected_prefix (str, optional): Prefix for deselected option ([ ]).
        selected_prefix (str, optional): Prefix for selected option ([x]).
        caption_prefix (str, optional): Prefix for captions ().
        selected_index (int, optional): The index to be selected at first.
        confirm_on_select (bool, optional): Select keys also confirm.

    Returns:
        int: The index that has been selected.
    """
    print("\n" * (len(options) - 1))
    if caption_indices is None:
        caption_indices = []
    while True:
        print(f"\033[{len(options) + 1}A")
        for i, option in enumerate(options):
            if i not in caption_indices:
                print(
                    "\033[K{}{}".format(
                        selected_prefix if i == selected_index else deselected_prefix,
                        option,
                    )
                )
            elif i in caption_indices:
                print("\033[K{}{}".format(caption_prefix, options[i]))
        keypress = readchar.readkey()
        if keypress in DefaultKeys.up:
            new_index = selected_index
            while new_index > 0:
                new_index -= 1
                if new_index not in caption_indices:
                    selected_index = new_index
                    break
        elif keypress in DefaultKeys.down:
            new_index = selected_index
            while new_index < len(options) - 1:
                new_index += 1
                if new_index not in caption_indices:
                    selected_index = new_index
                    break
        elif (
            keypress in DefaultKeys.confirm
            or confirm_on_select
            and keypress in DefaultKeys.select
        ):
            break
        elif keypress in DefaultKeys.interrupt:
            raise KeyboardInterrupt
    return selected_index


def select_multiple(
    options: List[str],
    caption_indices: Optional[List[int]] = None,
    deselected_unticked_prefix: str = "\033[1m( )\033[0m ",
    deselected_ticked_prefix: str = "\033[1m(\033[32mx\033[0;1m)\033[0m ",
    selected_unticked_prefix: str = "\033[32;1m{ }\033[0m ",
    selected_ticked_prefix: str = "\033[32;1m{x}\033[0m ",
    caption_prefix: str = "",
    ticked_indices: Optional[List[int]] = None,
    cursor_index: int = 0,
    minimal_count: int = 0,
    maximal_count: Optional[int] = None,
    hide_confirm: bool = True,
    deselected_confirm_label: str = "\033[1m(( confirm ))\033[0m",
    selected_confirm_label: str = "\033[1;32m{{ confirm }}\033[0m",
) -> List[int]:
    """Select multiple options from a list.

    Args:
        options (List[str]): The options to select from.
        caption_indices (List[int], optional): Non-selectable indices.
        deselected_unticked_prefix (str, optional): Prefix for lines that are
            not selected and not ticked (( )).
        deselected_ticked_prefix (str, optional): Prefix for lines that are
            not selected but ticked ((x)).
        selected_unticked_prefix (str, optional): Prefix for lines that are
            selected but not ticked ({ }).
        selected_ticked_prefix (str, optional): Prefix for lines that are
            selected and ticked ({x}).
        caption_prefix (str, optional): Prefix for captions ().
        ticked_indices (List[int], optional): Indices that are
            ticked initially.
        cursor_index (int, optional): The index the cursor starts at.
        minimal_count (int, optional): The minimal amount of lines
            that have to be ticked.
        maximal_count (int, optional): The maximal amount of lines
            that have to be ticked.
        hide_confirm (bool, optional): Hide the confirm button.
            This causes <ENTER> to confirm the entire selection and not just
            tick the line.
        deselected_confirm_label (str, optional): The confirm label
            if not selected ((( confirm ))).
        selected_confirm_label (str, optional): The confirm label
            if selected ({{ confirm }}).

    Returns:
        List[int]: The indices that have been selected
    """
    print("\n" * (len(options) - 1))
    if caption_indices is None:
        caption_indices = []
    if ticked_indices is None:
        ticked_indices = []
    max_index = len(options) - (1 if hide_confirm else 0)
    error_message = ""
    while True:
        print(f"\033[{len(options) + 1}A")
        for i, option in enumerate(options):
            prefix = ""
            if i in caption_indices:
                prefix = caption_prefix
            elif i == cursor_index:
                if i in ticked_indices:
                    prefix = selected_ticked_prefix
                else:
                    prefix = selected_unticked_prefix
            else:
                if i in ticked_indices:
                    prefix = deselected_ticked_prefix
                else:
                    prefix = deselected_unticked_prefix
            print("\033[K{}{}".format(prefix, option))
        if hide_confirm:
            print(f"{error_message}\033[K", end="", flush=True)
        else:
            if cursor_index == max_index:
                print(
                    f"{selected_confirm_label} {error_message}\033[K",
                    end="",
                    flush=True,
                )
            else:
                print(
                    f"{deselected_confirm_label} {error_message}\033[K",
                    end="",
                    flush=True,
                )
        error_message = ""
        keypress = readchar.readkey()
        if keypress in DefaultKeys.up:
            new_index = cursor_index
            while new_index > 0:
                new_index -= 1
                if new_index not in caption_indices:
                    cursor_index = new_index
                    break
        elif keypress in DefaultKeys.down:
            new_index = cursor_index
            while new_index + 1 <= max_index:
                new_index += 1
                if new_index not in caption_indices:
                    cursor_index = new_index
                    break
        elif (
            hide_confirm
            and keypress in DefaultKeys.confirm
            or not hide_confirm
            and cursor_index == max_index
        ):
            if minimal_count > len(ticked_indices):
                error_message = f"Must select at least {minimal_count} options"
            elif maximal_count is not None and maximal_count < len(ticked_indices):
                error_message = f"Must select at most {maximal_count} options"
            else:
                break
        elif (
            keypress in DefaultKeys.select
            or not hide_confirm
            and keypress in DefaultKeys.confirm
        ):
            if cursor_index in ticked_indices:
                ticked_indices.remove(cursor_index)
            else:
                ticked_indices.append(cursor_index)
        elif keypress in DefaultKeys.interrupt:
            raise KeyboardInterrupt
    print("\r\033[K", end="", flush=True)
    return ticked_indices


def prompt_yes_or_no(
    question: str,
    yes_text: str = "Yes",
    no_text: str = "No",
    has_to_match_case: bool = False,
    enter_empty_confirms: bool = True,
    default_is_yes: bool = False,
    deselected_prefix: str = "  ",
    selected_prefix: str = "\033[31m>\033[0m ",
    char_prompt: bool = True,
) -> Optional[bool]:
    """Prompt the user to input yes or no.

    Args:
        question (str): The prompt asking the user to input.
        yes_text (str, optional): The text corresponding to 'yes'.
        no_text (str, optional): The text corresponding to 'no'.
        has_to_match_case (bool, optional): Does the case have to match.
        enter_empty_confirms (bool, optional): Does enter on empty string work.
        default_is_yes (bool, optional): Is yes selected by default (no).
        deselected_prefix (str, optional): Prefix if something is deselected.
        selected_prefix (str, optional): Prefix if something is selected (> )
        char_prompt (bool, optional): Add a [Y/N] to the prompt.

    Returns:
        Optional[bool]: The bool what has been selected.
    """
    is_yes = default_is_yes
    is_selected = enter_empty_confirms
    current_message = ""
    yn_prompt = f" ({yes_text[0]}/{no_text[0]}) " if char_prompt else ": "
    print()
    while True:
        yes = is_yes and is_selected
        no = not is_yes and is_selected
        print("\033[K" f"{selected_prefix if yes else deselected_prefix}{yes_text}")
        print("\033[K" f"{selected_prefix if no else deselected_prefix}{no_text}")
        print(
            "\033[3A\r\033[K" f"{question}{yn_prompt}{current_message}",
            end="",
            flush=True,
        )
        keypress = readchar.readkey()
        if keypress in DefaultKeys.down or keypress in DefaultKeys.up:
            is_yes = not is_yes
            is_selected = True
            current_message = yes_text if is_yes else no_text
        elif keypress in DefaultKeys.delete:
            if current_message:
                current_message = current_message[:-1]
        elif keypress in DefaultKeys.interrupt:
            raise KeyboardInterrupt
        elif keypress in DefaultKeys.confirm:
            if is_selected:
                break
        elif keypress in "\t":
            if is_selected:
                current_message = yes_text if is_yes else no_text
        else:
            current_message += keypress
            match_yes = yes_text
            match_no = no_text
            match_text = current_message
            if not has_to_match_case:
                match_yes = match_yes.upper()
                match_no = match_no.upper()
                match_text = match_text.upper()
            if match_no.startswith(match_text):
                is_selected = True
                is_yes = False
            elif match_yes.startswith(match_text):
                is_selected = True
                is_yes = True
            else:
                is_selected = False
        print()
    print("\033[K\n\033[K\n\033[K\n\033[3A")
    return is_selected and is_yes


================================================
FILE: example.py
================================================
#! /usr/bin/env python3
"""Example script demonstrating usage of cutie.
"""

import cutie


def main():
    """Main."""
    if cutie.prompt_yes_or_no("Are you brave enough to continue?"):
        # List of names to select from, including some captions
        names = [
            "Kings:",
            "Arthur, King of the Britons",
            "Knights of the Round Table:",
            "Sir Lancelot the Brave",
            "Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot",
            "Sir Bedevere the Wise",
            "Sir Galahad the Pure",
            "Swedish captions:",
            "Møøse",
        ]
        # Names which are captions and thus not selectable
        captions = [0, 2, 7]
        # Get the name
        name = names[cutie.select(names, caption_indices=captions, selected_index=8)]
        print(f"Welcome, {name}")
        # Get an integer greater or equal to 0
        age = cutie.get_number("What is your age?", min_value=0, allow_float=False)
        nemeses_options = [
            "The French",
            "The Police",
            "The Knights Who Say Ni",
            "Women",
            "The Black Knight",
            "The Bridge Keeper",
            "Especially terrifying:",
            "The Rabbit of Caerbannog",
        ]
        print("Choose your nemeses")
        # Choose multiple options from a list
        nemeses_indices = cutie.select_multiple(
            nemeses_options, caption_indices=[6], hide_confirm=False
        )
        nemeses = [
            nemesis
            for nemesis_index, nemesis in enumerate(nemeses_options)
            if nemesis_index in nemeses_indices
        ]
        # Get input without showing it being typed
        quest = cutie.secure_input("What is your quest?")
        print(f"{name}'s quest (who is {age}) is {quest}.")
        if nemeses:
            if len(nemeses) == 1:
                print(f"His nemesis is {nemeses[0]}.")
            else:
                print(f'His nemeses are {" and ".join(nemeses)}.')
        else:
            print("He has no nemesis.")


if __name__ == "__main__":
    main()


================================================
FILE: license.md
================================================
# The MIT License (MIT)

Copyright © 2018 Hans Schülein

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the “Software”), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: readme.md
================================================
# CUTIE

*Command line User Tools for Input Easification*

[![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)
[![Coverage Status](https://coveralls.io/repos/github/Kamik423/cutie/badge.svg?branch=coveralls_integration)](https://coveralls.io/github/Kamik423/cutie?branch=coveralls_integration)
[![PRs Welcome](https://img.shields.io/badge/Homepage-GitHub-green.svg)](https://github.com/kamik423/cutie)
[![PyPI version](https://badge.fury.io/py/cutie.svg)](https://badge.fury.io/py/cutie)
[![PyPI license](https://img.shields.io/pypi/l/cutie.svg)](https://pypi.python.org/pypi/cutie/)
[![PyPI pyversions](https://img.shields.io/pypi/pyversions/cutie.svg)](https://pypi.python.org/pypi/cutie/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![GitHub contributors](https://img.shields.io/github/contributors/Kamik423/cutie.svg)](https://GitHub.com/Kamik423/cutie/graphs/contributors/)

A tool for handling common user input functions in an elegant way.
It 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.

For example the yes or no input supports forcing the user to match case, tab autocomplete and switching option with the arrow keys.
The number input allows setting a minum and a maximum, entering floats or forcing the user to use integers.
It will only return once the user inputs a number in that format, showing a warning to them if it does not conform.

It should work on all major operating systems (Mac, Linux, Windows).

![example](https://github.com/Kamik423/cutie/blob/main/example.gif?raw=true)

## Usage

These are the main functions of cutie.
[example.py](https://github.com/Kamik423/cutie/blob/main/example.py) contains an extended version of this also showing off the `select_multiple` option.

```python
import cutie

if cutie.prompt_yes_or_no("Are you brave enough to continue?"):
    # List of names to select from, including some captions
    names = [
        "Kings:",
        "Arthur, King of the Britons",
        "Knights of the Round Table:",
        "Sir Lancelot the Brave",
        "Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot",
        "Sir Bedevere the Wise",
        "Sir Galahad the Pure",
        "Swedish captions:",
        "Møøse",
    ]
    # Names which are captions and thus not selectable
    captions = [0, 2, 7]
    # Get the name
    name = names[cutie.select(names, caption_indices=captions, selected_index=8)]
    print(f"Welcome, {name}")
    # Get an integer greater or equal to 0
    age = cutie.get_number("What is your age?", min_value=0, allow_float=False)
    # Get input without showing it being typed
    quest = cutie.secure_input("What is your quest?")
    print(f"{name}'s quest (who is {age}) is {quest}.")
```

When run, as demonstrated in the gif above it yields this output:

```
Are you brave enough to continue? (Y/N) Yes
Kings:
[ ] Arthur, King of the Britons
Knights of the Round Table:
[ ] Sir Lancelot the Brave
[x] Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot
[ ] Sir Bedevere the Wise
[ ] Sir Galahad the Pure
Swedish captions:
[ ] Møøse
Welcome, Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot
What is your age? 31
What is your quest?
Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot's quest (who is 31) is to find the holy grail.
```

## Installation

With pip from pypi:

```bash
pip3 install cutie
```

With pip from source or in a virtual environment:

```bash
pip3 install -r requirements.txt
```

## Documentation

All functions of cutie are explained here.
If 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).
The implementation is rather straight forward.

### get\_number

Get a number from user input.

If an invalid number is entered the user will be prompted again.
A minimum and maximum value can be supplied. They are inclusive.
If the `allow_float` option, which is `True` by default is set to `False` it forces the user to enter an integer.

Getting any three digit number for example could be done like that:

```python
number = cutie.get_number(
    "Please enter a three digit number:",
    min_value=100,
    max_value=999,
    allow_float=False
)
# which is equivalent to
number = cutie.get_number("Please enter a three digit number", 100, 999, False)
```

#### Arguments

| argument      | type            | default    | description                          |
|:--------------|:----------------|:-----------|:-------------------------------------|
| `prompt`      | str             |            | The prompt asking the user to input. |
| `min_value`   | float, optional | - infinity | The [inclusive] minimum value.       |
| `max_value`   | float, optional | infinity   | The [inclusive] maximum value.       |
| `allow_float` | bool, optional  | True       | Allow floats or force integers.      |

#### Returns

The number input by the user.

### secure\_input

Get secure input without showing it in the command line.

This could be used for passwords:

```python
password = cutie.secure_input("Please enter your password:")
```

#### Arguments

| argument | type | description                          |
|:---------|:-----|:-------------------------------------|
| `prompt` | str  | The prompt asking the user to input. |

#### Returns

The secure input.

### select

Select an option from a list.

Captions or separators can be included between options by adding them as an option and including their index in `caption_indices`.
A preselected index can be supplied.

In its simplest case it could be used like this:

```python
colors = ["red", "green", "blue", "yellow"]
print("What is your favorite color?")
favorite_color = colors[cutie.select(colors)]
```

With the high degree of customizability, however it is possible to do things like:

```python
print("Select server to ping")
server_id = cutie.select(
    servers,
    deselected_prefix="    ",
    selected_prefix="PING",
    selected_index=default_server_ip
)
```

#### Arguments

| argument            | type                | default | description                        |
|:--------------------|:--------------------|:--------|:-----------------------------------|
| `options`           | List[str]           |         | The options to select from.        |
| `caption_indices`   | List[int], optional | `None`  | Non-selectable indices.            |
| `deselected_prefix` | str, optional       | `[ ]`   | Prefix for deselected option.      |
| `selected_prefix`   | str, optional       | `[x]`   | Prefix for selected option.        |
| `caption_prefix`    | str, optional       | ` `     | Prefix for captions.               |
| `selected_index`    | int, optional       | 0       | The index to be selected at first. |
| `confirm_on_select` | bool, optional      | True    | Select keys also confirm.          |

#### Returns

The index that has been selected.

### select\_multiple

Select multiple options from a list.

It per default shows a "confirm" button.
In that case space bar and enter select a line.
The button can be hidden.
In that case space bar selects the line and enter confirms the selection.

This is not in the example in this readme, but in [example.py](https://github.com/Kamik423/cutie/blob/main/example.py).

```python
packages_to_update = cutie.select_multiple(
    outdated_packages,
    deselected_unticked_prefix="  KEEP  ",
    deselected_ticked_prefix=" UPDATE ",
    selected_unticked_prefix="[ KEEP ]",
    selected_ticked_prefix="[UPDATE]",
    ticked_indices=list(range(len(outdated_packages))),
    deselected_confirm_label="  [[[[ UPDATE ]]]]  ",
    selected_confirm_label="[ [[[[ UPDATE ]]]] ]"
)
```

#### Arguments

| argument                     | type                | default         | description                                                                                                |
|:-----------------------------|:--------------------|:----------------|:-----------------------------------------------------------------------------------------------------------|
| `options`                    | List[str]           |                 | The options to select from.                                                                                |
| `caption_indices`            | List[int], optional |                 | Non-selectable indices.                                                                                    |
| `deselected_unticked_prefix` | str, optional       | `( )`           | Prefix for lines that are not selected and not ticked .                                                    |
| `deselected_ticked_prefix`   | str, optional       | `(x)`           | Prefix for lines that are not selected but ticked .                                                        |
| `selected_unticked_prefix`   | str, optional       | `{ }`           | Prefix for lines that are selected but not ticked .                                                        |
| `selected_ticked_prefix`     | str, optional       | `{x}`           | Prefix for lines that are selected and ticked .                                                            |
| `caption_prefix`             | str, optional       | ` `             | Prefix for captions.                                                                                       |
| `ticked_indices`             | List[int], optional | `[]`            | Indices that are ticked initially.                                                                         |
| `cursor_index`               | int, optional       | 0               | The index the cursor starts at.                                                                            |
| `minimal_count`              | int, optional       | 0               | The minimal amount of lines that have to be ticked.                                                        |
| `maximal_count`              | int, optional       | infinity        | The maximal amount of lines that have to be ticked.                                                        |
| `hide_confirm`               | bool, optional      | `True`          | Hide the confirm button. This causes `<ENTER>` to confirm the entire selection and not just tick the line. |
| `deselected_confirm_label`   | str, optional       | `(( confirm ))` | The confirm label if not selected.                                                                         |
| `selected_confirm_label`     | str, optional       | `{{ confirm }}` | The confirm label if selected.                                                                             |

#### Returns

A list of indices that have been selected.

### prompt\_yes\_or\_no

Prompt the user to input yes or no.

This again can range from very simple to very highly customized:

```python
if cutie.prompt_yes_or_no("Do you want to continue?"):
    do_continue()
```

```python
if cutie.prompt_yes_or_no(
    "Do you want to hear ze funniest joke in ze world? Proceed at your own risk.",
    yes_text="JA",
    no_text="nein",
    has_to_match_case=True, # The user has to type the exact case
    enter_empty_confirms=False, # An answer has to be selected
    )
```

#### Arguments

| argument               | type           | default | description                          |
|:-----------------------|:---------------|:--------|:-------------------------------------|
| `question`             | str            |         | The prompt asking the user to input. |
| `yes_text`             | str, optional  | `Yes`   | The text corresponding to "yes".     |
| `no_text`              | str, optional  | `No`    | The text corresponding to "no".      |
| `has_to_match_case`    | bool, optional | `False` | Does the case have to match.         |
| `enter_empty_confirms` | bool, optional | True    | Does enter on empty string work.     |
| `default_is_yes`       | bool, optional | False   | Is yes selected by default           |
| `deselected_prefix`    | str, optional  | `  `    | Prefix if something is deselected.   |
| `selected_prefix`      | str, optional  | `> `    | Prefix if something is selected      |
| `char_prompt`          | bool, optional | `True`  | Add a [Y/N] to the prompt.           |

#### Returns

The bool what has been selected.

## Changelog

### 0.3.2

* CircleCI Integration

### 0.3.1

* Readme fixes for PyPi

### 0.3.0

* Unittests by [provinzkraut](https://github.com/provinzkraut)
* Travis CI integration
* Vim Arrow keys (`jk`)
* Also showing error messages with `hide_confirm` option enabled in `select_multiple`
* Consistenly crash on keyboard interrupt (Removes `prompt_yes_or_no`'s `abort_value`)
* Set `hide_confirm` to default in `select_multiple` ([#9](https://github.com/Kamik423/cutie/issues/9))
* Black code style

### 0.2.2

* Fixed Python in examples
* PEP8 Compliance by [Christopher Bilger](https://github.com/ChristopherBilg)
* Fixed critical issue with pypi download ([#15](https://github.com/Kamik423/cutie/issues/15))

### 0.2.1

* Expanded readme descriptions

### 0.2.0

* `select_multiple`
* Tweaks to the readme

### 0.1.1

* Fixed pypi download not working

### 0.1.0

* `caption_indices` option by [dherrada](https://github.com/dherrada)

### 0.0.7

* Windows support by [Lhitrom](https://github.com/Lhitrom)

### 0.0.x

* Initial upload and got everything working


## Contributing

If you want to contribute, please feel free to suggest features or implement them yourself.

Also **please report any issues and bugs you might find!**

If you have a project that uses cutie please let me know and I'll link it here!

## Authors

* Main project by [me](https://github.com/Kamik423).
* Unittests, issues and advice by [provinzkraut](https://github.com/provinzkraut).
* Windows support by [Lhitrom](https://github.com/Lhitrom).
* `caption_indices` and tidbits by [dherrada](https://github.com/dherrada).
* PEP8 Compliance by [Christopher Bilger](https://github.com/ChristopherBilg).

## License

The project is licensed under the [MIT-License](https://github.com/Kamik423/cutie/blob/main/license.md).

## Acknowledgments

* This project uses the module [Readchar](https://pypi.org/project/readchar/) for direct input handling.

---

*GNU Terry Pratchett*


================================================
FILE: requirements.txt
================================================
colorama
readchar != 3.0.5


================================================
FILE: setup.py
================================================
"""Setup module for PyPI / pip integration.
"""

import imp

import setuptools

with open("readme.md", encoding="utf-8") as file:
    LONG_DESCRIPTION = file.read()

with open("cutie.py", encoding="utf-8") as file:
    # only go to first import since that module is not yet installed
    CUTIE_CONTENTS = file.read().split("import")[0]

cutie = imp.new_module("cutie")
exec(CUTIE_CONTENTS, cutie.__dict__)

setuptools.setup(
    name="cutie",
    version=cutie.__version__,
    author=cutie.__author__,
    author_email="contact.kamik423@gmail.com",
    description=cutie.__doc__,
    long_description=LONG_DESCRIPTION,
    long_description_content_type="text/markdown",
    url="https://github.com/kamik423/cutie",
    py_modules=["cutie"],
    license=cutie.__license__,
    install_requires=["colorama", "readchar!=3.0.5"],
    python_requires=">=3.6",
    classifiers=[
        "Programming Language :: Python :: 3.6",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3 :: Only",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
)


================================================
FILE: test/__init__.py
================================================
import readchar

import cutie


def PrintCall(states):

    def func(msg=None, state="selectable"):
        state_ = states[state]
        state_name: str
        kwargs = {}
        if isinstance(state_, str):
            state_name = state_
        elif isinstance(state_, tuple):
            state_name = state_[0]
            if len(state) > 1:
                kwargs = state_[1]

        if msg:
            return ((state_name + msg,), kwargs)
        else:
            return ((state_name,), kwargs)

    return func


def yield_input(*data, raise_on_empty=False):
    """
    Closure that returns predefined data.

    If the data is exhausted raise a MockException or reraise the IndexError
    """
    data = list(data)

    def func(*a, **kw):
        try:
            return data.pop(0)
        except IndexError as e:
            if raise_on_empty:
                raise MockException()
            else:
                raise e

    return func


class InputContext:
    """
    Context manager to simulate keyboard input returned by `readchar.readkey`,
    by replacing it in `cutie` with `yield_input`

    When the supplied keystrokes are exhausted a `MockException` will be raised.
    This can be used to terminate the execution at any desired point, rather than
    relying on internal control mechanisms.

    Usage:
        with InputContext(" ", "\r"):
            cutie.select(["foo", "bar"])
    This will issue a space and enter keypress, selecting the first item and
    confirming.
    """

    def __init__(self, *data, raise_on_empty=True):
        cutie.readchar.readkey = yield_input(*data, raise_on_empty=raise_on_empty)

    def __enter__(self):
        pass

    def __exit__(self, *a):
        cutie.readchar.readkey = readchar.readkey


class MockException(Exception):
    pass


================================================
FILE: test/test_get_number.py
================================================
import unittest
from unittest import mock

import cutie

from . import MockException


class TestCutieGetNumber(unittest.TestCase):
    @mock.patch("cutie.print", side_effect=MockException)
    def test_invalid_number(self, mock_print):
        with mock.patch("cutie.input", return_value="foo"):
            with self.assertRaises(MockException):
                cutie.get_number("bar")
            mock_print.assert_called_once_with(
                "Not a valid number.\033[K\033[1A\r\033[K", end=""
            )

    @mock.patch("cutie.print", side_effect=MockException)
    def test_not_allow_float(self, mock_print):
        with mock.patch("cutie.input", return_value="1.2"):
            with self.assertRaises(MockException):
                cutie.get_number("foo", allow_float=False)
            mock_print.assert_called_once_with(
                "Has to be an integer.\033[K\033[1A\r\033[K", end=""
            )

    def test_allow_float_returns_float(self):
        with mock.patch("cutie.input", return_value="1.2"):
            val = cutie.get_number("foo")
            self.assertIsInstance(val, float)
            self.assertEqual(val, 1.2)

    def test_not_allow_float_returns_int(self):
        with mock.patch("cutie.input", return_value="1"):
            val = cutie.get_number("foo", allow_float=False)
            self.assertIsInstance(val, int)
            self.assertEqual(val, 1)

    @mock.patch("cutie.print", side_effect=MockException)
    def test_min_value_float_too_low(self, mock_print):
        with mock.patch("cutie.input", return_value="1.2"):
            with self.assertRaises(MockException):
                cutie.get_number("foo", min_value=1.3)
            mock_print.assert_called_once_with(
                "Has to be at least 1.3.\033[K\033[1A\r\033[K", end=""
            )

    def test_min_value_float_equal(self):
        with mock.patch("cutie.input", return_value="1.2"):
            self.assertEqual(cutie.get_number("foo", min_value=1.2), 1.2)

    def test_min_value_float_greater(self):
        with mock.patch("cutie.input", return_value="1.3"):
            self.assertEqual(cutie.get_number("foo", min_value=1.2), 1.3)

    @mock.patch("cutie.print", side_effect=MockException)
    def test_min_value_int_too_low(self, mock_print):
        with mock.patch("cutie.input", return_value="1"):
            with self.assertRaises(MockException):
                cutie.get_number("foo", min_value=2)
            mock_print.assert_called_once_with(
                "Has to be at least 2.\033[K\033[1A\r\033[K", end=""
            )

    def test_min_value_int_equal(self):
        with mock.patch("cutie.input", return_value="1"):
            self.assertEqual(cutie.get_number("foo", min_value=1), 1)

    def test_min_value_int_greater(self):
        with mock.patch("cutie.input", return_value="2"):
            self.assertEqual(cutie.get_number("foo", min_value=1), 2)

    @mock.patch("cutie.print", side_effect=MockException)
    def test_max_value_float_too_high(self, mock_print):
        with mock.patch("cutie.input", return_value="1.2"):
            with self.assertRaises(MockException):
                cutie.get_number("foo", max_value=1.1)
            mock_print.assert_called_once_with(
                "Has to be at most 1.1.\033[1A\r\033[K", end=""
            )

    def test_max_value_float_equal(self):
        with mock.patch("cutie.input", return_value="1.1"):
            self.assertEqual(cutie.get_number("foo", max_value=1.1), 1.1)

    def test_max_value_float_smaller(self):
        with mock.patch("cutie.input", return_value="1.1"):
            self.assertEqual(cutie.get_number("foo", max_value=1.2), 1.1)

    @mock.patch("cutie.print", side_effect=MockException)
    def test_max_value_int_too_high(self, mock_print):
        with mock.patch("cutie.input", return_value="2"):
            with self.assertRaises(MockException):
                cutie.get_number("foo", max_value=1)
            mock_print.assert_called_once_with(
                "Has to be at most 1.\033[1A\r\033[K", end=""
            )

    def test_max_value_int_equal(self):
        with mock.patch("cutie.input", return_value="1"):
            self.assertEqual(cutie.get_number("foo", max_value=1), 1)

    def test_max_value_int_smaller(self):
        with mock.patch("cutie.input", return_value="1"):
            self.assertEqual(cutie.get_number("foo", max_value=2), 1)

    @mock.patch("cutie.print")
    def test_print_finalize(self, mock_print):
        with mock.patch("cutie.input", return_value="1"):
            cutie.get_number("foo")
        mock_print.assert_called_once_with("\033[K", end="")


================================================
FILE: test/test_prompt_yes_or_no.py
================================================
import unittest
from unittest import mock

import readchar

from . import InputContext, MockException, PrintCall, cutie

print_call = PrintCall(
    {
        "selected": "\x1b[K\x1b[31m>\x1b[0m ",
        "selectable": "\x1b[K  ",
    }
)


class TestPromtYesOrNo(unittest.TestCase):

    default_yes_print_calls = [
        (tuple(),),
        (("\x1b[K\x1b[31m>\x1b[0m Yes",),),
        (("\x1b[K  No",),),
        (("\x1b[3A\r\x1b[Kfoo (Y/N) Yes",), {"end": "", "flush": True}),
        (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
    ]

    default_no_print_calls = [
        (tuple(),),
        (("\x1b[K  Yes",),),
        (("\x1b[K\x1b[31m>\x1b[0m No",),),
        (("\x1b[3A\r\x1b[Kfoo (Y/N) No",), {"end": "", "flush": True}),
        (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
    ]

    @mock.patch("cutie.print")
    def test_print_message(self, mock_print):
        expected_calls = [
            (tuple(),),
            (("\x1b[K  Yes",),),
            (("\x1b[K\x1b[31m>\x1b[0m No",),),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) ",),
                {"end": "", "flush": True},
            ),
            (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
        ]
        with InputContext(readchar.key.ENTER):
            cutie.prompt_yes_or_no("foo")
            self.assertEqual(mock_print.call_args_list, expected_calls)

    @mock.patch("cutie.print")
    def test_print_message_custom_prefixes(self, mock_print):
        expected_calls = [
            (("\x1b[K+Yes",),),
            (("\x1b[K*No",),),
        ]
        with InputContext(readchar.key.ENTER):
            cutie.prompt_yes_or_no("foo", selected_prefix="*", deselected_prefix="+")
            self.assertEqual(mock_print.call_args_list[1:3], expected_calls)

    @mock.patch("cutie.print")
    def test_print_message_custom_yes_no_text(self, mock_print):
        expected_calls = [
            (("\x1b[K  bar",),),
            (("\x1b[K\x1b[31m>\x1b[0m baz",),),
        ]
        with InputContext(readchar.key.ENTER):
            cutie.prompt_yes_or_no("foo", yes_text="bar", no_text="baz")
            self.assertEqual(mock_print.call_args_list[1:3], expected_calls)

    @mock.patch("cutie.print")
    def test_print_message_default_is_yes(self, mock_print):
        expected_calls = [
            (("\x1b[K\x1b[31m>\x1b[0m Yes",),),
            (("\x1b[K  No",),),
        ]
        with InputContext(readchar.key.ENTER):
            cutie.prompt_yes_or_no("foo", default_is_yes=True)
            self.assertEqual(mock_print.call_args_list[1:3], expected_calls)

    @mock.patch("cutie.print")
    def test_move_up(self, mock_print):
        with InputContext(readchar.key.UP, readchar.key.ENTER):
            self.assertTrue(cutie.prompt_yes_or_no("foo"))
            self.assertEqual(
                mock_print.call_args_list[-5:], self.default_yes_print_calls
            )

    @mock.patch("cutie.print")
    def test_move_up_over_boundary(self, mock_print):
        with InputContext(readchar.key.UP, readchar.key.UP, readchar.key.ENTER):
            self.assertFalse(cutie.prompt_yes_or_no("foo"))
            self.assertEqual(
                mock_print.call_args_list[-5:], self.default_no_print_calls
            )

    @mock.patch("cutie.print")
    def test_move_down(self, mock_print):
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            self.assertFalse(cutie.prompt_yes_or_no("foo", default_is_yes=True))
            self.assertEqual(
                mock_print.call_args_list[-5:], self.default_no_print_calls
            )

    @mock.patch("cutie.print")
    def test_move_down_over_boundary(self, mock_print):
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            self.assertTrue(cutie.prompt_yes_or_no("foo"))
            self.assertEqual(
                mock_print.call_args_list[-5:], self.default_yes_print_calls
            )

    @mock.patch("cutie.print")
    def test_backspace_delete_char(self, mock_print):
        expected_calls = [
            (tuple(),),
            print_call("Yes", "selected"),
            print_call("No"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) Ye",),
                {"end": "", "flush": True},
            ),
            (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
        ]
        with InputContext(readchar.key.UP, readchar.key.BACKSPACE, readchar.key.ENTER):
            cutie.prompt_yes_or_no("foo")
            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)

    @mock.patch("cutie.print")
    def test_ctrl_c_abort(self, *m):
        with InputContext(readchar.key.CTRL_C):
            with self.assertRaises(KeyboardInterrupt):
                cutie.prompt_yes_or_no("")

    @mock.patch("cutie.print")
    def test_ctrl_c_abort_with_input(self, *m):
        with InputContext(readchar.key.UP, readchar.key.CTRL_D):
            with self.assertRaises(KeyboardInterrupt):
                cutie.prompt_yes_or_no("")

    @mock.patch("cutie.print")
    def test_ctrl_d_abort(self, *m):
        with InputContext(readchar.key.CTRL_D):
            with self.assertRaises(KeyboardInterrupt):
                cutie.prompt_yes_or_no("")

    @mock.patch("cutie.print")
    def test_ctrl_d_abort_with_input(self, *m):
        with InputContext(readchar.key.UP, readchar.key.CTRL_D):
            with self.assertRaises(KeyboardInterrupt):
                cutie.prompt_yes_or_no("")

    @mock.patch("cutie.print")
    def test_enter_confirm_default(self, *m):
        with InputContext(readchar.key.ENTER):
            self.assertFalse(cutie.prompt_yes_or_no(""))

    @mock.patch("cutie.print")
    def test_enter_confirm_selection(self, *m):
        with InputContext(readchar.key.UP, readchar.key.ENTER):
            self.assertTrue(cutie.prompt_yes_or_no(""))

    @mock.patch("cutie.print")
    def test_tab_select(self, mock_print):
        expected_calls = [
            (tuple(),),
            print_call("Yes"),
            print_call("No", "selected"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) No",),
                {"end": "", "flush": True},
            ),
            (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
        ]
        with InputContext("\t", readchar.key.ENTER):
            cutie.prompt_yes_or_no("foo")
            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)

    @mock.patch("cutie.print")
    def test_write_keypress_to_terminal(self, mock_print):
        expected_calls = [
            (tuple(),),
            print_call("Yes"),
            print_call("No", "selected"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) ",),
                {"end": "", "flush": True},
            ),
            (tuple(),),
            print_call("Yes"),
            print_call("No"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) f",),
                {"end": "", "flush": True},
            ),
            (tuple(),),
            print_call("Yes"),
            print_call("No"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) fo",),
                {"end": "", "flush": True},
            ),
            (tuple(),),
            print_call("Yes"),
            print_call("No"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) foo",),
                {"end": "", "flush": True},
            ),
        ]
        with InputContext("f", "o", "o", readchar.key.CTRL_C):
            with self.assertRaises(KeyboardInterrupt):
                cutie.prompt_yes_or_no("foo")
            self.assertEqual(mock_print.call_args_list, expected_calls)

    @mock.patch("cutie.print")
    def test_write_keypress_to_terminal_resume_selection(self, mock_print):
        expected_calls = [
            (tuple(),),
            print_call("Yes", "selected"),
            print_call("No"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) Yes",),
                {"end": "", "flush": True},
            ),
            (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
        ]
        with InputContext("f", readchar.key.DOWN, readchar.key.ENTER):
            self.assertTrue(cutie.prompt_yes_or_no("foo"))
            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)

    @mock.patch("cutie.print")
    def test_evaluate_written_input_yes_ignorecase(self, mock_print):
        expected_calls = [
            (tuple(),),
            print_call("Yes", "selected"),
            print_call("No"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) yes",),
                {"end": "", "flush": True},
            ),
            (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
        ]
        with InputContext("y", "e", "s", readchar.key.ENTER):
            self.assertTrue(cutie.prompt_yes_or_no("foo"))
            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)

    @mock.patch("cutie.print")
    def test_evaluate_written_input_yes_case_sensitive(self, mock_print):
        expected_calls = (
            ("\x1b[3A\r\x1b[Kfoo (Y/N) yes",),
            {"end": "", "flush": True},
        )

        with InputContext("y", "e", "s", readchar.key.CTRL_C):
            res = None
            with self.assertRaises(KeyboardInterrupt):
                res = cutie.prompt_yes_or_no("foo", has_to_match_case=True)
            self.assertIsNone(res)
            self.assertEqual(mock_print.call_args_list[-1], expected_calls)

    @mock.patch("cutie.print")
    def test_evaluate_written_input_no_ignorecase(self, mock_print):
        expected_calls = [
            (tuple(),),
            print_call("Yes"),
            print_call("No", "selected"),
            (
                ("\x1b[3A\r\x1b[Kfoo (Y/N) no",),
                {"end": "", "flush": True},
            ),
            (("\x1b[K\n\x1b[K\n\x1b[K\n\x1b[3A",),),
        ]
        with InputContext("n", "o", readchar.key.ENTER):
            self.assertFalse(cutie.prompt_yes_or_no("foo"))
            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)

    @mock.patch("cutie.print")
    def test_evaluate_written_input_no_case_sensitive(self, mock_print):
        expected_calls = (
            ("\x1b[3A\r\x1b[Kfoo (Y/N) no",),
            {"end": "", "flush": True},
        )

        with InputContext("n", "o", readchar.key.CTRL_C):
            res = None
            with self.assertRaises(KeyboardInterrupt):
                res = cutie.prompt_yes_or_no("foo", has_to_match_case=True)
            self.assertIsNone(res)
            self.assertEqual(mock_print.call_args_list[-1], expected_calls)


================================================
FILE: test/test_secure_input.py
================================================
import unittest
from unittest import mock

import cutie


class TestSecureInput(unittest.TestCase):
    def test_secure_input(self):
        with mock.patch("cutie.getpass.getpass", return_value="foo") as mock_getpass:
            self.assertEqual(cutie.secure_input("foo"), "foo")
            mock_getpass.assert_called_once_with("foo ")


================================================
FILE: test/test_select.py
================================================
import string
import unittest
from unittest import mock

import readchar

from . import InputContext, MockException, PrintCall, cutie

print_call = PrintCall(
    {
        "selectable": "\x1b[K\x1b[1m[ ]\x1b[0m ",
        "selected": "\x1b[K\x1b[1m[\x1b[32;1mx\x1b[0;1m]\x1b[0m ",
        "caption": "\x1b[K",
    }
)


class TestSelect(unittest.TestCase):
    @mock.patch("cutie.print", side_effect=MockException)
    def test_print_list_newlines(self, mock_print):
        args_list = ["foo", "bar"]
        with self.assertRaises(MockException):
            cutie.select(args_list)
        mock_print.assert_called_once_with("\n" * (len(args_list) - 1))

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_move_to_first_item(self, mock_print, *m):
        args_list = ["foo", "bar"]
        with self.assertRaises(MockException):
            cutie.select(args_list)
        self.assertEqual(
            mock_print.call_args_list[1], ((f"\033[{len(args_list) + 1}A",),)
        )

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [print_call("foo", "selected"), print_call("bar")]
        with self.assertRaises(MockException):
            cutie.select(args_list)
        self.assertEqual(mock_print.call_args_list[2:], expected_calls)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options_selected_index_set(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [print_call("foo"), print_call("bar", "selected")]
        with self.assertRaises(MockException):
            cutie.select(args_list, selected_index=1)
        self.assertEqual(mock_print.call_args_list[2:], expected_calls)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_non_selectable(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [print_call("foo", "selected"), print_call("bar", "caption")]
        with self.assertRaises(MockException):
            cutie.select(args_list, caption_indices=[1])
        self.assertEqual(mock_print.call_args_list[2:], expected_calls)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options_custom_prefixes(self, mock_print, *m):
        args_list = ["foo", "bar", "baz"]
        expected_calls = [(("\x1b[K*foo",),), (("\x1b[K+bar",),), (("\x1b[K$baz",),)]
        with self.assertRaises(MockException):
            cutie.select(
                args_list,
                caption_indices=[2],
                selected_prefix="*",
                deselected_prefix="+",
                caption_prefix="$",
            )
        self.assertEqual(mock_print.call_args_list[2:], expected_calls)

    @mock.patch("cutie.print")
    def test_ignore_unrecognized_key(self, mock_print):
        exclude = [
            "__builtins__",
            "__cached__",
            "__doc__",
            "__file__",
            "__loader__",
            "__name__",
            "__package__",
            "__spec__",
            "UP",
            "DOWN",
            "ENTER",
            "CTRL_C",
            "CTRL_D",
        ]
        all_keys = [
            getattr(readchar.key, k) for k in dir(readchar.key) if k not in exclude
        ]
        all_keys.extend(string.printable)
        expected_calls = [
            (("",),),
            (("\x1b[2A",),),
            (("\x1b[K\x1b[1m[\x1b[32;1mx\x1b[0;1m]\x1b[0m foo",),),
        ]

        for key in all_keys:
            with InputContext(readchar.key.DOWN, key, readchar.key.ENTER):
                selindex = cutie.select(["foo"])
                self.assertEqual(selindex, 0)
                self.assertEqual(mock_print.call_args_list[:3], expected_calls)
                mock_print.reset_mock()

    @mock.patch("cutie.print")
    def test_move_up(self, *m):
        with InputContext(readchar.key.UP, readchar.key.ENTER):
            args_list = ["foo", "bar"]
            selindex = cutie.select(args_list, selected_index=1)
            self.assertEqual(selindex, 0)

    @mock.patch("cutie.print")
    def test_move_up_skip_caption(self, *m):
        with InputContext(readchar.key.UP, readchar.key.ENTER):
            args_list = ["foo", "bar", "baz"]
            selindex = cutie.select(args_list, selected_index=2, caption_indices=[1])
            self.assertEqual(selindex, 0)

    @mock.patch("cutie.print")
    def test_move_down(self, *m):
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            args_list = ["foo", "bar"]
            selindex = cutie.select(args_list)
            self.assertEqual(selindex, 1)

    @mock.patch("cutie.print")
    def test_move_down_skip_caption(self, *m):
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            args_list = ["foo", "bar", "baz"]
            selindex = cutie.select(args_list, caption_indices=[1])
            self.assertEqual(selindex, 2)

    @mock.patch("cutie.print")
    def test_keyboard_interrupt_ctrl_c_no_input(self, *m):
        with InputContext(readchar.key.CTRL_C):
            with self.assertRaises(KeyboardInterrupt):
                cutie.select(["foo"])

    @mock.patch("cutie.print")
    def test_keyboard_interrupt_ctrl_c_selected(self, *m):
        with InputContext(readchar.key.DOWN, readchar.key.CTRL_C):
            with self.assertRaises(KeyboardInterrupt):
                cutie.select(["foo"], selected_index=0)

    @mock.patch("cutie.print")
    def test_keyboard_interrupt_ctrl_d_no_input(self, *m):
        with InputContext(readchar.key.CTRL_D):
            with self.assertRaises(KeyboardInterrupt):
                cutie.select(["foo"])

    @mock.patch("cutie.print")
    def test_keyboard_interrupt_ctrl_d_selected(self, *m):
        with InputContext(readchar.key.DOWN, readchar.key.CTRL_D):
            with self.assertRaises(KeyboardInterrupt):
                cutie.select(["foo"], selected_index=0)


================================================
FILE: test/test_select_multiple.py
================================================
import string
import unittest
from unittest import mock

import readchar

from . import InputContext, MockException, PrintCall, cutie, yield_input

print_call = PrintCall(
    {
        "selectable": "\x1b[K\x1b[1m( )\x1b[0m ",
        "selected": "\x1b[K\x1b[1m(\x1b[32mx\x1b[0;1m)\x1b[0m ",
        "caption": "\x1b[K",
        "active": "\x1b[K\x1b[32;1m{ }\x1b[0m ",
        "active-selected": "\x1b[K\x1b[32;1m{x}\x1b[0m ",
        "confirm": ("\x1b[1m(( confirm ))\x1b[0m \x1b[K", {"end": "", "flush": True}),
        "confirm-active": (
            "\x1b[1;32m{{ confirm }}\x1b[0m \x1b[K",
            {"end": "", "flush": True},
        ),
        "no_confirm_line": ("\033[K", {"end": "", "flush": True}),
    }
)


PRINT_CALL_END = (("\r\x1b[K",), {"end": "", "flush": True})


class TestSelectMultiplePrint(unittest.TestCase):
    @mock.patch("cutie.print", side_effect=MockException)
    def test_list_newlines(self, mock_print):
        args_list = ["foo", "bar"]
        with self.assertRaises(MockException):
            cutie.select_multiple(args_list)
        mock_print.assert_called_once_with("\n" * (len(args_list) - 1))

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_move_to_first_item(self, mock_print, *m):
        args_list = ["foo", "bar"]
        with self.assertRaises(MockException):
            cutie.select_multiple(args_list)
        self.assertEqual(
            mock_print.call_args_list[1], ((f"\033[{len(args_list) + 1}A",),)
        )

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [
            print_call("foo", "active"),
            print_call("bar", "selectable"),
            print_call(state="confirm"),
        ]
        with self.assertRaises(MockException):
            cutie.select_multiple(args_list)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options_caption_indices(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [
            print_call("foo", "caption"),
            print_call("bar"),
            print_call(state="no_confirm_line"),
        ]
        with self.assertRaises(MockException):
            cutie.select_multiple(args_list, caption_indices=[0])
        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options_selected(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [
            print_call("foo"),
            print_call("bar", "active"),
            print_call(state="no_confirm_line"),
        ]
        with self.assertRaises(MockException):
            cutie.select_multiple(args_list, cursor_index=1)
        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options_selected_and_ticked(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [
            print_call("foo", "active-selected"),
            print_call("bar"),
            print_call(state="no_confirm_line"),
        ]
        with self.assertRaises(MockException):
            cutie.select_multiple(args_list, ticked_indices=[0])
        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_options_deselected_unticked(self, mock_print, *m):
        args_list = ["foo", "bar"]
        expected_calls = [
            print_call("foo"),
            print_call("bar"),
            print_call(state="no_confirm_line"),
        ]
        with self.assertRaises(MockException):
            cutie.select_multiple(args_list, cursor_index=2)
        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_deselected_confirm(self, mock_print, *m):
        expected_call = print_call(state="confirm")
        with self.assertRaises(MockException):
            cutie.select_multiple([], cursor_index=1, hide_confirm=False)
        self.assertEqual(mock_print.call_args_list[-1], expected_call)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_selected_confirm(self, mock_print, *m):
        expected_call = print_call(state="confirm-active")
        with self.assertRaises(MockException):
            cutie.select_multiple([], hide_confirm=False)
        self.assertEqual(mock_print.call_args_list[-1], expected_call)

    @mock.patch("cutie.readchar.readkey", side_effect=MockException)
    @mock.patch("cutie.print")
    def test_print_show_confirm(self, mock_print, *m):
        expected_calls = [print_call("foo", "active"), print_call(state="confirm")]
        with self.assertRaises(MockException):
            cutie.select_multiple(["foo"], hide_confirm=False)
        self.assertEqual(mock_print.call_args_list[2:], expected_calls)


class TestSelectMultipleMoveAndSelect(unittest.TestCase):
    @mock.patch("cutie.print")
    def test_move_up(self, mock_print):
        call_args = ["foo", "bar"]
        expected_calls = [
            print_call("foo", "active"),
            print_call("bar"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(readchar.key.UP, readchar.key.ENTER):
            cutie.select_multiple(call_args, cursor_index=1)
        self.assertEqual(mock_print.call_args_list[-4:], expected_calls)

    @mock.patch("cutie.print")
    def test_move_up_skip_caption(self, mock_print):
        call_args = ["foo", "bar", "baz"]
        expected_calls = [
            print_call("foo", "active"),
            print_call("bar", "caption"),
            print_call("baz"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(readchar.key.UP, readchar.key.ENTER):
            cutie.select_multiple(call_args, cursor_index=2, caption_indices=[1])
        self.assertEqual(mock_print.call_args_list[-5:], expected_calls)

    @mock.patch("cutie.print")
    def test_move_down(self, mock_print):
        call_args = ["foo", "bar"]
        expected_calls = [
            print_call("foo"),
            print_call("bar", "active"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            cutie.select_multiple(call_args)
        self.assertEqual(mock_print.call_args_list[-4:], expected_calls)

    @mock.patch("cutie.print")
    def test_move_down_skip_caption(self, mock_print):
        call_args = ["foo", "bar", "baz"]
        expected_calls = [
            print_call("foo"),
            print_call("bar", "caption"),
            print_call("baz", "active"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            cutie.select_multiple(call_args, caption_indices=[1])
        self.assertEqual(mock_print.call_args_list[-5:], expected_calls)

    @mock.patch("cutie.print")
    def test_select(self, mock_print):
        call_args = ["foo", "bar"]
        expected_calls = [
            print_call("foo", "selected"),
            print_call("bar", "active-selected"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(" ", readchar.key.DOWN, " ", readchar.key.ENTER):
            selected_indices = cutie.select_multiple(call_args)
        self.assertEqual(mock_print.call_args_list[-4:], expected_calls)
        self.assertEqual(selected_indices, [0, 1])

    @mock.patch("cutie.print")
    def test_select_min_too_few(self, mock_print):
        call_args = ["foo"]
        expected_call = (
            ("Must select at least 1 options\x1b[K",),
            {"end": "", "flush": True},
        )
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            with self.assertRaises(MockException):
                cutie.select_multiple(call_args, minimal_count=1)
            self.assertEqual(mock_print.call_args_list[-1], expected_call)

    @mock.patch("cutie.print")
    def test_select_max_too_many(self, mock_print):
        call_args = ["foo"]
        expected_call = (
            ("Must select at most 0 options\x1b[K",),
            {"end": "", "flush": True},
        )
        with InputContext(readchar.key.ENTER):
            with self.assertRaises(MockException):
                cutie.select_multiple(call_args, maximal_count=0, ticked_indices=[0])
            self.assertEqual(mock_print.call_args_list[-1], expected_call)

    @mock.patch("cutie.print")
    def test_select_min_sufficient(self, mock_print):
        call_args = ["foo"]
        expected_calls = [
            print_call("foo", "active-selected"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(" ", readchar.key.ENTER):
            selected_indices = cutie.select_multiple(call_args, minimal_count=1)
            self.assertEqual(mock_print.call_args_list[-3:], expected_calls)
            self.assertEqual(selected_indices, [0])

    @mock.patch("cutie.print")
    def test_deselect_on_min_sufficient(self, mock_print):
        call_args = ["foo", "bar"]
        expected_calls = [
            print_call("foo", "selectable"),
            print_call("bar", "active-selected"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(" ", readchar.key.DOWN, readchar.key.ENTER):
            selected_indices = cutie.select_multiple(
                call_args, minimal_count=1, ticked_indices=[0, 1]
            )
            self.assertEqual(mock_print.call_args_list[-4:], expected_calls)
            self.assertEqual(selected_indices, [1])

    @mock.patch("cutie.print")
    def test_select_max_okay(self, mock_print):
        call_args = ["foo"]
        expected_calls = [
            print_call("foo", "active-selected"),
            print_call(state="no_confirm_line"),
            PRINT_CALL_END,
        ]
        with InputContext(" ", readchar.key.ENTER):
            selected_indices = cutie.select_multiple(call_args, maximal_count=1)
            self.assertEqual(mock_print.call_args_list[-3:], expected_calls)
            self.assertEqual(selected_indices, [0])

    @mock.patch("cutie.print")
    def test_select_min_too_few_hide_confirm(self, mock_print):
        """
        This should prompt the user with an error message
        """
        call_args = ["foo"]
        expected_call = (
            ("Must select at least 1 options\x1b[K",),
            {"end": "", "flush": True},
        )
        with InputContext(readchar.key.ENTER):
            with self.assertRaises(MockException):
                cutie.select_multiple(call_args, minimal_count=1)
            self.assertEqual(mock_print.call_args_list[-1], expected_call)

    @mock.patch("cutie.print")
    def test_select_max_too_many_show_confirm(self, mock_print):
        """
        This should prompt the user with an error message
        """
        call_args = ["foo"]
        expected_call = (
            ("\x1b[1;32m{{ confirm }}\x1b[0m Must select at most 0 options\x1b[K",),
            {"end": "", "flush": True},
        )
        with InputContext(readchar.key.DOWN, readchar.key.ENTER):
            with self.assertRaises(MockException):
                cutie.select_multiple(
                    call_args, maximal_count=0, ticked_indices=[0], hide_confirm=False
                )
            self.assertEqual(mock_print.call_args_list[-1], expected_call)


class TestSelectMultipleMisc(unittest.TestCase):
    @mock.patch("cutie.print")
    def test_keyboard_interrupt(self, mock_print):
        call_args = ["foo", "bar"]
        with InputContext(readchar.key.CTRL_C):
            with self.assertRaises(KeyboardInterrupt):
                cutie.select_multiple(call_args)
Download .txt
gitextract_er58z1a9/

├── .circleci/
│   └── config.yml
├── .gitignore
├── Makefile
├── cutie.py
├── example.py
├── license.md
├── readme.md
├── requirements.txt
├── setup.py
└── test/
    ├── __init__.py
    ├── test_get_number.py
    ├── test_prompt_yes_or_no.py
    ├── test_secure_input.py
    ├── test_select.py
    └── test_select_multiple.py
Download .txt
SYMBOL INDEX (99 symbols across 8 files)

FILE: cutie.py
  class DefaultKeys (line 20) | class DefaultKeys:
  function get_number (line 40) | def get_number(
  function secure_input (line 85) | def secure_input(prompt: str) -> str:
  function select (line 97) | def select(
  function select_multiple (line 161) | def select_multiple(
  function prompt_yes_or_no (line 291) | def prompt_yes_or_no(

FILE: example.py
  function main (line 8) | def main():

FILE: test/__init__.py
  function PrintCall (line 6) | def PrintCall(states):
  function yield_input (line 27) | def yield_input(*data, raise_on_empty=False):
  class InputContext (line 47) | class InputContext:
    method __init__ (line 63) | def __init__(self, *data, raise_on_empty=True):
    method __enter__ (line 66) | def __enter__(self):
    method __exit__ (line 69) | def __exit__(self, *a):
  class MockException (line 73) | class MockException(Exception):

FILE: test/test_get_number.py
  class TestCutieGetNumber (line 9) | class TestCutieGetNumber(unittest.TestCase):
    method test_invalid_number (line 11) | def test_invalid_number(self, mock_print):
    method test_not_allow_float (line 20) | def test_not_allow_float(self, mock_print):
    method test_allow_float_returns_float (line 28) | def test_allow_float_returns_float(self):
    method test_not_allow_float_returns_int (line 34) | def test_not_allow_float_returns_int(self):
    method test_min_value_float_too_low (line 41) | def test_min_value_float_too_low(self, mock_print):
    method test_min_value_float_equal (line 49) | def test_min_value_float_equal(self):
    method test_min_value_float_greater (line 53) | def test_min_value_float_greater(self):
    method test_min_value_int_too_low (line 58) | def test_min_value_int_too_low(self, mock_print):
    method test_min_value_int_equal (line 66) | def test_min_value_int_equal(self):
    method test_min_value_int_greater (line 70) | def test_min_value_int_greater(self):
    method test_max_value_float_too_high (line 75) | def test_max_value_float_too_high(self, mock_print):
    method test_max_value_float_equal (line 83) | def test_max_value_float_equal(self):
    method test_max_value_float_smaller (line 87) | def test_max_value_float_smaller(self):
    method test_max_value_int_too_high (line 92) | def test_max_value_int_too_high(self, mock_print):
    method test_max_value_int_equal (line 100) | def test_max_value_int_equal(self):
    method test_max_value_int_smaller (line 104) | def test_max_value_int_smaller(self):
    method test_print_finalize (line 109) | def test_print_finalize(self, mock_print):

FILE: test/test_prompt_yes_or_no.py
  class TestPromtYesOrNo (line 16) | class TestPromtYesOrNo(unittest.TestCase):
    method test_print_message (line 35) | def test_print_message(self, mock_print):
    method test_print_message_custom_prefixes (line 51) | def test_print_message_custom_prefixes(self, mock_print):
    method test_print_message_custom_yes_no_text (line 61) | def test_print_message_custom_yes_no_text(self, mock_print):
    method test_print_message_default_is_yes (line 71) | def test_print_message_default_is_yes(self, mock_print):
    method test_move_up (line 81) | def test_move_up(self, mock_print):
    method test_move_up_over_boundary (line 89) | def test_move_up_over_boundary(self, mock_print):
    method test_move_down (line 97) | def test_move_down(self, mock_print):
    method test_move_down_over_boundary (line 105) | def test_move_down_over_boundary(self, mock_print):
    method test_backspace_delete_char (line 113) | def test_backspace_delete_char(self, mock_print):
    method test_ctrl_c_abort (line 129) | def test_ctrl_c_abort(self, *m):
    method test_ctrl_c_abort_with_input (line 135) | def test_ctrl_c_abort_with_input(self, *m):
    method test_ctrl_d_abort (line 141) | def test_ctrl_d_abort(self, *m):
    method test_ctrl_d_abort_with_input (line 147) | def test_ctrl_d_abort_with_input(self, *m):
    method test_enter_confirm_default (line 153) | def test_enter_confirm_default(self, *m):
    method test_enter_confirm_selection (line 158) | def test_enter_confirm_selection(self, *m):
    method test_tab_select (line 163) | def test_tab_select(self, mock_print):
    method test_write_keypress_to_terminal (line 179) | def test_write_keypress_to_terminal(self, mock_print):
    method test_write_keypress_to_terminal_resume_selection (line 216) | def test_write_keypress_to_terminal_resume_selection(self, mock_print):
    method test_evaluate_written_input_yes_ignorecase (line 232) | def test_evaluate_written_input_yes_ignorecase(self, mock_print):
    method test_evaluate_written_input_yes_case_sensitive (line 248) | def test_evaluate_written_input_yes_case_sensitive(self, mock_print):
    method test_evaluate_written_input_no_ignorecase (line 262) | def test_evaluate_written_input_no_ignorecase(self, mock_print):
    method test_evaluate_written_input_no_case_sensitive (line 278) | def test_evaluate_written_input_no_case_sensitive(self, mock_print):

FILE: test/test_secure_input.py
  class TestSecureInput (line 7) | class TestSecureInput(unittest.TestCase):
    method test_secure_input (line 8) | def test_secure_input(self):

FILE: test/test_select.py
  class TestSelect (line 18) | class TestSelect(unittest.TestCase):
    method test_print_list_newlines (line 20) | def test_print_list_newlines(self, mock_print):
    method test_print_move_to_first_item (line 28) | def test_print_move_to_first_item(self, mock_print, *m):
    method test_print_options (line 38) | def test_print_options(self, mock_print, *m):
    method test_print_options_selected_index_set (line 47) | def test_print_options_selected_index_set(self, mock_print, *m):
    method test_print_non_selectable (line 56) | def test_print_non_selectable(self, mock_print, *m):
    method test_print_options_custom_prefixes (line 65) | def test_print_options_custom_prefixes(self, mock_print, *m):
    method test_ignore_unrecognized_key (line 79) | def test_ignore_unrecognized_key(self, mock_print):
    method test_move_up (line 113) | def test_move_up(self, *m):
    method test_move_up_skip_caption (line 120) | def test_move_up_skip_caption(self, *m):
    method test_move_down (line 127) | def test_move_down(self, *m):
    method test_move_down_skip_caption (line 134) | def test_move_down_skip_caption(self, *m):
    method test_keyboard_interrupt_ctrl_c_no_input (line 141) | def test_keyboard_interrupt_ctrl_c_no_input(self, *m):
    method test_keyboard_interrupt_ctrl_c_selected (line 147) | def test_keyboard_interrupt_ctrl_c_selected(self, *m):
    method test_keyboard_interrupt_ctrl_d_no_input (line 153) | def test_keyboard_interrupt_ctrl_d_no_input(self, *m):
    method test_keyboard_interrupt_ctrl_d_selected (line 159) | def test_keyboard_interrupt_ctrl_d_selected(self, *m):

FILE: test/test_select_multiple.py
  class TestSelectMultiplePrint (line 29) | class TestSelectMultiplePrint(unittest.TestCase):
    method test_list_newlines (line 31) | def test_list_newlines(self, mock_print):
    method test_move_to_first_item (line 39) | def test_move_to_first_item(self, mock_print, *m):
    method test_print_options (line 49) | def test_print_options(self, mock_print, *m):
    method test_print_options_caption_indices (line 61) | def test_print_options_caption_indices(self, mock_print, *m):
    method test_print_options_selected (line 74) | def test_print_options_selected(self, mock_print, *m):
    method test_print_options_selected_and_ticked (line 87) | def test_print_options_selected_and_ticked(self, mock_print, *m):
    method test_print_options_deselected_unticked (line 100) | def test_print_options_deselected_unticked(self, mock_print, *m):
    method test_print_deselected_confirm (line 113) | def test_print_deselected_confirm(self, mock_print, *m):
    method test_print_selected_confirm (line 121) | def test_print_selected_confirm(self, mock_print, *m):
    method test_print_show_confirm (line 129) | def test_print_show_confirm(self, mock_print, *m):
  class TestSelectMultipleMoveAndSelect (line 136) | class TestSelectMultipleMoveAndSelect(unittest.TestCase):
    method test_move_up (line 138) | def test_move_up(self, mock_print):
    method test_move_up_skip_caption (line 151) | def test_move_up_skip_caption(self, mock_print):
    method test_move_down (line 165) | def test_move_down(self, mock_print):
    method test_move_down_skip_caption (line 178) | def test_move_down_skip_caption(self, mock_print):
    method test_select (line 192) | def test_select(self, mock_print):
    method test_select_min_too_few (line 206) | def test_select_min_too_few(self, mock_print):
    method test_select_max_too_many (line 218) | def test_select_max_too_many(self, mock_print):
    method test_select_min_sufficient (line 230) | def test_select_min_sufficient(self, mock_print):
    method test_deselect_on_min_sufficient (line 243) | def test_deselect_on_min_sufficient(self, mock_print):
    method test_select_max_okay (line 259) | def test_select_max_okay(self, mock_print):
    method test_select_min_too_few_hide_confirm (line 272) | def test_select_min_too_few_hide_confirm(self, mock_print):
    method test_select_max_too_many_show_confirm (line 287) | def test_select_max_too_many_show_confirm(self, mock_print):
  class TestSelectMultipleMisc (line 304) | class TestSelectMultipleMisc(unittest.TestCase):
    method test_keyboard_interrupt (line 306) | def test_keyboard_interrupt(self, mock_print):
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (74K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 414,
    "preview": "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"
  },
  {
    "path": ".gitignore",
    "chars": 104,
    "preview": "._*\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",
    "chars": 317,
    "preview": ".PHONY: tests, coverage, lint, release\n\ntests:\n\tpython -m unittest\n\ncoverage:\n\tpython -m coverage erase\n\tpython -m cover"
  },
  {
    "path": "cutie.py",
    "chars": 13692,
    "preview": "#! /usr/bin/env python3\n\"\"\"\nCommandline User Tools for Input Easification\n\"\"\"\n\n__version__ = \"0.3.2\"\n__author__ = \"Hans "
  },
  {
    "path": "example.py",
    "chars": 2106,
    "preview": "#! /usr/bin/env python3\n\"\"\"Example script demonstrating usage of cutie.\n\"\"\"\n\nimport cutie\n\n\ndef main():\n    \"\"\"Main.\"\"\"\n"
  },
  {
    "path": "license.md",
    "chars": 1080,
    "preview": "# The MIT License (MIT)\n\nCopyright © 2018 Hans Schülein\n\nPermission is hereby granted, free of charge, to any person\nobt"
  },
  {
    "path": "readme.md",
    "chars": 14517,
    "preview": "# CUTIE\n\n*Command line User Tools for Input Easification*\n\n[![CircleCI](https://dl.circleci.com/status-badge/img/gh/Kami"
  },
  {
    "path": "requirements.txt",
    "chars": 27,
    "preview": "colorama\nreadchar != 3.0.5\n"
  },
  {
    "path": "setup.py",
    "chars": 1280,
    "preview": "\"\"\"Setup module for PyPI / pip integration.\n\"\"\"\n\nimport imp\n\nimport setuptools\n\nwith open(\"readme.md\", encoding=\"utf-8\")"
  },
  {
    "path": "test/__init__.py",
    "chars": 1815,
    "preview": "import readchar\n\nimport cutie\n\n\ndef PrintCall(states):\n\n    def func(msg=None, state=\"selectable\"):\n        state_ = sta"
  },
  {
    "path": "test/test_get_number.py",
    "chars": 4662,
    "preview": "import unittest\nfrom unittest import mock\n\nimport cutie\n\nfrom . import MockException\n\n\nclass TestCutieGetNumber(unittest"
  },
  {
    "path": "test/test_prompt_yes_or_no.py",
    "chars": 10511,
    "preview": "import unittest\nfrom unittest import mock\n\nimport readchar\n\nfrom . import InputContext, MockException, PrintCall, cutie\n"
  },
  {
    "path": "test/test_secure_input.py",
    "chars": 339,
    "preview": "import unittest\nfrom unittest import mock\n\nimport cutie\n\n\nclass TestSecureInput(unittest.TestCase):\n    def test_secure_"
  },
  {
    "path": "test/test_select.py",
    "chars": 6217,
    "preview": "import string\nimport unittest\nfrom unittest import mock\n\nimport readchar\n\nfrom . import InputContext, MockException, Pri"
  },
  {
    "path": "test/test_select_multiple.py",
    "chars": 12463,
    "preview": "import string\nimport unittest\nfrom unittest import mock\n\nimport readchar\n\nfrom . import InputContext, MockException, Pri"
  }
]

About this extraction

This page contains the full source code of the Kamik423/cutie GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (67.9 KB), approximately 16.7k tokens, and a symbol index with 99 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!