@ -33,9 +33,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 = ' ▶ '
CL I_STAR = ' * '
ASCI I_STAR = ' * '
CL I_ARROW = ' --> '
ASCI I_ARROW = ' --> '
CL I_CAT = ' =^.^= '
ASCI I_CAT = ' =^.^= '
CAT = ' 😸 '
CAT = ' 😸 '
ARROW = ' → '
ARROW = ' → '
@ -61,6 +61,15 @@ class CliMenuStyle:
# self.header_indent = 4
# self.header_indent = 4
class CliSelectionStyle :
SQUARE_BRACKETS = ( ' [x] ' , ' [ ] ' )
ROUND_BRACKETS = ( ' (x) ' , ' ( ) ' )
CHECKMARK = ( ' ✔ ' , ' ✖ ' )
THUMBS = ( ' 👍 ' , ' 👎 ' )
SMILEY = ( ' 🙂 ' , ' 🙁 ' )
SMILEY_EXTREME = ( ' 😁 ' , ' 😨 ' )
class CliMenuTheme :
class CliMenuTheme :
BASIC = CliMenuStyle ( )
BASIC = CliMenuStyle ( )
BASIC_BOLD = CliMenuStyle ( header_style = ' bold ' , highlight_style = ' bold ' )
BASIC_BOLD = CliMenuStyle ( header_style = ' bold ' , highlight_style = ' bold ' )
@ -75,6 +84,14 @@ 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 ) :
self . _items = [ ]
self . _items = [ ]
@ -86,25 +103,28 @@ class CliMenu:
self . _header_indent = indent
self . _header_indent = indent
self . _dedent_selection = dedent_selection
self . _dedent_selection = dedent_selection
self . _cursor = cursor
self . _cursor = cursor or self . default_cursor
if not self . _cursor :
self . _style = style or 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 )
else :
elif isinstance ( option , dict ) :
self . add_option ( * * option )
elif isinstance ( option , str ) :
self . add_option ( option , option )
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 ' ) :
for text in title . split ( ' \n ' ) :
self . _items . append ( CliMenuHeader ( text , indent = indent ) )
self . _items . append ( CliMenuHeader ( text , indent = indent ) )
@ -129,7 +149,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 )
@ -148,6 +167,9 @@ class CliMenu:
# cursor with spaces minus dedent
# cursor with spaces minus dedent
return ' ' * ( len ( self . _cursor ) + 1 * self . _dedent_selection )
return ' ' * ( len ( self . _cursor ) + 1 * self . _dedent_selection )
def _transform_prefix ( self , item , lineno , prefix ) :
return prefix
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 )
@ -173,6 +195,7 @@ class CliMenu:
style = s . header_style
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 )
@ -221,6 +244,9 @@ class CliMenu:
return lines
return lines
def _register_extra_kb_cbs ( self , kb ) :
pass
def _run ( self ) :
def _run ( self ) :
class MenuColorizer ( Processor ) :
class MenuColorizer ( Processor ) :
def apply_transformation ( _self , ti ) :
def apply_transformation ( _self , ti ) :
@ -258,7 +284,6 @@ 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 . _success = True
event . app . exit ( )
event . app . exit ( )
@ -269,6 +294,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 ) )
@ -295,6 +322,63 @@ class CliMenu:
self . _ran = True
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 ,
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 """