margins.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. """
  2. Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`.
  3. """
  4. from __future__ import unicode_literals
  5. from abc import ABCMeta, abstractmethod
  6. from six import with_metaclass
  7. from six.moves import range
  8. from prompt_toolkit.filters import to_cli_filter
  9. from prompt_toolkit.token import Token
  10. from prompt_toolkit.utils import get_cwidth
  11. from .utils import token_list_to_text
  12. __all__ = (
  13. 'Margin',
  14. 'NumberredMargin',
  15. 'ScrollbarMargin',
  16. 'ConditionalMargin',
  17. 'PromptMargin',
  18. )
  19. class Margin(with_metaclass(ABCMeta, object)):
  20. """
  21. Base interface for a margin.
  22. """
  23. @abstractmethod
  24. def get_width(self, cli, get_ui_content):
  25. """
  26. Return the width that this margin is going to consume.
  27. :param cli: :class:`.CommandLineInterface` instance.
  28. :param get_ui_content: Callable that asks the user control to create
  29. a :class:`.UIContent` instance. This can be used for instance to
  30. obtain the number of lines.
  31. """
  32. return 0
  33. @abstractmethod
  34. def create_margin(self, cli, window_render_info, width, height):
  35. """
  36. Creates a margin.
  37. This should return a list of (Token, text) tuples.
  38. :param cli: :class:`.CommandLineInterface` instance.
  39. :param window_render_info:
  40. :class:`~prompt_toolkit.layout.containers.WindowRenderInfo`
  41. instance, generated after rendering and copying the visible part of
  42. the :class:`~prompt_toolkit.layout.controls.UIControl` into the
  43. :class:`~prompt_toolkit.layout.containers.Window`.
  44. :param width: The width that's available for this margin. (As reported
  45. by :meth:`.get_width`.)
  46. :param height: The height that's available for this margin. (The height
  47. of the :class:`~prompt_toolkit.layout.containers.Window`.)
  48. """
  49. return []
  50. class NumberredMargin(Margin):
  51. """
  52. Margin that displays the line numbers.
  53. :param relative: Number relative to the cursor position. Similar to the Vi
  54. 'relativenumber' option.
  55. :param display_tildes: Display tildes after the end of the document, just
  56. like Vi does.
  57. """
  58. def __init__(self, relative=False, display_tildes=False):
  59. self.relative = to_cli_filter(relative)
  60. self.display_tildes = to_cli_filter(display_tildes)
  61. def get_width(self, cli, get_ui_content):
  62. line_count = get_ui_content().line_count
  63. return max(3, len('%s' % line_count) + 1)
  64. def create_margin(self, cli, window_render_info, width, height):
  65. relative = self.relative(cli)
  66. token = Token.LineNumber
  67. token_current = Token.LineNumber.Current
  68. # Get current line number.
  69. current_lineno = window_render_info.ui_content.cursor_position.y
  70. # Construct margin.
  71. result = []
  72. last_lineno = None
  73. for y, lineno in enumerate(window_render_info.displayed_lines):
  74. # Only display line number if this line is not a continuation of the previous line.
  75. if lineno != last_lineno:
  76. if lineno is None:
  77. pass
  78. elif lineno == current_lineno:
  79. # Current line.
  80. if relative:
  81. # Left align current number in relative mode.
  82. result.append((token_current, '%i' % (lineno + 1)))
  83. else:
  84. result.append((token_current, ('%i ' % (lineno + 1)).rjust(width)))
  85. else:
  86. # Other lines.
  87. if relative:
  88. lineno = abs(lineno - current_lineno) - 1
  89. result.append((token, ('%i ' % (lineno + 1)).rjust(width)))
  90. last_lineno = lineno
  91. result.append((Token, '\n'))
  92. # Fill with tildes.
  93. if self.display_tildes(cli):
  94. while y < window_render_info.window_height:
  95. result.append((Token.Tilde, '~\n'))
  96. y += 1
  97. return result
  98. class ConditionalMargin(Margin):
  99. """
  100. Wrapper around other :class:`.Margin` classes to show/hide them.
  101. """
  102. def __init__(self, margin, filter):
  103. assert isinstance(margin, Margin)
  104. self.margin = margin
  105. self.filter = to_cli_filter(filter)
  106. def get_width(self, cli, ui_content):
  107. if self.filter(cli):
  108. return self.margin.get_width(cli, ui_content)
  109. else:
  110. return 0
  111. def create_margin(self, cli, window_render_info, width, height):
  112. if width and self.filter(cli):
  113. return self.margin.create_margin(cli, window_render_info, width, height)
  114. else:
  115. return []
  116. class ScrollbarMargin(Margin):
  117. """
  118. Margin displaying a scrollbar.
  119. :param display_arrows: Display scroll up/down arrows.
  120. """
  121. def __init__(self, display_arrows=False):
  122. self.display_arrows = to_cli_filter(display_arrows)
  123. def get_width(self, cli, ui_content):
  124. return 1
  125. def create_margin(self, cli, window_render_info, width, height):
  126. total_height = window_render_info.content_height
  127. display_arrows = self.display_arrows(cli)
  128. window_height = window_render_info.window_height
  129. if display_arrows:
  130. window_height -= 2
  131. try:
  132. items_per_row = float(total_height) / min(total_height, window_height)
  133. except ZeroDivisionError:
  134. return []
  135. else:
  136. def is_scroll_button(row):
  137. " True if we should display a button on this row. "
  138. current_row_middle = int((row + .5) * items_per_row)
  139. return current_row_middle in window_render_info.displayed_lines
  140. # Up arrow.
  141. result = []
  142. if display_arrows:
  143. result.extend([
  144. (Token.Scrollbar.Arrow, '^'),
  145. (Token.Scrollbar, '\n')
  146. ])
  147. # Scrollbar body.
  148. for i in range(window_height):
  149. if is_scroll_button(i):
  150. result.append((Token.Scrollbar.Button, ' '))
  151. else:
  152. result.append((Token.Scrollbar, ' '))
  153. result.append((Token, '\n'))
  154. # Down arrow
  155. if display_arrows:
  156. result.append((Token.Scrollbar.Arrow, 'v'))
  157. return result
  158. class PromptMargin(Margin):
  159. """
  160. Create margin that displays a prompt.
  161. This can display one prompt at the first line, and a continuation prompt
  162. (e.g, just dots) on all the following lines.
  163. :param get_prompt_tokens: Callable that takes a CommandLineInterface as
  164. input and returns a list of (Token, type) tuples to be shown as the
  165. prompt at the first line.
  166. :param get_continuation_tokens: Callable that takes a CommandLineInterface
  167. and a width as input and returns a list of (Token, type) tuples for the
  168. next lines of the input.
  169. :param show_numbers: (bool or :class:`~prompt_toolkit.filters.CLIFilter`)
  170. Display line numbers instead of the continuation prompt.
  171. """
  172. def __init__(self, get_prompt_tokens, get_continuation_tokens=None,
  173. show_numbers=False):
  174. assert callable(get_prompt_tokens)
  175. assert get_continuation_tokens is None or callable(get_continuation_tokens)
  176. show_numbers = to_cli_filter(show_numbers)
  177. self.get_prompt_tokens = get_prompt_tokens
  178. self.get_continuation_tokens = get_continuation_tokens
  179. self.show_numbers = show_numbers
  180. def get_width(self, cli, ui_content):
  181. " Width to report to the `Window`. "
  182. # Take the width from the first line.
  183. text = token_list_to_text(self.get_prompt_tokens(cli))
  184. return get_cwidth(text)
  185. def create_margin(self, cli, window_render_info, width, height):
  186. # First line.
  187. tokens = self.get_prompt_tokens(cli)[:]
  188. # Next lines. (Show line numbering when numbering is enabled.)
  189. if self.get_continuation_tokens:
  190. # Note: we turn this into a list, to make sure that we fail early
  191. # in case `get_continuation_tokens` returns something else,
  192. # like `None`.
  193. tokens2 = list(self.get_continuation_tokens(cli, width))
  194. else:
  195. tokens2 = []
  196. show_numbers = self.show_numbers(cli)
  197. last_y = None
  198. for y in window_render_info.displayed_lines[1:]:
  199. tokens.append((Token, '\n'))
  200. if show_numbers:
  201. if y != last_y:
  202. tokens.append((Token.LineNumber, ('%i ' % (y + 1)).rjust(width)))
  203. else:
  204. tokens.extend(tokens2)
  205. last_y = y
  206. return tokens