1import code
2import contextlib
3import errno
4import itertools
5import logging
6import os
7import re
8import signal
9import subprocess
10import sys
11import tempfile
12import time
13import unicodedata
14from enum import Enum
15from types import TracebackType
16from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type
17from typing_extensions import Literal
18
19import blessings
20import greenlet
21from curtsies import (
22    FSArray,
23    fmtstr,
24    FmtStr,
25    Termmode,
26    fmtfuncs,
27    events,
28    __version__ as curtsies_version,
29)
30from curtsies.configfile_keynames import keymap as key_dispatch
31from curtsies.input import is_main_thread
32from cwcwidth import wcswidth
33from pygments import format as pygformat
34from pygments.formatters import TerminalFormatter
35from pygments.lexers import Python3Lexer
36
37from . import events as bpythonevents, sitefix, replpainter as paint
38from ..config import Config
39from .coderunner import (
40    CodeRunner,
41    FakeOutput,
42)
43from .filewatch import ModuleChangedEventHandler
44from .interaction import StatusBar
45from .interpreter import (
46    Interp,
47    code_finished_will_parse,
48)
49from .manual_readline import edit_keys
50from .parse import parse as bpythonparse, func_for_letter, color_for_letter
51from .preprocess import preprocess
52from .. import __version__
53from ..config import getpreferredencoding
54from ..formatter import BPythonFormatter
55from ..pager import get_pager_command
56from ..repl import (
57    Repl,
58    SourceNotFound,
59)
60from ..translations import _
61
62logger = logging.getLogger(__name__)
63
64INCONSISTENT_HISTORY_MSG = "#<---History inconsistent with output shown--->"
65CONTIGUITY_BROKEN_MSG = "#<---History contiguity broken by rewind--->"
66EXAMPLE_CONFIG_URL = "https://raw.githubusercontent.com/bpython/bpython/master/bpython/sample-config"
67EDIT_SESSION_HEADER = """### current bpython session - make changes and save to reevaluate session.
68### lines beginning with ### will be ignored.
69### To return to bpython without reevaluating make no changes to this file
70### or save an empty file.
71"""
72
73# more than this many events will be assumed to be a true paste event,
74# i.e. control characters like '<Ctrl-a>' will be stripped
75MAX_EVENTS_POSSIBLY_NOT_PASTE = 20
76
77
78class LineType(Enum):
79    """Used when adding a tuple to all_logical_lines, to get input / output values
80    having to actually type/know the strings"""
81
82    INPUT = "input"
83    OUTPUT = "output"
84
85
86class FakeStdin:
87    """The stdin object user code will reference
88
89    In user code, sys.stdin.read() asks the user for interactive input,
90    so this class returns control to the UI to get that input."""
91
92    def __init__(self, coderunner, repl, configured_edit_keys=None):
93        self.coderunner = coderunner
94        self.repl = repl
95        self.has_focus = False  # whether FakeStdin receives keypress events
96        self.current_line = ""
97        self.cursor_offset = 0
98        self.old_num_lines = 0
99        self.readline_results = []
100        if configured_edit_keys:
101            self.rl_char_sequences = configured_edit_keys
102        else:
103            self.rl_char_sequences = edit_keys
104
105    def process_event(self, e: Union[events.Event, str]) -> None:
106        assert self.has_focus
107
108        logger.debug("fake input processing event %r", e)
109        if isinstance(e, events.PasteEvent):
110            for ee in e.events:
111                if ee not in self.rl_char_sequences:
112                    self.add_input_character(ee)
113        elif e in self.rl_char_sequences:
114            self.cursor_offset, self.current_line = self.rl_char_sequences[e](
115                self.cursor_offset, self.current_line
116            )
117        elif isinstance(e, events.SigIntEvent):
118            self.coderunner.sigint_happened_in_main_context = True
119            self.has_focus = False
120            self.current_line = ""
121            self.cursor_offset = 0
122            self.repl.run_code_and_maybe_finish()
123        elif e in ("<ESC>",):
124            pass
125        elif e in ("<Ctrl-d>",):
126            if self.current_line == "":
127                self.repl.send_to_stdin("\n")
128                self.has_focus = False
129                self.current_line = ""
130                self.cursor_offset = 0
131                self.repl.run_code_and_maybe_finish(for_code="")
132            else:
133                pass
134        elif e in ("\n", "\r", "<Ctrl-j>", "<Ctrl-m>"):
135            line = self.current_line
136            self.repl.send_to_stdin(line + "\n")
137            self.has_focus = False
138            self.current_line = ""
139            self.cursor_offset = 0
140            self.repl.run_code_and_maybe_finish(for_code=line + "\n")
141        else:  # add normal character
142            self.add_input_character(e)
143
144        if self.current_line.endswith(("\n", "\r")):
145            pass
146        else:
147            self.repl.send_to_stdin(self.current_line)
148
149    def add_input_character(self, e):
150        if e in ("<SPACE>",):
151            e = " "
152        if e.startswith("<") and e.endswith(">"):
153            return
154        assert len(e) == 1, "added multiple characters: %r" % e
155        logger.debug("adding normal char %r to current line", e)
156
157        c = e
158        self.current_line = (
159            self.current_line[: self.cursor_offset]
160            + c
161            + self.current_line[self.cursor_offset :]
162        )
163        self.cursor_offset += 1
164
165    def readline(self):
166        self.has_focus = True
167        self.repl.send_to_stdin(self.current_line)
168        value = self.coderunner.request_from_main_context()
169        self.readline_results.append(value)
170        return value
171
172    def readlines(self, size=-1):
173        return list(iter(self.readline, ""))
174
175    def __iter__(self):
176        return iter(self.readlines())
177
178    def isatty(self):
179        return True
180
181    def flush(self):
182        """Flush the internal buffer. This is a no-op. Flushing stdin
183        doesn't make any sense anyway."""
184
185    def write(self, value):
186        # XXX IPython expects sys.stdin.write to exist, there will no doubt be
187        # others, so here's a hack to keep them happy
188        raise OSError(errno.EBADF, "sys.stdin is read-only")
189
190    def close(self):
191        # hack to make closing stdin a nop
192        # This is useful for multiprocessing.Process, which does work
193        # for the most part, although output from other processes is
194        # discarded.
195        pass
196
197    @property
198    def encoding(self):
199        return sys.__stdin__.encoding
200
201    # TODO write a read() method?
202
203
204class ReevaluateFakeStdin:
205    """Stdin mock used during reevaluation (undo) so raw_inputs don't have to
206    be reentered"""
207
208    def __init__(self, fakestdin, repl):
209        self.fakestdin = fakestdin
210        self.repl = repl
211        self.readline_results = fakestdin.readline_results[:]
212
213    def readline(self):
214        if self.readline_results:
215            value = self.readline_results.pop(0)
216        else:
217            value = "no saved input available"
218        self.repl.send_to_stdouterr(value)
219        return value
220
221
222class ImportLoader:
223    """Wrapper for module loaders to watch their paths with watchdog."""
224
225    def __init__(self, watcher, loader):
226        self.watcher = watcher
227        self.loader = loader
228
229    def __getattr__(self, name):
230        if name == "create_module" and hasattr(self.loader, name):
231            return self._create_module
232        if name == "load_module" and hasattr(self.loader, name):
233            return self._load_module
234        return getattr(self.loader, name)
235
236    def _create_module(self, spec):
237        spec = self.loader.create_module(spec)
238        if (
239            getattr(spec, "origin", None) is not None
240            and spec.origin != "builtin"
241        ):
242            self.watcher.track_module(spec.origin)
243        return spec
244
245    def _load_module(self, name):
246        module = self.loader.load_module(name)
247        if hasattr(module, "__file__"):
248            self.watcher.track_module(module.__file__)
249        return module
250
251
252class ImportFinder:
253    """Wrapper for finders in sys.meta_path to replace wrap all loaders with ImportLoader."""
254
255    def __init__(self, finder, watcher):
256        self.watcher = watcher
257        self.finder = finder
258
259    def __getattr__(self, name):
260        if name == "find_spec" and hasattr(self.finder, name):
261            return self._find_spec
262        if name == "find_module" and hasattr(self.finder, name):
263            return self._find_module
264        return getattr(self.finder, name)
265
266    def _find_spec(self, fullname, path, target=None):
267        # Attempt to find the spec
268        spec = self.finder.find_spec(fullname, path, target)
269        if spec is not None:
270            if getattr(spec, "__loader__", None) is not None:
271                # Patch the loader to enable reloading
272                spec.__loader__ = ImportLoader(self.watcher, spec.__loader__)
273        return spec
274
275    def _find_module(self, fullname, path=None):
276        loader = self.finder.find_module(fullname, path)
277        if loader is not None:
278            return ImportLoader(self.watcher, loader)
279
280
281def _process_ps(ps, default_ps: str):
282    """Replace ps1/ps2 with the default if the user specified value contains control characters."""
283    if not isinstance(ps, str):
284        return ps
285
286    return ps if wcswidth(ps) >= 0 else default_ps
287
288
289class BaseRepl(Repl):
290    """Python Repl
291
292    Reacts to events like
293     - terminal dimensions and change events
294     - keystrokes
295    Behavior altered by
296     - number of scroll downs that were necessary to render array after each
297       display
298     - initial cursor position
299    outputs:
300     - 2D array to be rendered
301
302    BaseRepl is mostly view-independent state of Repl - but self.width and
303    self.height are important for figuring out how to wrap lines for example.
304    Usually self.width and self.height should be set by receiving a window
305    resize event, not manually set to anything - as long as the first event
306    received is a window resize event, this works fine.
307
308    Subclasses are responsible for implementing several methods.
309    """
310
311    def __init__(
312        self,
313        config: Config,
314        locals_: Dict[str, Any] = None,
315        banner: str = None,
316        interp: code.InteractiveInterpreter = None,
317        orig_tcattrs: List[Any] = None,
318    ):
319        """
320        locals_ is a mapping of locals to pass into the interpreter
321        config is a bpython config.Struct with config attributes
322        banner is a string to display briefly in the status bar
323        interp is an interpreter instance to use
324        original terminal state, useful for shelling out with normal terminal
325        """
326
327        logger.debug("starting init")
328
329        # If creating a new interpreter on undo would be unsafe because initial
330        # state was passed in
331        self.weak_rewind = bool(locals_ or interp)
332
333        if interp is None:
334            interp = Interp(locals=locals_)
335            interp.write = self.send_to_stdouterr  # type: ignore
336        if banner is None:
337            if config.help_key:
338                banner = (
339                    _("Welcome to bpython!")
340                    + " "
341                    + _("Press <%s> for help.") % config.help_key
342                )
343            else:
344                banner = None
345        if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1:
346            config.cli_suggestion_width = 1
347
348        self.reevaluating = False
349        self.fake_refresh_requested = False
350
351        self.status_bar = StatusBar(
352            config,
353            "",
354            request_refresh=self.request_refresh,
355            schedule_refresh=self.schedule_refresh,
356        )
357        self.edit_keys = edit_keys.mapping_with_config(config, key_dispatch)
358        logger.debug("starting parent init")
359        super().__init__(interp, config)
360
361        self.formatter = BPythonFormatter(config.color_scheme)
362
363        # overwriting what bpython.Repl put there
364        # interact is called to interact with the status bar,
365        # so we're just using the same object
366        self.interact = self.status_bar
367
368        # logical line currently being edited, without ps1 (usually '>>> ')
369        self._current_line = ""
370
371        # current line of output - stdout and stdin go here
372        self.current_stdouterr_line = ""  # Union[str, FmtStr]
373
374        # this is every line that's been displayed (input and output)
375        # as with formatting applied. Logical lines that exceeded the terminal width
376        # at the time of output are split across multiple entries in this list.
377        self.display_lines: List[FmtStr] = []
378
379        # this is every line that's been executed; it gets smaller on rewind
380        self.history = []
381
382        # This is every logical line that's been displayed, both input and output.
383        # Like self.history, lines are unwrapped, uncolored, and without prompt.
384        # Entries are tuples, where
385        #   - the first element the line (string, not fmtsr)
386        #   - the second element is one of 2 global constants: "input" or "output"
387        #     (use LineType.INPUT or LineType.OUTPUT to avoid typing these strings)
388        self.all_logical_lines: List[Tuple[str, LineType]] = []
389
390        # formatted version of lines in the buffer kept around so we can
391        # unhighlight parens using self.reprint_line as called by bpython.Repl
392        self.display_buffer: List[FmtStr] = []
393
394        # how many times display has been scrolled down
395        # because there wasn't room to display everything
396        self.scroll_offset = 0
397
398        # cursor position relative to start of current_line, 0 is first char
399        self._cursor_offset = 0
400
401        self.orig_tcattrs: Optional[List[Any]] = orig_tcattrs
402
403        self.coderunner = CodeRunner(self.interp, self.request_refresh)
404
405        # filenos match the backing device for libs that expect it,
406        # but writing to them will do weird things to the display
407        self.stdout = FakeOutput(
408            self.coderunner,
409            self.send_to_stdouterr,
410            real_fileobj=sys.__stdout__,
411        )
412        self.stderr = FakeOutput(
413            self.coderunner,
414            self.send_to_stdouterr,
415            real_fileobj=sys.__stderr__,
416        )
417        self.stdin = FakeStdin(self.coderunner, self, self.edit_keys)
418
419        # next paint should clear screen
420        self.request_paint_to_clear_screen = False
421
422        self.request_paint_to_pad_bottom = 0
423
424        # offscreen command yields results different from scrollback buffer
425        self.inconsistent_history = False
426
427        # history error message has already been displayed
428        self.history_already_messed_up = False
429
430        # some commands act differently based on the prev event
431        # this list doesn't include instances of event.Event,
432        # only keypress-type events (no refresh screen events etc.)
433        self.last_events: List[Optional[str]] = [None] * 50
434
435        # displays prev events in a column on the right hand side
436        self.presentation_mode = False
437
438        self.paste_mode = False
439        self.current_match = None
440        self.list_win_visible = False
441        # whether auto reloading active
442        self.watching_files = config.default_autoreload
443
444        # 'reverse_incremental_search', 'incremental_search' or None
445        self.incr_search_mode = None
446
447        self.incr_search_target = ""
448
449        self.original_modules = set(sys.modules.keys())
450
451        # as long as the first event received is a window resize event,
452        # this works fine...
453        self.width: int = cast(int, None)
454        self.height: int = cast(int, None)
455
456        self.status_bar.message(banner)
457
458        self.watcher = ModuleChangedEventHandler([], self.request_reload)
459        if self.watcher and config.default_autoreload:
460            self.watcher.activate()
461
462    # The methods below should be overridden, but the default implementations
463    # below can be used as well.
464
465    def get_cursor_vertical_diff(self):
466        """Return how the cursor moved due to a window size change"""
467        return 0
468
469    def get_top_usable_line(self):
470        """Return the top line of display that can be rewritten"""
471        return 0
472
473    def get_term_hw(self):
474        """Returns the current width and height of the display area."""
475        return (50, 10)
476
477    def _schedule_refresh(self, when: float):
478        """Arrange for the bpython display to be refreshed soon.
479
480        This method will be called when the Repl wants the display to be
481        refreshed at a known point in the future, and as such it should
482        interrupt a pending request to the user for input.
483
484        Because the worst-case effect of not refreshing
485        is only having an out of date UI until the user enters input, a
486        default NOP implementation is provided."""
487
488    # The methods below must be overridden in subclasses.
489
490    def _request_refresh(self):
491        """Arrange for the bpython display to be refreshed soon.
492
493        This method will be called when the Repl wants to refresh the display,
494        but wants control returned to it afterwards. (it is assumed that simply
495        returning from process_event will cause an event refresh)
496
497        The very next event received by process_event should be a
498        RefreshRequestEvent."""
499        raise NotImplementedError
500
501    def _request_reload(self, files_modified=("?",)):
502        """Like request_refresh, but for reload requests events."""
503        raise NotImplementedError
504
505    def request_undo(self, n=1):
506        """Like request_refresh, but for undo request events."""
507        raise NotImplementedError
508
509    def on_suspend(self):
510        """Will be called on sigtstp.
511
512        Do whatever cleanup would allow the user to use other programs."""
513        raise NotImplementedError
514
515    def after_suspend(self):
516        """Will be called when process foregrounded after suspend.
517
518        See to it that process_event is called with None to trigger a refresh
519        if not in the middle of a process_event call when suspend happened."""
520        raise NotImplementedError
521
522    # end methods that should be overridden in subclass
523
524    def request_refresh(self):
525        """Request that the bpython display to be refreshed soon."""
526        if self.reevaluating or self.paste_mode:
527            self.fake_refresh_requested = True
528        else:
529            self._request_refresh()
530
531    def request_reload(self, files_modified=()):
532        """Request that a ReloadEvent be passed next into process_event"""
533        if self.watching_files:
534            self._request_reload(files_modified=files_modified)
535
536    def schedule_refresh(self, when="now"):
537        """Schedule a ScheduledRefreshRequestEvent for when.
538
539        Such a event should interrupt if blockied waiting for keyboard input"""
540        if self.reevaluating or self.paste_mode:
541            self.fake_refresh_requested = True
542        else:
543            self._schedule_refresh(when=when)
544
545    def __enter__(self):
546        self.orig_stdout = sys.stdout
547        self.orig_stderr = sys.stderr
548        self.orig_stdin = sys.stdin
549        sys.stdout = self.stdout
550        sys.stderr = self.stderr
551        sys.stdin = self.stdin
552        self.orig_sigwinch_handler = signal.getsignal(signal.SIGWINCH)
553        self.orig_sigtstp_handler = signal.getsignal(signal.SIGTSTP)
554
555        if is_main_thread():
556            # This turns off resize detection and ctrl-z suspension.
557            signal.signal(signal.SIGWINCH, self.sigwinch_handler)
558            signal.signal(signal.SIGTSTP, self.sigtstp_handler)
559
560        self.orig_meta_path = sys.meta_path
561        if self.watcher:
562            meta_path = []
563            for finder in sys.meta_path:
564                # All elements get wrapped in ImportFinder instances execepted for instances of
565                # _SixMetaPathImporter (from six). When importing six, it will check if the importer
566                # is already part of sys.meta_path and will remove instances. We do not want to
567                # break this feature (see also #874).
568                if type(finder).__name__ == "_SixMetaPathImporter":
569                    meta_path.append(finder)
570                else:
571                    meta_path.append(ImportFinder(finder, self.watcher))
572            sys.meta_path = meta_path
573
574        sitefix.monkeypatch_quit()
575        return self
576
577    def __exit__(
578        self,
579        exc_type: Optional[Type[BaseException]],
580        exc_val: Optional[BaseException],
581        exc_tb: Optional[TracebackType],
582    ) -> Literal[False]:
583        sys.stdin = self.orig_stdin
584        sys.stdout = self.orig_stdout
585        sys.stderr = self.orig_stderr
586
587        if is_main_thread():
588            # This turns off resize detection and ctrl-z suspension.
589            signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler)
590            signal.signal(signal.SIGTSTP, self.orig_sigtstp_handler)
591
592        sys.meta_path = self.orig_meta_path
593        return False
594
595    def sigwinch_handler(self, signum, frame):
596        old_rows, old_columns = self.height, self.width
597        self.height, self.width = self.get_term_hw()
598        cursor_dy = self.get_cursor_vertical_diff()
599        self.scroll_offset -= cursor_dy
600        logger.info(
601            "sigwinch! Changed from %r to %r",
602            (old_rows, old_columns),
603            (self.height, self.width),
604        )
605        logger.info(
606            "decreasing scroll offset by %d to %d",
607            cursor_dy,
608            self.scroll_offset,
609        )
610
611    def sigtstp_handler(self, signum, frame):
612        self.scroll_offset = len(self.lines_for_display)
613        self.__exit__()
614        self.on_suspend()
615        os.kill(os.getpid(), signal.SIGTSTP)
616        self.after_suspend()
617        self.__enter__()
618
619    def clean_up_current_line_for_exit(self):
620        """Called when trying to exit to prep for final paint"""
621        logger.debug("unhighlighting paren for exit")
622        self.cursor_offset = -1
623        self.unhighlight_paren()
624
625    # Event handling
626    def process_event(self, e: Union[events.Event, str]) -> Optional[bool]:
627        """Returns True if shutting down, otherwise returns None.
628        Mostly mutates state of Repl object"""
629
630        logger.debug("processing event %r", e)
631        if isinstance(e, events.Event):
632            return self.process_control_event(e)
633        else:
634            self.last_events.append(e)
635            self.last_events.pop(0)
636            self.process_key_event(e)
637            return None
638
639    def process_control_event(self, e) -> Optional[bool]:
640
641        if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent):
642            # This is a scheduled refresh - it's really just a refresh (so nop)
643            pass
644
645        elif isinstance(e, bpythonevents.RefreshRequestEvent):
646            logger.info("received ASAP refresh request event")
647            if self.status_bar.has_focus:
648                self.status_bar.process_event(e)
649            else:
650                assert self.coderunner.code_is_waiting
651                self.run_code_and_maybe_finish()
652
653        elif self.status_bar.has_focus:
654            self.status_bar.process_event(e)
655
656        # handles paste events for both stdin and repl
657        elif isinstance(e, events.PasteEvent):
658            ctrl_char = compress_paste_event(e)
659            if ctrl_char is not None:
660                return self.process_event(ctrl_char)
661            with self.in_paste_mode():
662                # Might not really be a paste, UI might just be lagging
663                if len(e.events) <= MAX_EVENTS_POSSIBLY_NOT_PASTE and any(
664                    not is_simple_event(ee) for ee in e.events
665                ):
666                    for ee in e.events:
667                        if self.stdin.has_focus:
668                            self.stdin.process_event(ee)
669                        else:
670                            self.process_event(ee)
671                else:
672                    simple_events = just_simple_events(e.events)
673                    source = preprocess(
674                        "".join(simple_events), self.interp.compile
675                    )
676                    for ee in source:
677                        if self.stdin.has_focus:
678                            self.stdin.process_event(ee)
679                        else:
680                            self.process_simple_keypress(ee)
681
682        elif isinstance(e, bpythonevents.RunStartupFileEvent):
683            try:
684                self.startup()
685            except OSError as e:
686                self.status_bar.message(
687                    _("Executing PYTHONSTARTUP failed: %s") % (e,)
688                )
689
690        elif isinstance(e, bpythonevents.UndoEvent):
691            self.undo(n=e.n)
692
693        elif self.stdin.has_focus:
694            self.stdin.process_event(e)
695
696        elif isinstance(e, events.SigIntEvent):
697            logger.debug("received sigint event")
698            self.keyboard_interrupt()
699
700        elif isinstance(e, bpythonevents.ReloadEvent):
701            if self.watching_files:
702                self.clear_modules_and_reevaluate()
703                self.status_bar.message(
704                    _("Reloaded at %s because %s modified.")
705                    % (time.strftime("%X"), " & ".join(e.files_modified))
706                )
707
708        else:
709            raise ValueError("Don't know how to handle event type: %r" % e)
710        return None
711
712    def process_key_event(self, e: str) -> None:
713        # To find the curtsies name for a keypress, try
714        # python -m curtsies.events
715        if self.status_bar.has_focus:
716            return self.status_bar.process_event(e)
717        if self.stdin.has_focus:
718            return self.stdin.process_event(e)
719
720        if (
721            e
722            in (
723                key_dispatch[self.config.right_key]
724                + key_dispatch[self.config.end_of_line_key]
725                + ("<RIGHT>",)
726            )
727            and self.config.curtsies_right_arrow_completion
728            and self.cursor_offset == len(self.current_line)
729            # if at end of current line and user presses RIGHT (to autocomplete)
730        ):
731            # then autocomplete
732            self.current_line += self.current_suggestion
733            self.cursor_offset = len(self.current_line)
734        elif e in ("<UP>",) + key_dispatch[self.config.up_one_line_key]:
735            self.up_one_line()
736        elif e in ("<DOWN>",) + key_dispatch[self.config.down_one_line_key]:
737            self.down_one_line()
738        elif e in ("<Ctrl-d>",):
739            self.on_control_d()
740        elif e in ("<Ctrl-o>",):
741            self.operate_and_get_next()
742        elif e in ("<Esc+.>",):
743            self.get_last_word()
744        elif e in key_dispatch[self.config.reverse_incremental_search_key]:
745            self.incremental_search(reverse=True)
746        elif e in key_dispatch[self.config.incremental_search_key]:
747            self.incremental_search()
748        elif (
749            e in (("<BACKSPACE>",) + key_dispatch[self.config.backspace_key])
750            and self.incr_search_mode
751        ):
752            self.add_to_incremental_search(self, backspace=True)
753        elif e in self.edit_keys.cut_buffer_edits:
754            self.readline_kill(e)
755        elif e in self.edit_keys.simple_edits:
756            self.cursor_offset, self.current_line = self.edit_keys.call(
757                e,
758                cursor_offset=self.cursor_offset,
759                line=self.current_line,
760                cut_buffer=self.cut_buffer,
761            )
762        elif e in key_dispatch[self.config.cut_to_buffer_key]:
763            self.cut_to_buffer()
764        elif e in key_dispatch[self.config.reimport_key]:
765            self.clear_modules_and_reevaluate()
766        elif e in key_dispatch[self.config.toggle_file_watch_key]:
767            self.toggle_file_watch()
768        elif e in key_dispatch[self.config.clear_screen_key]:
769            self.request_paint_to_clear_screen = True
770        elif e in key_dispatch[self.config.show_source_key]:
771            self.show_source()
772        elif e in key_dispatch[self.config.help_key]:
773            self.pager(self.help_text())
774        elif e in key_dispatch[self.config.exit_key]:
775            raise SystemExit()
776        elif e in ("\n", "\r", "<PADENTER>", "<Ctrl-j>", "<Ctrl-m>"):
777            self.on_enter()
778        elif e in ("<TAB>",):  # tab
779            self.on_tab()
780        elif e in ("<Shift-TAB>",):
781            self.on_tab(back=True)
782        elif e in key_dispatch[self.config.undo_key]:  # ctrl-r for undo
783            self.prompt_undo()
784        elif e in key_dispatch[self.config.redo_key]:  # ctrl-g for redo
785            self.redo()
786        elif e in key_dispatch[self.config.save_key]:  # ctrl-s for save
787            greenlet.greenlet(self.write2file).switch()
788        elif e in key_dispatch[self.config.pastebin_key]:  # F8 for pastebin
789            greenlet.greenlet(self.pastebin).switch()
790        elif e in key_dispatch[self.config.copy_clipboard_key]:
791            greenlet.greenlet(self.copy2clipboard).switch()
792        elif e in key_dispatch[self.config.external_editor_key]:
793            self.send_session_to_external_editor()
794        elif e in key_dispatch[self.config.edit_config_key]:
795            greenlet.greenlet(self.edit_config).switch()
796        # TODO add PAD keys hack as in bpython.cli
797        elif e in key_dispatch[self.config.edit_current_block_key]:
798            self.send_current_block_to_external_editor()
799        elif e in ("<ESC>",):
800            self.incr_search_mode = None
801        elif e in ("<SPACE>",):
802            self.add_normal_character(" ")
803        else:
804            self.add_normal_character(e)
805
806    def get_last_word(self):
807
808        previous_word = _last_word(self.rl_history.entry)
809        word = _last_word(self.rl_history.back())
810        line = self.current_line
811        self._set_current_line(
812            line[: len(line) - len(previous_word)] + word,
813            reset_rl_history=False,
814        )
815        self._set_cursor_offset(
816            self.cursor_offset - len(previous_word) + len(word),
817            reset_rl_history=False,
818        )
819
820    def incremental_search(self, reverse=False, include_current=False):
821        if self.incr_search_mode is None:
822            self.rl_history.enter(self.current_line)
823            self.incr_search_target = ""
824        else:
825            if self.incr_search_target:
826                line = (
827                    self.rl_history.back(
828                        False,
829                        search=True,
830                        target=self.incr_search_target,
831                        include_current=include_current,
832                    )
833                    if reverse
834                    else self.rl_history.forward(
835                        False,
836                        search=True,
837                        target=self.incr_search_target,
838                        include_current=include_current,
839                    )
840                )
841                self._set_current_line(
842                    line, reset_rl_history=False, clear_special_mode=False
843                )
844                self._set_cursor_offset(
845                    len(self.current_line),
846                    reset_rl_history=False,
847                    clear_special_mode=False,
848                )
849        if reverse:
850            self.incr_search_mode = "reverse_incremental_search"
851        else:
852            self.incr_search_mode = "incremental_search"
853
854    def readline_kill(self, e):
855        func = self.edit_keys[e]
856        self.cursor_offset, self.current_line, cut = func(
857            self.cursor_offset, self.current_line
858        )
859        if self.last_events[-2] == e:  # consecutive kill commands accumulative
860            if func.kills == "ahead":
861                self.cut_buffer += cut
862            elif func.kills == "behind":
863                self.cut_buffer = cut + self.cut_buffer
864            else:
865                raise ValueError("cut value other than 'ahead' or 'behind'")
866        else:
867            self.cut_buffer = cut
868
869    def on_enter(self, new_code=True, reset_rl_history=True):
870        # so the cursor isn't touching a paren TODO: necessary?
871        if new_code:
872            self.redo_stack = []
873
874        self._set_cursor_offset(-1, update_completion=False)
875        if reset_rl_history:
876            self.rl_history.reset()
877
878        self.history.append(self.current_line)
879        self.all_logical_lines.append((self.current_line, LineType.INPUT))
880        self.push(self.current_line, insert_into_history=new_code)
881
882    def on_tab(self, back=False):
883        """Do something on tab key
884        taken from bpython.cli
885
886        Does one of the following:
887        1) add space to move up to the next %4==0 column
888        2) complete the current word with characters common to all completions
889        3) select the first or last match
890        4) select the next or previous match if already have a match
891        """
892
893        def only_whitespace_left_of_cursor():
894            """returns true if all characters before cursor are whitespace"""
895            return not self.current_line[: self.cursor_offset].strip()
896
897        logger.debug("self.matches_iter.matches:%r", self.matches_iter.matches)
898        if only_whitespace_left_of_cursor():
899            front_ws = len(self.current_line[: self.cursor_offset]) - len(
900                self.current_line[: self.cursor_offset].lstrip()
901            )
902            to_add = 4 - (front_ws % self.config.tab_length)
903            for unused in range(to_add):
904                self.add_normal_character(" ")
905            return
906
907        # run complete() if we don't already have matches
908        if len(self.matches_iter.matches) == 0:
909            self.list_win_visible = self.complete(tab=True)
910
911        # 3. check to see if we can expand the current word
912        if self.matches_iter.is_cseq():
913            cursor_and_line = self.matches_iter.substitute_cseq()
914            self._cursor_offset, self._current_line = cursor_and_line
915            # using _current_line so we don't trigger a completion reset
916            if not self.matches_iter.matches:
917                self.list_win_visible = self.complete()
918
919        elif self.matches_iter.matches:
920            self.current_match = (
921                back and self.matches_iter.previous() or next(self.matches_iter)
922            )
923            cursor_and_line = self.matches_iter.cur_line()
924            self._cursor_offset, self._current_line = cursor_and_line
925            # using _current_line so we don't trigger a completion reset
926            self.list_win_visible = True
927
928    def on_control_d(self):
929        if self.current_line == "":
930            raise SystemExit()
931        else:
932            self.current_line = (
933                self.current_line[: self.cursor_offset]
934                + self.current_line[(self.cursor_offset + 1) :]
935            )
936
937    def cut_to_buffer(self):
938        self.cut_buffer = self.current_line[self.cursor_offset :]
939        self.current_line = self.current_line[: self.cursor_offset]
940
941    def yank_from_buffer(self):
942        pass
943
944    def operate_and_get_next(self):
945        # If we have not navigated back in history
946        # ctrl+o will have the same effect as enter
947        self.on_enter(reset_rl_history=False)
948
949    def up_one_line(self):
950        self.rl_history.enter(self.current_line)
951        self._set_current_line(
952            tabs_to_spaces(
953                self.rl_history.back(
954                    False, search=self.config.curtsies_right_arrow_completion
955                )
956            ),
957            update_completion=False,
958            reset_rl_history=False,
959        )
960        self._set_cursor_offset(len(self.current_line), reset_rl_history=False)
961
962    def down_one_line(self):
963        self.rl_history.enter(self.current_line)
964        self._set_current_line(
965            tabs_to_spaces(
966                self.rl_history.forward(
967                    False, search=self.config.curtsies_right_arrow_completion
968                )
969            ),
970            update_completion=False,
971            reset_rl_history=False,
972        )
973        self._set_cursor_offset(len(self.current_line), reset_rl_history=False)
974
975    def process_simple_keypress(self, e):
976        # '\n' needed for pastes
977        if e in ("<Ctrl-j>", "<Ctrl-m>", "<PADENTER>", "\n", "\r"):
978            self.on_enter()
979            while self.fake_refresh_requested:
980                self.fake_refresh_requested = False
981                self.process_event(bpythonevents.RefreshRequestEvent())
982        elif isinstance(e, events.Event):
983            pass  # ignore events
984        elif e in ("<SPACE>",):
985            self.add_normal_character(" ")
986        else:
987            self.add_normal_character(e)
988
989    def send_current_block_to_external_editor(self, filename=None):
990        """
991        Sends the current code block to external editor to be edited. Usually bound to C-x.
992        """
993        text = self.send_to_external_editor(self.get_current_block())
994        lines = [line for line in text.split("\n")]
995        while lines and not lines[-1].split():
996            lines.pop()
997        events = "\n".join(lines + ([""] if len(lines) == 1 else ["", ""]))
998        self.clear_current_block()
999        with self.in_paste_mode():
1000            for e in events:
1001                self.process_simple_keypress(e)
1002        self.cursor_offset = len(self.current_line)
1003
1004    def send_session_to_external_editor(self, filename=None):
1005        """
1006        Sends entire bpython session to external editor to be edited. Usually bound to F7.
1007        """
1008        for_editor = EDIT_SESSION_HEADER
1009        for_editor += self.get_session_formatted_for_file()
1010
1011        text = self.send_to_external_editor(for_editor)
1012        if text == for_editor:
1013            self.status_bar.message(
1014                _("Session not reevaluated because it was not edited")
1015            )
1016            return
1017        lines = text.split("\n")
1018        if len(lines) and not lines[-1].strip():
1019            lines.pop()  # strip last line if empty
1020        if len(lines) and lines[-1].startswith("### "):
1021            current_line = lines[-1][4:]
1022        else:
1023            current_line = ""
1024        from_editor = [
1025            line for line in lines if line[:6] != "# OUT:" and line[:3] != "###"
1026        ]
1027        if all(not line.strip() for line in from_editor):
1028            self.status_bar.message(
1029                _("Session not reevaluated because saved file was blank")
1030            )
1031            return
1032
1033        source = preprocess("\n".join(from_editor), self.interp.compile)
1034        lines = source.split("\n")
1035        self.history = lines
1036        self.reevaluate(new_code=True)
1037        self.current_line = current_line
1038        self.cursor_offset = len(self.current_line)
1039        self.status_bar.message(_("Session edited and reevaluated"))
1040
1041    def clear_modules_and_reevaluate(self):
1042        if self.watcher:
1043            self.watcher.reset()
1044        cursor, line = self.cursor_offset, self.current_line
1045        for modname in set(sys.modules.keys()) - self.original_modules:
1046            del sys.modules[modname]
1047        self.reevaluate(new_code=False)
1048        self.cursor_offset, self.current_line = cursor, line
1049        self.status_bar.message(
1050            _("Reloaded at %s by user.") % (time.strftime("%X"),)
1051        )
1052
1053    def toggle_file_watch(self):
1054        if self.watcher:
1055            if self.watching_files:
1056                msg = _("Auto-reloading deactivated.")
1057                self.status_bar.message(msg)
1058                self.watcher.deactivate()
1059                self.watching_files = False
1060            else:
1061                msg = _("Auto-reloading active, watching for file changes...")
1062                self.status_bar.message(msg)
1063                self.watching_files = True
1064                self.watcher.activate()
1065        else:
1066            self.status_bar.message(
1067                _(
1068                    "Auto-reloading not available because "
1069                    "watchdog not installed."
1070                )
1071            )
1072
1073    # Handler Helpers
1074    def add_normal_character(self, char):
1075        if len(char) > 1 or is_nop(char):
1076            return
1077        if self.incr_search_mode:
1078            self.add_to_incremental_search(char)
1079        else:
1080            self._set_current_line(
1081                (
1082                    self.current_line[: self.cursor_offset]
1083                    + char
1084                    + self.current_line[self.cursor_offset :]
1085                ),
1086                update_completion=False,
1087                reset_rl_history=False,
1088                clear_special_mode=False,
1089            )
1090            self.cursor_offset += 1
1091        if self.config.cli_trim_prompts and self.current_line.startswith(
1092            self.ps1
1093        ):
1094            self.current_line = self.current_line[4:]
1095            self.cursor_offset = max(0, self.cursor_offset - 4)
1096
1097    def add_to_incremental_search(self, char=None, backspace=False):
1098        """Modify the current search term while in incremental search.
1099
1100        The only operations allowed in incremental search mode are
1101        adding characters and backspacing."""
1102        if char is None and not backspace:
1103            raise ValueError("must provide a char or set backspace to True")
1104        if backspace:
1105            self.incr_search_target = self.incr_search_target[:-1]
1106        else:
1107            self.incr_search_target += char
1108        if self.incr_search_mode == "reverse_incremental_search":
1109            self.incremental_search(reverse=True, include_current=True)
1110        elif self.incr_search_mode == "incremental_search":
1111            self.incremental_search(include_current=True)
1112        else:
1113            raise ValueError("add_to_incremental_search not in a special mode")
1114
1115    def update_completion(self, tab=False):
1116        """Update visible docstring and matches and box visibility"""
1117        # Update autocomplete info; self.matches_iter and self.funcprops
1118        # Should be called whenever the completion box might need to appear
1119        # or disappear; whenever current line or cursor offset changes,
1120        # unless this happened via selecting a match
1121        self.current_match = None
1122        self.list_win_visible = self.complete(tab)
1123
1124    def predicted_indent(self, line):
1125        # TODO get rid of this! It's repeated code! Combine with Repl.
1126        logger.debug("line is %r", line)
1127        indent = len(re.match(r"[ ]*", line).group())
1128        if line.endswith(":"):
1129            indent = max(0, indent + self.config.tab_length)
1130        elif line and line.count(" ") == len(line):
1131            indent = max(0, indent - self.config.tab_length)
1132        elif (
1133            line
1134            and ":" not in line
1135            and line.strip().startswith(("return", "pass", "raise", "yield"))
1136        ):
1137            indent = max(0, indent - self.config.tab_length)
1138        logger.debug("indent we found was %s", indent)
1139        return indent
1140
1141    def push(self, line, insert_into_history=True):
1142        """Push a line of code onto the buffer, start running the buffer
1143
1144        If the interpreter successfully runs the code, clear the buffer
1145        """
1146        # Note that push() overrides its parent without calling it, unlike
1147        # urwid and cli which implement custom behavior and call repl.Repl.push
1148        if self.paste_mode:
1149            self.saved_indent = 0
1150        else:
1151            self.saved_indent = self.predicted_indent(line)
1152
1153        if self.config.syntax:
1154            display_line = bpythonparse(
1155                pygformat(self.tokenize(line), self.formatter)
1156            )
1157            # self.tokenize requires that the line not be in self.buffer yet
1158
1159            logger.debug(
1160                "display line being pushed to buffer: %r -> %r",
1161                line,
1162                display_line,
1163            )
1164            self.display_buffer.append(display_line)
1165        else:
1166            self.display_buffer.append(fmtstr(line))
1167
1168        if insert_into_history:
1169            self.insert_into_history(line)
1170        self.buffer.append(line)
1171
1172        code_to_run = "\n".join(self.buffer)
1173
1174        logger.debug("running %r in interpreter", self.buffer)
1175        c, code_will_parse = code_finished_will_parse(
1176            "\n".join(self.buffer), self.interp.compile
1177        )
1178        self.saved_predicted_parse_error = not code_will_parse
1179        if c:
1180            logger.debug("finished - buffer cleared")
1181            self.cursor_offset = 0
1182            self.display_lines.extend(self.display_buffer_lines)
1183            self.display_buffer = []
1184            self.buffer = []
1185
1186        self.coderunner.load_code(code_to_run)
1187        self.run_code_and_maybe_finish()
1188
1189    def run_code_and_maybe_finish(self, for_code=None):
1190        r = self.coderunner.run_code(for_code=for_code)
1191        if r:
1192            logger.debug("----- Running finish command stuff -----")
1193            logger.debug("saved_indent: %r", self.saved_indent)
1194            err = self.saved_predicted_parse_error
1195            self.saved_predicted_parse_error = False
1196
1197            indent = self.saved_indent
1198            if err:
1199                indent = 0
1200
1201            if self.rl_history.index == 0:
1202                self._set_current_line(" " * indent, update_completion=True)
1203            else:
1204                self._set_current_line(
1205                    self.rl_history.entries[-self.rl_history.index],
1206                    reset_rl_history=False,
1207                )
1208            self.cursor_offset = len(self.current_line)
1209
1210    def keyboard_interrupt(self):
1211        # TODO factor out the common cleanup from running a line
1212        self.cursor_offset = -1
1213        self.unhighlight_paren()
1214        self.display_lines.extend(self.display_buffer_lines)
1215        self.display_lines.extend(
1216            paint.display_linize(self.current_cursor_line, self.width)
1217        )
1218        self.display_lines.extend(
1219            paint.display_linize("KeyboardInterrupt", self.width)
1220        )
1221        self.clear_current_block(remove_from_history=False)
1222
1223    def unhighlight_paren(self):
1224        """Modify line in self.display_buffer to unhighlight a paren if
1225        possible.
1226
1227        self.highlighted_paren should be a line in ?
1228        """
1229        if self.highlighted_paren is not None and self.config.syntax:
1230            lineno, saved_tokens = self.highlighted_paren
1231            if lineno == len(self.display_buffer):
1232                # then this is the current line, so don't worry about it
1233                return
1234            self.highlighted_paren = None
1235            logger.debug("trying to unhighlight a paren on line %r", lineno)
1236            logger.debug("with these tokens: %r", saved_tokens)
1237            new = bpythonparse(pygformat(saved_tokens, self.formatter))
1238            self.display_buffer[lineno] = self.display_buffer[
1239                lineno
1240            ].setslice_with_length(
1241                0, len(new), new, len(self.display_buffer[lineno])
1242            )
1243
1244    def clear_current_block(self, remove_from_history=True):
1245        self.display_buffer = []
1246        if remove_from_history:
1247            for unused in self.buffer:
1248                self.history.pop()
1249                self.all_logical_lines.pop()
1250        self.buffer = []
1251        self.cursor_offset = 0
1252        self.saved_indent = 0
1253        self.current_line = ""
1254        self.cursor_offset = len(self.current_line)
1255
1256    def get_current_block(self):
1257        """
1258        Returns the current code block as string (without prompts)
1259        """
1260        return "\n".join(self.buffer + [self.current_line])
1261
1262    def send_to_stdouterr(self, output):
1263        """Send unicode strings or FmtStr to Repl stdout or stderr
1264
1265        Must be able to handle FmtStrs because interpreter pass in
1266        tracebacks already formatted."""
1267        lines = output.split("\n")
1268        logger.debug("display_lines: %r", self.display_lines)
1269        self.current_stdouterr_line += lines[0]
1270        if len(lines) > 1:
1271            self.display_lines.extend(
1272                paint.display_linize(
1273                    self.current_stdouterr_line, self.width, blank_line=True
1274                )
1275            )
1276            self.display_lines.extend(
1277                sum(
1278                    (
1279                        paint.display_linize(line, self.width, blank_line=True)
1280                        for line in lines[1:-1]
1281                    ),
1282                    [],
1283                )
1284            )
1285            # These can be FmtStrs, but self.all_logical_lines only wants strings
1286            for line in itertools.chain(
1287                (self.current_stdouterr_line,), lines[1:-1]
1288            ):
1289                if isinstance(line, FmtStr):
1290                    self.all_logical_lines.append((line.s, LineType.OUTPUT))
1291                else:
1292                    self.all_logical_lines.append((line, LineType.OUTPUT))
1293
1294            self.current_stdouterr_line = lines[-1]
1295        logger.debug("display_lines: %r", self.display_lines)
1296
1297    def send_to_stdin(self, line):
1298        if line.endswith("\n"):
1299            self.display_lines.extend(
1300                paint.display_linize(self.current_output_line, self.width)
1301            )
1302            self.current_output_line = ""
1303
1304    # formatting, output
1305    @property
1306    def done(self):
1307        """Whether the last block is complete - which prompt to use, ps1 or
1308        ps2"""
1309        return not self.buffer
1310
1311    @property
1312    def current_line_formatted(self):
1313        """The colored current line (no prompt, not wrapped)"""
1314        if self.config.syntax:
1315            fs = bpythonparse(
1316                pygformat(self.tokenize(self.current_line), self.formatter)
1317            )
1318            if self.incr_search_mode:
1319                if self.incr_search_target in self.current_line:
1320                    fs = fmtfuncs.on_magenta(self.incr_search_target).join(
1321                        fs.split(self.incr_search_target)
1322                    )
1323            elif (
1324                self.rl_history.saved_line
1325                and self.rl_history.saved_line in self.current_line
1326            ):
1327                if (
1328                    self.config.curtsies_right_arrow_completion
1329                    and self.rl_history.index != 0
1330                ):
1331                    fs = fmtfuncs.on_magenta(self.rl_history.saved_line).join(
1332                        fs.split(self.rl_history.saved_line)
1333                    )
1334            logger.debug("Display line %r -> %r", self.current_line, fs)
1335        else:
1336            fs = fmtstr(self.current_line)
1337        if hasattr(self, "old_fs") and str(fs) != str(self.old_fs):
1338            pass
1339        self.old_fs = fs
1340        return fs
1341
1342    @property
1343    def lines_for_display(self):
1344        """All display lines (wrapped, colored, with prompts)"""
1345        return self.display_lines + self.display_buffer_lines
1346
1347    @property
1348    def display_buffer_lines(self):
1349        """The display lines (wrapped, colored, +prompts) of current buffer"""
1350        lines = []
1351        for display_line in self.display_buffer:
1352            prompt = func_for_letter(self.config.color_scheme["prompt"])
1353            more = func_for_letter(self.config.color_scheme["prompt_more"])
1354            display_line = (
1355                more(self.ps2) if lines else prompt(self.ps1)
1356            ) + display_line
1357            for line in paint.display_linize(display_line, self.width):
1358                lines.append(line)
1359        return lines
1360
1361    @property
1362    def display_line_with_prompt(self):
1363        """colored line with prompt"""
1364        prompt = func_for_letter(self.config.color_scheme["prompt"])
1365        more = func_for_letter(self.config.color_scheme["prompt_more"])
1366        if self.incr_search_mode == "reverse_incremental_search":
1367            return (
1368                prompt(f"(reverse-i-search)`{self.incr_search_target}': ")
1369                + self.current_line_formatted
1370            )
1371        elif self.incr_search_mode == "incremental_search":
1372            return prompt(f"(i-search)`%s': ") + self.current_line_formatted
1373        return (
1374            prompt(self.ps1) if self.done else more(self.ps2)
1375        ) + self.current_line_formatted
1376
1377    @property
1378    def current_cursor_line_without_suggestion(self):
1379        """
1380        Current line, either output/input or Python prompt + code
1381
1382        :returns: FmtStr
1383        """
1384        value = self.current_output_line + (
1385            "" if self.coderunner.running else self.display_line_with_prompt
1386        )
1387        logger.debug("current cursor line: %r", value)
1388        return value
1389
1390    @property
1391    def current_cursor_line(self):
1392        if self.config.curtsies_right_arrow_completion:
1393            suggest = func_for_letter(
1394                self.config.color_scheme["right_arrow_suggestion"]
1395            )
1396            return self.current_cursor_line_without_suggestion + suggest(
1397                self.current_suggestion
1398            )
1399        else:
1400            return self.current_cursor_line_without_suggestion
1401
1402    @property
1403    def current_suggestion(self):
1404        if self.current_line:
1405            for entry in reversed(self.rl_history.entries):
1406                if entry.startswith(self.current_line):
1407                    return entry[len(self.current_line) :]
1408        return ""
1409
1410    @property
1411    def current_output_line(self):
1412        """line of output currently being written, and stdin typed"""
1413        return self.current_stdouterr_line + self.stdin.current_line
1414
1415    @current_output_line.setter
1416    def current_output_line(self, value):
1417        self.current_stdouterr_line = ""
1418        self.stdin.current_line = "\n"
1419
1420    def number_of_padding_chars_on_current_cursor_line(self):
1421        """To avoid cutting off two-column characters at the end of lines where
1422        there's only one column left, curtsies adds a padding char (u' ').
1423        It's important to know about these for cursor positioning.
1424
1425        Should return zero unless there are fullwidth characters."""
1426        full_line = self.current_cursor_line_without_suggestion
1427        line_with_padding = "".join(
1428            line.s
1429            for line in paint.display_linize(
1430                self.current_cursor_line_without_suggestion.s, self.width
1431            )
1432        )
1433
1434        # the difference in length here is how much padding there is
1435        return len(line_with_padding) - len(full_line)
1436
1437    def paint(
1438        self,
1439        about_to_exit=False,
1440        user_quit=False,
1441        try_preserve_history_height=30,
1442        min_infobox_height=5,
1443    ) -> Tuple[FSArray, Tuple[int, int]]:
1444        """Returns an array of min_height or more rows and width columns, plus
1445        cursor position
1446
1447        Paints the entire screen - ideally the terminal display layer will take
1448        a diff and only write to the screen in portions that have changed, but
1449        the idea is that we don't need to worry about that here, instead every
1450        frame is completely redrawn because less state is cool!
1451
1452        try_preserve_history_height is the the number of rows of content that
1453        must be visible before the suggestion box scrolls the terminal in order
1454        to display more than min_infobox_height rows of suggestions, docs etc.
1455        """
1456        # The hairiest function in the curtsies
1457        if about_to_exit:
1458            # exception to not changing state!
1459            self.clean_up_current_line_for_exit()
1460
1461        width, min_height = self.width, self.height
1462        show_status_bar = (
1463            bool(self.status_bar.should_show_message)
1464            or self.status_bar.has_focus
1465        ) and not self.request_paint_to_pad_bottom
1466        if show_status_bar:
1467            # because we're going to tack the status bar on at the end, shoot
1468            # for an array one less than the height of the screen
1469            min_height -= 1
1470
1471        current_line_start_row = len(self.lines_for_display) - max(
1472            0, self.scroll_offset
1473        )
1474        # TODO how is the situation of self.scroll_offset < 0 possible?
1475        # or show_status_bar and about_to_exit ?
1476        if self.request_paint_to_clear_screen:
1477            self.request_paint_to_clear_screen = False
1478            arr = FSArray(min_height + current_line_start_row, width)
1479        elif self.request_paint_to_pad_bottom:
1480            # min_height - 1 for startup banner with python version
1481            height = min(self.request_paint_to_pad_bottom, min_height - 1)
1482            arr = FSArray(height, width)
1483            self.request_paint_to_pad_bottom = 0
1484        else:
1485            arr = FSArray(0, width)
1486        # TODO test case of current line filling up the whole screen (there
1487        # aren't enough rows to show it)
1488
1489        current_line = paint.paint_current_line(
1490            min_height, width, self.current_cursor_line
1491        )
1492        # needs to happen before we calculate contents of history because
1493        # calculating self.current_cursor_line has the side effect of
1494        # unhighlighting parens in buffer
1495
1496        def move_screen_up(current_line_start_row):
1497            # move screen back up a screen minus a line
1498            while current_line_start_row < 0:
1499                logger.debug(
1500                    "scroll_offset was %s, current_line_start_row " "was %s",
1501                    self.scroll_offset,
1502                    current_line_start_row,
1503                )
1504                self.scroll_offset = self.scroll_offset - self.height
1505                current_line_start_row = len(self.lines_for_display) - max(
1506                    -1, self.scroll_offset
1507                )
1508                logger.debug(
1509                    "scroll_offset changed to %s, "
1510                    "current_line_start_row changed to %s",
1511                    self.scroll_offset,
1512                    current_line_start_row,
1513                )
1514            return current_line_start_row
1515
1516        if self.inconsistent_history and not self.history_already_messed_up:
1517            logger.debug(INCONSISTENT_HISTORY_MSG)
1518            self.history_already_messed_up = True
1519            msg = INCONSISTENT_HISTORY_MSG
1520            arr[0, 0 : min(len(msg), width)] = [msg[:width]]
1521            current_line_start_row += 1  # for the message
1522
1523            # to make up for the scroll that will be received after the
1524            # scrolls are rendered down a line
1525            self.scroll_offset -= 1
1526
1527            current_line_start_row = move_screen_up(current_line_start_row)
1528            logger.debug("current_line_start_row: %r", current_line_start_row)
1529
1530            history = paint.paint_history(
1531                max(0, current_line_start_row - 1),
1532                width,
1533                self.lines_for_display,
1534            )
1535            arr[1 : history.height + 1, : history.width] = history
1536
1537            if arr.height <= min_height:
1538                # force scroll down to hide broken history message
1539                arr[min_height, 0] = " "
1540
1541        elif current_line_start_row < 0:
1542            # if current line trying to be drawn off the top of the screen
1543            logger.debug(CONTIGUITY_BROKEN_MSG)
1544            msg = CONTIGUITY_BROKEN_MSG
1545            arr[0, 0 : min(len(msg), width)] = [msg[:width]]
1546
1547            current_line_start_row = move_screen_up(current_line_start_row)
1548
1549            history = paint.paint_history(
1550                max(0, current_line_start_row - 1),
1551                width,
1552                self.lines_for_display,
1553            )
1554            arr[1 : history.height + 1, : history.width] = history
1555
1556            if arr.height <= min_height:
1557                # force scroll down to hide broken history message
1558                arr[min_height, 0] = " "
1559
1560        else:
1561            assert current_line_start_row >= 0
1562            logger.debug("no history issues. start %i", current_line_start_row)
1563            history = paint.paint_history(
1564                current_line_start_row, width, self.lines_for_display
1565            )
1566            arr[: history.height, : history.width] = history
1567
1568        self.inconsistent_history = False
1569
1570        if user_quit:  # quit() or exit() in interp
1571            current_line_start_row = (
1572                current_line_start_row - current_line.height
1573            )
1574        logger.debug(
1575            "---current line row slice %r, %r",
1576            current_line_start_row,
1577            current_line_start_row + current_line.height,
1578        )
1579        logger.debug("---current line col slice %r, %r", 0, current_line.width)
1580        arr[
1581            current_line_start_row : (
1582                current_line_start_row + current_line.height
1583            ),
1584            0 : current_line.width,
1585        ] = current_line
1586
1587        if current_line.height > min_height:
1588            return arr, (0, 0)  # short circuit, no room for infobox
1589
1590        lines = paint.display_linize(self.current_cursor_line + "X", width)
1591        # extra character for space for the cursor
1592        current_line_end_row = current_line_start_row + len(lines) - 1
1593        current_line_height = current_line_end_row - current_line_start_row
1594
1595        if self.stdin.has_focus:
1596            logger.debug(
1597                "stdouterr when self.stdin has focus: %r %r",
1598                type(self.current_stdouterr_line),
1599                self.current_stdouterr_line,
1600            )
1601            # mypy can't do ternary type guards yet
1602            stdouterr = self.current_stdouterr_line
1603            if isinstance(stdouterr, FmtStr):
1604                stdouterr_width = stdouterr.width
1605            else:
1606                stdouterr_width = len(stdouterr)
1607            cursor_row, cursor_column = divmod(
1608                stdouterr_width
1609                + wcswidth(
1610                    self.stdin.current_line, max(0, self.stdin.cursor_offset)
1611                ),
1612                width,
1613            )
1614            assert cursor_row >= 0 and cursor_column >= 0, (
1615                cursor_row,
1616                cursor_column,
1617                self.current_stdouterr_line,
1618                self.stdin.current_line,
1619            )
1620        elif self.coderunner.running:  # TODO does this ever happen?
1621            cursor_row, cursor_column = divmod(
1622                len(self.current_cursor_line_without_suggestion)
1623                + self.cursor_offset,
1624                width,
1625            )
1626            assert cursor_row >= 0 and cursor_column >= 0, (
1627                cursor_row,
1628                cursor_column,
1629                len(self.current_cursor_line),
1630                len(self.current_line),
1631                self.cursor_offset,
1632            )
1633        else:  # Common case for determining cursor position
1634            cursor_row, cursor_column = divmod(
1635                wcswidth(self.current_cursor_line_without_suggestion.s)
1636                - wcswidth(self.current_line)
1637                + wcswidth(self.current_line, max(0, self.cursor_offset))
1638                + self.number_of_padding_chars_on_current_cursor_line(),
1639                width,
1640            )
1641            assert cursor_row >= 0 and cursor_column >= 0, (
1642                cursor_row,
1643                cursor_column,
1644                self.current_cursor_line_without_suggestion.s,
1645                self.current_line,
1646                self.cursor_offset,
1647            )
1648        cursor_row += current_line_start_row
1649
1650        if self.list_win_visible and not self.coderunner.running:
1651            logger.debug("infobox display code running")
1652            visible_space_above = history.height
1653            potential_space_below = min_height - current_line_end_row - 1
1654            visible_space_below = (
1655                potential_space_below - self.get_top_usable_line()
1656            )
1657
1658            if self.config.curtsies_list_above:
1659                info_max_rows = max(visible_space_above, visible_space_below)
1660            else:
1661                # Logic for determining size of completion box
1662                # smallest allowed over-full completion box
1663                preferred_height = max(
1664                    # always make infobox at least this height
1665                    min_infobox_height,
1666                    # use this value if there's so much space that we can
1667                    # preserve this try_preserve_history_height rows history
1668                    min_height - try_preserve_history_height,
1669                )
1670
1671                info_max_rows = min(
1672                    max(visible_space_below, preferred_height),
1673                    min_height - current_line_height - 1,
1674                )
1675            infobox = paint.paint_infobox(
1676                info_max_rows,
1677                int(width * self.config.cli_suggestion_width),
1678                self.matches_iter.matches,
1679                self.funcprops,
1680                self.arg_pos,
1681                self.current_match,
1682                self.docstring,
1683                self.config,
1684                self.matches_iter.completer.format
1685                if self.matches_iter.completer
1686                else None,
1687            )
1688
1689            if (
1690                visible_space_below >= infobox.height
1691                or not self.config.curtsies_list_above
1692            ):
1693                arr[
1694                    current_line_end_row
1695                    + 1 : (current_line_end_row + 1 + infobox.height),
1696                    0 : infobox.width,
1697                ] = infobox
1698            else:
1699                arr[
1700                    current_line_start_row
1701                    - infobox.height : current_line_start_row,
1702                    0 : infobox.width,
1703                ] = infobox
1704                logger.debug(
1705                    "infobox of shape %r added to arr of shape %r",
1706                    infobox.shape,
1707                    arr.shape,
1708                )
1709
1710        logger.debug("about to exit: %r", about_to_exit)
1711        if show_status_bar:
1712            statusbar_row = (
1713                min_height if arr.height == min_height else arr.height
1714            )
1715            if about_to_exit:
1716                arr[statusbar_row, :] = FSArray(1, width)
1717            else:
1718                arr[statusbar_row, :] = paint.paint_statusbar(
1719                    1, width, self.status_bar.current_line, self.config
1720                )
1721
1722        if self.presentation_mode:
1723            rows = arr.height
1724            columns = arr.width
1725            last_key_box = paint.paint_last_events(
1726                rows,
1727                columns,
1728                [events.pp_event(x) for x in self.last_events if x],
1729                self.config,
1730            )
1731            arr[
1732                arr.height - last_key_box.height : arr.height,
1733                arr.width - last_key_box.width : arr.width,
1734            ] = last_key_box
1735
1736        if self.config.color_scheme["background"] not in ("d", "D"):
1737            for r in range(arr.height):
1738                bg = color_for_letter(self.config.color_scheme["background"])
1739                arr[r] = fmtstr(arr[r], bg=bg)
1740        logger.debug("returning arr of size %r", arr.shape)
1741        logger.debug("cursor pos: %r", (cursor_row, cursor_column))
1742        return arr, (cursor_row, cursor_column)
1743
1744    @contextlib.contextmanager
1745    def in_paste_mode(self):
1746        orig_value = self.paste_mode
1747        self.paste_mode = True
1748        yield
1749        self.paste_mode = orig_value
1750        if not self.paste_mode:
1751            self.update_completion()
1752
1753    def __repr__(self):
1754        return f"""<{type(self)}
1755  cursor_offset: {self.cursor_offset}
1756  num display lines: {len(self.display_lines)}
1757  lines scrolled down: {self.scroll_offset}
1758>"""
1759
1760    @property
1761    def current_line(self):
1762        """The current line"""
1763        return self._current_line
1764
1765    @current_line.setter
1766    def current_line(self, value):
1767        self._set_current_line(value)
1768
1769    def _set_current_line(
1770        self,
1771        line,
1772        update_completion=True,
1773        reset_rl_history=True,
1774        clear_special_mode=True,
1775    ):
1776        if self._current_line == line:
1777            return
1778        self._current_line = line
1779        if self.paste_mode:
1780            return
1781        if update_completion:
1782            self.update_completion()
1783        if reset_rl_history:
1784            self.rl_history.reset()
1785        if clear_special_mode:
1786            self.special_mode = None
1787        self.unhighlight_paren()
1788
1789    @property
1790    def cursor_offset(self):
1791        """The current cursor offset from the front of the "line"."""
1792        return self._cursor_offset
1793
1794    @cursor_offset.setter
1795    def cursor_offset(self, value):
1796        self._set_cursor_offset(value)
1797
1798    def _set_cursor_offset(
1799        self,
1800        offset,
1801        update_completion=True,
1802        reset_rl_history=False,
1803        clear_special_mode=True,
1804    ):
1805        if self._cursor_offset == offset:
1806            return
1807        if self.paste_mode:
1808            self._cursor_offset = offset
1809            self.unhighlight_paren()
1810            return
1811        if reset_rl_history:
1812            self.rl_history.reset()
1813        if clear_special_mode:
1814            self.incr_search_mode = None
1815        self._cursor_offset = offset
1816        if update_completion:
1817            self.update_completion()
1818        self.unhighlight_paren()
1819
1820    def echo(self, msg, redraw=True):
1821        """
1822        Notification that redrawing the current line is necessary (we don't
1823        care, since we always redraw the whole screen)
1824
1825        Supposed to parse and echo a formatted string with appropriate
1826        attributes. It's not supposed to update the screen if it's reevaluating
1827        the code (as it does with undo)."""
1828        logger.debug("echo called with %r" % msg)
1829
1830    @property
1831    def cpos(self):
1832        "many WATs were had - it's the pos from the end of the line back"
1833        return len(self.current_line) - self.cursor_offset
1834
1835    def reprint_line(self, lineno, tokens):
1836        logger.debug("calling reprint line with %r %r", lineno, tokens)
1837        if self.config.syntax:
1838            self.display_buffer[lineno] = bpythonparse(
1839                pygformat(tokens, self.formatter)
1840            )
1841
1842    def take_back_buffer_line(self):
1843        assert len(self.buffer) > 0
1844        if len(self.buffer) == 1:
1845            self._cursor_offset = 0
1846            self.current_line = ""
1847        else:
1848            line = self.buffer[-1]
1849            indent = self.predicted_indent(line)
1850            self._current_line = indent * " "
1851            self.cursor_offset = len(self.current_line)
1852        self.display_buffer.pop()
1853        self.buffer.pop()
1854        self.history.pop()
1855        self.all_logical_lines.pop()
1856
1857    def take_back_empty_line(self):
1858        assert self.history and not self.history[-1]
1859        self.history.pop()
1860        self.display_lines.pop()
1861        self.all_logical_lines.pop()
1862
1863    def prompt_undo(self):
1864        if self.buffer:
1865            return self.take_back_buffer_line()
1866        if self.history and not self.history[-1]:
1867            return self.take_back_empty_line()
1868
1869        def prompt_for_undo():
1870            n = super(BaseRepl, self).prompt_undo()
1871            if n > 0:
1872                self.request_undo(n=n)
1873
1874        greenlet.greenlet(prompt_for_undo).switch()
1875
1876    def redo(self):
1877        if self.redo_stack:
1878            temp = self.redo_stack.pop()
1879            self.history.append(temp)
1880            self.all_logical_lines.append((temp, LineType.INPUT))
1881            self.push(temp)
1882        else:
1883            self.status_bar.message("Nothing to redo.")
1884
1885    def reevaluate(self, new_code=False):
1886        """bpython.Repl.undo calls this"""
1887        if self.watcher:
1888            self.watcher.reset()
1889        old_logical_lines = self.history
1890        old_display_lines = self.display_lines
1891        self.history = []
1892        self.display_lines = []
1893        self.all_logical_lines = []
1894
1895        if not self.weak_rewind:
1896            self.interp = self.interp.__class__()
1897            self.interp.write = self.send_to_stdouterr
1898            self.coderunner.interp = self.interp
1899            self.initialize_interp()
1900
1901        self.buffer = []
1902        self.display_buffer = []
1903        self.highlighted_paren = None
1904
1905        self.process_event(bpythonevents.RunStartupFileEvent())
1906        self.reevaluating = True
1907        sys.stdin = ReevaluateFakeStdin(self.stdin, self)
1908        for line in old_logical_lines:
1909            self._current_line = line
1910            self.on_enter(new_code=new_code)
1911            while self.fake_refresh_requested:
1912                self.fake_refresh_requested = False
1913                self.process_event(bpythonevents.RefreshRequestEvent())
1914        sys.stdin = self.stdin
1915        self.reevaluating = False
1916
1917        num_lines_onscreen = len(self.lines_for_display) - max(
1918            0, self.scroll_offset
1919        )
1920        display_lines_offscreen = self.display_lines[
1921            : len(self.display_lines) - num_lines_onscreen
1922        ]
1923        old_display_lines_offscreen = old_display_lines[
1924            : (len(self.display_lines) - num_lines_onscreen)
1925        ]
1926        logger.debug(
1927            "old_display_lines_offscreen %s",
1928            "|".join(str(x) for x in old_display_lines_offscreen),
1929        )
1930        logger.debug(
1931            "    display_lines_offscreen %s",
1932            "|".join(str(x) for x in display_lines_offscreen),
1933        )
1934        if (
1935            old_display_lines_offscreen[: len(display_lines_offscreen)]
1936            != display_lines_offscreen
1937        ) and not self.history_already_messed_up:
1938            self.inconsistent_history = True
1939        logger.debug(
1940            "after rewind, self.inconsistent_history is %r",
1941            self.inconsistent_history,
1942        )
1943
1944        self._cursor_offset = 0
1945        self.current_line = ""
1946
1947    def initialize_interp(self) -> None:
1948        self.coderunner.interp.locals["_repl"] = self
1949        self.coderunner.interp.runsource(
1950            "from bpython.curtsiesfrontend._internal import _Helper\n"
1951        )
1952        self.coderunner.interp.runsource("help = _Helper(_repl)\n")
1953        self.coderunner.interp.runsource("del _Helper\n")
1954
1955        del self.coderunner.interp.locals["_repl"]
1956
1957    def getstdout(self):
1958        """
1959        Returns a string of the current bpython session, wrapped, WITH prompts.
1960        """
1961        lines = self.lines_for_display + [self.current_line_formatted]
1962        s = (
1963            "\n".join(x.s if isinstance(x, FmtStr) else x for x in lines)
1964            if lines
1965            else ""
1966        )
1967        return s
1968
1969    def focus_on_subprocess(self, args):
1970        prev_sigwinch_handler = signal.getsignal(signal.SIGWINCH)
1971        try:
1972            signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler)
1973            with Termmode(self.orig_stdin, self.orig_tcattrs):
1974                terminal = blessings.Terminal(stream=sys.__stdout__)
1975                with terminal.fullscreen():
1976                    sys.__stdout__.write(terminal.save)
1977                    sys.__stdout__.write(terminal.move(0, 0))
1978                    sys.__stdout__.flush()
1979                    p = subprocess.Popen(
1980                        args,
1981                        stdin=self.orig_stdin,
1982                        stderr=sys.__stderr__,
1983                        stdout=sys.__stdout__,
1984                    )
1985                    p.wait()
1986                    sys.__stdout__.write(terminal.restore)
1987                    sys.__stdout__.flush()
1988        finally:
1989            signal.signal(signal.SIGWINCH, prev_sigwinch_handler)
1990
1991    def pager(self, text):
1992        """Runs an external pager on text
1993
1994        text must be a str"""
1995        command = get_pager_command()
1996        with tempfile.NamedTemporaryFile() as tmp:
1997            tmp.write(text.encode(getpreferredencoding()))
1998            tmp.flush()
1999            self.focus_on_subprocess(command + [tmp.name])
2000
2001    def show_source(self):
2002        try:
2003            source = self.get_source_of_current_name()
2004        except SourceNotFound as e:
2005            self.status_bar.message(f"{e}")
2006        else:
2007            if self.config.highlight_show_source:
2008                source = pygformat(
2009                    Python3Lexer().get_tokens(source), TerminalFormatter()
2010                )
2011            self.pager(source)
2012
2013    def help_text(self):
2014        return self.version_help_text() + "\n" + self.key_help_text()
2015
2016    def version_help_text(self):
2017        help_message = _(
2018            """
2019Thanks for using bpython!
2020
2021See http://bpython-interpreter.org/ for more information and http://docs.bpython-interpreter.org/ for docs.
2022Please report issues at https://github.com/bpython/bpython/issues
2023
2024Features:
2025Try using undo ({config.undo_key})!
2026Edit the current line ({config.edit_current_block_key}) or the entire session ({config.external_editor_key}) in an external editor. (currently {config.editor})
2027Save sessions ({config.save_key}) or post them to pastebins ({config.pastebin_key})! Current pastebin helper: {config.pastebin_helper}
2028Reload all modules and rerun session ({config.reimport_key}) to test out changes to a module.
2029Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute the current session when a module you've imported is modified.
2030
2031bpython -i your_script.py runs a file in interactive mode
2032bpython -t your_script.py pastes the contents of a file into the session
2033
2034A config file at {config.config_path} customizes keys and behavior of bpython.
2035You can also set which pastebin helper and which external editor to use.
2036See {example_config_url} for an example config file.
2037Press {config.edit_config_key} to edit this config file.
2038"""
2039        ).format(example_config_url=EXAMPLE_CONFIG_URL, config=self.config)
2040
2041        return f"bpython-curtsies version {__version__} using curtsies version {curtsies_version}\n{help_message}"
2042
2043    def key_help_text(self):
2044        NOT_IMPLEMENTED = (
2045            "suspend",
2046            "cut to buffer",
2047            "search",
2048            "last output",
2049            "yank from buffer",
2050            "cut to buffer",
2051        )
2052        pairs = [
2053            ["complete history suggestion", "right arrow at end of line"],
2054            ["previous match with current line", "up arrow"],
2055        ]
2056        for functionality, key in (
2057            (attr[:-4].replace("_", " "), getattr(self.config, attr))
2058            for attr in self.config.__dict__
2059            if attr.endswith("key")
2060        ):
2061            if functionality in NOT_IMPLEMENTED:
2062                key = "Not Implemented"
2063            if key == "":
2064                key = "Disabled"
2065
2066            pairs.append([functionality, key])
2067
2068        max_func = max(len(func) for func, key in pairs)
2069        return "\n".join(
2070            "{} : {}".format(func.rjust(max_func), key) for func, key in pairs
2071        )
2072
2073    def get_session_formatted_for_file(self) -> str:
2074        def process():
2075            for line, lineType in self.all_logical_lines:
2076                if lineType == LineType.INPUT:
2077                    yield line
2078                elif line.rstrip():
2079                    yield "# OUT: %s" % line
2080            yield "### %s" % self.current_line
2081
2082        return "\n".join(process())
2083
2084    @property
2085    def ps1(self):
2086        return _process_ps(super().ps1, ">>> ")
2087
2088    @property
2089    def ps2(self):
2090        return _process_ps(super().ps2, "... ")
2091
2092
2093def is_nop(char):
2094    return unicodedata.category(str(char)) == "Cc"
2095
2096
2097def tabs_to_spaces(line):
2098    return line.replace("\t", "    ")
2099
2100
2101def _last_word(line):
2102    split_line = line.split()
2103    return split_line.pop() if split_line else ""
2104
2105
2106def compress_paste_event(paste_event):
2107    """If all events in a paste event are identical and not simple characters,
2108    returns one of them
2109
2110    Useful for when the UI is running so slowly that repeated keypresses end up
2111    in a paste event.  If we value not getting delayed and assume the user is
2112    holding down a key to produce such frequent key events, it makes sense to
2113    drop some of the events.
2114    """
2115    if not all(paste_event.events[0] == e for e in paste_event.events):
2116        return None
2117    event = paste_event.events[0]
2118    # basically "is there a special curtsies names for this key?"
2119    if len(event) > 1:
2120        return event
2121    else:
2122        return None
2123
2124
2125def just_simple_events(event_list):
2126    simple_events = []
2127    for e in event_list:
2128        # '\n' necessary for pastes
2129        if e in ("<Ctrl-j>", "<Ctrl-m>", "<PADENTER>", "\n", "\r"):
2130            simple_events.append("\n")
2131        elif isinstance(e, events.Event):
2132            pass  # ignore events
2133        elif e in ("<SPACE>",):
2134            simple_events.append(" ")
2135        elif len(e) > 1:
2136            pass  # get rid of <Ctrl-a> etc.
2137        else:
2138            simple_events.append(e)
2139    return simple_events
2140
2141
2142def is_simple_event(e):
2143    if isinstance(e, events.Event):
2144        return False
2145    if e in ("<Ctrl-j>", "<Ctrl-m>", "<PADENTER>", "\n", "\r", "<SPACE>"):
2146        return True
2147    if len(e) > 1:
2148        return False
2149    else:
2150        return True
2151