v2.0
This commit is contained in:
10
README.md
10
README.md
@@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
命令行交互辅助工具
|
命令行交互辅助工具
|
||||||
|
|
||||||
v1.3
|
**v2.0**
|
||||||
|
|
||||||
|
1. 代码重构,使用方式变更
|
||||||
|
|
||||||
|
**v1.3**
|
||||||
|
|
||||||
1. 菜单的空白字符可以自定义了
|
1. 菜单的空白字符可以自定义了
|
||||||
|
|
||||||
v1.2
|
**v1.2**
|
||||||
|
|
||||||
1. 增加显示版本号
|
1. 增加显示版本号
|
||||||
2. 禁用获取菜单选项时的退出值
|
2. 禁用获取菜单选项时的退出值
|
||||||
|
|
||||||
v1.1
|
**v1.1**
|
||||||
|
|
||||||
1. 增加注释
|
1. 增加注释
|
||||||
2. 其他改动
|
2. 其他改动
|
||||||
|
|||||||
206
clihelper.py
206
clihelper.py
@@ -12,9 +12,9 @@
|
|||||||
#######################################################
|
#######################################################
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Callable
|
from typing import Callable, TypedDict
|
||||||
|
|
||||||
version = (1, 3, 20230705)
|
version = (2, 0, 20231219)
|
||||||
|
|
||||||
|
|
||||||
def request_input(prompt_msg: str,
|
def request_input(prompt_msg: str,
|
||||||
@@ -24,7 +24,7 @@ def request_input(prompt_msg: str,
|
|||||||
default_val: str = "",
|
default_val: str = "",
|
||||||
has_quit_val: bool = True,
|
has_quit_val: bool = True,
|
||||||
quit_val: str = "q",
|
quit_val: str = "q",
|
||||||
check_func: Callable[[str], bool] | None = None,
|
check_func: Callable[[str], bool] = None,
|
||||||
ask_again: bool = True,
|
ask_again: bool = True,
|
||||||
need_confirm: bool = False) -> tuple[str, bool]:
|
need_confirm: bool = False) -> tuple[str, bool]:
|
||||||
"""
|
"""
|
||||||
@@ -79,157 +79,158 @@ def request_input(prompt_msg: str,
|
|||||||
return input_val, True
|
return input_val, True
|
||||||
|
|
||||||
|
|
||||||
class CliHelper(object):
|
class MenuConfig(TypedDict, total=False):
|
||||||
"""命令行辅助工具"""
|
left_padding: int
|
||||||
|
right_padding: int
|
||||||
|
top_padding: int
|
||||||
|
bottom_padding: int
|
||||||
|
edge_char: str
|
||||||
|
wall_char: str
|
||||||
|
serial_marker: str
|
||||||
|
draw_menu_again: bool
|
||||||
|
|
||||||
class _OptionNode(object):
|
|
||||||
|
DEFAULT_MENU_CONFIG: MenuConfig = {
|
||||||
|
"left_padding": 2,
|
||||||
|
"right_padding": 10,
|
||||||
|
"top_padding": 1,
|
||||||
|
"bottom_padding": 1,
|
||||||
|
"edge_char": "#",
|
||||||
|
"wall_char": " ",
|
||||||
|
"serial_marker": ". ",
|
||||||
|
"draw_menu_again": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _OptionNode(object):
|
||||||
"""选项节点"""
|
"""选项节点"""
|
||||||
|
|
||||||
|
# 返回上级菜单的执行函数的返回值
|
||||||
|
_RETURN_CODE = 0xbabe
|
||||||
|
# 进入下级菜单的执行函数的返回值
|
||||||
|
_ENTER_CODE = 0xbeef
|
||||||
|
# 退出整个工具的返回值
|
||||||
|
_QUIT_CODE = 0xcafe
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
title: str = "Untitled Option",
|
title: str = "Untitled Option",
|
||||||
exec_func: Callable = None,
|
exec_func: Callable = None,
|
||||||
args: tuple | None = None,
|
args: tuple = None,
|
||||||
kwargs: dict | None = None):
|
kwargs: dict = None,
|
||||||
|
menu_config: MenuConfig = None,
|
||||||
|
_is_root: bool = False):
|
||||||
"""
|
"""
|
||||||
:param title: 选项标题
|
:param title: 选项标题
|
||||||
:param exec_func: 选择该选项后要执行的函数;对于用户设定的节点,该执行函数必须返回 None
|
:param exec_func: 选择该选项后要执行的函数;对于用户设定的节点,该执行函数必须返回 None
|
||||||
:param args: 执行函数的位置参数
|
:param args: 执行函数的位置参数
|
||||||
:param kwargs: 执行函数的命名参数
|
:param kwargs: 执行函数的命名参数
|
||||||
|
:param menu_config: 绘制子选项菜单的参数;只在根选项设置,子选项继承
|
||||||
|
:param _is_root: 标记当前节点是否是根节点;内部使用
|
||||||
"""
|
"""
|
||||||
self.title = title
|
self.title = title
|
||||||
self.exec_func = exec_func if exec_func is not None else self._default_func
|
self.exec_func = exec_func if exec_func is not None else self._default_func
|
||||||
self.args = args if args is not None else ()
|
self.args = args if args is not None else ()
|
||||||
self.kwargs = kwargs if kwargs is not None else {}
|
self.kwargs = kwargs if kwargs is not None else {}
|
||||||
self.children = [] # type: list[CliHelper._OptionNode]
|
self.menu_config = menu_config if menu_config is not None else DEFAULT_MENU_CONFIG
|
||||||
|
self.children = [] # type: list[_OptionNode]
|
||||||
|
self._is_root = _is_root
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _default_func():
|
def _default_func():
|
||||||
"""用户没有指定执行函数时执行的默认函数"""
|
"""用户没有指定执行函数时执行的默认函数"""
|
||||||
print("No function")
|
print("No function")
|
||||||
|
|
||||||
# 返回上级菜单的执行函数的返回值
|
def _get_max_length_of_option_titles(self) -> int:
|
||||||
_RETURN_CODE = 0xbabe
|
|
||||||
# 进入下级菜单的执行函数的返回值
|
|
||||||
_ENTER_CODE = 0xbeef
|
|
||||||
|
|
||||||
def __init__(self, *,
|
|
||||||
left_padding: int = 2,
|
|
||||||
right_padding: int = 10,
|
|
||||||
top_padding: int = 1,
|
|
||||||
bottom_padding: int = 1,
|
|
||||||
border_char: str = "#",
|
|
||||||
wall_char: str = " ",
|
|
||||||
serial_marker: str = ". ",
|
|
||||||
draw_menu_again: bool = False,
|
|
||||||
show_version: bool = True):
|
|
||||||
|
|
||||||
self._root = self._OptionNode("Main Menu")
|
|
||||||
|
|
||||||
self._left_padding = left_padding
|
|
||||||
self._right_padding = right_padding
|
|
||||||
self._top_padding = top_padding
|
|
||||||
self._bottom_padding = bottom_padding
|
|
||||||
self._border_char = border_char
|
|
||||||
self._wall_char = wall_char
|
|
||||||
self._serial_marker = serial_marker
|
|
||||||
self._draw_menu_again = draw_menu_again
|
|
||||||
|
|
||||||
if show_version:
|
|
||||||
print(f"CliHelper v{version[0]}.{version[1]} ({version[-1]})\n")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_max_length_of_option_titles(p_option: _OptionNode) -> int:
|
|
||||||
"""
|
"""
|
||||||
获取菜单选项中最长的选项标题的长度
|
获取菜单选项中最长的选项标题的长度
|
||||||
|
|
||||||
:param p_option: 菜单选项的父选项节点
|
|
||||||
:return: 菜单选项中最长的选项标题的长度
|
:return: 菜单选项中最长的选项标题的长度
|
||||||
"""
|
"""
|
||||||
return max([len(c.title) for c in p_option.children])
|
return max([len(c.title) for c in self.children])
|
||||||
|
|
||||||
def _draw_menu(self, p_option: _OptionNode):
|
def _draw_menu(self):
|
||||||
"""
|
"""
|
||||||
绘制菜单选项
|
绘制菜单选项
|
||||||
|
|
||||||
:param p_option: 要绘制的菜单选项的父选项节点
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
left = self._left_padding
|
left = self.menu_config["left_padding"]
|
||||||
right = self._right_padding
|
right = self.menu_config["right_padding"]
|
||||||
top = self._top_padding
|
top = self.menu_config["top_padding"]
|
||||||
bottom = self._bottom_padding
|
bottom = self.menu_config["bottom_padding"]
|
||||||
bc = self._border_char
|
ec = self.menu_config["edge_char"]
|
||||||
wc = self._wall_char
|
wc = self.menu_config["wall_char"]
|
||||||
sm = self._serial_marker
|
sm = self.menu_config["serial_marker"]
|
||||||
|
|
||||||
max_len_option = self._get_max_length_of_option_titles(p_option)
|
max_len_option = self._get_max_length_of_option_titles()
|
||||||
# 获取最大选项序号的位数
|
# 获取最大选项序号的位数
|
||||||
max_len_serial = len(str(len(p_option.children)))
|
max_len_serial = len(str(len(self.children)))
|
||||||
# 一个选项的总宽度
|
# 一个选项的总宽度
|
||||||
option_width = max_len_serial + len(sm) + max_len_option
|
option_width = max_len_serial + len(sm) + max_len_option
|
||||||
# 菜单的宽度(加上左右两边的空白,但不包括边界)
|
# 菜单的宽度(加上左右两边的空白,但不包括边界)
|
||||||
menu_width = left + option_width + right
|
menu_width = left + option_width + right
|
||||||
# 上下边界
|
# 上下边界字符
|
||||||
top_bottom_border = bc * (menu_width + len(bc) * 2)
|
top_bottom_edge = ec * (menu_width + len(ec) * 2)
|
||||||
# 墙是包括边界和空白
|
# 墙是包括边界和空白(单行)
|
||||||
top_bottom_wall = f"{bc}{wc * menu_width}{bc}"
|
top_bottom_wall = f"{ec}{wc * menu_width}{ec}"
|
||||||
left_wall = f"{bc}{wc * left}"
|
left_wall = f"{ec}{wc * left}"
|
||||||
right_wall = f"{wc * right}{bc}"
|
right_wall = f"{wc * right}{ec}"
|
||||||
|
|
||||||
print("\n".join([top_bottom_border] + [top_bottom_wall] * top))
|
print("\n".join([top_bottom_edge] + [top_bottom_wall] * top))
|
||||||
for i, opt in enumerate(p_option.children):
|
for i, opt in enumerate(self.children):
|
||||||
# 序号右对齐,选项标题左对齐
|
# 序号右对齐,选项标题左对齐
|
||||||
print(f"{left_wall}{i + 1:>{max_len_serial}}{sm}{opt.title:<{max_len_option}}{right_wall}")
|
print(f"{left_wall}{i + 1:>{max_len_serial}}{sm}{opt.title:<{max_len_option}}{right_wall}")
|
||||||
print("\n".join([top_bottom_wall] * bottom + [top_bottom_border]))
|
print("\n".join([top_bottom_wall] * bottom + [top_bottom_edge]))
|
||||||
|
|
||||||
@staticmethod
|
def _request_option(self) -> _OptionNode | None:
|
||||||
def _request_option(p_option: _OptionNode) -> _OptionNode | None:
|
|
||||||
"""
|
"""
|
||||||
请求用户输入选项序号
|
请求用户输入选项序号
|
||||||
|
|
||||||
:param p_option: 当前菜单选项的父选项节点
|
|
||||||
:return: 用户选择的选项节点或 None
|
:return: 用户选择的选项节点或 None
|
||||||
"""
|
"""
|
||||||
choice, state = request_input(
|
choice, state = request_input(
|
||||||
"\nYour option", "No such option.",
|
"\nYour option", "No such option.",
|
||||||
has_default_val=False, has_quit_val=False,
|
has_default_val=False, has_quit_val=False,
|
||||||
check_func=lambda x: x.isdigit() and 1 <= int(x) <= len(p_option.children),
|
check_func=lambda x: x.isdigit() and 1 <= int(x) <= len(self.children),
|
||||||
ask_again=False, need_confirm=False
|
ask_again=False, need_confirm=False
|
||||||
)
|
)
|
||||||
if state is False:
|
if state is False:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return p_option.children[int(choice) - 1]
|
return self.children[int(choice) - 1]
|
||||||
|
|
||||||
def _enter_next_level(self, p_option: _OptionNode) -> int:
|
def _enter_next_level(self) -> int:
|
||||||
"""
|
"""
|
||||||
进入下一级菜单(执行函数)
|
进入下一级菜单(执行函数)
|
||||||
|
|
||||||
:param p_option: 下一级菜单的入口选项节点
|
|
||||||
:return: 状态码
|
:return: 状态码
|
||||||
"""
|
"""
|
||||||
state = self._ENTER_CODE
|
state = self._ENTER_CODE
|
||||||
|
|
||||||
while state != self._RETURN_CODE:
|
while state not in (self._RETURN_CODE, self._QUIT_CODE):
|
||||||
# 如果上次执行不是返回上级菜单,就继续在当前菜单循环
|
# 如果上次执行不是返回上级菜单或退出,就继续在当前菜单循环
|
||||||
# 用户指定是否需要重新绘制菜单,
|
# 用户指定是否需要重新绘制菜单,
|
||||||
# 或者如果上次执行是进入下级菜单,则一定要绘制菜单
|
# 或者如果上次执行是进入下级菜单,则一定要绘制菜单
|
||||||
# 这里 state == self._ENTER_CODE 有两种情况,
|
# 这里 state == self._ENTER_CODE 有两种情况,
|
||||||
# 一种是第一次进入 while 循环的时候,此时表示用户选择进入下级菜单
|
# 一种是第一次进入 while 循环的时候,此时表示用户选择进入下级菜单
|
||||||
# 另一种是用户选择返回上级菜单,结束了 while 循环,
|
# 另一种是用户选择返回上级菜单,结束了 while 循环,
|
||||||
# 此函数返回 self._ENTER_CODE,被上层的该函数的 while 循环捕获到
|
# 此函数返回 self._ENTER_CODE,被上层的该函数的 while 循环捕获到
|
||||||
if self._draw_menu_again or state == self._ENTER_CODE:
|
if self.menu_config["draw_menu_again"] or state == self._ENTER_CODE:
|
||||||
self._draw_menu(p_option)
|
self._draw_menu()
|
||||||
|
|
||||||
# 此处设置为 None,是为了防止用户输入不符合要求的序号时,
|
# 此处设置为 None,是为了防止用户输入不符合要求的序号时,
|
||||||
# 重新循环 state 还是 self._ENTER_CODE 导致又绘制了一遍菜单
|
# 重新循环 state 还是 self._ENTER_CODE 导致又绘制了一遍菜单
|
||||||
state = None
|
state = None
|
||||||
opt = self._request_option(p_option)
|
opt = self._request_option()
|
||||||
if opt is None:
|
if opt is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
state = opt.exec_func(*opt.args, **opt.kwargs)
|
state = opt.exec_func(*opt.args, **opt.kwargs)
|
||||||
|
|
||||||
|
# 如果是退出信号,就一路退回到根节点
|
||||||
|
if state == self._QUIT_CODE:
|
||||||
|
return self._QUIT_CODE
|
||||||
|
else:
|
||||||
return self._ENTER_CODE
|
return self._ENTER_CODE
|
||||||
|
|
||||||
def _return_previous_level(self) -> int:
|
def _return_previous_level(self) -> int:
|
||||||
@@ -240,8 +241,15 @@ class CliHelper(object):
|
|||||||
"""
|
"""
|
||||||
return self._RETURN_CODE
|
return self._RETURN_CODE
|
||||||
|
|
||||||
|
def _quiting_level(self) -> int:
|
||||||
|
"""
|
||||||
|
退出该工具(执行函数)
|
||||||
|
|
||||||
|
:return: 状态码
|
||||||
|
"""
|
||||||
|
return self._QUIT_CODE
|
||||||
|
|
||||||
def add_option(self,
|
def add_option(self,
|
||||||
parent: _OptionNode | None = None,
|
|
||||||
title: str = "Untitled Option",
|
title: str = "Untitled Option",
|
||||||
exec_func: Callable = None,
|
exec_func: Callable = None,
|
||||||
args: tuple = None,
|
args: tuple = None,
|
||||||
@@ -249,50 +257,54 @@ class CliHelper(object):
|
|||||||
"""
|
"""
|
||||||
添加选项
|
添加选项
|
||||||
|
|
||||||
:param parent: 要添加的选项的父选项节点,若为 None 则为根节点(显示在主菜单)
|
|
||||||
:param title: 选项标题
|
:param title: 选项标题
|
||||||
:param exec_func: 选择该选项后要执行的函数;对于用户设定的节点,该执行函数必须返回 None
|
:param exec_func: 选择该选项后要执行的函数;对于用户设定的节点,该执行函数必须返回 None
|
||||||
:param args: 执行函数的位置参数
|
:param args: 执行函数的位置参数
|
||||||
:param kwargs: 执行函数的命名参数
|
:param kwargs: 执行函数的命名参数
|
||||||
:return: 选项节点对象
|
:return: 选项节点对象
|
||||||
"""
|
"""
|
||||||
if parent is None:
|
|
||||||
parent = self._root
|
|
||||||
# 如果在此父选项下创建了新的选项,该父选项会成为下一级的菜单入口
|
# 如果在此父选项下创建了新的选项,该父选项会成为下一级的菜单入口
|
||||||
# 此处进行后缀标记
|
# 此处进行后缀标记
|
||||||
if not parent.title.endswith(" [d]"):
|
if not self.title.endswith(" [d]"):
|
||||||
parent.title = f"{parent.title} [d]"
|
self.title = f"{self.title} [d]"
|
||||||
|
|
||||||
option = self._OptionNode(title, exec_func, args, kwargs)
|
option = _OptionNode(title, exec_func, args, kwargs, self.menu_config, _is_root=False)
|
||||||
parent.children.append(option)
|
self.children.append(option)
|
||||||
|
|
||||||
# 父选项成为菜单入口之后,其执行函数只能为“进入下一级菜单”
|
# 父选项成为菜单入口之后,其执行函数只能为“进入下一级菜单”
|
||||||
parent.exec_func = self._enter_next_level
|
self.exec_func = self._enter_next_level
|
||||||
# “进入下一级菜单”这个执行函数的参数就是其选项本身
|
|
||||||
parent.args = (parent, )
|
|
||||||
|
|
||||||
return option
|
return option
|
||||||
|
|
||||||
def add_return_option(self, p_option: _OptionNode):
|
def add_return_option(self):
|
||||||
"""
|
"""
|
||||||
添加返回上级菜单的选项
|
添加返回上级菜单的选项
|
||||||
|
|
||||||
:param p_option: 要添加该选项的父选项节点
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.add_option(p_option, title="[Back]", exec_func=self._return_previous_level)
|
self.add_option(title="[Back]", exec_func=self._return_previous_level)
|
||||||
|
|
||||||
def add_exit_option(self, p_option: _OptionNode | None = None):
|
def add_exit_option(self):
|
||||||
"""
|
"""
|
||||||
添加退出菜单选项
|
添加退出菜单选项
|
||||||
|
|
||||||
:param p_option: 要添加该选项的父选项节点
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if p_option is None:
|
self.add_option(title="[Exit]", exec_func=self._quiting_level)
|
||||||
p_option = self._root
|
|
||||||
self.add_option(p_option, title="[Exit]", exec_func=exit, args=(0,))
|
|
||||||
|
|
||||||
def start_loop(self):
|
def start_loop(self):
|
||||||
"""开启主循环"""
|
"""开启循环,只在根选项调用"""
|
||||||
self._enter_next_level(self._root)
|
if self._is_root:
|
||||||
|
self._enter_next_level()
|
||||||
|
print("Exit CliHelper. Bye.")
|
||||||
|
else:
|
||||||
|
print("Current option is not the root option. Exit.")
|
||||||
|
|
||||||
|
|
||||||
|
class CliHelper(_OptionNode):
|
||||||
|
"""命令行辅助工具"""
|
||||||
|
|
||||||
|
def __init__(self, menu_config: MenuConfig = None, show_version: bool = True):
|
||||||
|
super().__init__("Main Menu", menu_config=menu_config, _is_root=True)
|
||||||
|
|
||||||
|
if show_version:
|
||||||
|
print(f"CliHelper v{version[0]}.{version[1]} ({version[-1]})\n")
|
||||||
|
|||||||
33
test.py
33
test.py
@@ -11,18 +11,31 @@ def new_folder(name):
|
|||||||
|
|
||||||
|
|
||||||
def test_helper():
|
def test_helper():
|
||||||
c = CliHelper(right_padding=20, draw_menu_again=False)
|
# c = CliHelper(right_padding=20, draw_menu_again=False)
|
||||||
n = c.add_option(title="New")
|
# n = c.add_option(title="New")
|
||||||
c.add_option(title="Open File", exec_func=open_file)
|
# c.add_option(title="Open File", exec_func=open_file)
|
||||||
c.add_option(title="Save")
|
# c.add_option(title="Save")
|
||||||
c.add_exit_option()
|
# c.add_exit_option()
|
||||||
|
#
|
||||||
|
# c.add_return_option(n)
|
||||||
|
# c.add_option(n, "New File")
|
||||||
|
# c.add_option(n, "New Folder", new_folder, ("Temp",))
|
||||||
|
# c.add_exit_option(n)
|
||||||
|
#
|
||||||
|
# c.start_loop()
|
||||||
|
ch = CliHelper()
|
||||||
|
n = ch.add_option("New")
|
||||||
|
n.add_return_option()
|
||||||
|
n.add_option("New File")
|
||||||
|
n.add_option("New Folder", new_folder, ("Temp", ))
|
||||||
|
n.add_exit_option()
|
||||||
|
|
||||||
c.add_return_option(n)
|
ch.add_option("Open File", open_file)
|
||||||
c.add_option(n, "New File")
|
ch.add_option("Save")
|
||||||
c.add_option(n, "New Folder", new_folder, ("Temp",))
|
ch.add_exit_option()
|
||||||
c.add_exit_option(n)
|
|
||||||
|
|
||||||
c.start_loop()
|
ch.start_loop()
|
||||||
|
# n.start_loop()
|
||||||
|
|
||||||
|
|
||||||
def test_req():
|
def test_req():
|
||||||
|
|||||||
Reference in New Issue
Block a user