|
|
|
@ -33,9 +33,9 @@ class CliMenuCursor:
|
|
|
|
|
"""Collection of cursors pointing at the active menu item"""
|
|
|
|
|
BULLET = '●'
|
|
|
|
|
TRIANGLE = '▶'
|
|
|
|
|
CLI_STAR = '*'
|
|
|
|
|
CLI_ARROW = '-->'
|
|
|
|
|
CLI_CAT = '=^.^='
|
|
|
|
|
ASCII_STAR = '*'
|
|
|
|
|
ASCII_ARROW = '-->'
|
|
|
|
|
ASCII_CAT = '=^.^='
|
|
|
|
|
CAT = '😸'
|
|
|
|
|
ARROW = '→'
|
|
|
|
|
|
|
|
|
@ -61,6 +61,15 @@ class CliMenuStyle:
|
|
|
|
|
# self.header_indent = 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CliSelectionStyle:
|
|
|
|
|
SQUARE_BRACKETS = ('[x]', '[ ]')
|
|
|
|
|
ROUND_BRACKETS = ('(x)', '( )')
|
|
|
|
|
CHECKMARK = ('✔', '✖')
|
|
|
|
|
THUMBS = ('👍', '👎')
|
|
|
|
|
SMILEY = ('🙂', '🙁')
|
|
|
|
|
SMILEY_EXTREME = ('😁', '😨')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CliMenuTheme:
|
|
|
|
|
BASIC = CliMenuStyle()
|
|
|
|
|
BASIC_BOLD = CliMenuStyle(header_style='bold', highlight_style='bold')
|
|
|
|
@ -75,6 +84,14 @@ class CliMenu:
|
|
|
|
|
default_style = CliMenuTheme.BASIC
|
|
|
|
|
default_cursor = CliMenuCursor.TRIANGLE
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def set_default_style(cls, style):
|
|
|
|
|
cls.default_style = style
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def set_default_cursor(cls, cursor):
|
|
|
|
|
cls.default_cursor = cursor
|
|
|
|
|
|
|
|
|
|
def __init__(self, options=None, header=None, cursor=None, style=None,
|
|
|
|
|
indent=2, dedent_selection=False):
|
|
|
|
|
self._items = []
|
|
|
|
@ -86,25 +103,28 @@ class CliMenu:
|
|
|
|
|
self._header_indent = indent
|
|
|
|
|
self._dedent_selection = dedent_selection
|
|
|
|
|
|
|
|
|
|
self._cursor = cursor
|
|
|
|
|
if not self._cursor:
|
|
|
|
|
self._cursor = self.default_cursor
|
|
|
|
|
|
|
|
|
|
self._style = style
|
|
|
|
|
if not self._style:
|
|
|
|
|
self._style = self.default_style
|
|
|
|
|
self._cursor = cursor or self.default_cursor
|
|
|
|
|
self._style = style or self.default_style
|
|
|
|
|
|
|
|
|
|
if header:
|
|
|
|
|
self.add_header(header, indent=False)
|
|
|
|
|
self.add_text(header, indent=False)
|
|
|
|
|
|
|
|
|
|
if options:
|
|
|
|
|
for option in options:
|
|
|
|
|
if isinstance(option, tuple) and len(option) == 2:
|
|
|
|
|
if isinstance(option, tuple):
|
|
|
|
|
self.add_option(*option)
|
|
|
|
|
else:
|
|
|
|
|
elif isinstance(option, dict):
|
|
|
|
|
self.add_option(**option)
|
|
|
|
|
elif isinstance(option, str):
|
|
|
|
|
self.add_option(option, option)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("Option needs to be either tuple, dict or string, found '{}' of type {}"
|
|
|
|
|
.format(option, type(option)))
|
|
|
|
|
|
|
|
|
|
def add_header(self, title, indent=True):
|
|
|
|
|
def add_header(self, *args, **kwargs):
|
|
|
|
|
return self.add_text(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def add_text(self, title, indent=True):
|
|
|
|
|
for text in title.split('\n'):
|
|
|
|
|
self._items.append(CliMenuHeader(text, indent=indent))
|
|
|
|
|
|
|
|
|
@ -129,7 +149,6 @@ class CliMenu:
|
|
|
|
|
def get_selection(self):
|
|
|
|
|
if self.success:
|
|
|
|
|
item = self._items[self._pos]
|
|
|
|
|
|
|
|
|
|
return (item.num, item.item)
|
|
|
|
|
else:
|
|
|
|
|
return (None, None)
|
|
|
|
@ -148,6 +167,9 @@ class CliMenu:
|
|
|
|
|
# cursor with spaces minus dedent
|
|
|
|
|
return ' ' * (len(self._cursor) + 1 * self._dedent_selection)
|
|
|
|
|
|
|
|
|
|
def _transform_prefix(self, item, lineno, prefix):
|
|
|
|
|
return prefix
|
|
|
|
|
|
|
|
|
|
def _transform_line(self, ti):
|
|
|
|
|
if len(list(ti.fragments)) == 0:
|
|
|
|
|
return Transformation(ti.fragments)
|
|
|
|
@ -173,6 +195,7 @@ class CliMenu:
|
|
|
|
|
style = s.header_style
|
|
|
|
|
|
|
|
|
|
items = [(s if s else style, t) for s, t in ti.fragments]
|
|
|
|
|
prefix = self._transform_prefix(item, ti.lineno, prefix)
|
|
|
|
|
|
|
|
|
|
return Transformation([('', indent), (style, prefix)] + items)
|
|
|
|
|
|
|
|
|
@ -221,6 +244,9 @@ class CliMenu:
|
|
|
|
|
|
|
|
|
|
return lines
|
|
|
|
|
|
|
|
|
|
def _register_extra_kb_cbs(self, kb):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def _run(self):
|
|
|
|
|
class MenuColorizer(Processor):
|
|
|
|
|
def apply_transformation(_self, ti):
|
|
|
|
@ -258,7 +284,6 @@ class CliMenu:
|
|
|
|
|
|
|
|
|
|
@self._kb.add('c-m', filter=~is_searching)
|
|
|
|
|
@self._kb.add('right', filter=~is_searching)
|
|
|
|
|
@self._kb.add('c-space', filter=~is_searching)
|
|
|
|
|
def accept(event):
|
|
|
|
|
self._success = True
|
|
|
|
|
event.app.exit()
|
|
|
|
@ -269,6 +294,8 @@ class CliMenu:
|
|
|
|
|
new_line, _ = self._doc.translate_index_to_position(self._buf.cursor_position)
|
|
|
|
|
self.sync_cursor_to_line(new_line)
|
|
|
|
|
|
|
|
|
|
self._register_extra_kb_cbs(self._kb)
|
|
|
|
|
|
|
|
|
|
self._searchbar = SearchToolbar(ignore_case=True)
|
|
|
|
|
|
|
|
|
|
text = '\n'.join(map(lambda _x: _x.text, self._items))
|
|
|
|
@ -295,6 +322,63 @@ class CliMenu:
|
|
|
|
|
self._ran = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CliMultiMenu(CliMenu):
|
|
|
|
|
default_selected_icon = '[x]'
|
|
|
|
|
default_unselected_icon = '[ ]'
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def set_default_selector_icons(cls, selected_icon, unselected_icon):
|
|
|
|
|
cls.default_selected_icon = selected_icon
|
|
|
|
|
cls.default_unselected_icon = unselected_icon
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, selected_icon=None, unselected_icon=None, **kwargs):
|
|
|
|
|
self._multi_selected = []
|
|
|
|
|
self._selected_icon = selected_icon or self.default_selected_icon
|
|
|
|
|
self._unselected_icon = unselected_icon or self.default_unselected_icon
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def add_option(self, text, item=None, selected=False):
|
|
|
|
|
super().add_option(text, item)
|
|
|
|
|
if selected:
|
|
|
|
|
self._multi_selected.append(len(self._items) - 1)
|
|
|
|
|
|
|
|
|
|
def get_selection(self):
|
|
|
|
|
if self.success:
|
|
|
|
|
return [(self._items[n].num, self._items[n].item) for n in self._multi_selected]
|
|
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_selection_num(self):
|
|
|
|
|
if self.success:
|
|
|
|
|
return [self._items[n].num for n in self._multi_selected]
|
|
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_selection_item(self):
|
|
|
|
|
if self.success:
|
|
|
|
|
return [self._items[n].item for n in self._multi_selected]
|
|
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _register_extra_kb_cbs(self, kb):
|
|
|
|
|
@kb.add('space', filter=~is_searching)
|
|
|
|
|
def mark(event):
|
|
|
|
|
if self._pos not in self._multi_selected:
|
|
|
|
|
self._multi_selected.append(self._pos)
|
|
|
|
|
else:
|
|
|
|
|
self._multi_selected.remove(self._pos)
|
|
|
|
|
|
|
|
|
|
def _transform_prefix(self, item, lineno, prefix):
|
|
|
|
|
if item.focusable:
|
|
|
|
|
if lineno in self._multi_selected:
|
|
|
|
|
icon = self._selected_icon
|
|
|
|
|
else:
|
|
|
|
|
icon = self._unselected_icon
|
|
|
|
|
return "{}{} ".format(prefix, icon)
|
|
|
|
|
else:
|
|
|
|
|
return prefix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cli_select_item(options, header=None, abort_exc=ValueError, abort_text="Selection aborted.", style=None,
|
|
|
|
|
return_single=True):
|
|
|
|
|
"""Helper function to quickly get a selection with just a few arguments"""
|
|
|
|
|