Compare commits

...

15 Commits
master ... dev

Author SHA1 Message Date
Sebastian Lohff 411bb3a7fe Group selection icons in CliMultiMenu API
4 years ago
Sebastian Lohff 12552d8e5b Use right arrow to select/deselect in CliMultiMenu
4 years ago
Sebastian Lohff d01024d1be Add initial_pos to choose initially selected item
4 years ago
Sebastian Lohff 3e3ea53f5e Remove dead code
4 years ago
Sebastian Lohff c363c187bd Remove unused methods and unused arguments
4 years ago
Sebastian Lohff 94bb03dc07 Don't ignore cursor/style/icon if it is not None
4 years ago
Sebastian Lohff 4ba22a3a33 Make setup.cfg release ready, move to GitHub
4 years ago
Sebastian Lohff 34a3279e2f Improve README.md a tiny bit
4 years ago
Sebastian Lohff f20b5eac89 Add more examples
4 years ago
Sebastian Lohff 9b833db364 Check type of options passed to __init__
4 years ago
Sebastian Lohff c74e3d68d6 Implement multiple selections with CliMultiMenu
4 years ago
Sebastian Lohff 46a863b3cf Remove broken space keyboard binding for accept
4 years ago
Sebastian Lohff a28c76e001 Allow a default theme and cursor to be set
4 years ago
Sebastian Lohff 45d3395642 Rename add_header() to add_text()
4 years ago
Sebastian Lohff a17f7693e9 Renamed CLI_* cursors to ASCII_* cursors
4 years ago

@ -1,5 +1,9 @@
# CLI-Intermission
Tool to easily ask your user questions on the commandline. Interactive!
...by abusing python prompt-toolkit's `Buffer` class.
# Clintermission - Python CLI Intermission Library
`clintermission` is a small Python library designed to quickly get a selection
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>
# 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__ = [
CliMenu,
CliMultiMenu,
CliMenuStyle,
CliSelectionStyle,
CliMenuCursor,
CliMenuTheme,
cli_select_item,

@ -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 = ''
@ -50,15 +50,14 @@ class CliMenuStyle:
self.highlight_style = highlight_style
self.header_style = header_style
# self.option_color = '#aa0000'
# #self.highlight_color = 'fg:ansiblue bg:ansired bold'
# self.highlight_color = 'bold'
# self.cursor = ' --> '
# self.cursor = ' ● '
# self.no_cursor = ' '
# self.header_color = '#aa22cc'
# self.option_indent = 4
# self.header_indent = 4
class CliSelectionStyle:
SQUARE_BRACKETS = ('[x]', '[ ]')
ROUND_BRACKETS = ('(x)', '( )')
CHECKMARK = ('', '')
THUMBS = ('👍', '👎')
SMILEY = ('🙂', '🙁')
SMILEY_EXTREME = ('😁', '😨')
class CliMenuTheme:
@ -75,36 +74,48 @@ 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):
indent=2, dedent_selection=False, initial_pos=0):
self._items = []
self._item_num = 0
self._ran = False
self._success = None
self._pos = 0
self._initial_pos = initial_pos
self._option_indent = indent
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 if cursor is not None else self.default_cursor
self._style = style if style is not None else 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, *args, **kwargs):
return self.add_text(*args, **kwargs)
def add_header(self, title, indent=True):
def add_text(self, title, indent=True):
for text in title.split('\n'):
self._items.append(CliMenuHeader(text, indent=indent))
@ -129,7 +140,6 @@ class CliMenu:
def get_selection(self):
if self.success:
item = self._items[self._pos]
return (item.num, item.item)
else:
return (None, None)
@ -140,13 +150,8 @@ class CliMenu:
def get_selection_item(self):
return self.get_selection()[1]
def cursor(self):
return '{} '.format(self._cursor)
@property
def no_cursor(self):
# 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:
@ -173,6 +178,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,7 +227,14 @@ class CliMenu:
return lines
def _register_extra_kb_cbs(self, kb):
pass
def _run(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))
class MenuColorizer(Processor):
def apply_transformation(_self, ti):
return self._transform_line(ti)
@ -246,7 +259,7 @@ class CliMenu:
@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:
return
@ -258,7 +271,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 +281,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))
@ -284,7 +298,8 @@ class CliMenu:
self._searchbar])
# 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),
key_bindings=self._kb,
@ -295,6 +310,61 @@ class CliMenu:
self._ran = True
class CliMultiMenu(CliMenu):
default_selection_icons = ('[x]', '[ ]')
@classmethod
def set_default_selector_icons(cls, selection_icons):
cls.default_selection_icons = selection_icons
def __init__(self, *args, selection_icons=None, **kwargs):
self._multi_selected = []
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=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)
@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 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"""

@ -0,0 +1,70 @@
#!/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_style='blue', highlight_style='cyan', header_style='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)

@ -0,0 +1,17 @@
#!/usr/bin/env python3
from clintermission import CliMultiMenu, 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=("", ""))
print("You selected", m.get_selection())
print("You selected num:", m.get_selection_num())
print("You selected item:", m.get_selection_item())

@ -3,9 +3,11 @@ name = clintermission
version = 0.1.0
author = Sebastian Lohff
author-email = seba@someserver.de
home-page = https://git.someserver.de/seba/clintermission
description = Non-fullscreen commandline selection menu
license = Apache-2.0
home-page = https://github.com/sebageek/clintermission
description = Non-fullscreen command-line selection menu
long-description = file: README.md
long-description-content-type = text/markdown
platform = any
keywords = cli, menu
classifiers =
@ -15,6 +17,7 @@ classifiers =
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
License :: OSI Approved :: Apache Software License
[options]

Loading…
Cancel
Save