No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

climenu.py 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. # Written by Sebastian Lohff <seba@someserver.de>
  2. # Licensed under Apache License 2.0
  3. from prompt_toolkit.application import Application
  4. from prompt_toolkit.layout import Layout, Window, HSplit
  5. from prompt_toolkit.buffer import Buffer
  6. from prompt_toolkit.layout.controls import BufferControl
  7. from prompt_toolkit.key_binding import KeyBindings
  8. from prompt_toolkit.document import Document
  9. from prompt_toolkit.layout.processors import Processor, Transformation
  10. class CliMenuHeader:
  11. """Hold a menu header"""
  12. def __init__(self, text, indent=False):
  13. self.text = text
  14. self.indent = indent
  15. self.focusable = False
  16. class CliMenuOption:
  17. """Hold a menu option"""
  18. def __init__(self, text, num, item=None):
  19. self.text = text
  20. self.num = num
  21. self.item = item
  22. self.focusable = True
  23. class CliMenuCursor:
  24. """Collection of cursors pointing at the active menu item"""
  25. BULLET = '●'
  26. TRIANGLE = '▶'
  27. CLI_STAR = '*'
  28. CLI_ARROW = '-->'
  29. CLI_CAT = '=^.^='
  30. CAT = '😸'
  31. ARROW = '→'
  32. class CliMenuStyle:
  33. """Style for a menu
  34. Allows to select header, option and selected option color
  35. """
  36. def __init__(self, option_style='', highlight_style='', header_style=''):
  37. self.option_style = option_style
  38. self.highlight_style = highlight_style
  39. self.header_style = header_style
  40. # self.option_color = '#aa0000'
  41. # #self.highlight_color = 'fg:ansiblue bg:ansired bold'
  42. # self.highlight_color = 'bold'
  43. # self.cursor = ' --> '
  44. # self.cursor = ' ● '
  45. # self.no_cursor = ' '
  46. # self.header_color = '#aa22cc'
  47. # self.option_indent = 4
  48. # self.header_indent = 4
  49. class CliMenuTheme:
  50. BASIC = CliMenuStyle()
  51. BASIC_BOLD = CliMenuStyle(header_style='bold', highlight_style='bold')
  52. RED = CliMenuStyle('#aa0000', '#ee0000', '#aa0000')
  53. CYAN = CliMenuStyle('cyan', 'lightcyan', 'cyan')
  54. BLUE = CliMenuStyle('ansiblue', 'ansired', 'ansiblue')
  55. ANSI_CYAN = CliMenuStyle('ansicyan', 'ansibrightcyan', 'ansicyan')
  56. class CliMenu:
  57. default_stye = CliMenuTheme.BASIC
  58. default_cursor = CliMenuCursor.TRIANGLE
  59. def __init__(self, options=None, header=None, cursor=None, style=None,
  60. indent=2, dedent_selection=False):
  61. self._items = []
  62. self._item_num = 0
  63. self._ran = False
  64. self._success = None
  65. self._pos = 0
  66. self._option_indent = indent
  67. self._header_indent = indent
  68. self._dedent_selection = dedent_selection
  69. self._cursor = cursor
  70. if not self._cursor:
  71. self._cursor = self.default_cursor
  72. self._style = style
  73. if not self._style:
  74. self._style = self.default_stye
  75. if header:
  76. self.add_header(header)
  77. if options:
  78. for option in options:
  79. self.add_option(option)
  80. def add_header(self, title, indent=False):
  81. for text in title.split('\n'):
  82. self._items.append(CliMenuHeader(text))
  83. def add_option(self, text, item=None):
  84. self._items.append(CliMenuOption(text, self._item_num, item=item))
  85. self._item_num += 1
  86. def get_selection(self):
  87. if not self._ran:
  88. self._run()
  89. item = self._items[self._pos]
  90. return (item.num, item.item)
  91. def get_selection_num(self):
  92. return self.get_selection()[0]
  93. def get_selection_item(self):
  94. return self.get_selection()[1]
  95. def cursor(self):
  96. return '{} '.format(self._cursor)
  97. @property
  98. def no_cursor(self):
  99. # cursor with spaces minus dedent
  100. return ' ' * (len(self._cursor) + 1 * self._dedent_selection)
  101. def _transform_line(self, ti):
  102. style, text = list(ti.fragments)[0]
  103. item = self._items[ti.lineno]
  104. s = self._style
  105. # cursor
  106. indent = ''
  107. prefix = ''
  108. if item.focusable:
  109. indent += ' ' * self._option_indent
  110. if ti.lineno == self._pos:
  111. prefix += '{} '.format(self._cursor)
  112. style = s.highlight_style
  113. else:
  114. prefix += ' ' * (len(self._cursor) + 1 + 1 * self._dedent_selection)
  115. style = s.option_style
  116. else:
  117. if item.indent:
  118. indent += ' ' * self._header_indent
  119. style = s.header_style
  120. return Transformation([('', indent), (style, prefix + text)])
  121. def next_item(self, direction):
  122. if not any(item.focusable for item in self._items):
  123. raise RuntimeError("No focusable item found")
  124. while True:
  125. self._pos = (self._pos + direction) % len(self._items)
  126. if self._items[self._pos].focusable:
  127. break
  128. def _run(self):
  129. class MenuColorizer(Processor):
  130. def apply_transformation(_self, ti):
  131. return self._transform_line(ti)
  132. # keybindings
  133. self._kb = KeyBindings()
  134. @self._kb.add('q')
  135. @self._kb.add('c-c')
  136. def quit(event):
  137. event.app.exit()
  138. @self._kb.add('down')
  139. def down(event):
  140. self.next_item(1)
  141. @self._kb.add('up')
  142. def up(event):
  143. self.next_item(-1)
  144. @self._kb.add('right')
  145. @self._kb.add('c-m')
  146. @self._kb.add('c-space')
  147. def accept(event):
  148. self._success = True
  149. event.app.exit()
  150. # set initial pos
  151. if not self._items[self._pos].focusable:
  152. self.next_item(1)
  153. text = '\n'.join(map(lambda _x: _x.text, self._items))
  154. self._doc = Document(text, cursor_position=self._pos)
  155. self._buf = Buffer(read_only=True, document=self._doc)
  156. self._bufctrl = BufferControl(self._buf,
  157. input_processors=[MenuColorizer()])
  158. split = HSplit([Window(self._bufctrl,
  159. wrap_lines=True,
  160. always_hide_cursor=True)])
  161. app = Application(layout=Layout(split),
  162. key_bindings=self._kb,
  163. full_screen=False,
  164. mouse_support=False)
  165. app.run()
  166. self._ran = True