Source code for default_profile.startup.bottom_toolbar
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Draw a toolbar for the shell using prompt_toolkit.
Takes into consideration whether Emacs mode or Vi mode is set
and adds :kbd:`F4` as a keybindings to toggle between each.
TODO: currently initialize a titlebar, an exit button and a few
other things that aren't utilized at all.
"""
import sys
import textwrap
import time
from datetime import date
from pathlib import Path
from shutil import get_terminal_size
from typing import Dict, List, Any, AnyStr, Optional
import prompt_toolkit
from IPython.core.getipython import get_ipython
from IPython.core.interactiveshell import InteractiveShell
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.formatted_text import (
PygmentsTokens,
to_formatted_text,
)
from prompt_toolkit.shortcuts.utils import print_container
from prompt_toolkit.styles import default_pygments_style, style_from_pygments_cls
from prompt_toolkit.styles import merge_styles
from prompt_toolkit.widgets import Frame, TextArea
from pygments.formatters.terminal256 import TerminalTrueColorFormatter
from pygments.lexers.python import PythonLexer
from pygments.token import Token
try:
from gruvbox import GruvboxStyle
except ImportError:
from pygments.styles.inkpot import InkPotStyle
pygments_style = InkPotStyle
else:
pygments_style = GruvboxStyle
[docs]def get_app(shell=None) -> prompt_toolkit.application.Application:
"""A patch to cover up the fact that get_app() returns a DummyApplication."""
shell = shell if shell is not None else get_ipython()
if hasattr(shell, "pt_app"):
if hasattr(shell.pt_app, "app"):
return shell.pt_app.app
[docs]def exit_clicked():
"""Exit from the prompt_toolkit side of things."""
get_app().exit(result=False, exception=EOFError)
[docs]def init_style() -> prompt_toolkit.styles.Style:
"""Merges the styles from default_pygments_style and the previously imported `pygments_style`."""
return merge_styles(
[style_from_pygments_cls(pygments_style), default_pygments_style()]
)
[docs]def show_header(header_text: Optional[AnyStr] = None) -> prompt_toolkit.widgets.Frame:
if header_text is None:
header_text = textwrap.dedent(str(
"Press Control-Y to paste from the system _clipboard.\n"
"Press Control-Space or Control-@ to enter selection mode.\n"
"Press Control-W to cut to _clipboard.\n"
))
text_area = TextArea(header_text, style="#ebdbb2")
return Frame(text_area)
[docs]def terminal_width() -> int:
"""Returns `shutil.get_terminal_size.columns`."""
return get_terminal_size().columns
[docs]class LineCounter:
"""Simple counter inspired by Doug Hellman. Could set it to sys.displayhook.
:URL: https://pymotw.com/3/sys/interpreter.html
"""
[docs] def display(self):
self.count += 1
ret = [
Token.String.Subheading,
f"< In[{self.count:3d}]:",
[Token.Literal, f"Time:{self.time}"],
]
return ret
def __call__(self):
"""Yes!!! This now behaves as expected."""
return self.display()
def __repr__(self) -> AnyStr:
return f"<{self.__class__.__name__}:> {self.__call__}"
def __pt_formatted_text__(self):
"""A list of ``(style, text)`` tuples.
(In some situations, this can also be ``(style, text, mouse_handler)``
tuples.)
"""
return self.display()
[docs]class BottomToolbar:
"""Display the current input mode.
As the bottom_toolbar property exists in both a prompt_toolkit
PromptSession and Application, both are accessible from the `session`
and `pt_app` attributes.
Defines a method :meth:`rerender` and calls it whenever the instance
is called via ``__call__``.
"""
shell: InteractiveShell
# are you allowed to doctest fstrings
[docs] def __init__(
self, _style: Optional[prompt_toolkit.styles.Style] = None, *args: List, **kwargs: Dict
):
"""Require an 'app' for initialization.
This will eliminate all IPython code out of this class and make things
a little more modular for the tests.
"""
self.shell = kwargs.pop("shell", None) if kwargs.pop("shell", None) is not None else get_ipython()
self.app = get_app()
self.PythonLexer = PythonLexer()
self.Formatter = TerminalTrueColorFormatter()
self._style = _style if _style is not None else self.app.style
[docs] @property
def session(self) -> prompt_toolkit.shortcuts.PromptSession:
return self.shell.pt_app
[docs] @property
def is_vi_mode(self) -> prompt_toolkit.enums.EditingMode:
if self.app.editing_mode == EditingMode.VI:
return True
else:
return False
def __str__(self):
return f"<{self.__class__.__name__!s}:>"
def __iter__(self):
for i in self.rerender():
yield i
def __repr__(self):
return f"<{self.__class__.__name__!r}:>"
def __call__(self):
return f"{self.rerender()}"
[docs] @style.setter
def reset_style(self, new_style: prompt_toolkit.styles.Style):
# do these function names even show up in `dir`?
self._style = new_style
def __len__(self):
"""The length of the text we display."""
return len(self.rerender())
[docs] def full_width(self) -> bool:
"""Bool indicating bottom toolbar == shutil.get_terminal_size().columns."""
return len(self) == terminal_width()
[docs] def rerender(self) -> AnyStr:
"""Render the toolbar at the bottom for prompt_toolkit.
.. warning::
Simple reminder about the difference between running an
expression and returning one.
If you accidentally forget the `return` keyword, nothing will
display. That's all.
"""
if self.is_vi_mode:
toolbar = PygmentsTokens(self._render_vi())
else:
toolbar = PygmentsTokens(self._render_emacs())
return to_formatted_text(toolbar)
def _render_vi(self):
current_vi_mode = self.app.vi_state.input_mode
_toolbar = [
(Token.Keyword, f" [F4] {self.app.editing_mode!r} "),
(Token.String.Heading, f" {current_vi_mode!r} "),
(Token.Name.Tag, f" cwd: {Path.cwd().stem!r} "),
(Token.Number.Integer, f" Clock: {time.ctime()!r} "),
]
# how do i fill all this dead space?
# remaining_space = terminal_width() - len(self)
# _toolbar.append((Token.Operator, remaining_space * " "))
# This crashes in a seemingly random spot and the whole interpreter
# dies
return _toolbar
def _render_emacs(self):
toolbar = f" [F4] {self.app.editing_mode}: {Path.cwd()!r} {date.today()!a}"
return toolbar
def __pt_formatted_text__(self):
"""A list of ``(style, text)`` tuples.
(In some situations, this can also be ``(style, text, mouse_handler)``
tuples.)
"""
return self.rerender()
[docs]def add_toolbar(toolbar=None):
"""Get the running IPython instance and add 'bottom_toolbar'."""
_ip = get_ipython()
if _ip is not None:
if hasattr(_ip, "pt_app"):
if _ip.pt_app.bottom_toolbar is None:
_ip.pt_app.bottom_toolbar = toolbar
if __name__ == "__main__":
bottom_text = BottomToolbar(_style=pygments_style)
add_toolbar(bottom_text)
print_container(show_header())
# TODO:
# partial_window = Window(width=60, height=3, style=pygments_style)