simple_term_menu.py
#!/usr/bin/env python3
import argparse
import os
import re
import shlex
import signal
import subprocess
import sys
from locale import getlocale
from types import FrameType
from typing import cast, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
try:
import termios
except ImportError:
import platform
raise NotImplementedError('"{}" is currently not supported.'.format(platform.system()))
__author__ = "Ingo Heimbach"
__email__ = "i.heimbach@fz-juelich.de"
__copyright__ = "Copyright © 2019 Forschungszentrum Jülich GmbH. All rights reserved."
__license__ = "MIT"
__version_info__ = (0, 6, 7)
__version__ = ".".join(map(str, __version_info__))
DEFAULT_MENU_CURSOR = "> "
DEFAULT_MENU_CURSOR_STYLE = ("fg_red", "bold")
DEFAULT_MENU_HIGHLIGHT_STYLE = ("standout",)
DEFAULT_CYCLE_CURSOR = True
DEFAULT_CLEAR_SCREEN = False
DEFAULT_PREVIEW_SIZE = 0.25
MIN_VISIBLE_MENU_ENTRIES_COUNT = 3
class InvalidStyleError(Exception):
pass
class NoMenuEntriesError(Exception):
pass
class PreviewCommandFailedError(Exception):
pass
def static_variables(**variables: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
for key, value in variables.items():
setattr(f, key, value)
return f
return decorator
class BoxDrawingCharacters:
if getlocale()[1] == "UTF-8":
# Unicode box characters
horizontal = "─"
vertical = "│"
upper_left = "┌"
upper_right = "┐"
lower_left = "└"
lower_right = "┘"
else:
# ASCII box characters
horizontal = "-"
vertical = "|"
upper_left = "+"
upper_right = "+"
lower_left = "+"
lower_right = "+"
class TerminalMenu:
class Viewport:
def __init__(
self,
num_menu_entries: int,
title_lines_count: int,
preview_lines_count: int,
initial_cursor_position: int = 0,
):
self._num_menu_entries = num_menu_entries
self._title_lines_count = title_lines_count
# Use the property setter since it has some more logic
self.preview_lines_count = preview_lines_count
self._num_lines = TerminalMenu._num_lines() - self._title_lines_count - self._preview_lines_count
self._viewport = (0, min(self._num_menu_entries, self._num_lines) - 1)
self.keep_visible(initial_cursor_position, refresh_terminal_size=False)
def keep_visible(self, cursor_position: int, refresh_terminal_size: bool = True) -> None:
if refresh_terminal_size:
self.update_terminal_size()
if self._viewport[0] <= cursor_position <= self._viewport[1]:
# Cursor is already visible
return
if cursor_position < self._viewport[0]:
scroll_num = cursor_position - self._viewport[0]
else:
scroll_num = cursor_position - self._viewport[1]
self._viewport = (self._viewport[0] + scroll_num, self._viewport[1] + scroll_num)
def update_terminal_size(self) -> None:
num_lines = TerminalMenu._num_lines() - self._title_lines_count - self._preview_lines_count
if num_lines != self._num_lines:
# First let the upper index grow or shrink
upper_index = min(num_lines, self._num_menu_entries) - 1
# Then, use as much space as possible for the `lower_index`
lower_index = max(0, upper_index - num_lines)
self._viewport = (lower_index, upper_index)
self._num_lines = num_lines
@property
def lower_index(self) -> int:
return self._viewport[0]
@property
def upper_index(self) -> int:
return self._viewport[1]
@property
def viewport(self) -> Tuple[int, int]:
return self._viewport
@property
def size(self) -> int:
return self._viewport[1] - self._viewport[0] + 1
@property
def num_menu_entries(self) -> int:
return self._num_menu_entries
@property
def title_lines_count(self) -> int:
return self._title_lines_count
@property
def preview_lines_count(self) -> int:
return self._preview_lines_count
@preview_lines_count.setter
def preview_lines_count(self, value: int) -> None:
self._preview_lines_count = min(
value if value >= 3 else 0,
TerminalMenu._num_lines() - self._title_lines_count - MIN_VISIBLE_MENU_ENTRIES_COUNT,
)
_codename_to_capname = {
"bg_black": "setab 0",
"bg_blue": "setab 4",
"bg_cyan": "setab 6",
"bg_gray": "setab 7",
"bg_green": "setab 2",
"bg_purple": "setab 5",
"bg_red": "setab 1",
"bg_yellow": "setab 3",
"bold": "bold",
"clear": "clear",
"colors": "colors",
"cursor_down": "cud1",
"cursor_invisible": "civis",
"cursor_up": "cuu1",
"cursor_visible": "cnorm",
"delete_line": "dl1",
"down": "kcud1",
"enter_application_mode": "smkx",
"exit_application_mode": "rmkx",
"fg_black": "setaf 0",
"fg_blue": "setaf 4",
"fg_cyan": "setaf 6",
"fg_gray": "setaf 7",
"fg_green": "setaf 2",
"fg_purple": "setaf 5",
"fg_red": "setaf 1",
"fg_yellow": "setaf 3",
"italics": "sitm",
"reset_attributes": "sgr0",
"standout": "smso",
"underline": "smul",
"up": "kcuu1",
}
_name_to_control_character = {"enter": "\012", "escape": "\033"}
_codenames = tuple(_codename_to_capname.keys())
_codename_to_terminal_code = None # type: Optional[Dict[str, str]]
_terminal_code_to_codename = None # type: Optional[Dict[str, str]]
def __init__(
self,
menu_entries: Iterable[str],
title: Optional[Union[str, Iterable[str]]] = None,
menu_cursor: Optional[str] = DEFAULT_MENU_CURSOR,
menu_cursor_style: Optional[Iterable[str]] = DEFAULT_MENU_CURSOR_STYLE,
menu_highlight_style: Optional[Iterable[str]] = DEFAULT_MENU_HIGHLIGHT_STYLE,
cycle_cursor: bool = DEFAULT_CYCLE_CURSOR,
clear_screen: bool = DEFAULT_CLEAR_SCREEN,
preview_command: Optional[Union[str, Callable[[str], str]]] = None,
preview_size: float = DEFAULT_PREVIEW_SIZE,
):
def extract_menu_entries_and_preview_arguments(entries: Iterable[str]) -> Tuple[List[str], List[str]]:
separator_pattern = re.compile(r"([^\\])\|")
escaped_separator_pattern = re.compile(r"\\\|")
menu_entry_pattern = re.compile(r"^([^\x1F]+)(\x1F([^\x1F]*))?")
menu_entries = []
preview_arguments = []
for entry in entries:
unit_separated_entry = escaped_separator_pattern.sub("|", separator_pattern.sub("\\1\x1F", entry))
match_obj = menu_entry_pattern.match(unit_separated_entry)
assert match_obj is not None
display_text = match_obj.group(1)
preview_argument = match_obj.group(3)
menu_entries.append(display_text)
preview_arguments.append(preview_argument)
return menu_entries, preview_arguments
self._fd = sys.stdin.fileno()
self._menu_entries, self._preview_arguments = extract_menu_entries_and_preview_arguments(menu_entries)
if title is None:
self._title_lines = () # type: Tuple[str, ...]
elif isinstance(title, str):
self._title_lines = tuple(title.split("\n"))
else:
self._title_lines = tuple(title)
self._menu_cursor = menu_cursor if menu_cursor is not None else ""
self._menu_cursor_style = tuple(menu_cursor_style) if menu_cursor_style is not None else ()
self._menu_highlight_style = tuple(menu_highlight_style) if menu_highlight_style is not None else ()
self._cycle_cursor = cycle_cursor
self._clear_screen = clear_screen
self._preview_command = preview_command
self._preview_size = preview_size
self._selected_index = None # type: Optional[int]
self._viewport = self.Viewport(len(self._menu_entries), len(self._title_lines), 0)
self._previous_preview_num_lines = None # type: Optional[int]
self._reading_next_key = False
self._paint_before_next_read = False
self._old_term = None # type: Optional[List[Union[int, List[bytes]]]]
self._new_term = None # type: Optional[List[Union[int, List[bytes]]]]
self._check_for_valid_styles()
self._init_terminal_codes()
@classmethod
def _init_terminal_codes(cls) -> None:
if cls._codename_to_terminal_code is not None:
return
supported_colors = int(cls._query_terminfo_database("colors"))
cls._codename_to_terminal_code = {
codename: cls._query_terminfo_database(codename)
if not (codename.startswith("bg_") or codename.startswith("fg_")) or supported_colors >= 8
else ""
for codename in cls._codenames
}
cls._codename_to_terminal_code.update(cls._name_to_control_character)
cls._terminal_code_to_codename = {
terminal_code: codename for codename, terminal_code in cls._codename_to_terminal_code.items()
}
@classmethod
def _query_terminfo_database(cls, codename: str) -> str:
if codename in cls._codename_to_capname:
capname = cls._codename_to_capname[codename]
else:
capname = codename
try:
return str(subprocess.check_output(["tput"] + capname.split(), universal_newlines=True))
except subprocess.CalledProcessError as e:
# The return code 1 indicates a missing terminal capability
if e.returncode == 1:
return ""
raise e
@classmethod
def _num_lines(self) -> int:
return int(self._query_terminfo_database("lines"))
@classmethod
def _num_cols(self) -> int:
return int(self._query_terminfo_database("cols"))
def _check_for_valid_styles(self) -> None:
invalid_styles = []
for style_tuple in (self._menu_cursor_style, self._menu_highlight_style):
for style in style_tuple:
if style not in self._codename_to_capname:
invalid_styles.append(style)
if invalid_styles:
if len(invalid_styles) == 1:
raise InvalidStyleError('The style "{}" does not exist.'.format(invalid_styles[0]))
else:
raise InvalidStyleError('The styles ("{}") do not exist.'.format('", "'.join(invalid_styles)))
def _init_term(self) -> None:
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
self._old_term = termios.tcgetattr(self._fd)
self._new_term = termios.tcgetattr(self._fd)
self._new_term[3] = cast(int, self._new_term[3]) & ~termios.ICANON & ~termios.ECHO # unbuffered and no echo
termios.tcsetattr(self._fd, termios.TCSAFLUSH, self._new_term)
# Enter terminal application mode to get expected escape codes for arrow keys
sys.stdout.write(self._codename_to_terminal_code["enter_application_mode"])
sys.stdout.write(self._codename_to_terminal_code["cursor_invisible"])
if self._clear_screen:
sys.stdout.write(self._codename_to_terminal_code["clear"])
def _reset_term(self) -> None:
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
assert self._old_term is not None
termios.tcsetattr(self._fd, termios.TCSAFLUSH, self._old_term)
sys.stdout.write(self._codename_to_terminal_code["cursor_visible"])
sys.stdout.write(self._codename_to_terminal_code["exit_application_mode"])
if self._clear_screen:
sys.stdout.write(self._codename_to_terminal_code["clear"])
def _paint_menu(self) -> None:
def print_menu_entries() -> None:
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
num_cols = self._num_cols()
if self._title_lines:
sys.stdout.write(
len(self._title_lines) * self._codename_to_terminal_code["cursor_up"]
+ "\r"
+ "\n".join(
(title_line[:num_cols] + (num_cols - len(title_line)) * " ") for title_line in self._title_lines
)
+ "\n"
)
for i, menu_entry in enumerate(
self._menu_entries[self._viewport.lower_index :], self._viewport.lower_index
):
sys.stdout.write(len(self._menu_cursor) * " ")
if i == self._selected_index:
for style in self._menu_highlight_style:
sys.stdout.write(self._codename_to_terminal_code[style])
sys.stdout.write(menu_entry[: num_cols - len(self._menu_cursor)])
if i == self._selected_index:
sys.stdout.write(self._codename_to_terminal_code["reset_attributes"])
sys.stdout.write((num_cols - len(menu_entry) - len(self._menu_cursor)) * " ")
if i >= self._viewport.upper_index:
break
if i < len(self._menu_entries) - 1:
sys.stdout.write("\n")
sys.stdout.write("\r" + (self._viewport.size - 1) * self._codename_to_terminal_code["cursor_up"])
def print_preview(preview_max_num_lines: int) -> None:
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
if self._preview_command is None or preview_max_num_lines < 3:
return
def get_preview_string() -> Optional[str]:
assert self._preview_command is not None
assert self._selected_index is not None
preview_argument = (
self._preview_arguments[self._selected_index]
if self._preview_arguments[self._selected_index] is not None
else self._menu_entries[self._selected_index]
)
if preview_argument == "":
return None
if isinstance(self._preview_command, str):
try:
preview_string = subprocess.check_output(
[cmd_part.format(preview_argument) for cmd_part in shlex.split(self._preview_command)],
stderr=subprocess.PIPE,
universal_newlines=True,
).strip()
except subprocess.CalledProcessError as e:
raise PreviewCommandFailedError(e.stderr.strip())
else:
preview_string = self._preview_command(preview_argument)
return preview_string
@static_variables(
# Regex taken from https://stackoverflow.com/a/14693789/5958465
ansi_escape_regex=re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"),
# Modified version of https://stackoverflow.com/a/2188410/5958465
ansi_sgr_regex=re.compile(r"\x1B\[[;\d]*m"),
)
def strip_ansi_codes_except_styling(string: str) -> str:
stripped_string = strip_ansi_codes_except_styling.ansi_escape_regex.sub( # type: ignore
lambda match_obj: match_obj.group(0)
if strip_ansi_codes_except_styling.ansi_sgr_regex.match(match_obj.group(0)) # type: ignore
else "",
string,
)
return cast(str, stripped_string)
@static_variables(
regular_text_regex=re.compile(r"([^\x1B]+)(.*)"),
ansi_escape_regex=re.compile(r"(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))(.*)"),
)
def limit_string_with_escape_codes(string: str, max_len: int) -> Tuple[str, int]:
if max_len <= 0:
return "", 0
string_parts = []
string_len = 0
while string:
regular_text_match = limit_string_with_escape_codes.regular_text_regex.match(string) # type: ignore
if regular_text_match is not None:
regular_text = regular_text_match.group(1)
regular_text_len = len(regular_text)
if string_len + regular_text_len > max_len:
string_parts.append(regular_text[: max_len - string_len])
string_len = max_len
break
string_parts.append(regular_text)
string_len += regular_text_len
string = regular_text_match.group(2)
else:
ansi_escape_match = limit_string_with_escape_codes.ansi_escape_regex.match( # type: ignore
string
)
if ansi_escape_match is not None:
# Adopt the ansi escape code but do not count its length
ansi_escape_code_text = ansi_escape_match.group(1)
string_parts.append(ansi_escape_code_text)
string = ansi_escape_match.group(2)
else:
# It looks like an escape code (starts with escape), but it is something else
# -> skip the escape character and continue the loop
string_parts.append("\x1B")
string = string[1:]
return "".join(string_parts), string_len
num_cols = self._num_cols()
try:
preview_string = get_preview_string()
if preview_string is not None:
preview_string = strip_ansi_codes_except_styling(preview_string)
except PreviewCommandFailedError as e:
preview_string = "The preview command failed with error message:\n\n" + str(e)
sys.stdout.write((self._viewport.size - 1) * self._codename_to_terminal_code["cursor_down"])
if preview_string is not None:
sys.stdout.write(
self._codename_to_terminal_code["cursor_down"]
+ "\r"
+ (
BoxDrawingCharacters.upper_left
+ (2 * BoxDrawingCharacters.horizontal + " preview")[: num_cols - 3]
+ " "
+ (num_cols - 13) * BoxDrawingCharacters.horizontal
+ BoxDrawingCharacters.upper_right
)[:num_cols]
+ "\n"
)
# `finditer` can be used as a generator version of `str.join`
for i, line in enumerate(
match.group(0) for match in re.finditer(r"^.*$", preview_string, re.MULTILINE)
):
if i >= preview_max_num_lines - 2:
preview_num_lines = preview_max_num_lines
break
limited_line, limited_line_len = limit_string_with_escape_codes(line, num_cols - 3)
sys.stdout.write(
(
BoxDrawingCharacters.vertical
+ (
" "
+ limited_line
+ self._codename_to_terminal_code["reset_attributes"]
+ max(num_cols - limited_line_len - 3, 0) * " "
)
+ BoxDrawingCharacters.vertical
)
+ "\n"
)
else:
preview_num_lines = i + 3
sys.stdout.write(
(
BoxDrawingCharacters.lower_left
+ (num_cols - 2) * BoxDrawingCharacters.horizontal
+ BoxDrawingCharacters.lower_right
)[:num_cols]
+ "\r"
)
else:
preview_num_lines = 0
if self._previous_preview_num_lines is not None and self._previous_preview_num_lines > preview_num_lines:
sys.stdout.write(self._codename_to_terminal_code["cursor_down"])
sys.stdout.write(
(self._previous_preview_num_lines - preview_num_lines)
* self._codename_to_terminal_code["delete_line"]
)
sys.stdout.write(self._codename_to_terminal_code["cursor_up"])
sys.stdout.write(
(self._viewport.size + preview_num_lines - 1) * self._codename_to_terminal_code["cursor_up"]
)
self._previous_preview_num_lines = preview_num_lines
def position_cursor() -> None:
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
assert self._selected_index is not None
# delete the first column
sys.stdout.write(
(self._viewport.size - 1)
* (len(self._menu_cursor) * " " + "\r" + self._codename_to_terminal_code["cursor_down"])
+ len(self._menu_cursor) * " "
+ "\r"
)
sys.stdout.write((self._viewport.size - 1) * self._codename_to_terminal_code["cursor_up"])
# position cursor and print menu selection character
sys.stdout.write(
(self._selected_index - self._viewport.lower_index) * self._codename_to_terminal_code["cursor_down"]
)
for style in self._menu_cursor_style:
sys.stdout.write(self._codename_to_terminal_code[style])
sys.stdout.write(self._menu_cursor)
sys.stdout.write(self._codename_to_terminal_code["reset_attributes"] + "\r")
sys.stdout.write(
(self._selected_index - self._viewport.lower_index) * self._codename_to_terminal_code["cursor_up"]
)
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
if self._selected_index is None:
self._selected_index = 0
if self._preview_command is not None:
self._viewport.preview_lines_count = int(self._preview_size * self._num_lines())
preview_max_num_lines = self._viewport.preview_lines_count
self._viewport.keep_visible(self._selected_index)
print_menu_entries()
if self._preview_command is not None:
print_preview(preview_max_num_lines)
position_cursor()
def _clear_menu(self) -> None:
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
if self._title_lines:
sys.stdout.write(len(self._title_lines) * self._codename_to_terminal_code["cursor_up"])
sys.stdout.write(len(self._title_lines) * self._codename_to_terminal_code["delete_line"])
preview_num_lines = self._previous_preview_num_lines if self._previous_preview_num_lines is not None else 0
sys.stdout.write((self._viewport.size + preview_num_lines) * self._codename_to_terminal_code["delete_line"])
sys.stdout.flush()
def _read_next_key(self, ignore_case: bool = True) -> str:
# pylint: disable=unsubscriptable-object,unsupported-membership-test
assert self._terminal_code_to_codename is not None
# Needed for asynchronous handling of terminal resize events
self._reading_next_key = True
if self._paint_before_next_read:
self._paint_menu()
self._paint_before_next_read = False
code = os.read(self._fd, 80).decode("ascii") # blocks until any amount of bytes is available
self._reading_next_key = False
if code in self._terminal_code_to_codename:
return self._terminal_code_to_codename[code]
elif ignore_case:
return code.lower()
else:
return code
def show(self) -> Optional[int]:
def init_signal_handling() -> None:
# `SIGWINCH` is send on terminal resizes
def handle_sigwinch(signum: signal.Signals, frame: FrameType) -> None:
# pylint: disable=unused-argument
if self._reading_next_key:
self._paint_menu()
else:
self._paint_before_next_read = True
signal.signal(signal.SIGWINCH, handle_sigwinch)
def reset_signal_handling() -> None:
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
# pylint: disable=unsubscriptable-object
assert self._codename_to_terminal_code is not None
self._init_term()
self._selected_index = 0
if self._title_lines:
# `print_menu` expects the cursor on the first menu item -> reserve one line for the title
sys.stdout.write(len(self._title_lines) * self._codename_to_terminal_code["cursor_down"])
try:
init_signal_handling()
while True:
self._paint_menu()
next_key = self._read_next_key(ignore_case=True)
if next_key in ("up", "k"):
self._selected_index -= 1
if self._selected_index < 0:
if self._cycle_cursor:
self._selected_index = len(self._menu_entries) - 1
else:
self._selected_index = 0
elif next_key in ("down", "j"):
self._selected_index += 1
if self._selected_index >= len(self._menu_entries):
if self._cycle_cursor:
self._selected_index = 0
else:
self._selected_index = len(self._menu_entries) - 1
elif next_key in ("enter",):
break
elif next_key in ("escape", "q"):
self._selected_index = None
break
except KeyboardInterrupt:
self._selected_index = None
finally:
reset_signal_handling()
self._clear_menu()
self._reset_term()
return self._selected_index
class AttributeDict(dict): # type: ignore
def __getattr__(self, attr: str) -> Any:
return self[attr]
def __setattr__(self, attr: str, value: Any) -> None:
self[attr] = value
def get_argumentparser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""
%(prog)s creates simple interactive menus in the terminal and returns the selected entry as exit code.
""",
)
parser.add_argument("-t", "--title", action="store", dest="title", help="menu title")
parser.add_argument(
"-c",
"--cursor",
action="store",
dest="cursor",
default=DEFAULT_MENU_CURSOR,
help="menu cursor (default: %(default)s)",
)
parser.add_argument(
"-s",
"--cursor_style",
action="store",
dest="cursor_style",
default=",".join(DEFAULT_MENU_CURSOR_STYLE),
help="style for the menu cursor as comma separated list (default: %(default)s)",
)
parser.add_argument(
"-m",
"--highlight_style",
action="store",
dest="highlight_style",
default=",".join(DEFAULT_MENU_HIGHLIGHT_STYLE),
help="style for the selected menu entry as comma separated list (default: %(default)s)",
)
parser.add_argument("-C", "--no-cycle", action="store_false", dest="cycle", help="do not cycle the menu selection")
parser.add_argument(
"-l",
"--clear-screen",
action="store_true",
dest="clear_screen",
help="clear the screen before the menu is shown",
)
parser.add_argument(
"-p",
"--preview",
action="store",
dest="preview_command",
help=(
"Command to generate a preview for the selected menu entry. "
'"{}" can be used as placeholder for the menu text. '
'If the menu entry has a data component (separated by "|"), this is used instead.'
),
)
parser.add_argument(
"--preview-size",
action="store",
dest="preview_size",
type=float,
default=DEFAULT_PREVIEW_SIZE,
help="maximum height of the preview window in fractions of the terminal height (default: %(default)s)",
)
parser.add_argument(
"-V", "--version", action="store_true", dest="print_version", help="print the version number and exit"
)
parser.add_argument("entries", action="store", nargs="*", help="the menu entries to show")
return parser
def parse_arguments() -> AttributeDict:
parser = get_argumentparser()
args = AttributeDict({key: value for key, value in vars(parser.parse_args()).items()})
if not args.print_version and not args.entries:
raise NoMenuEntriesError("No menu entries given!")
if args.cursor_style != "":
args.cursor_style = tuple(args.cursor_style.split(","))
else:
args.cursor_style = None
if args.highlight_style != "":
args.highlight_style = tuple(args.highlight_style.split(","))
else:
args.highlight_style = None
return args
def main() -> None:
try:
args = parse_arguments()
except SystemExit:
sys.exit(0) # Error code 0 is the error case in this program
except NoMenuEntriesError as e:
print(str(e), file=sys.stderr)
sys.exit(0)
if args.print_version:
print("{}, version {}".format(os.path.basename(sys.argv[0]), __version__))
sys.exit(0)
try:
terminal_menu = TerminalMenu(
menu_entries=args.entries,
title=args.title,
menu_cursor=args.cursor,
menu_cursor_style=args.cursor_style,
menu_highlight_style=args.highlight_style,
cycle_cursor=args.cycle,
clear_screen=args.clear_screen,
preview_command=args.preview_command,
preview_size=args.preview_size,
)
except InvalidStyleError as e:
print(str(e), file=sys.stderr)
sys.exit(0)
chosen_entry = terminal_menu.show()
if chosen_entry is None:
sys.exit(0)
else:
sys.exit(chosen_entry + 1)
if __name__ == "__main__":
main()