Compare commits
25 Commits
master
...
return-wit
Author | SHA1 | Date |
---|---|---|
Sebastian Lohff | f9b429c696 | |
Sebastian Lohff | 1a309d28c0 | |
Sebastian Lohff | 775c50ed6f | |
Sebastian Lohff | 7a09d45ab8 | |
Sebastian Lohff | 19ad4f3e8b | |
Sebastian Lohff | 030492b353 | |
Sebastian Lohff | 22307bc4ae | |
Sebastian Lohff | 85c3c5c947 | |
Sebastian Lohff | 68e6ecbe4f | |
Sebastian Lohff | 24dab5f69b | |
Sebastian Lohff | 56b76706f6 | |
Sebastian Lohff | 12552d8e5b | |
Sebastian Lohff | d01024d1be | |
Sebastian Lohff | 3e3ea53f5e | |
Sebastian Lohff | c363c187bd | |
Sebastian Lohff | 94bb03dc07 | |
Sebastian Lohff | 4ba22a3a33 | |
Sebastian Lohff | 34a3279e2f | |
Sebastian Lohff | f20b5eac89 | |
Sebastian Lohff | 9b833db364 | |
Sebastian Lohff | c74e3d68d6 | |
Sebastian Lohff | 46a863b3cf | |
Sebastian Lohff | a28c76e001 | |
Sebastian Lohff | 45d3395642 | |
Sebastian Lohff | a17f7693e9 |
12
README.md
12
README.md
|
@ -1,5 +1,9 @@
|
||||||
# CLI-Intermission
|
# Clintermission - Python CLI Intermission Library
|
||||||
Tool to easily ask your user questions on the commandline. Interactive!
|
`clintermission` is a small Python library designed to quickly get a selection
|
||||||
...by abusing python prompt-toolkit's `Buffer` class.
|
of a single or multiple items from a user. The menu shown is explicitly not a
|
||||||
|
fullscreen menu so the user can use information in their console history to
|
||||||
|
make a selection.
|
||||||
|
|
||||||
Highly inspired by [go promptui](https://github.com/manifoldco/promptui)
|
The menu is implemented by using `prompt-toolkit`'s `Buffer` class.
|
||||||
|
|
||||||
|
Inspired by [go promptui](https://github.com/manifoldco/promptui).
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
# Written by Sebastian Lohff <seba@someserver.de>
|
# Written by Sebastian Lohff <seba@someserver.de>
|
||||||
# Licensed under Apache License 2.0
|
# Licensed under Apache License 2.0
|
||||||
|
|
||||||
from clintermission.climenu import CliMenu, CliMenuStyle, CliMenuCursor, CliMenuTheme, cli_select_item
|
from clintermission.climenu import CliMenu, CliMultiMenu, CliMenuStyle, CliSelectionStyle, CliMenuCursor, CliMenuTheme, cli_select_item
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
CliMenu,
|
CliMenu,
|
||||||
|
CliMultiMenu,
|
||||||
CliMenuStyle,
|
CliMenuStyle,
|
||||||
|
CliSelectionStyle,
|
||||||
CliMenuCursor,
|
CliMenuCursor,
|
||||||
CliMenuTheme,
|
CliMenuTheme,
|
||||||
cli_select_item,
|
cli_select_item,
|
||||||
|
|
|
@ -12,20 +12,23 @@ from prompt_toolkit import search
|
||||||
from prompt_toolkit.widgets import SearchToolbar
|
from prompt_toolkit.widgets import SearchToolbar
|
||||||
|
|
||||||
|
|
||||||
class CliMenuHeader:
|
class _CliMenuHeader:
|
||||||
"""Hold a menu header"""
|
"""Hold a menu header"""
|
||||||
def __init__(self, text, indent=False):
|
def __init__(self, text, indent=False, style=None):
|
||||||
self.text = text
|
self.text = text
|
||||||
self.indent = indent
|
self.indent = indent
|
||||||
|
self.style = style
|
||||||
self.focusable = False
|
self.focusable = False
|
||||||
|
|
||||||
|
|
||||||
class CliMenuOption:
|
class _CliMenuOption:
|
||||||
"""Hold a menu option"""
|
"""Hold a menu option"""
|
||||||
def __init__(self, text, num, item=None):
|
def __init__(self, text, num, item=None, style=None, highlighted_style=None):
|
||||||
self.text = text
|
self.text = text
|
||||||
self.num = num
|
self.num = num
|
||||||
self.item = item
|
self.item = item
|
||||||
|
self.style = style
|
||||||
|
self.highlighted_style = highlighted_style
|
||||||
self.focusable = True
|
self.focusable = True
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,9 +36,9 @@ class CliMenuCursor:
|
||||||
"""Collection of cursors pointing at the active menu item"""
|
"""Collection of cursors pointing at the active menu item"""
|
||||||
BULLET = '●'
|
BULLET = '●'
|
||||||
TRIANGLE = '▶'
|
TRIANGLE = '▶'
|
||||||
CLI_STAR = '*'
|
ASCII_STAR = '*'
|
||||||
CLI_ARROW = '-->'
|
ASCII_ARROW = '-->'
|
||||||
CLI_CAT = '=^.^='
|
ASCII_CAT = '=^.^='
|
||||||
CAT = '😸'
|
CAT = '😸'
|
||||||
ARROW = '→'
|
ARROW = '→'
|
||||||
|
|
||||||
|
@ -45,71 +48,95 @@ class CliMenuStyle:
|
||||||
|
|
||||||
Allows to select header, option and selected option color
|
Allows to select header, option and selected option color
|
||||||
"""
|
"""
|
||||||
def __init__(self, option_style='', highlight_style='', header_style=''):
|
def __init__(self, option='', highlighted='', text='', selected=None, selected_highlighted=None):
|
||||||
self.option_style = option_style
|
self.option = option
|
||||||
self.highlight_style = highlight_style
|
self.highlighted = highlighted
|
||||||
self.header_style = header_style
|
self.text = text
|
||||||
|
self.selected = selected
|
||||||
|
self.selected_highlighted = selected_highlighted
|
||||||
|
|
||||||
# self.option_color = '#aa0000'
|
|
||||||
# #self.highlight_color = 'fg:ansiblue bg:ansired bold'
|
class CliSelectionStyle:
|
||||||
# self.highlight_color = 'bold'
|
SQUARE_BRACKETS = ('[x]', '[ ]')
|
||||||
# self.cursor = ' --> '
|
ROUND_BRACKETS = ('(x)', '( )')
|
||||||
# self.cursor = ' ● '
|
CHECKMARK = ('✔', '✖')
|
||||||
# self.no_cursor = ' '
|
THUMBS = ('👍', '👎')
|
||||||
# self.header_color = '#aa22cc'
|
SMILEY = ('🙂', '🙁')
|
||||||
# self.option_indent = 4
|
SMILEY_EXTREME = ('😁', '😨')
|
||||||
# self.header_indent = 4
|
|
||||||
|
|
||||||
|
|
||||||
class CliMenuTheme:
|
class CliMenuTheme:
|
||||||
BASIC = CliMenuStyle()
|
BASIC = CliMenuStyle()
|
||||||
BASIC_BOLD = CliMenuStyle(header_style='bold', highlight_style='bold')
|
BASIC_BOLD = CliMenuStyle(text='bold', highlighted='bold')
|
||||||
RED = CliMenuStyle('#aa0000', '#ee0000', '#aa0000')
|
RED = CliMenuStyle('#aa0000', '#ee0000', '#aa0000')
|
||||||
CYAN = CliMenuStyle('cyan', 'lightcyan', 'cyan')
|
CYAN = CliMenuStyle('cyan', 'lightcyan', 'cyan')
|
||||||
BLUE = CliMenuStyle('ansiblue', 'ansired', 'ansiblue')
|
BLUE = CliMenuStyle('ansiblue', 'ansired', 'ansiblue')
|
||||||
ANSI_CYAN = CliMenuStyle('ansicyan', 'ansibrightcyan', 'ansicyan')
|
ANSI_CYAN = CliMenuStyle('ansicyan', 'ansibrightcyan', 'ansicyan')
|
||||||
BOLD_HIGHLIGHT = CliMenuStyle(header_style='bold', highlight_style='bold fg:black bg:white')
|
BOLD_HIGHLIGHT = CliMenuStyle(text='bold', highlighted='bold fg:black bg:white')
|
||||||
|
|
||||||
|
|
||||||
|
class _EmptyParameter:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CliMenu:
|
class CliMenu:
|
||||||
default_style = CliMenuTheme.BASIC
|
default_style = CliMenuTheme.BASIC
|
||||||
default_cursor = CliMenuCursor.TRIANGLE
|
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,
|
def __init__(self, options=None, header=None, cursor=None, style=None,
|
||||||
indent=2, dedent_selection=False):
|
indent=2, dedent_selection=False, initial_pos=0,
|
||||||
|
option_prefix=' ', option_suffix='', right_pad_options=False):
|
||||||
self._items = []
|
self._items = []
|
||||||
self._item_num = 0
|
self._item_num = 0
|
||||||
self._ran = False
|
self._ran = False
|
||||||
self._success = None
|
self._success = None
|
||||||
self._pos = 0
|
self._pos = 0
|
||||||
|
self._initial_pos = initial_pos
|
||||||
|
self._option_prefix = option_prefix
|
||||||
|
self._option_suffix = option_suffix
|
||||||
self._option_indent = indent
|
self._option_indent = indent
|
||||||
self._header_indent = indent
|
self._header_indent = indent
|
||||||
self._dedent_selection = dedent_selection
|
self._dedent_selection = dedent_selection
|
||||||
|
self._right_pad_options = right_pad_options
|
||||||
|
|
||||||
self._cursor = cursor
|
self._cursor = cursor if cursor is not None else self.default_cursor
|
||||||
if not self._cursor:
|
self._style = style if style is not None else self.default_style
|
||||||
self._cursor = self.default_cursor
|
|
||||||
|
|
||||||
self._style = style
|
|
||||||
if not self._style:
|
|
||||||
self._style = self.default_style
|
|
||||||
|
|
||||||
if header:
|
if header:
|
||||||
self.add_header(header, indent=False)
|
self.add_text(header, indent=False)
|
||||||
|
|
||||||
if options:
|
if options:
|
||||||
for option in options:
|
for option in options:
|
||||||
if isinstance(option, tuple) and len(option) == 2:
|
if isinstance(option, tuple):
|
||||||
self.add_option(*option)
|
self.add_option(*option)
|
||||||
|
elif isinstance(option, dict):
|
||||||
|
self.add_option(**option)
|
||||||
|
elif isinstance(option, str):
|
||||||
|
self.add_option(option)
|
||||||
else:
|
else:
|
||||||
self.add_option(option, option)
|
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, style=None):
|
||||||
for text in title.split('\n'):
|
for text in title.split('\n'):
|
||||||
self._items.append(CliMenuHeader(text, indent=indent))
|
self._items.append(_CliMenuHeader(text, indent=indent, style=style))
|
||||||
|
|
||||||
def add_option(self, text, item=None):
|
def add_option(self, text, item=_EmptyParameter, style=None, highlighted_style=None):
|
||||||
self._items.append(CliMenuOption(text, self._item_num, item=item))
|
if item == _EmptyParameter:
|
||||||
|
item = text
|
||||||
|
self._items.append(_CliMenuOption(text, self._item_num, item=item,
|
||||||
|
style=style, highlighted_style=highlighted_style))
|
||||||
self._item_num += 1
|
self._item_num += 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -120,7 +147,7 @@ class CliMenu:
|
||||||
return self._success
|
return self._success
|
||||||
|
|
||||||
def get_options(self):
|
def get_options(self):
|
||||||
return [_item for _item in self._items if isinstance(_item, CliMenuOption)]
|
return [_item for _item in self._items if isinstance(_item, _CliMenuOption)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def num_options(self):
|
def num_options(self):
|
||||||
|
@ -129,7 +156,6 @@ class CliMenu:
|
||||||
def get_selection(self):
|
def get_selection(self):
|
||||||
if self.success:
|
if self.success:
|
||||||
item = self._items[self._pos]
|
item = self._items[self._pos]
|
||||||
|
|
||||||
return (item.num, item.item)
|
return (item.num, item.item)
|
||||||
else:
|
else:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
@ -140,41 +166,46 @@ class CliMenu:
|
||||||
def get_selection_item(self):
|
def get_selection_item(self):
|
||||||
return self.get_selection()[1]
|
return self.get_selection()[1]
|
||||||
|
|
||||||
def cursor(self):
|
def _transform_prefix(self, item, lineno, prefix):
|
||||||
return '{} '.format(self._cursor)
|
return prefix
|
||||||
|
|
||||||
@property
|
def _get_style(self, item, lineno, highlighted):
|
||||||
def no_cursor(self):
|
s = self._style
|
||||||
# cursor with spaces minus dedent
|
if item.focusable:
|
||||||
return ' ' * (len(self._cursor) + 1 * self._dedent_selection)
|
if highlighted:
|
||||||
|
return item.highlighted_style if item.highlighted_style is not None else s.highlighted
|
||||||
|
else:
|
||||||
|
return item.style if item.style is not None else s.option
|
||||||
|
else:
|
||||||
|
return item.style if item.style is not None else s.text
|
||||||
|
|
||||||
def _transform_line(self, ti):
|
def _transform_line(self, ti):
|
||||||
if len(list(ti.fragments)) == 0:
|
if len(list(ti.fragments)) == 0:
|
||||||
return Transformation(ti.fragments)
|
return Transformation(ti.fragments)
|
||||||
style, text = list(ti.fragments)[0]
|
style, text = list(ti.fragments)[0]
|
||||||
item = self._items[ti.lineno]
|
item = self._items[ti.lineno]
|
||||||
s = self._style
|
style = self._get_style(item, ti.lineno, ti.lineno == self._pos)
|
||||||
|
|
||||||
# cursor
|
# cursor
|
||||||
indent = ''
|
indent = ''
|
||||||
prefix = ''
|
prefix = ''
|
||||||
|
suffix = ''
|
||||||
if item.focusable:
|
if item.focusable:
|
||||||
indent += ' ' * self._option_indent
|
indent += ' ' * self._option_indent
|
||||||
|
suffix = self._option_suffix
|
||||||
|
|
||||||
if ti.lineno == self._pos:
|
if ti.lineno == self._pos:
|
||||||
prefix += '{} '.format(self._cursor)
|
prefix += '{}{}'.format(self._cursor, self._option_prefix)
|
||||||
style = s.highlight_style
|
|
||||||
else:
|
else:
|
||||||
prefix += ' ' * (len(self._cursor) + 1 + 1 * self._dedent_selection)
|
prefix += ' ' * len(self._cursor) + self._option_prefix + ' ' * self._dedent_selection
|
||||||
style = s.option_style
|
|
||||||
else:
|
else:
|
||||||
if item.indent:
|
if item.indent:
|
||||||
indent += ' ' * (self._header_indent + len(self._cursor) + 1)
|
indent += ' ' * (self._header_indent + len(self._cursor) + 1)
|
||||||
style = s.header_style
|
|
||||||
|
|
||||||
items = [(s if s else style, t) for s, t in ti.fragments]
|
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)
|
return Transformation([('', indent), (style, prefix)] + items + [(style, suffix)])
|
||||||
|
|
||||||
def next_item(self, direction):
|
def next_item(self, direction):
|
||||||
if not any(item.focusable for item in self._items):
|
if not any(item.focusable for item in self._items):
|
||||||
|
@ -221,7 +252,32 @@ class CliMenu:
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
def _register_extra_kb_cbs(self, kb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _preflight(self):
|
||||||
|
if self._initial_pos < 0 or self._initial_pos >= self._item_num:
|
||||||
|
raise ValueError("Initial position {} is out of range, needs to be in range of [0, {})"
|
||||||
|
.format(self._initial_pos, self._item_num))
|
||||||
|
|
||||||
|
if self._right_pad_options:
|
||||||
|
# pad all item labels with spaces to have the same length
|
||||||
|
max_item_len = max([len(_item.text) for _item in self._items if _item.focusable])
|
||||||
|
for item in self._items:
|
||||||
|
if item.focusable:
|
||||||
|
item.text += " " * (max_item_len - len(item.text))
|
||||||
|
|
||||||
|
def _accept(self, event):
|
||||||
|
self._success = True
|
||||||
|
event.app.exit()
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
|
if self._item_num == 0:
|
||||||
|
self._success = False
|
||||||
|
return
|
||||||
|
|
||||||
|
self._preflight()
|
||||||
|
|
||||||
class MenuColorizer(Processor):
|
class MenuColorizer(Processor):
|
||||||
def apply_transformation(_self, ti):
|
def apply_transformation(_self, ti):
|
||||||
return self._transform_line(ti)
|
return self._transform_line(ti)
|
||||||
|
@ -232,6 +288,7 @@ class CliMenu:
|
||||||
@self._kb.add('q', filter=~is_searching)
|
@self._kb.add('q', filter=~is_searching)
|
||||||
@self._kb.add('c-c')
|
@self._kb.add('c-c')
|
||||||
def quit(event):
|
def quit(event):
|
||||||
|
self._success = False
|
||||||
event.app.exit()
|
event.app.exit()
|
||||||
|
|
||||||
@self._kb.add('down', filter=~is_searching)
|
@self._kb.add('down', filter=~is_searching)
|
||||||
|
@ -246,7 +303,7 @@ class CliMenu:
|
||||||
|
|
||||||
@self._kb.add('N', filter=~is_searching)
|
@self._kb.add('N', filter=~is_searching)
|
||||||
@self._kb.add('n', filter=~is_searching)
|
@self._kb.add('n', filter=~is_searching)
|
||||||
def search_inc(event, filter=is_searching):
|
def search_inc(event):
|
||||||
if not self._bufctrl.search_state.text:
|
if not self._bufctrl.search_state.text:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -258,10 +315,8 @@ class CliMenu:
|
||||||
|
|
||||||
@self._kb.add('c-m', filter=~is_searching)
|
@self._kb.add('c-m', filter=~is_searching)
|
||||||
@self._kb.add('right', filter=~is_searching)
|
@self._kb.add('right', filter=~is_searching)
|
||||||
@self._kb.add('c-space', filter=~is_searching)
|
|
||||||
def accept(event):
|
def accept(event):
|
||||||
self._success = True
|
self._accept(event)
|
||||||
event.app.exit()
|
|
||||||
|
|
||||||
@self._kb.add('c-m', filter=is_searching)
|
@self._kb.add('c-m', filter=is_searching)
|
||||||
def accept_search(event):
|
def accept_search(event):
|
||||||
|
@ -269,6 +324,8 @@ class CliMenu:
|
||||||
new_line, _ = self._doc.translate_index_to_position(self._buf.cursor_position)
|
new_line, _ = self._doc.translate_index_to_position(self._buf.cursor_position)
|
||||||
self.sync_cursor_to_line(new_line)
|
self.sync_cursor_to_line(new_line)
|
||||||
|
|
||||||
|
self._register_extra_kb_cbs(self._kb)
|
||||||
|
|
||||||
self._searchbar = SearchToolbar(ignore_case=True)
|
self._searchbar = SearchToolbar(ignore_case=True)
|
||||||
|
|
||||||
text = '\n'.join(map(lambda _x: _x.text, self._items))
|
text = '\n'.join(map(lambda _x: _x.text, self._items))
|
||||||
|
@ -284,7 +341,8 @@ class CliMenu:
|
||||||
self._searchbar])
|
self._searchbar])
|
||||||
|
|
||||||
# set initial pos
|
# set initial pos
|
||||||
self.sync_cursor_to_line(0)
|
for _ in range(self._initial_pos + 1):
|
||||||
|
self.next_item(1)
|
||||||
|
|
||||||
app = Application(layout=Layout(split),
|
app = Application(layout=Layout(split),
|
||||||
key_bindings=self._kb,
|
key_bindings=self._kb,
|
||||||
|
@ -295,6 +353,92 @@ class CliMenu:
|
||||||
self._ran = True
|
self._ran = True
|
||||||
|
|
||||||
|
|
||||||
|
class CliMultiMenu(CliMenu):
|
||||||
|
default_selection_icons = CliSelectionStyle.SQUARE_BRACKETS
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_default_selector_icons(cls, selection_icons):
|
||||||
|
cls.default_selection_icons = selection_icons
|
||||||
|
|
||||||
|
def __init__(self, *args, selection_icons=None, min_selection_count=0, **kwargs):
|
||||||
|
self._multi_selected = []
|
||||||
|
self._min_selection_count = min_selection_count
|
||||||
|
self._selection_icons = selection_icons if selection_icons is not None else self.default_selection_icons
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def add_option(self, text, item=_EmptyParameter, selected=False,
|
||||||
|
style=None, highlighted_style=None, selected_style=None, selected_highlighted_style=None):
|
||||||
|
super().add_option(text, item, style, highlighted_style=highlighted_style)
|
||||||
|
self._items[-1].selected_style = selected_style
|
||||||
|
self._items[-1].selected_highlighted_style = selected_highlighted_style
|
||||||
|
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)
|
||||||
|
@kb.add('right', 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._selection_icons[0]
|
||||||
|
else:
|
||||||
|
icon = self._selection_icons[1]
|
||||||
|
return "{}{} ".format(prefix, icon)
|
||||||
|
else:
|
||||||
|
return prefix
|
||||||
|
|
||||||
|
def _get_style(self, item, lineno, highlighted):
|
||||||
|
s = self._style
|
||||||
|
if item.focusable and lineno in self._multi_selected:
|
||||||
|
if highlighted:
|
||||||
|
if item.selected_highlighted_style is not None:
|
||||||
|
return item.selected_highlighted_style
|
||||||
|
if s.selected_highlighted is not None:
|
||||||
|
return s.selected_highlighted
|
||||||
|
else:
|
||||||
|
if item.selected_style is not None:
|
||||||
|
return item.selected_style
|
||||||
|
if s.selected is not None:
|
||||||
|
return s.selected
|
||||||
|
|
||||||
|
# no style specified or no selected state, call parent
|
||||||
|
return super()._get_style(item, lineno, highlighted)
|
||||||
|
|
||||||
|
def _preflight(self):
|
||||||
|
super()._preflight()
|
||||||
|
if self._min_selection_count > self._item_num:
|
||||||
|
raise ValueError("A minimum of {} items was requested for successful selection but only {} exist"
|
||||||
|
.format(self._min_selection_count, self._item_num))
|
||||||
|
|
||||||
|
def _accept(self, event):
|
||||||
|
if len(self._multi_selected) >= self._min_selection_count:
|
||||||
|
super()._accept(event)
|
||||||
|
|
||||||
|
|
||||||
def cli_select_item(options, header=None, abort_exc=ValueError, abort_text="Selection aborted.", style=None,
|
def cli_select_item(options, header=None, abort_exc=ValueError, abort_text="Selection aborted.", style=None,
|
||||||
return_single=True):
|
return_single=True):
|
||||||
"""Helper function to quickly get a selection with just a few arguments"""
|
"""Helper function to quickly get a selection with just a few arguments"""
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from clintermission import CliMenu, CliMenuTheme, CliMenuStyle, CliMenuCursor, cli_select_item
|
||||||
|
|
||||||
|
# --- basic menu ---
|
||||||
|
q = ["Foo", "Bar", "Baz"]
|
||||||
|
m = CliMenu(q, "Time to choose:\n")
|
||||||
|
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# basic menu with an item assigned to each option and detentation of selection
|
||||||
|
q = [("Foo", 23), ("Bar", 34), ("Baz", 42)]
|
||||||
|
m = CliMenu(q, "Time to choose:\n", cursor="--->", dedent_selection=True)
|
||||||
|
|
||||||
|
print("You selected {} index {} item {}".format(m.get_selection(), m.get_selection_num(), m.get_selection_item()))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- themes ---
|
||||||
|
q = ["Foo", "Bar", "Baz"]
|
||||||
|
m = CliMenu(q, "Time to choose:\n", style=CliMenuTheme.RED)
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# --- custom themes ---
|
||||||
|
style = CliMenuStyle(option='blue', highlighted='cyan', text='green')
|
||||||
|
q = ["Foo", "Bar", "Baz"]
|
||||||
|
m = CliMenu(q, "Choose in style:\n", style=style)
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- theme defaults ---
|
||||||
|
CliMenu.set_default_cursor(CliMenuCursor.BULLET)
|
||||||
|
CliMenu.set_default_style(CliMenuTheme.BOLD_HIGHLIGHT)
|
||||||
|
|
||||||
|
q = ["Foo", "Bar", "Baz"]
|
||||||
|
m = CliMenu(q, "Time to choose:\n")
|
||||||
|
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- multiple headers ---
|
||||||
|
m = CliMenu()
|
||||||
|
|
||||||
|
m.add_header("Time to choose:\n", indent=False)
|
||||||
|
m.add_text("=== Category 1 ===")
|
||||||
|
m.add_option("Foo")
|
||||||
|
m.add_option("Bar")
|
||||||
|
m.add_option("Baz")
|
||||||
|
m.add_header('', indent=False)
|
||||||
|
|
||||||
|
m.add_text("=== Category 2 ===")
|
||||||
|
m.add_option("Cat 1")
|
||||||
|
m.add_option("Cat 2")
|
||||||
|
m.add_option("Cat 3")
|
||||||
|
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- with shortcut ---
|
||||||
|
try:
|
||||||
|
result = cli_select_item(["Foo", "Bar", "Baz"], abort_text="Selection faiiiled!")
|
||||||
|
print("You selected", result)
|
||||||
|
except ValueError as e:
|
||||||
|
print(e)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- with shortcut, shows no menu when only a single option is provided (can be disabled with return_single=False) ---
|
||||||
|
result = cli_select_item(["Single Foo"])
|
||||||
|
print("Directly selected for you as it was the only option:", result)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- prefix/suffix ---
|
||||||
|
q = ["Foo", "Bar", "Baz"]
|
||||||
|
m = CliMenu(q, "Time to choose:\n", option_prefix=' <<<', option_suffix='>>>')
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- colorize everything ---
|
||||||
|
m = CliMenu()
|
||||||
|
m.add_text("Time to choose:\n", style="green")
|
||||||
|
m.add_option("Yellow foo", style="yellow", highlighted_style="black bg:yellow")
|
||||||
|
m.add_option("Green bar", style="green", highlighted_style="black bg:green")
|
||||||
|
m.add_option("Blue baz", style="blue", highlighted_style="black bg:blue")
|
||||||
|
m.add_text("\n...go for it!")
|
||||||
|
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print()
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from clintermission import CliMenu, CliMenuTheme
|
||||||
|
|
||||||
|
|
||||||
|
q = ["Foo", "Bar", "Baz baz baz baz baz"]
|
||||||
|
m = CliMenu(q, "Time to choose:\n", style=CliMenuTheme.BOLD_HIGHLIGHT,
|
||||||
|
cursor='', option_prefix=' ', option_suffix=' ', right_pad_options=True)
|
||||||
|
m.get_selection()
|
||||||
|
print("You selected", m.get_selection())
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from clintermission import CliMultiMenu, CliSelectionStyle, CliMenuCursor
|
||||||
|
|
||||||
|
|
||||||
|
# --- simple multiselect ---
|
||||||
|
q = [
|
||||||
|
"Option 1",
|
||||||
|
"Option 2",
|
||||||
|
("Option 3 (preselected for your convenience)", "Option 3", True),
|
||||||
|
"Option 4"
|
||||||
|
]
|
||||||
|
m = CliMultiMenu(q, "Make your choice (<space> selects, <return> accepts):\n", cursor=CliMenuCursor.ASCII_ARROW,
|
||||||
|
selection_icons=CliSelectionStyle.CHECKMARK)
|
||||||
|
|
||||||
|
print("You selected", m.get_selection())
|
||||||
|
print("You selected num:", m.get_selection_num())
|
||||||
|
print("You selected item:", m.get_selection_item())
|
|
@ -1,11 +1,13 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = clintermission
|
name = clintermission
|
||||||
version = 0.1.0
|
version = 0.2.0
|
||||||
author = Sebastian Lohff
|
author = Sebastian Lohff
|
||||||
author-email = seba@someserver.de
|
author-email = seba@someserver.de
|
||||||
home-page = https://git.someserver.de/seba/clintermission
|
license = Apache-2.0
|
||||||
description = Non-fullscreen commandline selection menu
|
home-page = https://github.com/sebageek/clintermission
|
||||||
|
description = Non-fullscreen command-line selection menu
|
||||||
long-description = file: README.md
|
long-description = file: README.md
|
||||||
|
long-description-content-type = text/markdown
|
||||||
platform = any
|
platform = any
|
||||||
keywords = cli, menu
|
keywords = cli, menu
|
||||||
classifiers =
|
classifiers =
|
||||||
|
@ -15,6 +17,7 @@ classifiers =
|
||||||
Programming Language :: Python :: 3.5
|
Programming Language :: Python :: 3.5
|
||||||
Programming Language :: Python :: 3.6
|
Programming Language :: Python :: 3.6
|
||||||
Programming Language :: Python :: 3.7
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
License :: OSI Approved :: Apache Software License
|
License :: OSI Approved :: Apache Software License
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
|
Loading…
Reference in New Issue