# cli_tools_by_oleksa

[![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![PyPI](https://img.shields.io/pypi/v/cli-tools-by-oleksa?color=orange&label=version)](https://pypi.org/project/cli-tools-by-oleksa/)

A zero-dependency Python library for robust console input (validation, menus, type conversion) and cross-platform terminal control and styling.

***

### Purpose

This toolkit is designed to eliminate the notorious boilerplate required for reliable interactive console applications. It allows developers to completely bypass the need for writing repetitive input validation loops (`while True: try/except`) and managing fragile ANSI escape sequences for styling and cursor control.

### Target Audience

**`cli_tools`** is the perfect choice for projects where simplicity and minimal dependencies are paramount:

* **Students and Educators:** Write clean, readable code instantly without complex, low-level validation logic.
* **Utility & Script Developers:** Build robust, highly interactive command-line tools and prototypes quickly, without adopting heavy frameworks like Click, Typer, or Rich.
* **Clean Code Enthusiasts:** Achieve production-level reliability and polished user interaction while maintaining a zero-dependency footprint.

---

## Core Features

### Input & Validation

* **`get_input`** — The central function for user input. Features a robust pipeline (RegEx → Validator → Converter) and an optional **automatic retry loop** (`retry=True`).
* **`get_password`** — Securely prompts the user for a password (text is not echoed). It supports the full validation pipeline (RegEx → Validator) and **automatic retry logic**.
* **Specialized `get_*` Wrappers** — Dynamically generated functions (e.g., `get_int`, `get_email`, `get_date_dmy`) that pre-package specific RegEx patterns and converters for immediate use.
* **Validator Factories** — Reusable functions that **generate** validators (`is_in_range`, `is_list_of`, etc.), allowing users to easily compose complex input validation logic.
* **Predefined Patterns** — A set of included **RegEx constants** (`INT`, `FLOAT`, `EMAIL`, `DATE_DMY`) available for immediate use in input validation.


---

### Parsing, Splitting, and Converting

* **`extract_match`** — Extracts regex groups from a string and **converts** them to target types in a single step.
* **`split`** — Splits strings by delimiter or regex with optional automatic type conversion of elements.
* **`safe_int` / `safe_float`** — Fault-tolerant converters that return `None` on failure instead of raising exceptions.

---

### Menus & Selection

* **`menu`** — Generates a numbered console menu from a dictionary and returns the selected **key** (int).
* **`get_choice`** — Selects a string from a list of options. It is **case-insensitive by default** and includes auto-retry logic.
* **`yes_no`** — A standard confirmation prompt returning a `bool` (`True` for yes, `False` for no).

---

### Terminal Control & Dynamic Display

* **Cursor Control** — A comprehensive suite of functions (`move_to`, `move_to_column`, `move_up/down`, etc.) for building dynamic interfaces like spinners or progress bars.
* **Cross-Platform Reliability** — Includes a built-in **WinAPI fallback** mechanism, ensuring cursor movements and screen clearing work correctly on Windows even if ANSI is not natively supported.
* **`set_title`** — Sets the console window title.

---

### Styling & Formatting

* **`stylize` / `rgb`** — Apply standard ANSI colors or **24-bit TrueColor (RGB)** to text.
* **Style Helpers** — Ready-made, semantic functions (`error`, `success`, `warning`, `info`) for applying standard color schemes instantly.
* **Style Constants** — Ready-to-use constants for foregrounds (`RED`), backgrounds (`BG_BLUE`), and attributes (`BOLD`, `UNDERLINE`).
* **Pretty Printing** — `print_table` and `print_iterable` for formatting lists and tabular data with custom patterns.
* **`print_header`** — Displays a centered title with decorative separators.

---

### Utilities & Error Safety

* **`safe_run`** — A context manager that handles exceptions gracefully and ensures a clean exit on `KeyboardInterrupt` (Ctrl+C).
* **`try_until_ok`** — A universal wrapper to repeatedly execute unstable functions (e.g., network calls) until success.


#### Requires Python 3.10+ and has zero external dependencies.

---

## 📦 Installation
```bash
pip install --upgrade cli_tools_by_oleksa
```

---

## Examples

### 1. Basic Functions

```Python
from cli_tools import print_header, get_input, get_int, get_number
from cli_tools.validators import is_in_range


print_header('Basic CLI Functions')

name = get_input('What is your name? ',
                 converter=lambda s: s.strip().capitalize(),     # converts input before return it
                 default='Anonim')                               # returns 'Anonim' if the user presses Enter.


# get_int ensures integer format.
age = get_int('How old are you? ',
              validator=is_in_range(10, 100),   # checks if the number is within the range [10, 100].
              retry=True,                                 # forces re-entry until validation passes.
              if_invalid='Age must be an integer between 10 and 100.')  # message to show if input is incorrect

# get_number passes int and float numbers (returns always float).
num = get_number('What is your favourite number? ',
                 retry=True,
                 if_invalid='Please, enter a number.')


print(f"Nice to meet you, {name}! You are {age} years old and your favorite number is {num}. I like it!")

```

### 2. Menu, Choices and Format Output

```python
from cli_tools import print_header, get_choice, menu, yes_no, print_iterable, print_table


# Defines the main menu options for the menu() function.
# Keys (int) are the expected user input; values (str) are the displayed option text.
main_menu = {
    1: 'New game',
    2: 'Best players',
    3: 'Show results',
    0: 'Quit',
}

# Simple list of players for print_iterable demonstration.
players = [
    'You',
    'Not you',
    'Who?'
]

# Dictionary of results for print_table demonstration.
# The items (key-value pairs) will be unpacked into table rows.
results = {
    'Easy': 2308,
    'Medium': 1841,
    'Hard': 1550,
}


def play():
    """Starts the game flow, handling difficulty selection."""

    modes = ['Easy', 'Medium', 'Hard']

    # get_choice() validates user input against a list of options (modes).
    # show=True displays the options automatically before prompting.
    mode = get_choice(options = modes,
                      prompt = 'Choose difficulty: ',
                      if_invalid = 'Please, enter a valid mode name.',
                      show = True
                      ).lower()  # The validated string result is safe to manipulate

    print(f'Some game logic for {mode} mode...')


def main():
    """Main loop for the CLI application, controlling flow via the menu."""

    # print_header() adds visual separation and a centered title to the console.
    print_header('Choices And Format Demo', char='=', space=3)

    while True:
        # menu() handles display, validates input against the dictionary keys, and returns the selected key (int).
        match menu(main_menu, 'What would you like to do? '):
            case 1:
                play()
            case 2:
                # print_iterable() outputs a one-dimensional list with custom formatting.
                print_iterable(players,
                               start='\nBest players:\n',
                               item_pattern='- {}')
            case 3:
                # print_table() iterates over results.items() and unpacks each pair into the row_pattern.
                print_table(results.items(),
                            # Pattern uses format specifiers for alignment:
                            # {:<6} for left-aligned string, {:>8} for right-aligned integer
                            row_pattern = '{:<6}{:>8}',
                            start = '\nMode\tResults\n')
            case 0:
                # yes_no() prompts for confirmation and returns a boolean (True for 'yes', False for 'no').
                if yes_no('Exit? [y/N]: '):
                    break

    print('Bye!')


if __name__ == '__main__':
    main()

```

### 3. Project Example: Simple Calculator

```Python
import re
from cli_tools import (print_header, get_input,
                       extract_match) # extract list of all matches, optionally convert every match. [] if no matches
from cli_tools.patterns import NUMBER # Ready-made pattern: accepts both int and float

# Pattern <number> <operator> <number>, spaces ignored
simple_expr_pattern = re.compile(fr' *({NUMBER.pattern}) *([+\-*/^]) *({NUMBER.pattern}) *')
# Accepts either numbers or operators. Converts numbers to float
converter = lambda x: float(x) if x not in '+-*/^' else x

print_header('| Simple calculator |', '—')
print('Supports simple expressions in format <number> <operator> <number>. Press Ctrl+C to exit.')

while True:
    # Guaranteed to pass only input that matches the pattern
    expr = get_input('> ', pattern=simple_expr_pattern, retry=True, if_invalid='Wrong format!')
    # Unpack the input, immediately converting the numbers
    left, operator, right = extract_match(expr, simple_expr_pattern, converter=converter)

    match operator:
        case '+':
            print(left+right)
        case '-':
            print(left-right)
        case '*':
            print(left*right)
        case '/':
            if right == 0:
                print('Division by zero is not allowed!')
                continue
            print(left/right)
        case '^':
            print(left**right)
        case _:
            print('Wrong operator!')

```

### 4. Styles

```python
from cli_tools.styles import (error, success, warning, info,    # functions to apply standart styles
                              rgb, stylize,                     # functions to apply custom styles
                              ITALIC, BLACK, UNDERLINE)         # constants


# These functions apply a default foreground color and return the styled string.
error_msg = error('Error message')
warning_msg = warning('Warning message')
success_msg = success('Success message')
info_msg = info('Info message')

print(error_msg)
print(warning_msg)
print(success_msg)
print(info_msg)

# Define custom color using an RGB tuple (24-bit color).
tuple_yellow = (240, 240, 100)

# Apply the tuple color as foreground (text).
print(
    stylize('Yellow text', rgb(tuple_yellow))
)

# Apply the tuple color as background (using is_bg=True) and combine with BLACK foreground (text).
print(
    stylize('Yellow background', rgb(tuple_yellow, is_bg=True), BLACK)
)

# Define custom colors using the rgb function with HEX codes.
# The first color is foreground (text), the second specifies the background (bg#).
gray = rgb('#dddddd')
darkblue_bg = rgb('bg#0a0a88')

# wrap() applies multiple styles (foreground, background, and formatting constants).
print(
    stylize('Styled message', gray, darkblue_bg, ITALIC, UNDERLINE)
)

```

### 5. Terminal

```python
from time import sleep
from cli_tools import terminal as t


t.clear_screen()
t.home_cursor()
t.set_title('Title')

print('Hello!')

print('This text will disappear in a second.')
sleep(1)
t.move_up(1)
t.clear_line()

print('Simple progress bar example: ')
pattern = '[{:<10}]'

for i in range(1, 11):
    t.clear_line()
    t.move_to_column(1)
    print(pattern.format('='*i), end='', flush=True)
    sleep(0.2)
print()

print('Simple spinner example: ', end='')
for i in range(4):
    for status in ['|', '/', '—', '\\']:
        print(status, end='', flush=True)
        sleep(0.1)
        t.move_backward(1)

print('Done')

```

### 6. Error Handling

```python
import random, time

from cli_tools.exceptions import (CLIError,         # Base class for all raised errors
                                  APIError,         # Error that raised when you pass invalid data to a function.
                                  ValidationError,  # Data not validated
                                  ConversionError)  # The transferred converter caused an error
from cli_tools import safe_run, try_until_ok, print_header

print_header('Safe Execution Demo')

with safe_run(debug=False, exit_on_error=False):
    print("Press Ctrl+C to test graceful exit, or wait for the error...")

    for i in range(3, 0, -1):
        print(f"Crashing in {i}...")
        time.sleep(1)

    raise CLIError("Something went wrong inside the app!")

print("App still working.")

###

print_header('Retry Logic Demo')

def unstable_network_request():
    """Simulates a connection that fails 70% of the time."""
    if random.random() < 0.7:
        raise ConnectionError("Connection timed out")
    return "200 OK"

print("Attempting to connect to server...")

status = try_until_ok(
    unstable_network_request,
    exceptions=ConnectionError,
    on_exception="Connection failed. Retrying..."
)

print(f"Success! Server response: {status}")

```

## Roadmap

**Next Steps (Technical & Infrastructure):**
- Detailed documentation
- Tests
- Repository

**Next Steps (Feature Development):**
- Progress bar and Spinners (High-level API)
- Enhanced Validator Functionality:
    - Validator Composition: Implement the ability to easily combine multiple validators to create complex rules without writing additional `lambda` functions.
    - Expanding Built-in Validators: Adding new useful validators to the library.
  