sh_cpl/src/cpl_core/console/console.py

573 lines
18 KiB
Python

import os
import sys
import time
from collections.abc import Callable
from typing import Union, Optional
from art import text2art
import colorama
from pynput import keyboard
from pynput.keyboard import Key
from tabulate import tabulate
from termcolor import colored
from cpl_core.console.background_color_enum import BackgroundColorEnum
from cpl_core.console.console_call import ConsoleCall
from cpl_core.console.foreground_color_enum import ForegroundColorEnum
from cpl_core.console.spinner_thread import SpinnerThread
class Console:
r"""Useful functions for handling with input and output"""
colorama.init()
_is_first_write = True
_background_color: BackgroundColorEnum = BackgroundColorEnum.default
_foreground_color: ForegroundColorEnum = ForegroundColorEnum.default
_x: Optional[int] = None
_y: Optional[int] = None
_disabled: bool = False
_hold_back = False
_hold_back_calls: list[ConsoleCall] = []
_select_menu_items: list[str] = []
_is_first_select_menu_output = True
_selected_menu_item_index: int = 0
_selected_menu_item_char: str = ''
_selected_menu_option_foreground_color: ForegroundColorEnum = ForegroundColorEnum.default
_selected_menu_option_background_color: BackgroundColorEnum = BackgroundColorEnum.default
_selected_menu_cursor_foreground_color: ForegroundColorEnum = ForegroundColorEnum.default
_selected_menu_cursor_background_color: BackgroundColorEnum = BackgroundColorEnum.default
"""Properties"""
@classmethod
@property
def background_color(cls) -> str:
return str(cls._background_color.value)
@classmethod
@property
def foreground_color(cls) -> str:
return str(cls._foreground_color.value)
"""Settings"""
@classmethod
def set_hold_back(cls, value: bool):
cls._hold_back = value
@classmethod
def set_background_color(cls, color: Union[BackgroundColorEnum, str]):
r"""Sets the background color
Parameter
---------
color: Union[:class:`cpl_core.console.background_color_enum.BackgroundColorEnum`, :class:`str`]
Background color of the console
"""
if type(color) is str:
cls._background_color = BackgroundColorEnum[color]
else:
cls._background_color = color
@classmethod
def set_foreground_color(cls, color: Union[ForegroundColorEnum, str]):
r"""Sets the foreground color
Parameter
---------
color: Union[:class:`cpl_core.console.background_color_enum.BackgroundColorEnum`, :class:`str`]
Foreground color of the console
"""
if type(color) is str:
cls._foreground_color = ForegroundColorEnum[color]
else:
cls._foreground_color = color
@classmethod
def reset_cursor_position(cls):
r"""Resets cursor position"""
cls._x = None
cls._y = None
@classmethod
def set_cursor_position(cls, x: int, y: int):
r"""Sets cursor position
Parameter
---------
x: :class:`int`
X coordinate
y: :class:`int`
Y coordinate
"""
cls._x = x
cls._y = y
"""Useful protected functions"""
@classmethod
def _output(cls, string: str, x: int = None, y: int = None, end: str = None):
r"""Prints given output with given format
Parameter
---------
string: :class:`str`
Message to print
x: :class:`int`
X coordinate
y: :class:`int`
Y coordinate
end: :class:`str`
End character of the message (could be \n)
"""
if cls._is_first_write:
cls._is_first_write = False
if end is None:
end = '\n'
args = []
colored_args = []
if x is not None and y is not None:
args.append(f'\033[{y};{x}H')
elif cls._x is not None and cls._y is not None:
args.append(f'\033[{cls._y};{cls._x}H')
colored_args.append(string)
if cls._foreground_color != ForegroundColorEnum.default and cls._background_color == BackgroundColorEnum.default:
colored_args.append(cls._foreground_color.value)
elif cls._foreground_color == ForegroundColorEnum.default and cls._background_color != BackgroundColorEnum.default:
colored_args.append(cls._background_color.value)
elif cls._foreground_color != ForegroundColorEnum.default and cls._background_color != BackgroundColorEnum.default:
colored_args.append(cls._foreground_color.value)
colored_args.append(cls._background_color.value)
args.append(colored(*colored_args))
print(*args, end=end)
@classmethod
def _show_select_menu(cls):
r"""Shows the select menu"""
if not cls._is_first_select_menu_output:
for _ in range(0, len(cls._select_menu_items) + 1):
sys.stdout.write('\x1b[1A\x1b[2K')
else:
cls._is_first_select_menu_output = False
for i in range(0, len(cls._select_menu_items)):
Console.set_foreground_color(cls._selected_menu_cursor_foreground_color)
Console.set_background_color(cls._selected_menu_cursor_background_color)
placeholder = ''
for _ in cls._selected_menu_item_char:
placeholder += ' '
Console.write_line(
f'{cls._selected_menu_item_char if cls._selected_menu_item_index == i else placeholder} ')
Console.set_foreground_color(cls._selected_menu_option_foreground_color)
Console.set_background_color(cls._selected_menu_option_background_color)
Console.write(f'{cls._select_menu_items[i]}')
Console.write_line()
@classmethod
def _select_menu_key_press(cls, key: Key):
r"""Event function when key press is detected
Parameter
---------
key: :class:`pynput.keyboard.Key`
Pressed key
"""
if key == Key.down:
if cls._selected_menu_item_index == len(cls._select_menu_items) - 1:
return
cls._selected_menu_item_index += 1
cls._show_select_menu()
elif key == Key.up:
if cls._selected_menu_item_index == 0:
return
cls._selected_menu_item_index -= 1
cls._show_select_menu()
elif key == Key.enter:
return False
""" Useful public functions"""
@classmethod
def banner(cls, string: str):
r"""Prints the string as a banner
Parameter
---------
string: :class:`str`
Message to print as banner
"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.banner, string))
return
cls.write_line(text2art(string))
@classmethod
def color_reset(cls):
r"""Resets the color settings"""
cls._background_color = BackgroundColorEnum.default
cls._foreground_color = ForegroundColorEnum.default
@classmethod
def clear(cls):
r"""Clears the console"""
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.clear))
return
os.system('cls' if os.name == 'nt' else 'clear')
@classmethod
def close(cls):
r"""Closes the application"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.close))
return
Console.color_reset()
Console.write('\n\n\nPress any key to continue...')
Console.read()
sys.exit()
@classmethod
def disable(cls):
r"""Disables console interaction"""
cls._disabled = True
@classmethod
def error(cls, string: str, tb: str = None):
r"""Prints an error with traceback
Parameter
---------
string: :class:`str`
Error message
tb: :class:`str`
Error traceback
"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.error, string, tb))
return
cls.set_foreground_color('red')
if tb is not None:
cls.write_line(f'{string} -> {tb}')
else:
cls.write_line(string)
cls.set_foreground_color('default')
@classmethod
def enable(cls):
r"""Enables console interaction"""
cls._disabled = False
@classmethod
def read(cls, output: str = None) -> str:
r"""Reads in line
Parameter
---------
output: :class:`str`
String to print before input
Returns
-------
input()
"""
if output is not None and not cls._hold_back:
cls.write_line(output)
return input()
@classmethod
def read_line(cls, output: str = None) -> str:
r"""Reads in next line
Parameter
---------
output: :class:`str`
String to print before input
Returns
-------
input()
"""
if cls._disabled and not cls._hold_back:
return ''
if output is not None:
cls.write_line(output)
cls._output('\n', end='')
return input()
@classmethod
def table(cls, header: list[str], values: list[list[str]]):
r"""Prints a table with header and values
Parameter
---------
header: List[:class:`str`]
Header of the table
values: List[List[:class:`str`]]
Values of the table
"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.table, header, values))
return
table = tabulate(values, headers=header)
Console.write_line(table)
Console.write('\n')
@classmethod
def select(cls, char: str, message: str, options: list[str],
header_foreground_color: Union[str, ForegroundColorEnum] = ForegroundColorEnum.default,
header_background_color: Union[str, BackgroundColorEnum] = BackgroundColorEnum.default,
option_foreground_color: Union[str, ForegroundColorEnum] = ForegroundColorEnum.default,
option_background_color: Union[str, BackgroundColorEnum] = BackgroundColorEnum.default,
cursor_foreground_color: Union[str, ForegroundColorEnum] = ForegroundColorEnum.default,
cursor_background_color: Union[str, BackgroundColorEnum] = BackgroundColorEnum.default
) -> str:
r"""Prints select menu
Parameter
---------
char: :class:`str`
Character to show which element is selected
message: :class:`str`
Message or header of the selection
options: List[:class:`str`]
Selectable options
header_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the header
header_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`]
Background color of the header
option_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the options
option_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`]
Background color of the options
cursor_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the cursor
cursor_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`]
Background color of the cursor
Returns
-------
Selected option as :class:`str`
"""
cls._selected_menu_item_char = char
cls.options = options
cls._select_menu_items = cls.options
if option_foreground_color is not None:
cls._selected_menu_option_foreground_color = option_foreground_color
if option_background_color is not None:
cls._selected_menu_option_background_color = option_background_color
if cursor_foreground_color is not None:
cls._selected_menu_cursor_foreground_color = cursor_foreground_color
if cursor_background_color is not None:
cls._selected_menu_cursor_background_color = cursor_background_color
Console.set_foreground_color(header_foreground_color)
Console.set_background_color(header_background_color)
Console.write_line(message, '\n')
cls._show_select_menu()
with keyboard.Listener(
on_press=cls._select_menu_key_press, suppress=False
) as listener:
listener.join()
Console.color_reset()
return cls._select_menu_items[cls._selected_menu_item_index]
@classmethod
def spinner(cls, message: str, call: Callable, *args, text_foreground_color: Union[str, ForegroundColorEnum] = None,
spinner_foreground_color: Union[str, ForegroundColorEnum] = None,
text_background_color: Union[str, BackgroundColorEnum] = None,
spinner_background_color: Union[str, BackgroundColorEnum] = None, **kwargs) -> any:
r"""Shows spinner and calls given function, when function has ended the spinner stops
Parameter
---------
message: :class:`str`
Message of the spinner
call: :class:`Callable`
Function to call
args: :class:`list`
Arguments of the function
text_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the text
spinner_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the spinner
text_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`]
Background color of the text
spinner_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`]
Background color of the spinner
kwargs: :class:`dict`
Keyword arguments of the call
Returns
-------
Return value of call
"""
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.spinner, message, call, *args))
return
if text_foreground_color is not None:
cls.set_foreground_color(text_foreground_color)
if text_background_color is not None:
cls.set_background_color(text_background_color)
if type(spinner_foreground_color) is str:
spinner_foreground_color = ForegroundColorEnum[spinner_foreground_color]
if type(spinner_background_color) is str:
spinner_background_color = BackgroundColorEnum[spinner_background_color]
cls.write_line(message)
cls.set_hold_back(True)
spinner = SpinnerThread(len(message), spinner_foreground_color, spinner_background_color)
spinner.start()
return_value = None
try:
return_value = call(*args, **kwargs)
except KeyboardInterrupt:
spinner.exit()
cls.close()
spinner.stop_spinning()
cls.set_hold_back(False)
cls.set_foreground_color(ForegroundColorEnum.default)
cls.set_background_color(BackgroundColorEnum.default)
for call in cls._hold_back_calls:
call.function(*call.args)
cls._hold_back_calls = []
time.sleep(0.1)
return return_value
@classmethod
def write(cls, *args, end=''):
r"""Prints in active line
Parameter
---------
args: :class:`list`
Elements to print
end: :class:`str`
Last character to print
"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.write, args))
return
string = ' '.join(map(str, args))
cls._output(string, end=end)
@classmethod
def write_at(cls, x: int, y: int, *args):
r"""Prints at given position
Parameter
---------
x: :class:`int`
X coordinate
y: :class:`int`
Y coordinate
args: :class:`list`
Elements to print
"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.write_at, x, y, args))
return
string = ' '.join(map(str, args))
cls._output(string, x, y, end='')
@classmethod
def write_line(cls, *args):
r"""Prints to new line
Parameter
---------
args: :class:`list`
Elements to print
"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.write_line, args))
return
string = ' '.join(map(str, args))
if not cls._is_first_write:
cls._output('')
cls._output(string, end='')
@classmethod
def write_line_at(cls, x: int, y: int, *args):
r"""Prints new line at given position
Parameter
---------
x: :class:`int`
X coordinate
y: :class:`int`
Y coordinate
args: :class:`list`
Elements to print
"""
if cls._disabled:
return
if cls._hold_back:
cls._hold_back_calls.append(ConsoleCall(cls.write_line_at, x, y, args))
return
string = ' '.join(map(str, args))
if not cls._is_first_write:
cls._output('', end='')
cls._output(string, x, y, end='')