Compare commits
	
		
			9 Commits
		
	
	
		
			option-sea
			...
			master
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 648d5c956e | |
|  | 8c6ee790bf | |
|  | 9fe596bc01 | |
|  | d926606976 | |
|  | 3d1d2d5acd | |
|  | 58d6170a5c | |
|  | 206dcc46df | |
|  | 58455be58a | |
|  | f0e5b7bb58 | 
|  | @ -1,11 +1,12 @@ | |||
| # Written by Sebastian Lohff <seba@someserver.de> | ||||
| # Licensed under Apache License 2.0 | ||||
| 
 | ||||
| from clintermission.climenu import CliMenu, CliMenuStyle, CliMenuCursor, CliMenuTheme | ||||
| from clintermission.climenu import CliMenu, CliMenuStyle, CliMenuCursor, CliMenuTheme, cli_select_item | ||||
| 
 | ||||
| __all__ = [ | ||||
|     CliMenu, | ||||
|     CliMenuStyle, | ||||
|     CliMenuCursor, | ||||
|     CliMenuTheme, | ||||
|     cli_select_item, | ||||
| ] | ||||
|  |  | |||
|  | @ -3,10 +3,12 @@ | |||
| from prompt_toolkit.application import Application | ||||
| from prompt_toolkit.buffer import Buffer | ||||
| from prompt_toolkit.document import Document | ||||
| from prompt_toolkit.filters import is_searching | ||||
| from prompt_toolkit.key_binding import KeyBindings | ||||
| from prompt_toolkit.layout import Layout, Window, HSplit | ||||
| from prompt_toolkit.layout.controls import BufferControl | ||||
| from prompt_toolkit.layout.processors import Processor, Transformation | ||||
| from prompt_toolkit import search | ||||
| from prompt_toolkit.widgets import SearchToolbar | ||||
| 
 | ||||
| 
 | ||||
|  | @ -66,10 +68,11 @@ class CliMenuTheme: | |||
|     CYAN = CliMenuStyle('cyan', 'lightcyan', 'cyan') | ||||
|     BLUE = CliMenuStyle('ansiblue', 'ansired', 'ansiblue') | ||||
|     ANSI_CYAN = CliMenuStyle('ansicyan', 'ansibrightcyan', 'ansicyan') | ||||
|     BOLD_HIGHLIGHT = CliMenuStyle(header_style='bold', highlight_style='bold fg:black bg:white') | ||||
| 
 | ||||
| 
 | ||||
| class CliMenu: | ||||
|     default_stye = CliMenuTheme.BASIC | ||||
|     default_style = CliMenuTheme.BASIC | ||||
|     default_cursor = CliMenuCursor.TRIANGLE | ||||
| 
 | ||||
|     def __init__(self, options=None, header=None, cursor=None, style=None, | ||||
|  | @ -89,17 +92,17 @@ class CliMenu: | |||
| 
 | ||||
|         self._style = style | ||||
|         if not self._style: | ||||
|             self._style = self.default_stye | ||||
|             self._style = self.default_style | ||||
| 
 | ||||
|         if header: | ||||
|             self.add_header(header, indent=False) | ||||
| 
 | ||||
|         if options: | ||||
|             for option in options: | ||||
|                 if isinstance(option, tuple): | ||||
|                 if isinstance(option, tuple) and len(option) == 2: | ||||
|                     self.add_option(*option) | ||||
|                 else: | ||||
|                     self.add_option(option) | ||||
|                     self.add_option(option, option) | ||||
| 
 | ||||
|     def add_header(self, title, indent=True): | ||||
|         for text in title.split('\n'): | ||||
|  | @ -116,6 +119,13 @@ class CliMenu: | |||
| 
 | ||||
|         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): | ||||
|         if self.success: | ||||
|             item = self._items[self._pos] | ||||
|  | @ -139,6 +149,8 @@ class CliMenu: | |||
|         return ' ' * (len(self._cursor) + 1 * self._dedent_selection) | ||||
| 
 | ||||
|     def _transform_line(self, ti): | ||||
|         if len(list(ti.fragments)) == 0: | ||||
|             return Transformation(ti.fragments) | ||||
|         style, text = list(ti.fragments)[0] | ||||
|         item = self._items[ti.lineno] | ||||
|         s = self._style | ||||
|  | @ -160,7 +172,9 @@ class CliMenu: | |||
|                 indent += ' ' * (self._header_indent + len(self._cursor) + 1) | ||||
|             style = s.header_style | ||||
| 
 | ||||
|         return Transformation([('', indent), (style, prefix + text)]) | ||||
|         items = [(s if s else style, t) for s, t in ti.fragments] | ||||
| 
 | ||||
|         return Transformation([('', indent), (style, prefix)] + items) | ||||
| 
 | ||||
|     def next_item(self, direction): | ||||
|         if not any(item.focusable for item in self._items): | ||||
|  | @ -175,6 +189,38 @@ class CliMenu: | |||
|             if self._items[self._pos].focusable: | ||||
|                 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): | ||||
|         class MenuColorizer(Processor): | ||||
|             def apply_transformation(_self, ti): | ||||
|  | @ -183,40 +229,62 @@ class CliMenu: | |||
|         # keybindings | ||||
|         self._kb = KeyBindings() | ||||
| 
 | ||||
|         @self._kb.add('q') | ||||
|         @self._kb.add('q', filter=~is_searching) | ||||
|         @self._kb.add('c-c') | ||||
|         def quit(event): | ||||
|             event.app.exit() | ||||
| 
 | ||||
|         @self._kb.add('down') | ||||
|         @self._kb.add('j') | ||||
|         @self._kb.add('down', filter=~is_searching) | ||||
|         @self._kb.add('j', filter=~is_searching) | ||||
|         def down(event): | ||||
|             self.next_item(1) | ||||
| 
 | ||||
|         @self._kb.add('up') | ||||
|         @self._kb.add('k') | ||||
|         @self._kb.add('up', filter=~is_searching) | ||||
|         @self._kb.add('k', filter=~is_searching) | ||||
|         def up(event): | ||||
|             self.next_item(-1) | ||||
| 
 | ||||
|         @self._kb.add('right') | ||||
|         @self._kb.add('c-m') | ||||
|         @self._kb.add('c-space') | ||||
|         @self._kb.add('N', filter=~is_searching) | ||||
|         @self._kb.add('n', filter=~is_searching) | ||||
|         def search_inc(event, filter=is_searching): | ||||
|             if not self._bufctrl.search_state.text: | ||||
|                 return | ||||
| 
 | ||||
|             search_dir = 1 if event.data == 'n' else -1 | ||||
|             sr_lines = self._get_search_result_lines() | ||||
|             if sr_lines: | ||||
|                 line = sr_lines[search_dir] if len(sr_lines) > 1 else sr_lines[0] | ||||
|                 self.sync_cursor_to_line(line, search_dir) | ||||
| 
 | ||||
|         @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() | ||||
| 
 | ||||
|         @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)) | ||||
|         self._doc = Document(text, cursor_position=self._pos) | ||||
|         self._buf = Buffer(read_only=True, document=self._doc) | ||||
|         self._bufctrl = BufferControl(self._buf, | ||||
|                                       search_buffer_control=self._searchbar.control, | ||||
|                                       preview_search=True, | ||||
|                                       input_processors=[MenuColorizer()]) | ||||
|         split = HSplit([Window(self._bufctrl, | ||||
|                                wrap_lines=True, | ||||
|                                always_hide_cursor=True)]) | ||||
|                                always_hide_cursor=True), | ||||
|                         self._searchbar]) | ||||
| 
 | ||||
|         # set initial pos | ||||
|         if not self._items[self._pos].focusable: | ||||
|             self.next_item(1) | ||||
|         self.sync_cursor_to_line(0) | ||||
| 
 | ||||
|         app = Application(layout=Layout(split), | ||||
|                           key_bindings=self._kb, | ||||
|  | @ -225,3 +293,18 @@ class CliMenu: | |||
| 
 | ||||
|         app.run() | ||||
|         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() | ||||
|  |  | |||
|  | @ -0,0 +1,16 @@ | |||
| #!/usr/bin/env python3 | ||||
| from clintermission import CliMenu, CliMenuTheme | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     q = ["Foo", "Bar", "Baz baz baz baz baz"] | ||||
|     m = CliMenu(q, "Time to choose:\n", style=CliMenuTheme.BOLD_HIGHLIGHT) | ||||
| 
 | ||||
|     if m.success: | ||||
|         print("You selected", m.get_selection()) | ||||
|     else: | ||||
|         print("You aborted the selection") | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
		Loading…
	
		Reference in New Issue