@ -4,11 +4,11 @@ from prompt_toolkit.application import Application
from prompt_toolkit . buffer import Buffer
from prompt_toolkit . buffer import Buffer
from prompt_toolkit . document import Document
from prompt_toolkit . document import Document
from prompt_toolkit . filters import is_searching
from prompt_toolkit . filters import is_searching
from prompt_toolkit . filters . base import Condition
from prompt_toolkit . key_binding import KeyBindings
from prompt_toolkit . key_binding import KeyBindings
from prompt_toolkit . layout import Layout , Window , HSplit
from prompt_toolkit . layout import Layout , Window , HSplit
from prompt_toolkit . layout . controls import BufferControl
from prompt_toolkit . layout . controls import BufferControl
from prompt_toolkit . layout . processors import Processor , Transformation
from prompt_toolkit . layout . processors import Processor , Transformation
from prompt_toolkit import search
from prompt_toolkit . widgets import SearchToolbar
from prompt_toolkit . widgets import SearchToolbar
@ -68,16 +68,11 @@ class CliMenuTheme:
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 ' )
@Condition
def is_not_searching ( ) :
print ( " CALLED IT " )
return not is_searching ( )
class CliMenu :
class CliMenu :
default_sty e = CliMenuTheme . BASIC
default_style = CliMenuTheme . BASIC
default_cursor = CliMenuCursor . TRIANGLE
default_cursor = CliMenuCursor . TRIANGLE
def __init__ ( self , options = None , header = None , cursor = None , style = None ,
def __init__ ( self , options = None , header = None , cursor = None , style = None ,
@ -97,17 +92,17 @@ class CliMenu:
self . _style = style
self . _style = style
if not self . _style :
if not self . _style :
self . _style = self . default_sty e
self . _style = self . default_sty l e
if header :
if header :
self . add_header ( header , indent = False )
self . add_header ( header , indent = False )
if options :
if options :
for option in options :
for option in options :
if isinstance ( option , tuple ) :
if isinstance ( option , tuple ) and len ( option ) == 2 :
self . add_option ( * option )
self . add_option ( * option )
else :
else :
self . add_option ( option )
self . add_option ( option , option )
def add_header ( self , title , indent = True ) :
def add_header ( self , title , indent = True ) :
for text in title . split ( ' \n ' ) :
for text in title . split ( ' \n ' ) :
@ -124,6 +119,13 @@ class CliMenu:
return self . _success
return self . _success
def get_options ( self ) :
return [ _item for _item in self . _items if isinstance ( _item , CliMenuOption ) ]
@property
def num_options ( self ) :
return self . _item_num
def get_selection ( self ) :
def get_selection ( self ) :
if self . success :
if self . success :
item = self . _items [ self . _pos ]
item = self . _items [ self . _pos ]
@ -187,6 +189,38 @@ class CliMenu:
if self . _items [ self . _pos ] . focusable :
if self . _items [ self . _pos ] . focusable :
break
break
def sync_cursor_to_line ( self , line , sync_dir = 1 ) :
""" Sync cursor to next fousable item starting on `line` """
assert sync_dir in ( 1 , - 1 )
self . _pos = line
while not self . _items [ self . _pos ] . focusable :
self . _pos = ( self . _pos + sync_dir ) % len ( self . _items )
self . _buf . cursor_position = self . _doc . translate_row_col_to_index ( self . _pos , 0 )
def _get_search_result_lines ( self ) :
""" Get a list of all lines that have a match with the current search result """
if not self . _bufctrl . search_state . text :
return [ ]
idx_list = [ ]
i = 1
while True :
next_idx = self . _buf . get_search_position ( self . _bufctrl . search_state , count = i ,
include_current_position = False )
if next_idx in idx_list :
break
idx_list . append ( next_idx )
i + = 1
lines = [ ]
for idx in idx_list :
line , _ = self . _doc . translate_index_to_position ( idx )
if line not in lines :
lines . append ( line )
return lines
def _run ( self ) :
def _run ( self ) :
class MenuColorizer ( Processor ) :
class MenuColorizer ( Processor ) :
def apply_transformation ( _self , ti ) :
def apply_transformation ( _self , ti ) :
@ -195,43 +229,47 @@ class CliMenu:
# keybindings
# keybindings
self . _kb = KeyBindings ( )
self . _kb = KeyBindings ( )
@self._kb.add ( ' q ' )
@self._kb.add ( ' q ' , filter = ~ is_searching )
@self._kb.add ( ' c-c ' )
@self._kb.add ( ' c-c ' )
def quit ( event ) :
def quit ( event ) :
event . app . exit ( )
event . app . exit ( )
@self._kb.add ( ' down ' )
@self._kb.add ( ' down ' , filter = ~ is_searching )
@self._kb.add ( ' j ' )
@self._kb.add ( ' j ' , filter = ~ is_searching )
def down ( event ) :
def down ( event ) :
self . next_item ( 1 )
self . next_item ( 1 )
@self._kb.add ( ' up ' )
@self._kb.add ( ' up ' , filter = ~ is_searching )
@self._kb.add ( ' k ' )
@self._kb.add ( ' k ' , filter = ~ is_searching )
def up ( event ) :
def up ( event ) :
self . next_item ( - 1 )
self . next_item ( - 1 )
@self._kb.add ( ' n ' )
@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 , filter = is_searching ) :
print ( self . _bufctrl . search_state . text )
if not self . _bufctrl . search_state . text :
print ( " curr pos " , self . _buf . get_search_position ( self . _bufctrl . search_state ) )
return
# search_state.direction = search.SearchDirection.BACKWARD
next10 = [ ]
search_dir = 1 if event . data == ' n ' else - 1
for i in range ( 10 ) :
sr_lines = self . _get_search_result_lines ( )
p = self . _buf . get_search_position ( self . _bufctrl . search_state , count = i + 1 , include_current_position = False )
if sr_lines :
next10 . append ( p )
line = sr_lines [ search_dir ] if len ( sr_lines ) > 1 else sr_lines [ 0 ]
self . sync_cursor_to_line ( line , search_dir )
print ( " next 10 pos are " , next10 )
@self._kb.add ( ' c-m ' , filter = ~ is_searching )
#search.do_incremental_search(search.SearchDirection.FORWARD)
@self._kb.add ( ' right ' , filter = ~ is_searching )
@self._kb.add ( ' c-space ' , filter = ~ is_searching )
@self._kb.add ( ' right ' )
#@self._kb.add('c-m')
@self._kb.add ( ' c-space ' )
def accept ( event ) :
def accept ( event ) :
self . _success = True
self . _success = True
event . app . exit ( )
event . app . exit ( )
self . _searchbar = SearchToolbar ( text_if_not_searching = [ ( ' class:not-searching ' , ' NOOT NOOT ' ) ] , ignore_case = True )
@self._kb.add ( ' c-m ' , filter = is_searching )
def accept_search ( event ) :
search . accept_search ( )
new_line , _ = self . _doc . translate_index_to_position ( self . _buf . cursor_position )
self . sync_cursor_to_line ( new_line )
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 ) )
self . _doc = Document ( text , cursor_position = self . _pos )
self . _doc = Document ( text , cursor_position = self . _pos )
@ -246,8 +284,7 @@ class CliMenu:
self . _searchbar ] )
self . _searchbar ] )
# set initial pos
# set initial pos
if not self . _items [ self . _pos ] . focusable :
self . sync_cursor_to_line ( 0 )
self . next_item ( 1 )
app = Application ( layout = Layout ( split ) ,
app = Application ( layout = Layout ( split ) ,
key_bindings = self . _kb ,
key_bindings = self . _kb ,
@ -256,3 +293,18 @@ class CliMenu:
app . run ( )
app . run ( )
self . _ran = True
self . _ran = True
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 """
menu = CliMenu ( header = header , options = options , style = style )
if return_single and menu . num_options == 1 :
item = menu . get_options ( ) [ 0 ]
return item . num , item . item
if not menu . success :
raise abort_exc ( abort_text )
return menu . get_selection ( )