From 10eb5303d5083e8d06e01b6b4e18c1a6acd3a87d Mon Sep 17 00:00:00 2001 From: feluxe Date: Sat, 25 Nov 2023 05:50:00 +0100 Subject: [PATCH] Add multiline support. Fixes #8 --- prmt/__init__.py | 329 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 225 insertions(+), 104 deletions(-) diff --git a/prmt/__init__.py b/prmt/__init__.py index 6722dbd..333b87c 100644 --- a/prmt/__init__.py +++ b/prmt/__init__.py @@ -5,7 +5,13 @@ import tempfile import os import subprocess as sp -from dataclasses import dataclass +import platform +import sys + +if platform.system == "Windows": + import msvcrt +else: + import termios def get_input_from_texteditor( @@ -14,7 +20,6 @@ def get_input_from_texteditor( file_type=None, remove_comments=True, ) -> str: - if default: q = f"{default}\n" else: @@ -26,16 +31,14 @@ def get_input_from_texteditor( for line in instruction.splitlines(): q += f"# {line}\n" - editor = os.environ.get('EDITOR') or 'vi' + editor = os.environ.get("EDITOR") or "vi" - with tempfile.NamedTemporaryFile( - suffix=file_type or "", mode='w+' - ) as tmp_file: + with tempfile.NamedTemporaryFile(suffix=file_type or "", mode="w+") as tmp_file: tmp_file.write(q) tmp_file.flush() - if editor in ['vi', 'vim'] and file_type: - sp.run([editor, '-c', f'set filetype={file_type}', tmp_file.name]) + if editor in ["vi", "vim"] and file_type: + sp.run([editor, "-c", f"set filetype={file_type}", tmp_file.name]) else: sp.run([editor, tmp_file.name]) @@ -43,10 +46,10 @@ def get_input_from_texteditor( user_input_raw = tmp_file.read() - user_input_clean = '' + user_input_clean = "" if q: - user_input_clean = user_input_raw.replace(q, '') + user_input_clean = user_input_raw.replace(q, "") else: user_input_clean = user_input_raw @@ -55,6 +58,39 @@ def get_input_from_texteditor( return user_input_clean +def read_stdin_non_blocking_windows(): + """ + TODO: This is not tested... + """ + if msvcrt.kbhit(): + return msvcrt.getch().decode("utf-8") + else: + return None + + +def read_stdin_non_blocking_unix(): + fd = sys.stdin.fileno() + orig = termios.tcgetattr(fd) + + new = termios.tcgetattr(fd) + new[3] = new[3] & ~termios.ICANON + new[6][termios.VMIN] = 1 + new[6][termios.VTIME] = 0 + + try: + termios.tcsetattr(fd, termios.TCSAFLUSH, new) + return sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSAFLUSH, orig) + + +def read_stdin_non_blocking(): + if platform.system == "Windows": + return read_stdin_non_blocking_windows() + else: + return read_stdin_non_blocking_unix() + + def _string_base( question: str, default: Optional[str] = None, @@ -63,42 +99,65 @@ def _string_base( editor_instruction: Optional[str] = None, editor_file_type=None, editor_remove_comments=True, + multiline=False, fmt=["\n{}\n", "[{}]", "> {}\n"], fmt_question=None, fmt_default=None, fmt_prompt=None, ) -> str: - fmt = fmt or [None, None, None] fmt_question = fmt_question or fmt[0] or "\n{}\n" fmt_default = fmt_default or fmt[1] or "[{}]" fmt_prompt = fmt_prompt or fmt[2] or "> {}\n" - fmt_prompt_start = fmt_prompt.split('{}')[0] - fmt_prompt_end = fmt_prompt.split('{}')[1] + fmt_prompt_start = fmt_prompt.split("{}")[0] + fmt_prompt_end = fmt_prompt.split("{}")[1] - prompt = '' + prompt = "" if default: - prompt = fmt_question.format(question) +\ - fmt_default.format(default) +\ - fmt_prompt_start + prompt = ( + fmt_question.format(question) + + fmt_default.format(default) + + fmt_prompt_start + ) else: prompt = fmt_question.format(question) + fmt_prompt_start if open_editor: - print(prompt, end='') - answer = get_input_from_texteditor( - instruction=editor_instruction, - default=default, - file_type=editor_file_type, - remove_comments=editor_remove_comments, - ) or default or '' + print(prompt, end="") + answer = ( + get_input_from_texteditor( + instruction=editor_instruction, + default=default, + file_type=editor_file_type, + remove_comments=editor_remove_comments, + ) + or default + or "" + ) + + elif multiline: + print(prompt, end="") + + answer = "" + + try: + while True: + user_input = read_stdin_non_blocking() + if user_input is not None: + answer += user_input + except EOFError: + pass + except KeyboardInterrupt: + pass + + answer = answer or default or "" + else: - answer = input(prompt) or default or '' + answer = input(prompt) or default or "" if blacklist and answer in blacklist: - - print('Invalid input.' + fmt_prompt_end) + print("Invalid input." + fmt_prompt_end) answer = _string_base( question=question, @@ -111,7 +170,7 @@ def _string_base( fmt_prompt=fmt_prompt, ) else: - print(fmt_prompt_end, end='') + print(fmt_prompt_end, end="") return answer @@ -160,6 +219,7 @@ def string( question: str, default: Optional[str] = None, blacklist: Optional[list] = None, + multiline: bool = False, fmt=["\n{}\n", "[{}]", "> {}\n"], fmt_question=None, fmt_default=None, @@ -180,6 +240,7 @@ def string( default=default, blacklist=blacklist, open_editor=False, + multiline=multiline, fmt=fmt, fmt_question=fmt_question, fmt_default=fmt_default, @@ -210,19 +271,21 @@ def integer( fmt_question = fmt_question or fmt[0] or "\n{}\n" fmt_default = fmt_default or fmt[1] or "[{}]" fmt_prompt = fmt_prompt or fmt[2] or "> {}\n" - fmt_prompt_start = fmt_prompt.split('{}')[0] - fmt_prompt_end = fmt_prompt.split('{}')[1] + fmt_prompt_start = fmt_prompt.split("{}")[0] + fmt_prompt_end = fmt_prompt.split("{}")[1] if default: - prompt = fmt_question.format(question) +\ - fmt_default.format(default) +\ - fmt_prompt_start + prompt = ( + fmt_question.format(question) + + fmt_default.format(default) + + fmt_prompt_start + ) else: prompt = fmt_question.format(question) + fmt_prompt_start - answer: str = input(prompt) or default or '' + answer: str = input(prompt) or default or "" - print(fmt_prompt_end, end='') + print(fmt_prompt_end, end="") retry = False return_val: Union[int, None] @@ -240,7 +303,6 @@ def integer( retry = True if retry: - print("Invalid input." + fmt_prompt_end) return_val = integer( @@ -277,26 +339,28 @@ def confirm( fmt_question = fmt_question or fmt[0] or "\n{}\n" fmt_default = fmt_default or fmt[1] or "[{}]" fmt_prompt = fmt_prompt or fmt[2] or "> {}\n" - fmt_prompt_start = fmt_prompt.split('{}')[0] - fmt_prompt_end = fmt_prompt.split('{}')[1] + fmt_prompt_start = fmt_prompt.split("{}")[0] + fmt_prompt_end = fmt_prompt.split("{}")[1] if default: - prompt = fmt_question.format(question) +\ - fmt_default.format(default) +\ - fmt_prompt_start + prompt = ( + fmt_question.format(question) + + fmt_default.format(default) + + fmt_prompt_start + ) else: prompt = fmt_question.format(question) + fmt_prompt_start return_val = None - answer: str = input(prompt) or default or '' + answer: str = input(prompt) or default or "" - print(fmt_prompt_end, end='') + print(fmt_prompt_end, end="") - if answer and answer.lower() in ['y', 'yes', 'true', '1']: + if answer and answer.lower() in ["y", "yes", "true", "1"]: return_val = True - elif answer and answer.lower() in ['n', 'no', 'false', '0']: + elif answer and answer.lower() in ["n", "no", "false", "0"]: return_val = False else: @@ -335,10 +399,10 @@ def list_of_string( fmt_question = fmt_question or fmt[0] or "\n{}\n" fmt_default = fmt_default or fmt[1] or "[{}]" fmt_prompt = fmt_prompt or fmt[2] or "> {}\n" - fmt_prompt_end = fmt_prompt.split('{}')[1] + fmt_prompt_end = fmt_prompt.split("{}")[1] if isinstance(default, (list, tuple)): - default = ', '.join(default) + default = ", ".join(default) answer = string( question=question, @@ -352,7 +416,7 @@ def list_of_string( retry = False - return_val = [item.strip() for item in answer.split(',')] + return_val = [item.strip() for item in answer.split(",")] if blacklist: for item in return_val: @@ -360,8 +424,7 @@ def list_of_string( retry = True if retry: - - print('Invalid input.' + fmt_prompt_end) + print("Invalid input." + fmt_prompt_end) return_val = list_of_string( question=question, @@ -414,8 +477,8 @@ def select( fmt_options_end = fmt_options_end or fmt[2] or "\n" fmt_default = fmt_default or fmt[3] or "[{}]" fmt_prompt = fmt_prompt or fmt[4] or "> {}\n" - fmt_prompt_start = fmt_prompt.split('{}')[0] - fmt_prompt_end = fmt_prompt.split('{}')[1] + fmt_prompt_start = fmt_prompt.split("{}")[0] + fmt_prompt_end = fmt_prompt.split("{}")[1] fmt_custom = fmt_custom or [None, None, None] fmt_custom_question = fmt_custom_question or fmt_custom[0] or "\n{}\n" @@ -423,9 +486,7 @@ def select( fmt_custom_propmt = fmt_custom_propmt or fmt_custom[2] or "> {}\n" if default: - prompt = fmt_options_end +\ - fmt_default.format(default) +\ - fmt_prompt_start + prompt = fmt_options_end + fmt_default.format(default) + fmt_prompt_start else: prompt = fmt_options_end + fmt_prompt_start @@ -434,14 +495,12 @@ def select( # Print Options if isinstance(options, (list, tuple)): - for key, option in enumerate(options): print(fmt_option.format(key, str(option))) elif isinstance(options, dict): - for key, option in options.items(): - print(' {}: {}'.format(key, str(option))) + print(" {}: {}".format(key, str(option))) # Let User Choose Option @@ -451,7 +510,6 @@ def select( retry = True if isinstance(options, (list, tuple)): - try: selected_key = int(selected_key) selected_value = options[int(selected_key)] @@ -460,7 +518,6 @@ def select( pass elif isinstance(options, dict): - try: selected_value = options[selected_key] retry = False @@ -483,7 +540,7 @@ def select( ) # If Input Invalid, recurse. - print(fmt_prompt_end, end='') + print(fmt_prompt_end, end="") if retry: selected_key, selected_value = select( @@ -505,7 +562,6 @@ def select( class Prompt: - def __init__( self, fmt_question=None, @@ -565,14 +621,14 @@ def __init__( self.fmt_list_of_string_default = fmt_list_of_string_default self.fmt_list_of_string_prompt = fmt_list_of_string_prompt - self.fmt_select_question = fmt_select_question, - self.fmt_select_option = fmt_select_option, - self.fmt_select_options_end = fmt_select_options_end, - self.fmt_select_default = fmt_select_default, - self.fmt_select_prompt = fmt_select_prompt, - self.fmt_select_custom_question = fmt_select_custom_question, + self.fmt_select_question = (fmt_select_question,) + self.fmt_select_option = (fmt_select_option,) + self.fmt_select_options_end = (fmt_select_options_end,) + self.fmt_select_default = (fmt_select_default,) + self.fmt_select_prompt = (fmt_select_prompt,) + self.fmt_select_custom_question = (fmt_select_custom_question,) self.fmt_select_custom_default = fmt_select_custom_default - self.fmt_select_custom_prompt = fmt_select_custom_prompt, + self.fmt_select_custom_prompt = (fmt_select_custom_prompt,) def string_from_editor( self, @@ -600,13 +656,28 @@ def string_from_editor( :param fmt_default: Define a template for displaying the default value. :param fmt_prompt: Define a template for displaying the prompt line. """ - fmt_question = fmt_question or fmt[0] or self.fmt_string_from_editor_question or self.fmt_question # yapf: disable - fmt_default = fmt_default or fmt[1] or self.fmt_string_from_editor_default or self.fmt_default # yapf: disable - fmt_prompt = fmt_prompt or fmt[2] or self.fmt_string_from_editor_prompt or self.fmt_prompt # yapf: disable + fmt_question = ( + fmt_question + or fmt[0] + or self.fmt_string_from_editor_question + or self.fmt_question + ) # yapf: disable + fmt_default = ( + fmt_default + or fmt[1] + or self.fmt_string_from_editor_default + or self.fmt_default + ) # yapf: disable + fmt_prompt = ( + fmt_prompt + or fmt[2] + or self.fmt_string_from_editor_prompt + or self.fmt_prompt + ) # yapf: disable args = locals() - del args['self'] - del args['fmt'] + del args["self"] + del args["fmt"] return string_from_editor(**args) @@ -630,13 +701,19 @@ def string( :param fmt_default: Define a template for displaying the default value. :param fmt_prompt: Define a template for displaying the prompt line. """ - fmt_question = fmt_question or fmt[0] or self.fmt_string_question or self.fmt_question # yapf: disable - fmt_default = fmt_default or fmt[1] or self.fmt_string_default or self.fmt_default # yapf: disable - fmt_prompt = fmt_prompt or fmt[2] or self.fmt_string_prompt or self.fmt_prompt # yapf: disable + fmt_question = ( + fmt_question or fmt[0] or self.fmt_string_question or self.fmt_question + ) # yapf: disable + fmt_default = ( + fmt_default or fmt[1] or self.fmt_string_default or self.fmt_default + ) # yapf: disable + fmt_prompt = ( + fmt_prompt or fmt[2] or self.fmt_string_prompt or self.fmt_prompt + ) # yapf: disable args = locals() - del args['self'] - del args['fmt'] + del args["self"] + del args["fmt"] return string(**args) @@ -660,13 +737,19 @@ def integer( :param fmt_default: Define a template for displaying the default value. :param fmt_prompt: Define a template for displaying the prompt line. """ - fmt_question = fmt_question or fmt[0] or self.fmt_integer_question or self.fmt_question # yapf: disable - fmt_default = fmt_default or fmt[1] or self.fmt_integer_default or self.fmt_default # yapf: disable - fmt_prompt = fmt_prompt or fmt[2] or self.fmt_integer_prompt or self.fmt_prompt # yapf: disable + fmt_question = ( + fmt_question or fmt[0] or self.fmt_integer_question or self.fmt_question + ) # yapf: disable + fmt_default = ( + fmt_default or fmt[1] or self.fmt_integer_default or self.fmt_default + ) # yapf: disable + fmt_prompt = ( + fmt_prompt or fmt[2] or self.fmt_integer_prompt or self.fmt_prompt + ) # yapf: disable args = locals() - del args['self'] - del args['fmt'] + del args["self"] + del args["fmt"] return integer(**args) @@ -688,13 +771,19 @@ def confirm( :param fmt_default: Define a template for displaying the default value. :param fmt_prompt: Define a template for displaying the prompt line. """ - fmt_question = fmt_question or fmt[0] or self.fmt_confirm_question or self.fmt_question # yapf: disable - fmt_default = fmt_default or fmt[1] or self.fmt_confirm_default or self.fmt_default # yapf: disable - fmt_prompt = fmt_prompt or fmt[2] or self.fmt_confirm_prompt or self.fmt_prompt # yapf: disable + fmt_question = ( + fmt_question or fmt[0] or self.fmt_confirm_question or self.fmt_question + ) # yapf: disable + fmt_default = ( + fmt_default or fmt[1] or self.fmt_confirm_default or self.fmt_default + ) # yapf: disable + fmt_prompt = ( + fmt_prompt or fmt[2] or self.fmt_confirm_prompt or self.fmt_prompt + ) # yapf: disable args = locals() - del args['self'] - del args['fmt'] + del args["self"] + del args["fmt"] return confirm(**args) @@ -718,13 +807,22 @@ def list_of_string( :param fmt_default: Define a template for displaying the default value. :param fmt_prompt: Define a template for displaying the prompt line. """ - fmt_question = fmt_question or fmt[0] or self.fmt_list_of_string_question or self.fmt_question # yapf: disable - fmt_default = fmt_default or fmt[1] or self.fmt_list_of_string_default or self.fmt_default # yapf: disable - fmt_prompt = fmt_prompt or fmt[2] or self.fmt_list_of_string_prompt or self.fmt_prompt # yapf: disable + fmt_question = ( + fmt_question + or fmt[0] + or self.fmt_list_of_string_question + or self.fmt_question + ) # yapf: disable + fmt_default = ( + fmt_default or fmt[1] or self.fmt_list_of_string_default or self.fmt_default + ) # yapf: disable + fmt_prompt = ( + fmt_prompt or fmt[2] or self.fmt_list_of_string_prompt or self.fmt_prompt + ) # yapf: disable args = locals() - del args['self'] - del args['fmt'] + del args["self"] + del args["fmt"] return list_of_string(**args) @@ -761,18 +859,41 @@ def select( :param fmt_custom_default: Define a template for displaying the default value of the custom string input. :param fmt_custom_prompt: Define a template for displaying the prompt line of the custom string input. """ - fmt_question = fmt_question or fmt[0] or self.fmt_select_question or self.fmt_question # yapf: disable + fmt_question = ( + fmt_question or fmt[0] or self.fmt_select_question or self.fmt_question + ) # yapf: disable fmt_option = fmt_option or fmt[1] or self.fmt_select_option # yapf: disable - fmt_options_end = fmt_options_end or fmt[2] or self.fmt_select_options_end # yapf: disable - fmt_default = fmt_default or fmt[3] or self.fmt_select_default or self.fmt_default # yapf: disable - fmt_prompt = fmt_prompt or fmt[4] or self.fmt_select_prompt or self.fmt_prompt # yapf: disable - - fmt_custom_question = fmt_custom_question or fmt_custom[0] or self.fmt_select_custom_question or self.fmt_question # yapf: disable - fmt_custom_default = fmt_custom_default or fmt_custom[1] or self.fmt_select_custom_default or self.fmt_default # yapf: disable - fmt_custom_prompt = fmt_custom_prompt or fmt_custom[2] or self.fmt_select_custom_prompt or self.fmt_prompt # yapf: disable + fmt_options_end = ( + fmt_options_end or fmt[2] or self.fmt_select_options_end + ) # yapf: disable + fmt_default = ( + fmt_default or fmt[3] or self.fmt_select_default or self.fmt_default + ) # yapf: disable + fmt_prompt = ( + fmt_prompt or fmt[4] or self.fmt_select_prompt or self.fmt_prompt + ) # yapf: disable + + fmt_custom_question = ( + fmt_custom_question + or fmt_custom[0] + or self.fmt_select_custom_question + or self.fmt_question + ) # yapf: disable + fmt_custom_default = ( + fmt_custom_default + or fmt_custom[1] + or self.fmt_select_custom_default + or self.fmt_default + ) # yapf: disable + fmt_custom_prompt = ( + fmt_custom_prompt + or fmt_custom[2] + or self.fmt_select_custom_prompt + or self.fmt_prompt + ) # yapf: disable args = locals() - del args['self'] - del args['fmt'] + del args["self"] + del args["fmt"] return select(**args)