Repository: prompt-toolkit/python-prompt-toolkit Branch: main Commit: 940af53fa443 Files: 342 Total size: 1.9 MB Directory structure: gitextract_htk8glhs/ ├── .codecov.yml ├── .github/ │ └── workflows/ │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS.rst ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── PROJECTS.rst ├── README.rst ├── appveyor.yml ├── docs/ │ ├── conf.py │ ├── index.rst │ ├── make.bat │ ├── pages/ │ │ ├── advanced_topics/ │ │ │ ├── architecture.rst │ │ │ ├── asyncio.rst │ │ │ ├── filters.rst │ │ │ ├── index.rst │ │ │ ├── input_hooks.rst │ │ │ ├── key_bindings.rst │ │ │ ├── rendering_flow.rst │ │ │ ├── rendering_pipeline.rst │ │ │ ├── styling.rst │ │ │ └── unit_testing.rst │ │ ├── asking_for_a_choice.rst │ │ ├── asking_for_input.rst │ │ ├── dialogs.rst │ │ ├── full_screen_apps.rst │ │ ├── gallery.rst │ │ ├── getting_started.rst │ │ ├── printing_text.rst │ │ ├── progress_bars.rst │ │ ├── reference.rst │ │ ├── related_projects.rst │ │ ├── tutorials/ │ │ │ ├── index.rst │ │ │ └── repl.rst │ │ └── upgrading/ │ │ ├── 2.0.rst │ │ ├── 3.0.rst │ │ └── index.rst │ └── requirements.txt ├── examples/ │ ├── choices/ │ │ ├── color.py │ │ ├── default.py │ │ ├── frame-and-bottom-toolbar.py │ │ ├── gray-frame-on-accept.py │ │ ├── many-choices.py │ │ ├── mouse-support.py │ │ ├── simple-selection.py │ │ └── with-frame.py │ ├── dialogs/ │ │ ├── button_dialog.py │ │ ├── checkbox_dialog.py │ │ ├── input_dialog.py │ │ ├── messagebox.py │ │ ├── password_dialog.py │ │ ├── progress_dialog.py │ │ ├── radio_dialog.py │ │ ├── styled_messagebox.py │ │ └── yes_no_dialog.py │ ├── full-screen/ │ │ ├── ansi-art-and-textarea.py │ │ ├── buttons.py │ │ ├── calculator.py │ │ ├── dummy-app.py │ │ ├── full-screen-demo.py │ │ ├── hello-world.py │ │ ├── no-layout.py │ │ ├── pager.py │ │ ├── scrollable-panes/ │ │ │ ├── simple-example.py │ │ │ └── with-completion-menu.py │ │ ├── simple-demos/ │ │ │ ├── alignment.py │ │ │ ├── autocompletion.py │ │ │ ├── colorcolumn.py │ │ │ ├── cursorcolumn-cursorline.py │ │ │ ├── float-transparency.py │ │ │ ├── floats.py │ │ │ ├── focus.py │ │ │ ├── horizontal-align.py │ │ │ ├── horizontal-split.py │ │ │ ├── line-prefixes.py │ │ │ ├── margins.py │ │ │ ├── vertical-align.py │ │ │ └── vertical-split.py │ │ ├── split-screen.py │ │ └── text-editor.py │ ├── gevent-get-input.py │ ├── print-text/ │ │ ├── ansi-colors.py │ │ ├── ansi.py │ │ ├── html.py │ │ ├── named-colors.py │ │ ├── print-formatted-text.py │ │ ├── print-frame.py │ │ ├── prompt-toolkit-logo-ansi-art.py │ │ ├── pygments-tokens.py │ │ └── true-color-demo.py │ ├── progress-bar/ │ │ ├── a-lot-of-parallel-tasks.py │ │ ├── colored-title-and-label.py │ │ ├── custom-key-bindings.py │ │ ├── many-parallel-tasks.py │ │ ├── nested-progress-bars.py │ │ ├── scrolling-task-name.py │ │ ├── simple-progress-bar.py │ │ ├── styled-1.py │ │ ├── styled-2.py │ │ ├── styled-apt-get-install.py │ │ ├── styled-rainbow.py │ │ ├── styled-tqdm-1.py │ │ ├── styled-tqdm-2.py │ │ ├── two-tasks.py │ │ └── unknown-length.py │ ├── prompts/ │ │ ├── accept-default.py │ │ ├── asyncio-prompt.py │ │ ├── auto-completion/ │ │ │ ├── autocomplete-with-control-space.py │ │ │ ├── autocompletion-like-readline.py │ │ │ ├── autocompletion.py │ │ │ ├── colored-completions-with-formatted-text.py │ │ │ ├── colored-completions.py │ │ │ ├── combine-multiple-completers.py │ │ │ ├── fuzzy-custom-completer.py │ │ │ ├── fuzzy-word-completer.py │ │ │ ├── multi-column-autocompletion-with-meta.py │ │ │ ├── multi-column-autocompletion.py │ │ │ ├── nested-autocompletion.py │ │ │ └── slow-completions.py │ │ ├── auto-suggestion.py │ │ ├── autocorrection.py │ │ ├── bottom-toolbar.py │ │ ├── clock-input.py │ │ ├── colored-prompt.py │ │ ├── confirmation-prompt.py │ │ ├── cursor-shapes.py │ │ ├── custom-key-binding.py │ │ ├── custom-lexer.py │ │ ├── custom-vi-operator-and-text-object.py │ │ ├── enforce-tty-input-output.py │ │ ├── fancy-zsh-prompt.py │ │ ├── finalterm-shell-integration.py │ │ ├── get-input-vi-mode.py │ │ ├── get-input-with-default.py │ │ ├── get-input.py │ │ ├── get-multiline-input.py │ │ ├── get-password-with-toggle-display-shortcut.py │ │ ├── get-password.py │ │ ├── history/ │ │ │ ├── persistent-history.py │ │ │ └── slow-history.py │ │ ├── html-input.py │ │ ├── input-validation.py │ │ ├── inputhook.py │ │ ├── mouse-support.py │ │ ├── multiline-autosuggest.py │ │ ├── multiline-prompt.py │ │ ├── no-wrapping.py │ │ ├── operate-and-get-next.py │ │ ├── patch-stdout.py │ │ ├── placeholder-text.py │ │ ├── regular-language.py │ │ ├── rprompt.py │ │ ├── swap-light-and-dark-colors.py │ │ ├── switch-between-vi-emacs.py │ │ ├── system-clipboard-integration.py │ │ ├── system-prompt.py │ │ ├── terminal-title.py │ │ ├── up-arrow-partial-string-matching.py │ │ └── with-frames/ │ │ ├── frame-and-autocompletion.py │ │ ├── gray-frame-on-accept.py │ │ └── with-frame.py │ ├── ssh/ │ │ └── asyncssh-server.py │ ├── telnet/ │ │ ├── chat-app.py │ │ ├── dialog.py │ │ ├── hello-world.py │ │ └── toolbar.py │ └── tutorial/ │ ├── README.md │ └── sqlite-cli.py ├── pyproject.toml ├── src/ │ └── prompt_toolkit/ │ ├── __init__.py │ ├── application/ │ │ ├── __init__.py │ │ ├── application.py │ │ ├── current.py │ │ ├── dummy.py │ │ └── run_in_terminal.py │ ├── auto_suggest.py │ ├── buffer.py │ ├── cache.py │ ├── clipboard/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── in_memory.py │ │ └── pyperclip.py │ ├── completion/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── deduplicate.py │ │ ├── filesystem.py │ │ ├── fuzzy_completer.py │ │ ├── nested.py │ │ └── word_completer.py │ ├── contrib/ │ │ ├── __init__.py │ │ ├── completers/ │ │ │ ├── __init__.py │ │ │ └── system.py │ │ ├── regular_languages/ │ │ │ ├── __init__.py │ │ │ ├── compiler.py │ │ │ ├── completion.py │ │ │ ├── lexer.py │ │ │ ├── regex_parser.py │ │ │ └── validation.py │ │ ├── ssh/ │ │ │ ├── __init__.py │ │ │ └── server.py │ │ └── telnet/ │ │ ├── __init__.py │ │ ├── log.py │ │ ├── protocol.py │ │ └── server.py │ ├── cursor_shapes.py │ ├── data_structures.py │ ├── document.py │ ├── enums.py │ ├── eventloop/ │ │ ├── __init__.py │ │ ├── async_generator.py │ │ ├── inputhook.py │ │ ├── utils.py │ │ └── win32.py │ ├── filters/ │ │ ├── __init__.py │ │ ├── app.py │ │ ├── base.py │ │ ├── cli.py │ │ └── utils.py │ ├── formatted_text/ │ │ ├── __init__.py │ │ ├── ansi.py │ │ ├── base.py │ │ ├── html.py │ │ ├── pygments.py │ │ └── utils.py │ ├── history.py │ ├── input/ │ │ ├── __init__.py │ │ ├── ansi_escape_sequences.py │ │ ├── base.py │ │ ├── defaults.py │ │ ├── posix_pipe.py │ │ ├── posix_utils.py │ │ ├── typeahead.py │ │ ├── vt100.py │ │ ├── vt100_parser.py │ │ ├── win32.py │ │ └── win32_pipe.py │ ├── key_binding/ │ │ ├── __init__.py │ │ ├── bindings/ │ │ │ ├── __init__.py │ │ │ ├── auto_suggest.py │ │ │ ├── basic.py │ │ │ ├── completion.py │ │ │ ├── cpr.py │ │ │ ├── emacs.py │ │ │ ├── focus.py │ │ │ ├── mouse.py │ │ │ ├── named_commands.py │ │ │ ├── open_in_editor.py │ │ │ ├── page_navigation.py │ │ │ ├── scroll.py │ │ │ ├── search.py │ │ │ └── vi.py │ │ ├── defaults.py │ │ ├── digraphs.py │ │ ├── emacs_state.py │ │ ├── key_bindings.py │ │ ├── key_processor.py │ │ └── vi_state.py │ ├── keys.py │ ├── layout/ │ │ ├── __init__.py │ │ ├── containers.py │ │ ├── controls.py │ │ ├── dimension.py │ │ ├── dummy.py │ │ ├── layout.py │ │ ├── margins.py │ │ ├── menus.py │ │ ├── mouse_handlers.py │ │ ├── processors.py │ │ ├── screen.py │ │ ├── scrollable_pane.py │ │ └── utils.py │ ├── lexers/ │ │ ├── __init__.py │ │ ├── base.py │ │ └── pygments.py │ ├── log.py │ ├── mouse_events.py │ ├── output/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── color_depth.py │ │ ├── conemu.py │ │ ├── defaults.py │ │ ├── flush_stdout.py │ │ ├── plain_text.py │ │ ├── vt100.py │ │ ├── win32.py │ │ └── windows10.py │ ├── patch_stdout.py │ ├── py.typed │ ├── renderer.py │ ├── search.py │ ├── selection.py │ ├── shortcuts/ │ │ ├── __init__.py │ │ ├── choice_input.py │ │ ├── dialogs.py │ │ ├── progress_bar/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── formatters.py │ │ ├── prompt.py │ │ └── utils.py │ ├── styles/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── defaults.py │ │ ├── named_colors.py │ │ ├── pygments.py │ │ ├── style.py │ │ └── style_transformation.py │ ├── token.py │ ├── utils.py │ ├── validation.py │ ├── widgets/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── dialogs.py │ │ ├── menus.py │ │ └── toolbars.py │ └── win32_types.py ├── tests/ │ ├── test_async_generator.py │ ├── test_buffer.py │ ├── test_cli.py │ ├── test_completion.py │ ├── test_document.py │ ├── test_filter.py │ ├── test_formatted_text.py │ ├── test_history.py │ ├── test_inputstream.py │ ├── test_key_binding.py │ ├── test_layout.py │ ├── test_memory_leaks.py │ ├── test_print_formatted_text.py │ ├── test_regular_languages.py │ ├── test_shortcuts.py │ ├── test_style.py │ ├── test_style_transformation.py │ ├── test_utils.py │ ├── test_vt100_output.py │ ├── test_widgets.py │ └── test_yank_nth_arg.py ├── tools/ │ ├── debug_input_cross_platform.py │ └── debug_vt100_input.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ comment: off ================================================ FILE: .github/workflows/test.yaml ================================================ name: test on: push: # any branch pull_request: branches: [main] env: FORCE_COLOR: 1 jobs: test-ubuntu: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - uses: actions/checkout@v6.0.2 - uses: astral-sh/setup-uv@v7 with: python-version: ${{ matrix.python-version }} - name: Code formatting if: ${{ matrix.python-version == '3.14' }} run: | uv run ruff check . uv run ruff format --check . - name: Typos if: ${{ matrix.python-version == '3.14' }} run: | uv run typos . - name: Unit test run: | uv run coverage run -m pytest tests/ - name: Type Checking run: | uv run mypy --strict src/ --platform win32 uv run mypy --strict src/ --platform linux uv run mypy --strict src/ --platform darwin - name: Run codecov run: | uv run codecov - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Python 3rd Party Pipfile* # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml .pytest_cache coverage.xml # Translations *.mo # Makefile - for those who like that workflow Makefile # Mr Developer .mr.developer.cfg .project .pydevproject # Generated documentation docs/_build # pycharm metadata .idea # uv uv.lock # vscode metadata .vscode # virtualenvs .venv* ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v6.0.0" hooks: - id: check-case-conflict - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-toml - id: detect-private-key - id: end-of-file-fixer - id: fix-byte-order-marker - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.15.6" hooks: - id: ruff-format args: [--config=pyproject.toml] - id: ruff-check args: [--config=pyproject.toml, --fix, --exit-non-zero-on-fix] - repo: https://github.com/crate-ci/typos rev: v1.44.0 hooks: - id: typos exclude: | (?x)^( tests/.* )$ ================================================ FILE: .readthedocs.yml ================================================ version: 2 build: os: ubuntu-22.04 tools: python: "3.11" formats: - pdf - epub sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt - method: pip path: . ================================================ FILE: AUTHORS.rst ================================================ Authors ======= Creator ------- Jonathan Slenders Contributors ------------ - Amjith Ramanujam ================================================ FILE: CHANGELOG ================================================ CHANGELOG ========= 3.0.52: 2025-08-27 ------------------ New features: - Add `choice()` shortcut for selecting an option amongst a list of choices (see documentation for examples). - Add support for ANSI dim text formatting. - Add `frame=...` option for `prompt()` and `choice()` shortcuts to allow for displaying a frame around the input prompt. Fixes: - Fix button width when non English characters are displayed. - Implement flushing in Windows VT100 input. - Fix signal handling for GraalPy. - Fix handling of zero sized dimensions. 3.0.51: 2025-04-15 ------------------ New features: - Use pyproject.toml instead of setup.py. Fixes: - Fix edge case in `formatted_text.split_lines` when the input starts with a line ending. 3.0.50: 2025-01-20 ------------------ Fixes: - Fixes non user impacting regression on the output rendering. Don't render cursor hide/show ANSI escape codes if not needed. 3.0.49: 2025-01-20 ------------------ New features: - On Windows, use virtual terminal input when available. - Support for multiline suggestions. Fixes: - Handle `InvalidStateError` during termination when using `run_in_terminal`/`patch_stdout`. This can happen in some cases during cancellation, probably when using anyio. - Fix cursor that remains in hidden state when the application exits. This can happen when the application doesn't show the cursor and `erase_when_done` is being used. Breaking changes: - Drop support for Python 3.7: 3.0.48: 2024-09-25 ------------------ Fixes: - Typing improvements: * Add `@overload` to `contrib.regular_languages.compiler.Variables.get`. * Use `Sequence` instead of `list` for `words` argument in completers. - Improve `ModalCursorShapeConfig`: * Display an "underscore" cursor in Vi's "replace single" mode, like "replace" mode. * Display an "beam" cursor in Emacs (insert) mode. 3.0.47: 2024-06-10 ------------------ New features: - Allow passing exception classes for `KeyboardInterrupt` and `EOFError` in `PromptSession`. Fixes: - Compute padding parameters for `Box` widget lazily. 3.0.46: 2024-06-04 ------------------ Fixes: - Fix pytest capsys fixture compatibility. 3.0.45: 2024-05-28 ------------------ Fixes: - Improve performance of `GrammarCompleter` (faster deduplication of completions). 3.0.44: 2024-05-27 ------------------ New features: - Accept `os.PathLike` in `FileHistory` (typing fix). Fixes: - Fix memory leak in filters. - Improve performance of progress bar formatters. - Fix compatibility when a SIGINT handler is installed by non-Python (Rust, C). - Limit number of completions in buffer to 10k by default (for performance). 3.0.43: 2023-12-13 ------------------ Fixes: - Fix regression on Pypy: Don't use `ctypes.pythonapi` to restore SIGINT if not available. 3.0.42: 2023-12-12 ------------------ Fixes: - Fix line wrapping in `patch_stdout` on Windows. - Make `formatted_text.split_lines()` accept an iterable instead of lists only. - Disable the IPython workaround (from 3.0.41) for IPython >= 8.18. - Restore signal.SIGINT handler between prompts. 3.0.41: 2023-11-14 ------------------ Fixes: - Fix regression regarding IPython input hook (%gui) integration. 3.0.40: 2023-11-10 ------------------ Fixes: - Improved Python 3.12 support (fixes event loop `DeprecationWarning`). New features: - Vi key bindings: `control-t` and `control-d` for indent/unindent in insert mode. - Insert partial suggestion when `control+right` is pressed, similar to Fish. - Use sphinx-nefertiti theme for the docs. 3.0.39: 2023-07-04 ------------------ Fixes: - Fix `RuntimeError` when `__breakpointhook__` is called from another thread. - Fix memory leak in filters usage. - Ensure that key bindings are handled in the right context (when using contextvars). New features: - Accept `in_thread` keyword in `prompt_toolkit.shortcuts.prompt()`. - Support the `NO_COLOR` environment variable. 3.0.38: 2023-02-28 ------------------ Fixes: - Fix regression in filters. (Use of `WeakValueDictionary` caused filters to not be cached). New features: - Use 24-bit true color now by default on Windows 10/11. 3.0.37: 2023-02-21 ------------------ Bug fixes: - Fix `currentThread()` deprecation warning. - Fix memory leak in filters. - Make VERSION tuple numeric. New features: - Add `.run()` method in `TelnetServer`. (To be used instead of `.start()/.stop()`. Breaking changes: - Subclasses of `Filter` have to call `super()` in their `__init__`. - Drop support for Python 3.6: * This includes code cleanup for Python 3.6 compatibility. * Use `get_running_loop()` instead of `get_event_loop()`. * Use `asyncio.run()` instead of `asyncio.run_until_complete()`. 3.0.36: 2022-12-06 ------------------ Fixes: - Another Python 3.6 fix for a bug that was introduced in 3.0.34. 3.0.35: 2022-12-06 ------------------ Fixes: - Fix bug introduced in 3.0.34 for Python 3.6. Use asynccontextmanager implementation from prompt_toolkit itself. 3.0.34: 2022-12-06 ------------------ Fixes: - Improve completion performance in various places. - Improve renderer performance. - Handle `KeyboardInterrupt` when the stacktrace of an unhandled error is displayed. - Use correct event loop in `Application.create_background_task()`. - Fix `show_cursor` attribute in `ScrollablePane`. 3.0.33: 2022-11-21 ------------------ Fixes: - Improve termination of `Application`. Don't suppress `CancelledError`. This fixes a race condition when an `Application` gets cancelled while we're waiting for the background tasks to complete. - Fixed typehint for `OneStyleAndTextTuple`. - Small bugfix in `CombinedRegistry`. Fixed missing `@property`. 3.0.32: 2022-11-03 ------------------ Bug fixes: - Use `DummyInput` by default in `create_input()` if `sys.stdin` does not have a valid file descriptor. This fixes errors when `sys.stdin` is patched in certain situations. - Fix control-c key binding for `ProgressBar` when the progress bar was not created from the main thread. The current code would try to kill the main thread when control-c was pressed. New features: - Accept a `cancel_callback` in `ProgressBar` to specify the cancellation behavior for when `control-c` is pressed. - Small performance improvement in the renderer. 3.0.31: 2022-09-02 ------------------ New features: - Pass through `name` property in `TextArea` widget to `Buffer`. - Added a `enable_cpr` parameter to `Vt100_Output`, `TelnetServer` and `PromptToolkitSSHServer`, to completely disable CPR support instead of automatically detecting it. 3.0.30: 2022-06-27 ------------------ New features: - Allow zero-width-escape sequences in `print_formatted_text`. - Add default value option for input dialog. - Added `has_suggestion` filter. Fixes: - Fix rendering of control-shift-6 (or control-^). Render as '^^' - Always wrap lines in the Label widget by default. - Fix enter key binding in system toolbar in Vi mode. - Improved handling of stdout objects that don't have a 'buffer' attribute. For instance, when using `renderer_print_formatted_text` in a Jupyter Notebook. 3.0.29: 2022-04-04 ------------------ New features: - Accept 'handle_sigint' parameter in PromptSession. Fixes - Fix 'variable referenced before assignment' error in vt100 mouse bindings. - Pass `handle_sigint` from `Application.run` to `Application.run_async`. - Fix detection of telnet client side changes. - Fix `print_container` utility (handle `EOFError`). Breaking changes: - The following are now context managers: `create_pipe_input`, `PosixPipeInput` and `Win32PipeInput`. 3.0.28: 2022-02-11 ------------------ New features: - Support format specifiers for HTML and ANSI formatted text. - Accept defaults for checkbox and radio list, and their corresponding dialogs. Fixes: - Fix resetting of cursor shape after the application terminates. 3.0.27: 2022-02-07 ------------------ New features: - Support for cursor shapes. The cursor shape for prompts/applications can now be configured, either as a fixed cursor shape, or in case of Vi input mode, according to the current input mode. - Handle "cursor forward" command in ANSI formatted text. This makes it possible to render many kinds of generated ANSI art. - Accept `align` attribute in `Label` widget. - Added `PlainTextOutput`: an output implementation that doesn't render any ANSI escape sequences. This will be used by default when redirecting stdout to a file. - Added `create_app_session_from_tty`: a context manager that enforces input/output to go to the current TTY, even if stdin/stdout are attached to pipes. - Added `to_plain_text` utility for converting formatted text into plain text. Fixes: - Don't automatically use `sys.stderr` for output when `sys.stdout` is not a TTY, but `sys.stderr` is. The previous behavior was confusing, especially when rendering formatted text to the output, and we expect it to follow redirection. 3.0.26: 2022-01-27 ------------------ Fixes: - Fixes issue introduced in 3.0.25: Don't handle SIGINT on Windows. 3.0.25: 2022-01-27 ------------------ Fixes: - Use `DummyOutput` when `sys.stdout` is `None` and `DummyInput` when `sys.stdin` is `None`. This fixes an issue when the code runs on windows, using pythonw.exe and still tries to interact with the terminal. - Correctly reset `Application._is_running` flag in case of exceptions in some situations. - Handle SIGINT (when sent from another process) and allow binding it to a key binding. For prompt sessions, the behavior is now identical to pressing control-c. - Increase the event loop `slow_duration_callback` by default to 0.5. This prevents printing warnings if rendering takes too long on slow systems. 3.0.24: 2021-12-09 ------------------ Fixes: - Prevent window content overflowing when using scrollbars. - Handle `PermissionError` when trying to attach /dev/null in vt100 input. 3.0.23: 2021-11-26 ------------------ Fixes: - Fix multiline bracketed paste on Windows New features: - Add support for some CSI 27 modified variants of "Enter" for xterm in the vt100 input parser. 3.0.22: 2021-11-04 ------------------ Fixes: - Fix stopping of telnet server (capture cancellation exception). 3.0.21: 2021-10-21 ------------------ New features: - Improved mouse support: * Support for click-drag, which is useful for selecting text. * Detect mouse movements when no button is pressed. - Support for Python 3.10. 3.0.20: 2021-08-20 ------------------ New features: - Add support for strikethrough text attributes. - Set up custom breakpointhook while an application is running (if no other breakpointhook was installed). This enhances the usage of PDB for debugging applications. - Strict type checking is now enabled. Fixes: - Ensure that `print_formatted_text` is always printed above the running application, like `patch_stdout`. (Before, `patch_stdout` was even completely ignored in case of `print_formatted_text, so there was no proper way to use it in a running application.) - Fix handling of non-bmp unicode input on Windows. - Set minimum Python version to 3.6.2 (Some 3.6.2 features were used). 3.0.19: 2021-06-17 ------------------ Fixes: - Make the flush method of the vt100 output implementation re-entrant (fixes an issue when using aiogevent). - Fix off-by-one in `FormattedTextControl` mouse logic. - Run `print_container` always in a thread (avoid interfering with possible event loop). - Make sphinx autodoc generation platform agnostic (don't import Windows stuff when generating Sphinx docs). 3.0.18: 2021-03-22 ------------------ New features: - Added `in_thread` parameter to `Application.run`. This is useful for running an application in a background thread, while the main thread blocks. This way, we are sure not to interfere with an event loop in the current thread. (This simplifies some code in ptpython and fixes an issue regarding leaking file descriptors due to not closing the event loop that was created in this background thread.) 3.0.17: 2021-03-11 ------------------ New features: - Accept `style` parameter in `print_container` utility. - On Windows, handle Control-Delete. Fixes: - Avoid leaking file descriptors in SSH server. 3.0.16: 2021-02-11 ------------------ New features: - Added `ScrollablePane`: a scrollable layout container. This allows applications to build a layout, larger than the terminal, with a vertical scroll bar. The vertical scrolling will be done automatically when certain widgets receive the focus. - Added `DeduplicateCompleter and `ConditionalCompleter`. - Added `deduplicate` argument to `merge_completers`. 3.0.15: 2021-02-10 ------------------ Fixes: - Set stdout blocking when writing in vt100 output. Fixes an issue when uvloop is used and big amounts of text are written. - Guarantee height of at least 1 for both labels and text areas. - In the `Window` rendering, take `dont_extend_width`/`dont_extend_height` into account. This fixes issues where one window is enlarged unexpectedly because it's bundled with another window in a `HSplit`/`VSplit`, but with different width/height. - Don't handle `SIGWINCH` in progress bar anymore. (The UI runs in another thread, and we have terminal size polling now). - Fix several thread safety issues and a race condition in the progress bar. - Fix thread safety issues in `Application.invalidate()`. (Fixes a `RuntimeError` in some situations when using progress bars.) - Fix handling of mouse events on Windows if we have a Windows 10 console with ANSI support. - Disable `QUICK_EDIT_MODE` on Windows 10 when mouse support is requested. 3.0.14: 2021-01-24 ------------------ New features: - Disable bell when `PROMPT_TOOLKIT_BELL=false` environment variable has been set. Fixes: - Improve cancellation of history loading. 3.0.13: 2021-01-21 ------------------ Fixes: - Again, fixed the race condition in `ThreadedHistory`. Previous fix was not correct. 3.0.12: 2021-01-21 ------------------ Fixes: - Fixed a race condition in `ThreadedHistory` that happens when continuously pasting input text (which would continuously repopulate the history). - Move cursor key mode resetting (for vt100 terminals) to the renderer. (Mostly cleanup). 3.0.11: 2021-01-20 ------------------ New features: - Poll terminal size: better handle resize events when the application runs in a thread other than the main thread (where handling SIGWINCH doesn't work) or in the Windows console. Fixes: - Fix bug in system toolbar. The execution of system commands was broken. - A refactoring of patch_stdout that includes several fixes. * We know look at the `AppSession` in order to see which application is running, rather then looking at the event loop which is installed when `StdoutProxy` is created. This way, `patch_stdout` will work when prompt_toolkit applications with a different event loop run. * Fix printing when no application/event loop is running. * Fixed the `raw` argument of `PatchStdout`. - A refactoring of the `ThreadedHistory`, which includes several fixes, in particular a race condition (see issue #1158) that happened when editing input while a big history was still being loaded in the background. 3.0.10: 2021-01-08 ------------------ New features: - Improved `WordCompleter`: accept `display_dict`. Also accept formatted text for both `display_dict` and `meta_dict`. - Allow customization of button arrows. Fixes: - Correctly recognize backtab on Windows. - Show original display text in fuzzy completer if no filtering was done. 3.0.9: 2021-01-05 ----------------- New features: - Handle c-tab for TERM=linux. Fixes: - Improve rendering speed of `print_formatted_text`. (Don't render styling attributes to output between fragments that have identical styling.) - Gracefully handle `FileHistory` decoding errors. - Prevent asyncio deprecation warnings. 3.0.8: 2020-10-12 ----------------- New features: - Added `validator` parameter to `input_dialog`. Fixes: - Cope with stdout not having a working `fileno`. - Handle situation when /dev/null is piped into stdin, or when stdin is closed somehow. - Fix for telnet/ssh server: `isatty` method was not implemented. - Display correct error when a tuple is passed into `to_formatted_text`. - Pass along WORD parameter in `Document._is_word_before_cursor_complete`. Fixes some key bindings. - Expose `ProgressBarCounter` in shortcuts module. 3.0.7: 2020-08-29 ----------------- New features: - New "placeholder" parameter added to `PromptSession`. Other changes: - The "respond to CPR" logic has been moved from the `Input` to `Output` classes (this does clean up some code). Fixes: - Bugfix in shift-selection key bindings. - Fix height calculation of `FormattedTextControl` when line wrapping is turned on. - Fixes for SSH server: * Missing encoding property. * Fix failure in "set_line_mode" call. * Handle `BrokenPipeError`. 3.0.6: 2020-08-10 ----------------- New features: - The SSH/Telnet adaptors have been refactored and improved in several ways. See issues #876 and PR #1150 and #1184 on GitHub. * Handle terminal types for both telnet and SSH sessions. * Added pipe input abstraction. (base class for `PosixPipeInput` and `Win32PipeInput`). * The color depth logic has been refactored and moved to the `Output` implementations. Added `get_default_color_depth` method to `Output` objects. * All line feet are now preceded by a carriage return in the telnet connection stdout. - Introduce `REPLACE_SINGLE` input mode for Vi key bindings. - Improvements to the checkbox implementation: * Hide the scrollbar for a single checkbox. * Added a "checked" setter to the checkbox. - Expose `KeyPressEvent` in key_binding/__init__.py (often used in type annotations). - The renderer has been optimized so that no trailing spaces are generated (this improves copying in some terminals). Fixes: - Ignore F21..F24 key bindings by default. - Fix auto_suggest key bindings when suggestion text is empty. - Bugfix in SIGWINCH handling. - Handle bug in HSplit/VSplit when the number of children is zero. - Bugfix in CPR handling in renderer. Proper cancellation of pending tasks. - Ensure rprompt aligns with input. - Use `sys.stdin.encoding` for decoding stdin stream. 3.0.5: 2020-03-26 ----------------- Fixes: - Bugfix in mouse handling on Windows. 3.0.4: 2020-03-06 ----------------- New features: - Added many more vt100 ANSI sequences and keys. - Improved control/shift key support in Windows. - No Mypy errors in prompt_toolkit anymore. - Added `set_exception_handler` optional argument to `PromptSession.prompt()`. Fixes: - Bugfix in invalidate code. `PromptSession` was invalidating the UI continuously. - Add uvloop support (was broken due to an issue in our `call_soon_threadsafe`). - Forwarded `set_exception_handler` in `Application.run` to the `run_async` call. - Bugfix in `NestedCompleter` when there is a leading space. Breaking changes: - `ShiftControl` has been replaced with `ControlShift` and `s-c` with `c-s` in key bindings. Aliases for backwards-compatibility have been added. 3.0.3: 2020-01-26 ----------------- New features: - Improved support for "dumb" terminals. - Added support for new keys (vt100 ANSI sequences): Alt + home/end/page-up/page-down/insert. - Better performance for the "regular languages compiler". Generate fewer and better regular expressions. This should improve the start-up time for applications using this feature. - Better detection of default color depth. - Improved the progress bar: * Set "time left" to 0 when done or stopped. * Added `ProgressBarCounter.stopped`. - Accept callables for `scroll_offset`, `min_brightness` and `max_brightness`. - Added `always_prefer_tty` parameters to `create_input()` and `create_output()`. - Create a new event loop in `Application.run()` if `get_event_loop()` raises `Runtimeerror`. Fixes: - Correct cancellation of flush timers for input. (Fixes resource leak where too many useless coroutines were created.) - Improved the Win32 input event loop. This fixes a bug where the prompt_toolkit application is stopped by something other than user input. (In that case, the application would hang, waiting for input.) This also fixes a `RuntimeError` in the progress bar code. - Fixed `line-number.current` style. (was `current-line-number`.) - Handle situation where stdout is no longer a tty (fix bug in `get_size`). - Fix parsing of true color in ANSI strings. - Ignore `invalidate()` if the application is not running. 3.0.2: 2019-11-30 ----------------- Fixes: - Bugfix in the UI invalidation. Fixes an issue when the application runs again on another event loop. See: https://github.com/ipython/ipython/pull/11973 3.0.1: 2019-11-28 ----------------- New features: - Added `new_eventloop_with_inputhook` function. - Set exception handler from within `Application.run_async`. - Applied Black code style. Fixes: - No longer expect a working event loop in the `History` classes. (Fix for special situations when a `ThreadedHistory` is created before the event loop has been set up.) - Accept an empty prompt continuation. - A few fixes to the `Buffer` tempfile code. 3.0.0: 2019-11-24 ----------------- New features: - (almost) 100% type annotated. - Native asyncio instead of custom event loops. - Added shift-based text selection (use shift+arrows to start selecting text). Breaking changes: - Python 2 support has been dropped. Minimal Python version is now 3.6, although 3.7 is preferred (because of ContextVars). - Native asyncio, so some async code becomes slightly different. - The active `Application` became a contextvar. Which means that it should be propagated correctly to the code that requires it. However, random other threads or coroutines won't be able to know what the current application is. - The dialog shortcuts API changed. All dialog functions now return an `Application`. You still have to call either `run()` or `run_async` on the `Application` object. - The way inputhooks work is changed. - `patch_stdout` now requires an `Application` as input. 2.0.9: 2019-02-19 ----------------- Bug fixes: - Fixed `Application.run_system_command` on Windows. - Fixed bug in ANSI text formatting: correctly handle 256/true color sequences. - Fixed bug in WordCompleter. Provide completions when there's a space before the cursor. 2.0.8: 2019-01-27 ----------------- Bug fixes: - Fixes the issue where changes made to the buffer in the accept handler were not reflected in the history. - Fix in the application invalidate handler. This prevents a significant slow down in some applications after some time (especially if there is a refresh interval). - Make `print_container` utility work if the input is not a pty. New features: - Underline non breaking spaces instead of rendering as '&'. - Added mouse support for radio list. - Support completion styles for `READLINE_LIKE` display method. - Accept formatted text in the display text of completions. - Added a `FuzzyCompleter` and `FuzzyWordCompleter`. - Improved error handling in Application (avoid displaying a meaningless AssertionError in many cases). 2.0.7: 2018-10-30 ----------------- Bug fixes: - Fixed assertion in PromptSession: the style_transformation check was wrong. - Removed 'default' attribute in PromptSession. Only ask for it in the `prompt()` method. This fixes the issue that passing `default` once, will store it for all consequent calls in the `PromptSession`. - Ensure that `__pt_formatted_text__` always returns a `FormattedText` instance. This fixes an issue with `print_formatted_text`. New features: - Improved handling of situations where stdin or stdout are not a terminal. (Print warning instead of failing with an assertion.) - Added `print_container` utility. - Sound bell when attempting to edit read-only buffer. - Handle page-down and page-up keys in RadioList. - Accept any `collections.abc.Sequence` for HSplit/VSplit children (instead of lists only). - Improved Vi key bindings: return to navigation mode when Insert is pressed. 2.0.6: 2018-10-12 ----------------- Bug fixes: - Don't use the predefined ANSI colors for colors that are defined as RGB. (Terminals can assign different color schemes for ansi colors, and we don't want use any of those for colors that are defined like #aabbcc for instance.) - Fix in handling of CPRs when patch_stdout is used. Backwards incompatible changes: - Change to the `Buffer` class. Reset the buffer unless the `accept_handler` returns `True` (which means: "keep_text"). This doesn't affect applications that use `PromptSession`. New features: - Added `AdjustBrightnessStyleTransformation`. This is a simple style transformation that improves the rendering on terminals with light or dark background. - Improved performance (string width caching and line height calculation). - Improved `TextArea`: * Exposed `focus_on_click`. * Added attributes: `auto_suggest`, `complete_while_typing`, `history`, `get_line_prefix`, `input_processors`. * Made attributes writable: `lexer`, `completer`, `complete_while_typing`, `accept_handler`, `read_only`, `wrap_lines`. 2.0.5: 2018-09-30 ----------------- Bug fixes: - Fix in `DynamicContainer`. Return correct result for `get_children`. This fixes a bug related to focusing. - Properly compute length of `start`, `end` and `sym_b` characters of progress bar. - CPR (cursor position request) fix. Backwards incompatible changes: - Stop restoring `PromptSession` attributes when exiting prompt. New features: - Added `get_line_prefix` attribute to window. This opens many possibilities: * Line wrapping (soft and hard) can insert whitespace in front of the line, or insert some symbols in front. Like the Vim "breakindent" option. * Single line prompts also support line continuations now. * Line continuations can have a variable width. - For VI mode: implemented temporary normal mode (control-O in insert mode). - Added style transformations API. Useful for swapping between light and dark color schemes. Added `swap_light_and_dark_colors` parameter to `prompt()` function. - Added `format()` method to ANSI formatted text. - Set cursor position for Button widgets. - Added `pre_run` argument to `PromptSession.prompt()` method. 2.0.4: 2018-07-22 ----------------- Bug fixes: - Fix render height for rendering full screen applications in Windows. - Fix in `TextArea`. Set `accept_handler` to `None` if not given. - Go to the beginning of the next line when enter is pressed in Vi navigation mode, and the buffer doesn't have an accept handler. - Fix the `default` argument of the `prompt` function when called multiple times. - Display decomposed multiwidth characters correctly. - Accept `history` in `prompt()` function again. Backwards incompatible changes: - Renamed `PipeInput` to `PosixPipeInput`. Added `Win32PipeInput` and `create_input_pipe`. - Pass `buffer` argument to the `accept_handler` of `TextArea`. New features: - Added `accept_default` argument to `prompt()`. - Make it easier to change the body/title of a Frame/Dialog. - Added `DynamicContainer`. - Added `merge_completers` for merging multiple completers together. - Add vt100 data to key presses in Windows. - Handle left/right key bindings in Vi block insert mode. 2.0.3: 2018-06-08 ----------------- Bug fixes: - Fix in 'x' and 'X' Vi key bindings. Correctly handle line endings and args. - Fixed off by one error in Vi line selection. - Fixed bugs in Vi block selection. Correctly handle lines that the selection doesn't cross. - Python 2 bugfix. Handle str/unicode correctly. - Handle option+left/right in iTerm. 2.0.2: 2018-06-03 ----------------- Bug fixes: - Python 3.7 support: correctly handle StopIteration in asynchronous generator. - Fixed off-by-one bug in Vi visual block mode. - Bugfix in TabsProcessor: handle situations when the cursor is at the end of the line. 2.0.1: 2018-06-02 ----------------- Version 2.0 includes a big refactoring of the internal architecture. This includes the merge of the CommandLineInterface and the Application object, a rewrite of how user controls are focused, a rewrite of how event loops work and the removal of the buffers dictionary. This introduces many backwards incompatible changes, but the result is a very nice and powerful architecture. Most architectural changes effect full screen applications. For applications that use `prompt_toolkit.shortcuts` for simple prompts, there are fewer incompatibilities. Changes: - No automatic translation from \r into \n during the input processing. These are two different keys that can be handled independently. This is a big backward-incompatibility, because the `Enter` key is `ControlM`, not `ControlJ`. So, now that we stopped translating \r into \n, it could be that custom key bindings for `Enter` don't work anymore. Make sure to bind `Keys.Enter` instead of `Keys.ControlJ` for handling the `Enter` key. - The `CommandLineInterface` and the `Application` classes are merged. First, `CommandLineInterface` contained all the I/O objects (like the input, output and event loop), while the `Application` contained everything else. There was no practical reason to keep this separation. (`CommandLineInterface` was mostly a proxy to `Application`.) A consequence is that almost all code which used to receive a `CommandLineInterface`, will now use an `Application`. Usually, where we had an attribute `cli`, we'll now have an attribute `app`. Secondly, the `Application` object is no longer passed around. The `get_app` function can be used at any time to acquire the active application. (For backwards-compatibility, we have aliases to the old names, whenever possible.) - prompt_toolkit no longer depends on Pygments, but it can still use Pygments for its color schemes and lexers. In many places we used Pygments "Tokens", this has been replaced by the concept of class names, somewhat similar to HTML and CSS. * `PygmentsStyle` and `PygmentsLexer` adaptors are available for plugging in Pygments styles and lexers. * Wherever we had a list of `(Token, text)` tuples, we now have lists of `(style_string, text)` tuples. The style string can contain both inline styling as well as refer to a class from the style sheet. `PygmentsTokens` is an adaptor that converts a list of Pygments tokens into a list of `(style_string, text)` tuples. - Changes in the `Style` classes. * `style.from_dict` does not exist anymore. Instantiate the ``Style`` class directory to create a new style. ``Style.from_dict`` can be used to create a style from a dictionary, where the dictionary keys are a space separated list of class names, and the values, style strings (like before). * `print_tokens` was renamed to `print_formatted_text`. * In many places in the layout, we accept a parameter named `style`. All the styles from the layout hierarchy are combined to decide what style to be used. * The ANSI color names were confusing and inconsistent with common naming conventions. This has been fixed, but aliases for the original names were kept. - The way focusing works is different. Before it was always a `Buffer` that was focused, and because of that, any visible `BufferControl` that contained this `Buffer` would be focused. Now, any user control can be focused. All of this is handled in the `Application.layout` object. - The `buffers` dictionary (`CommandLineInterface.buffers`) does not exist anymore. Further, `buffers` was a `BufferMapping` that keeps track of which buffer has the focus. This significantly reduces the freedom for creating complex applications. We wanted to move toward a layout that can be defined as a (hierarchical) collection of user widgets. A user widget does not need to have a `Buffer` underneath and any widget should be focusable. * `layout.Layout` was introduced to contain the root layout widget and keep track of the focus. - The key bindings were refactored. It became much more flexible to combine sets of key bindings. * `Registry` has been renamed to `KeyBindings`. * The `add_binding` function has been renamed to simply `add`. * Every `load_*` function returns one `KeyBindings` objects, instead of populating an existing one, like before. * `ConditionalKeyBindings` was added. This can be used to enable/disable all the key bindings from a given `Registry`. * A function named `merge_key_bindings` was added. This takes a list of `KeyBindings` and merges them into one. * `key_binding.defaults.load_key_bindings` was added to load all the key bindings. * `KeyBindingManager` has been removed completely. * `input_processor` was renamed to `key_processor`. Further: * The `Key` class does not exist anymore. Every key is a string and it's considered fine to use string literals in the key bindings. This is more readable, but we still have run-time validation. The `Keys` enum still exist (for backwards-compatibility, but also to have an overview of which keys are supported.) * 'enter' and 'tab' are key aliases for 'c-m' and 'c-i'. - User controls can define key bindings, which are active when the user control is focused. * `UIControl` got a `get_key_bindings` (abstract) method. - Changes in the layout engine: * `LayoutDimension` was renamed to `Dimension`. * `VSplit` and `HSplit` now take a `padding` argument. * `VSplit` and `HSplit` now take an `align` argument. (TOP/CENTER/BOTTOM/JUSTIFY) or (LEFT/CENTER/RIGHT/JUSTIFY). * `Float` now takes `allow_cover_cursor` and `attach_to_window` arguments. * `Window` got an `WindowAlign` argument. This can be used for the alignment of the content. `TokenListControl` (renamed to `FormattedTextControl`) does not have an alignment argument anymore. * All container objects, like `Window`, got a `style` argument. The style for parent containers propagate to child containers, but can be overridden. This is in particular useful for setting a background color. * `FillControl` does not exist anymore. Use the `style` and `char` arguments of the `Window` class instead. * `DummyControl` was added. * The continuation function of `PromptMargin` now takes `line_number` and `is_soft_wrap` as input. - Changes to `BufferControl`: * The `InputProcessor` class has been refactored. The `apply_transformation` method should now takes a `TransformationInput` object as input. * The text `(reverse-i-search)` is now displayed through a processor. (See the `shortcuts` module for an example of its usage.) - `widgets` and `dialogs` modules: * A small collection of widgets was added. These are more complex collections of user controls that are ready to embed in a layout. A `shortcuts.dialogs` module was added as a high level API for displaying input, confirmation and message dialogs. * Every class that exposes a ``__pt_container__`` method (which is supposed to return a ``Container`` instance) is considered a widget. The ``to_container`` shortcut will call this method in situations where a ``Container`` object is expected. This avoids inheritance from other ``Container`` types, but also having to unpack the container object from the widget, in case we would have used composition. * Warning: The API of the widgets module is not considered stable yet, and can change is the future, if needed. - Changes to `Buffer`: * A `Buffer` no longer takes an `accept_action`. Both `AcceptAction` and `AbortAction` have been removed. Instead it takes an `accept_handler`. - Changes regarding auto completion: * The left and right arrows now work in the multi-column auto completion menu. * By default, autocompletion is synchronous. The completer needs to be wrapped in `ThreadedCompleter` in order to get asynchronous autocompletion. * When the completer runs in a background thread, completions will be displayed as soon as they are generated. This means that we don't have to wait for all the completions to be generated, before displaying the first one. The completion menus are updated as soon as new completions arrive. - Changes regarding input validation: * Added the `Validator.from_callable` class method for easy creation of new validators. - Changes regarding the `History` classes: * The `History` base class has a different interface. This was needed for asynchronous loading of the history. `ThreadedHistory` was added for this. - Changes related to `shortcuts.prompt`: * There is now a class `PromptSession` which also has a method `prompt`. Both the class and the method take about the same arguments. This can be used to create a session. Every `prompt` call of the same instance will reuse all the arguments given to the class itself. The input history is always shared during the entire session. Of course, it's still possible to call the global `prompt` function. This will create a new `PromptSession` every time when it's called. * The `prompt` function now takes a `key_bindings` argument instead of `key_bindings_registry`. This should only contain the additional bindings. (The default bindings are always included.) - Changes to the event loops: * The event loop API is now closer to how asyncio works. A prompt_toolkit `Application` now has a `Future` object. Calling the `.run_async()` method creates and returns that `Future`. An event loop has a `run_until_complete` method that takes a future and runs the event loop until the Future is set. The idea is to be able to transition easily to asyncio when Python 2 support can be dropped in the future. * `Application` still has a method `run()` that underneath still runs the event loop until the `Future` is set and returns that result. * The asyncio adaptors (like the asyncio event loop integration) now require Python 3.5. (We use the async/await syntax internally.) * The `Input` and `Output` classes have some changes. (Not really important.) * `Application.run_sub_applications` has been removed. The alternative is to call `run_coroutine_in_terminal` which returns a `Future`. - Changes to the `filters` module: * The `Application` is no longer passed around, so both `CLIFilter` and `SimpleFilter` were merged into `Filter`. `to_cli_filter` and `to_simple_filter` became `to_filter`. * All filters have been turned into functions. For instance, `IsDone` became `is_done` and `HasCompletions` became `has_completions`. This was done because almost all classes were called without any arguments in the `__init__` causing additional braces everywhere. This means that `HasCompletions()` has to be replaced by `has_completions` (without parenthesis). The few filters that took arguments as input, became functions, but still have to be called with the given arguments. For new filters, it is recommended to use the `@Condition` decorator, rather then inheriting from `Filter`. - Other renames: * `IncrementalSearchDirection` was renamed to `SearchDirection`. * The `use_alternate_screen` parameter has been renamed to `full_screen`. * `Buffer.initial_document` was renamed to `Buffer.document`. * `TokenListControl` has been renamed to `FormattedTextControl`. * `Application.set_return_value` has been renamed to `Application.set_result`. - Other new features: * `DummyAutoSuggest` and `DynamicAutoSuggest` were added. * `DummyClipboard` and `DynamicClipboard` were added. * `DummyCompleter` and `DynamicCompleter` were added. * `DummyHistory` and `DynamicHistory` was added. * `to_container` and `to_window` utilities were added. 1.0.9: 2016-11-07 ----------------- Fixes: - Fixed a bug in the `cooked_mode` context manager. This caused a bug in ptpython where executing `input()` would display ^M instead of accepting the input. - Handle race condition in eventloop/posix.py - Updated ANSI color names for vt100. (High and low intensity colors were swapped.) New features: - Added yank-nth-arg and yank-last-arg readline commands + Emacs bindings. - Allow searching in Vi selection mode. - Made text objects of the Vi 'n' and 'N' search bindings. This adds for instance the following bindings: cn, cN, dn, dN, yn, yN 1.0.8: 2016-10-16 ----------------- Fixes: - In 'shortcuts': complete_while_typing was a SimpleFilter, not a CLIFilter. - Always reset color attributes after rendering. - Handle bug in Windows when '$TERM' is not defined. - Ignore errors when calling tcgetattr/tcsetattr. (This handles the "Inappropriate ioctl for device" crash in some scenarios.) - Fix for Windows. Correctly recognize all Chinese and Lithuanian characters. New features: - Added shift+left/up/down/right keys. - Small performance optimization in the renderer. - Small optimization in the posix event loop. Don't call time.time() if we don't have an inputhook. (Less syscalls.) - Turned the _max_postpone_until argument of call_from_executor into a float. (As returned by `time.time`.) This will do less system calls. It's backwards-incompatible, but this is still a private API, used only by pymux.) - Added Shift-I/A commands in Vi block selection mode for inserting text at the beginning of each line of the block. - Refactoring of the 'selectors' module for the posix event loop. (Reuse the same selector object in one loop, don't recreate it for each select.) 1.0.7: 2016-08-21 ----------------- Fixes: - Bugfix in completion. When calculating the common completion to be inserted, the new completions were calculated wrong. - On Windows, avoid extra vertical scrolling if the cursor is already on screen. New features: - Support negative arguments for next/previous word ending/beginning. 1.0.6: 2016-08-15 ----------------- Fixes: - Go to the start of the line in Vi navigation mode, when 'j' or 'k' have been pressed to navigate to a new history entry. - Don't crash when pasting text that contains \r\n characters. (This could happen in iTerm2.) - Python 2.6 compatibility fix. - Allow pressing before each -ve argument. - Better support for conversion from #ffffff values to ANSI colors in Vt100_Output. * Prefer colors with some saturation, instead of gray colors, if the given color was not gray. * Prefer a different foreground and background color if they were originally not the same. (This avoids concealing text.) New features: - Improved ANSI color support. * If the $PROMPT_TOOLKIT_ANSI_COLORS_ONLY environment variable has been set, use the 16 ANSI colors only. * Take an `ansi_colors_only` parameter in `Vt100_Output` and `shortcuts.create_output`. 1.0.5: 2016-08-04 ----------------- Fixes: - Critical fix for running on Windows. The gevent work-around in the inputhook caused 'An operation was attempted on something that is not a socket'. 1.0.4: 2016-08-03 ----------------- Fixes: - Key binding fixes: * Improved handling of repeat arguments in Emacs mode. Pressing sequences like 'esc---123' do now work (like GNU Readline): - repetition of the minus sign is ignored. - No esc prefix is required for each digit. * Fix in ControlX-ControlX binding. * Fix in bracketed paste. * Pressing Control-U at the start of the line now deletes the newline. * Pressing Control-K at the end of the line, deletes the newline after the cursor. * Support negative argument for Control-K * Fixed cash when left/right were pressed with a negative argument. (In Emacs mode.) * Fix in ControlUp/ControlDown key bindings. * Distinguish backspace from Control-H. They are not the same. * Delete in front of the cursor when a negative argument has been given to backspace. * Handle arrow keys correctly in emacs-term. - Performance optimizations: * Performance optimization in Registry. * Several performance optimization in filters. * Import asyncio inline (only if required). - Use the best possible selector in the event loop. This fixes bugs in situations where we have too many open file descriptors. - Fix UI freeze when gevent monkey patch has been applied. - Fix segmentation fault in Alpine Linux. (Regarding the use of ioctl.) - Use the correct colors on Windows. (When the foreground/background colors have been modified.) - Display a better error message when running in Idle. - Additional flags for vt100 inputs: disable flow control. - Also patch stderr in CommandLineInterface.patch_stdout_context. New features: - Allow users to enter Vi digraphs in reverse order. - Improved autocompletion behavior. See IPython issue #9658. - Added a 'clear' function in the shortcuts module. For future compatibility: - `Keys.Enter` has been added. This is the key that should be bound for handling the enter key. Right now, prompt_toolkit translates \r into \n during the handling of the input; this is not correct and makes it impossible to distinguish between ControlJ and ControlM. Some applications bind ControlJ for custom handling of the enter key, because this equals \n. However, in a future version we will stop replacing \r by \n and at that point, the enter key will be ControlM. So better is to use `Keys.Enter`, which becomes an alias for whatever the enter key translates into. 1.0.3: 2016-06-20 ----------------- Fixes: - Bugfix for Python2 in readline-like completion. - Bugfix in readline-like completion visualization. New features: - Added `erase_when_done` parameter to the `Application` class. (This was required for the bug fixes.) - Added (experimental) `CommandLineInterface.run_application_generator` method. (Also required for the bug fix.) 1.0.2: 2016-06-16 ----------------- Fixes: - Don't select the first completion when `complete_while_typing` is False. (Restore the old behavior.) 1.0.1: 2016-06-15 ----------------- Fixes: - Bugfix in GrammarValidator and SentenceValidator. - Don't leave the alternate screen on resize events. - Use errors=surrogateescape, in order to handle mouse events in some terminals. - Ignore key presses in _InterfaceEventLoopCallbacks.feed_key when the CLI is in the done state. - Bugfix in get_common_complete_suffix. Don't return any suffix when there are completions that change whatever is before the cursor. - Bugfix for Win32/Python2: use unicode literals: This crashed arrow navigation on Windows. - Bugfix in InputProcessor: handling of more complex key bindings. - Fix: don't apply completions, if there is only one completion which doesn't have any effect. - Fix: correctly handle prompts starting with a newline in prompt_toolkit.shortcuts. - Fix: thread safety in autocomplete code. - Improve styling for matching brackets. (Allow individual styling for the bracket under the cursor and the other.) - Fix in ShowLeadingWhiteSpaceProcessor/ShowTrailingWhiteSpaceProcessor: take output encoding into account. (The signature had to change a little for this.) - Bug fix in key bindings: only activate Emacs system/open-in-editor bindings if editing_mode is emacs. - Added write_binary parameter to Vt100_Output. This fixes a bug in some cases where we expect it to write non-encoded strings. - Fix key bindings for Vi mode registers. New features (**): - Added shortcuts.confirm/create_confirm_application function. - Emulate bracketed paste on Windows. (When the input stream contains multiple key presses among which a newline and at least one other character, consider this a paste event, and handle as bracketed paste on Unix. - Added key handler for displaying completions, just like readline does. - Implemented Vi guu,gUU,g~~ key bindings. - Implemented Vi 'gJ' key binding. - Implemented Vi ab,ib,aB,iB text objects. - Support for ZeroWidthEscape tokens in prompt and token lists. Used to support final shell integration. - Fix: Make document.text/cursor_position/selection read-only. (Changing these would break the caching causing bigger issues.) - Using pytest for unit tests. - Allow key bindings to have Keys.Any at any possible position. (Not just the end.) This made it significantly easier to write the named register Vi bindings, resulting in an approved start-up time.) - Better feedback when entering multi-key key bindings in insert mode. (E.g. when 'jj' would be mapped to escape.) - Small improvement in key processor: allow key bindings to generate new key presses. - Handle ControlUp and ControlDown by default: move to the previous/next record in the history. - Accept 'char'/'get_char' parameters in FillControl. - Added refresh_interval method to prompt() function. Performance improvements: - Improve the performance of test_callable_args: this should significantly increase the start-up time. - Start-up time for creating the Vi bindings has been improved significantly. (**) Some small backwards-compatible features were allowed for this minor release. After evaluating the impact/risk/work involved we concluded that we could ship these in a minor release. 1.0.0: 2016-05-05 ----------------- Fixes: - Adjust minimum completion menu width to match UIControl and Window class. - Bugfix regarding weakref in InputProcessor. - Fix for pypy3: bug in WeakValueDictionary. - Correctly handle '0' key binding in Vi mode. - Also load Vi bindings by default in Application if no registry has been given. - Only go into selection mode if the current buffer is not empty. - Close PipeInput after usage. - Only use 16 colors in (Emacs) eterm-color. - Bugfix in "xP Vi key binding. - Bugfix in Vi { and } key binding. - Fix: use correct token for Scrollbar in MultiColumnCompletionMenuControl. - Handle negative values in translate_row_col_to_index. - Handle decomposed unicode characters. - Fixed Window.always_hide_cursor. (Parameter was ignored.) - Fix in zz Vi key binding. (When render info is not available.) - Fix in Document.get_cursor_up_position. (When an argument is given.) New features: - Separated `load_mouse_bindings`. - Refactoring/simplification of the key bindings: better use of filters and CLI.editing_mode. - Added DummyOutput class and a few unit tests that test the whole CLI. - Use the bisect module in Document._line_start_indexes instead of a custom binary search. This should improve the performance. - Stay in the same column when doing multiple up/down movements. - Visual improvements: * Implemented cursorcolumn, cursorline and colorcolumn. * Only reserve menu space when `complete_while_typing=True` or when there are completions to be displayed. * Support for chaining tokens for combined styles. SelectedText will now reverse the colors from the highlighting by default. Style `Token.SelectedText` to set a fixed foreground/background. Also for SearchMatch, we now use combined tokens. * Support for dark gray on Windows. * Default token for SystemToolbar and SearchToolbar. * Display selection also on empty lines. - Emacs key bindings improved: * Recognize + handle ControlDelete key. * Implemented meta-* and control-backslash key bindings. - Vi key bindings improved: * Handle inclusive and linewise motions properly. * Fix g_ motion off by one character, and don't work when cursor is in the trailing whitespace part of line. * Make a(/a)/i(/i)/... motions. Find enclosing brackets instead of the next bracket. * Update N% motion according to vim behaviors. * Fix | motion off by one character. * ge/gE motions go to end of previous word, not start. * Added Vi 'gm' key binding. * Implemented 'gq' key binding in Vi mode. (Reshape text.) * Vi operator/text object separation for key bindings. * Added 'ap' (auto-paragraph) text object. * Implemented Vi digraphs. ControlK will now insert a digraph. * Implemented vi tilde_operator. * Support named registers. * Vi < and > key bindings became operators. * Text objects and motions are now separate bindings. * Improved copy/paste in Vi mode. Backwards-incompatible changes: - Don't reset the current buffer anymore by default in CommandLineInterface.run(). Passing `reset_current_buffer=True` is now required. - Renamed MouseEventTypes to MouseEventType for consistency. The old name is still valid, but deprecated. - Refactoring of Callbacks. All events should now receive one argument, which is the sender. (Further, Callback was renamed to Event.) This is mostly used internally. - Moved on_invalidate callback from CommandLineInterface to Application - Renamed `PipeInput.send` to `PipeInput.send_text`. (Old deprecated name is still kept as a valid alias.) - Renamed SimpleLexer.default_token to SimpleLexer.token. (+ backwards-compatibility.) - Refactoring of the filters: `ViStateFilter` has been deprecated. (Should not be used anymore.) Use the filters, as defined in prompt_toolkit.filters. - `editing_mode` is now a property of `CommandLineInterface`. This is replacing the `vi_mode` parameter in `KeyBindingManager`. - The default accept_action for the default Buffer in Application now becomes IGNORE. This is a much more sensible default. Pass RETURN_DOCUMENT to get the previous behavior, - Always expect an EventLoop instance in CommandLineInterface. Creating it in __init__ caused a memory leak. 0.60: 2016-03-14 ---------------- Fixes: - Fix in Document.paste. (The screen was not updated after an undo of a paste.) - Don't use deprecated inspect.getargspec on Python 3. - Fixed reading input on Windows when input was piped in stdin. - Use correct file descriptors for input/output in run_system_command. - Always correctly split prompt in shortcuts.prompt. (Even when multiline=False) - Correctly align right prompt to the top when the left prompt consists of multiple lines. - Correctly use Token.Transparent as default token for a TokenListControl. - Fix in syntax synchronization. (Better handle the case when no synchronization point was found.) - Send SIGTSTP to the whole process group. - Correctly raise on_buffer_changed on all text changes. - Fix in regular_languages.GrammarLexer. (Fixes bug in ptipython syntax highlighting.) New features: - Add support for additional readers to the Win32 event loop. - Added on_render event. - Carry the weight in layout dimensions to allow stretching. 0.59: 2016-02-27 ---------------- Fixes: - Set correct default color on Windows. (Gray instead of high intensity gray.) - Reverse colors on Windows when foreground/background color have not been specified. - Correct handling of mouse events for FillControl. - Take margin into account when calculating Window height. (Fixes bug in multiline prompt.) - Handle division by zero in UIContent.get_height_for_text. 0.58: 2016-02-23 ---------------- Fixes: - Correctly return result for mouse handler in TokenListControl. - Bugfix in meta-backspace key binding. (Delete all whitespace before the cursor, when there is only whitespace.) - Bugfix in Vi gu, gU, g? and g~ key bindings (in selection mode). - Correctly restore default console attributes on Windows. - Disable bracketed paste support in ConEmu. (This was broken.) - When an unknown exception is raised in `CommandLineInterface.run()`, don't forget to redraw the CLI. New features: - Many performance improvements and better caching. (Especially in the `Document` class.) - Support for continuation tokens in `shortcuts.prompt` and `shortcuts.create_prompt_layout`. - Added `shortcuts.print_tokens` function for printing colored output. - Sound bell when nothing was deleted. - Added escape sequences for F1-F5 keys on the Linux console. - Improved support for the Linux console. (Switch back to 16 colors.) - Added F13-F24 input codes for xterm. - Created prompt_toolkit.token. A custom Token implementation, that is compatible with Pygments.token. (This way, Pygments becomes an optional dependency. For many use cases, nothing except the Token class from Pygments was used, so it was a bit overkill to install Pygments for only that.) - Refactoring of prompt_toolkit.styles. - `Float` objects got a `hide_when_covering_content` option. - Implementation of RPROMPT, like ZSH: Added `get_rprompt_tokens` to `create_prompt_layout`. - Some improvements to the default style. - Also handle Ctrl-R and Ctrl-S in Vi mode when searching. - Added TabsProcessor: a tool to visualize tabs instead of displaying ^I. - Give a better error message when trying to run in git-bash. - Support for ANSI color names in style dictionaries. - Big refactoring of the `Window` and `UIControl` classes. This should result in huge performance improvements on big inputs. (While first, a document could have 1,000 lines; now it can have about 100,000 lines on the same system.) The Window and UIControl have been rewritten very much. Rather than each time rendering the whole user control, we now only have to render the visible part. Because of this, many pieces had to be rewritten: - UIControls work differently. They return a `UIContent` instance that consist of a collection of lines. - All processors have been rewritten. (Their API changed as well, because they process one line at a time.) - Lexers work differently. `Lexer.lex_document` should now return a function that returns the tokens for one line. PygmentsLexer has been optimized that it becomes 'lazy', and it has optional syntax synchronization. That means, that the lexer doesn't have to start the lexing at the beginning of the document. (Which would be slow for big documents.) Backwards-incompatible changes: - As mentioned above, the refactoring of `Window` and `UIControl` caused many "internal" APIs to change. All custom `UIControl`, `Processor` and `Lexer` classes have to be rewritten. However, for most applications this should not be an issue. Especially, the `shortcuts.prompt` function is backwards-compatible. - `wrap_lines` became a property of `Window` instead of `BufferControl`. 0.57: 2016-01-04 ---------------- Fixes: - Made `max_render_postpone_time` configurable. The current default was bad. (We should probably always draw the UI once every cycle of the event loop.) 0.56: 2016-01-03 ---------------- Fixes: - Fix in bracketed paste. It was not correctly enabled for each prompt. 0.55: 2016-01-03 ---------------- New features: - Implemented bracketed paste mode. (This allows much faster pasting, as well as pasting without going into paste mode. This makes sure that indentation in ptpython for instance is kept correctly.) - Added support for italic output and blink. (For terminals that support it.) - Added get_horizontal_scroll, get_vertical_scroll and always_hide_cursor parameters to Window. - Refactoring of the posix event loop. Better scheduling of all tasks/FDs to avoid starvation. (Everything should feel more responsive in high CPU situations.) - Added get_default_char function to TokenListControl. - AppendAutoSuggestion now accepts a token parameter. - Support for ansi color names in styles. - Accept get_width/get_height parameters in Float. - Added Output.write_raw and accept 'raw' parameter in CommandLineInterface.stdout_proxy. - Better caching of tokens in TokenListControl. - Add mouse support to TokenListControl. - Display "Window too small" when the window becomes too small. - Added 'bell' function to Output. - Accept weights in HSplit/VSplit. - Added Registry.remove_binding method to dynamically remove key bindings. - Added focus_on_click parameter to BufferControl. - Introduced BufferMapping class as a wrapper around the buffers dictionary. This one also contains the focus stack. - Improved 'v' and 'V' key bindings. Allow switching between line and character selection modes. - Added layout.highlighters. A new, much faster way to do selection and search highlighting. - Make search_state dynamic for key bindings. - Added 'sentence' option to WordCompleter. - Cache Document.lines for better performance. - Implementation of BLOCK selections. (Cut, copy, paste.) - Accept a 'reserve_space_for_menu' parameter in the shortcuts. (This is an integer.) - Support for 24bit true color on vt100 terminals. - Added CommandLineInterface.on_invalidate event. - Added __version__ to __init__.py. Fixes: - Always show cursor in the 'done' state. - Allow HSplit to have zero children. - Bugfix for handling of backslash on Windows with some non-us keyboards. (Ptpython issue #28.) - Never render characters outside the visible screen region. - Fix in WordCompleter. When case insensitive and input contained uppercase. - Highlight search match when the cursor is at any position on the match. (not just the beginning.) Backwards-incompatible changes: (Most changes will probably not have an impact on external applications.) - Change in the `Style` API. This allows caching of Attrs in renderer and faster rendering. (Style now has a get_attrs_for_token instead of a get_token_to_attributes_dict method.) - Removed DefaultStyle. Created PygmentsStyle.from_defaults class method instead. - Removed AbortAction.IGNORE. This was ambiguous. - Accept 'cli' parameter in 'walk' and 'find_window_for_buffer_name'. - The focus stack is now stored in BufferMapping. - ViStateFilter and KeyBindingManager now accept a get_vi_state callable instead of vi_state itself. (This way a key bindings registry becomes stateless.) - HighlightSearchProcessor and HighlightSelectionProcessor became deprecated. (Use highlighters instead.) 0.54: 2015-10-29 ---------------- New features: - Allow CommandLineInterface to run in any thread. - Hide cursor while rendering. - Added add_reader/remove_reader methods to EventLoop. - Support for 'reverse' style. - Redraw more lazy, by using invalidate. - Added show_cursor property to Screen. - Center or right align text in TokenListControl also when it spans multiple lines. Fixes: - Bugfix in PathCompleter. (Expanduser issue.) - Fix in signal handler. - Use utf-8 encoding in Vt100_Output by default. - Use correct default token in BufferControl. - Fix in ControlL key binding. Use @handle to allow deactivation. Backwards-incompatible changes: - Renamed create_default_layout to create_prompt_layout - Renamed create_default_application to create_prompt_application - Renamed Layout to Container. - Renamed CommandLineInterfaces.request_redraw to invalidate. - Changed the actual value of SEARCH_BUFFER, DEFAULT_BUFFER, SYSTEM_BUFFER and DUMMY_BUFFER. - Changed order of keyword arguments of the BufferControl class. "buffer_name" now comes first. - Removed old pt(i)python code. 0.53: 2015-10-06 ---------------- New features: - Handling of the insert key in Vi mode. - Added 'zt' and 'zb' Vi key bindings. - Added delete key binding for deleting selected text. - Select word below cursor on double mouse click. - Added `wrap_lines` option to TokenListControl. - Added `KeyBindingManager.for_prompt`. Fixes: - Fix in rendering output. - Reset renderer correctly in run_in_terminal. - Only reset buffer when using `AbortAction.RETRY`. - Fix in handling of exit (Ctrl-D) key presses. - Fix in `CompleteEvent`. Correctly set `completion_requested`. Backwards-incompatible changes: - Renamed `ValidationError.index` to `ValidationError.cursor_position`. - Renamed `shortcuts.get_input` to `shortcuts.prompt`. - Return empty string instead of None in `Document.current_char`/`char_before_cursor`. 0.52: 2015-09-24 ---------------- Fixes: - Fix in auto suggestion: hide suggestion when accepting input. 0.51: 2015-09-24 ---------------- New features: - Mouse support. (Scrolling and clicking for vt100 terminals. For Windows only clicking.) Both the autocompletion menus and buffer controls respond to scrolling and clicking. - Added auto suggestions. (Like the fish shell.) - Stdout proxy become thread safe. - Linewrapping can now be disabled, instead we get horizontal scrolling. - Line numbering can now be relative. Like the vi 'relativenumber' option. Fixes: - Fixed excessive scrolling in Windows. - Bugfix in search highlighting. - Copy all words during repetition of Ctrl-W presses. - The 'libs' folder has been removed. - Fix in MultiColumnCompletionsMenu: don't create very big columns. Backwards-incompatible changes: - Disable search by default in KeyBindingManager. - Separated abort/exit key bindings. Disabled by default in KeyBindingManager. - 'Ignore' became the default on_abort action in `Application`. - 'Ignore' became the default accept_action in `Buffer`. - The layout processors have been refactored. The API is changed. - `SwitchableValidator` has been renamed to `ConditionalValidator`. - `WindowRenderInfo` has several incompatible changes. - Margins have been refactored completely. Now it's the window that has the margin instead of `BufferControl`. Is is both much more performant and flexible. 0.50: 2015-09-06 ---------------- Fix: - Leaving of alternate screen on Windows. 0.49: 2015-09-06 ---------------- New features: - Added MANIFEST.in - Better support for multiline prompts in shortcuts. - Added Document.set_document method. - Added 'default' argument to `shortcuts.create_default_application`. - Added `align_center` option for `TokenListControl`. - Added optional key bindings for full page navigation. (Moved key bindings from pyvim into prompt-toolkit.) - Accepts default_char in BufferControl for filling the background. - Added InFocusStack filter. Fixes: - Small fix in TokenListControl: use the right Char for aligning. Backwards-incompatible changes: - Removed deprecated 'tokens' attribute from GrammarLexer. 0.48: 2015-09-02 ---------------- New features: - run_in_terminal now returns the result of the called function. - Made history attribute of Buffer class public. - Added support for sub CommandLineInterfaces. - Accept optional vi_state parameter in KeyBindingManager. Fixes: - Pop-up menu positioning. The menu was shown too often above instead of below the cursor. - Fix in Control-W key binding. When there is only whitespace before the cursor, delete the whitespace. - Rendering bug fix in open_in_editor: run editor using cli.run_in_terminal. - Fix in renderer. Correctly reserve the vertical space as required by the layout. - Small fix in Margin ABC. - Added __iter__ to History ABC. - Small bugfix in CommandLineInterface: create correct eventloop when no eventloop was given. - Never schedule a second repaint operation when a previous was not yet executed. 0.47: 2015-08-19 ---------------- New features: - Added `prompt_toolkit.layout.utils.iter_token_lines`. - Allow `None` values on the focus stack. - Buffers can be readonly. Added `IsReadOnly` filter. - `eager` behavior for key bindings. When a key binding is eager it will be executed as soon as it's matched, even when there is another binding that starts with this key sequence. - Custom margins for BufferControl. Fixes: - Don't trigger autocompletion on paste. - Added `pre_run` parameter to CommandLineInterface. - Correct invalidation of BeforeInput and AfterInput. - Correctly handle transparency. (For floats.) - Small change in the algorithm to determine Window dimensions: keep in the bounds of the Window dimensions. Backwards-incompatible changes: - There a now a `Lexer` abstract base class. Every lexer should be an instance of that class, and Pygments lexers should be wrapped in a `PygmentsLexer` class. `prompt_toolkit.shortcuts` still accepts Pygments lexers directly for backwards-compatibility. - BufferControl no longer has a `show_line_numbers` argument. Pass a `NumberedMargin` instance instead. - The `History` class became an abstract base class and only defines an interface. The default history class is now called `InMemoryHistory`. 0.46: 2015-08-08 ---------------- New features: - By default, in shortcuts, only show search highlighting when the search is the current input buffer. - Accept 'count' for all search operations. (For repetition.) - `shortcuts.create_default_layout` accepts a `multiline` parameter. - Show meta display text for completions also in multi-column mode. Fixes: - Correct invalidation of DefaultPrompt when search direction changes. - Correctly include/exclude current cursor position in search. - More consistency in styles. - Fix in ConditionalProcessor.has_focus. - Python 2.6 compatibility fix. - Show cursor at the correct position during reverse-i-search. - Fixed stdout encoding bug for vt100 output. Backwards-incompatible changes: - Use of `ConditionalContainer` everywhere. The `Window` class no longer accepts a `filter` argument to decide about the visibility. Instead wrapping inside a `ConditionalContainer` class is required. 0.45: 2015-07-30 ---------------- Fixes: - Bug fix on OS X: correctly detect platform as not Windows. 0.44: 2015-07-30 ---------------- Fixes: - Fixed bug in eventloops: handle timeout correctly, even when there is an eventhook. - Bug fix in open-in-editor: set correct cursor position. New features: - CompletionsMenu got a scroll_offset. - Use 256 colors and ANSI sequences when ConEmu ANSI support has been detected. - Added PyperclipClipboard for synchronization with the system clipboard. and clipboard parameter in shortcut functions. - Filter for enabling/disabling handling of Vi 'v' binding. 0.43: 2015-07-15 ---------------- Fixes: - Windows bug fix. STD_INPUT_HANDLE should be c_ulong instead of HANDLE. (This caused crashes on some systems.) New features: - Added eventloop and patch_stdout parameters to get_input. - Inputhook support added. - Added ShowLeadingWhiteSpaceProcessor and ShowTrailingWhiteSpaceProcessor processors. - Accept Filter as multiline parameter in 'shortcuts'. - MultiColumnCompletionsMenu + display_completions_in_columns parameter in shortcuts. Backwards incompatible changes: - Layout.width was renamed to preferred_width and now receives a max_available_width parameter. 0.42: 2015-06-25 ---------------- Fixes: - Support for Windows cmder and conemu consoles. - Correct handling of unicode input and output on Windows. New features: - Support terminal titles. - Handle Control-Enter as Meta-Enter on Windows. - Control-Z key binding for Windows. - Implemented alternate screen buffer on Windows. - Clipboard became an ABC and InMemoryClipboard default implementation. 0.41: 2015-06-20 ---------------- Fixes: - Emacs Control-T key binding. - Color fix for Windows consoles. New features: - Allow both booleans and Filters in many places. - `password` can be a Filter now. 0.40: 2015-06-15 ---------------- Fixes: - Fix in output_screen_diff: reset correctly. - Ignore flush errors in vt100_output. - Implemented gg Vi key binding. - Bug fix in the renderer when the style changes. New features: - TokenListControl can now display the cursor somewhere. - Added SwitchableValidator class. - print_tokens function added. - get_style argument for Application added. - KeyBindingManager got an enable_all argument. Backwards incompatible changes: - history_search is now a SimpleFilter instance. 0.39: 2015-06-04 ---------------- Fixes: - Fixed layout.py example. - Fixed eventloop for Python 64bit on Windows. - Fix in history. - Fix in key bindings. 0.38: 2015-05-31 ---------------- New features: - Improved performance significantly for processing key bindings. (Pasting text will be a lot faster.) - Added 'M' Vi key binding. - Added 'z-' and 'z+' and 'z-[Enter]' Vi keybindings. - Correctly handle input and output encodings on Windows. Bug fixes: - Fix bug when completion cursor position is outside range of current text. - Don't crash Control-D is pressed while waiting for ENTER press (in run_system_command.) - On Ctrl-Z, don't suspend on Windows, where we don't have SIGTSTP. - Ignore result when open_in_editor received a nonzero return code. - Bug fix in displaying of menu meta information. Don't show 'None'. Backwards incompatible changes: - Refactoring of the I/O layer. Separation of the CommandLineInterface and Application class. - Renamed enable_system_prompt to enable_system_bindings. 0.37: 2015-05-11 ---------------- New features: - Handling of trailing input in contrib.regular_languages. Bug fixes: - Default message in shortcuts.get_input. - Windows compatibility for contrib.telnet. - OS X bugfix in contrib.telnet. 0.36: 2015-05-09 ---------------- New features: - Added get_prompt_tokens parameter to create_default_layout. - Show prompt in bold by default. Bug fixes: - Correct cache invalidation of DefaultPrompt. - Using text_type assertions in contrib.telnet. - Removed contrib.shortcuts completely. (The .pyc files still appeared incorrectly in the wheel.) 0.35: 2015-05-07 ---------------- New features: - WORD parameter for WordCompleter. - DefaultPrompt.from_message constructor. - Added reactive.py for simple integer data binding. - Implemented scroll_offset and scroll_beyond_bottom for Window. - Some performance improvements. Bug fixes: - Handling of relative path in PathCompleter. - unicode_literals for all examples. - Visibility of bottom toolbar in create_default_layout shortcut. - Correctly handle 'J' vi key binding. - Fix in indent/unindent. - Better Vi bindings in visual mode. Backwards incompatible changes: - Moved prompt_toolkit.contrib.shortcuts to prompt_toolkit.shortcuts. - Refactoring of contrib.telnet. 0.34: 2015-04-26 ---------------- Bug fixes: - Correct display of multi width characters in completion menu. Backwards incompatible changes: - Renamed Buffer.add_to_history to Buffer.append_to_history. 0.33: 2015-04-25 ---------------- Bug fixes: - Crash fixed in SystemCompleter when some directories didn't exist. - Made text/cursor_position in Document more atomic. - Fixed Char.__ne__, improves performance. - Better performance of the filter module. - Refactoring of the filter module. - Bugfix in BufferControl, caching was not done correctly. - fixed 'zz' Vi key binding. New features: - Do tilde expansion for system commands. - Added ignore_case option for CommandLineInterface. Backwards incompatible changes: - complete_while_typing parameter has been moved from CommandLineInterface to Buffer. 0.32: 2015-04-22 ---------------- New features: - Implemented repeat arg for '{' and '}' vi key binding. - Added autocorrection example. - first experimental telnet interface added. - Added contrib.validators.SentenceValidator. - Added Layout.walk generator to traverse the layout. - Improved 'L' and 'H' Vi key bindings. - Implemented Vi 'zz' key binding. - ValidationToolbar got a show_position parameter. - When only width or height are given for a float, the control is centered in the parent. - Added beforeKeyPress and afterKeyPress events. - Added HighlightMatchingBracketProcessor. - SearchToolbar got a vi_mode option to show '?' and '/' instead of 'I-search'. - Implemented vi '*' binding. - Added onBufferChanged event to CommandLineInterface. - Many performance improvements: some caching and not rendering after every single key stroke. - Added ConditionalProcessor. - Floating menus are now shown above the cursor, when below is not enough space, but above is enough space. - Improved vi 'G' key binding. - WindowRenderInfo got a full_height_visible, top_visible, and a few other attributes. - PathCompleter got an expanduser option to do tilde expansion. Fixed: - Always insert indentation when pressing enter. - vertical_scroll should be an int instead of a float. - Some bug fixes in renderer.Output. - Pressing backspace in an empty search in Vi mode now goes back to navigation mode. - Bug fix in TokenListControl (Correctly calculate height for multiline content.) - Only apply HighlightMatchingBracketProcessor when editing buffer. - Ensure that floating layouts never go out of bounds. - Home/End now go to the start and end of the line. - Fixed vi 'c' key binding. - Redraw the whole output when the style changes. - Don't trigger onTextInsert when working_index doesn't change. - Searching now wraps around the start/end of buffer/history. - Don't go to the start of the line when moving forward in history. Changes: - Don't show directory/file/link in the meta information of PathCompleter anymore. - Complete refactoring of the event loops. - Refactoring of the Renderer and CommandLineInterface class. - CommandLineInterface now accepts an optional Output argument. - CommandLineInterface now accepts a use_alternate_screen parameter. - Moved highlighting code for search/selection from BufferControl to processors. - Completers are now always run asynchronously. - Complete refactoring of the search. (Most responsibility move out of Buffer class. CommandLineInterface now got a search_state attribute.) Backwards incompatible changes: - get_input does now have a history attribute instead of history_filename. - EOFError and KeyboardInterrupt is raised for abort and exit instead of custom exceptions. - CommandLineInterface does no longer have a property 'is_reading_input'. - filters.AlwaysOn/AlwaysOff have been renamed to Always/Never. - AcceptAction has been moved from CommandLineInterface to Buffer. Now every buffer can define its own accept action. - CommandLineInterface now expects an Eventloop instance in __init__. 0.31: 2015-01-30 ---------------- Fixed: - Bug in float positioning - Show completion menu only for the default_buffer in get_input. New features: - PathCompleter got a get_paths parameter. - PathCompleter sorts alphabetically. - Added contrib.completers.SystemCompleter - Completion got a get_display_meta parameter. 0.30: 2015-01-26 ---------------- Fixed: - Backward compatibility with django_extensions. - Usage of alternate screen in the renderer. New features: - Vi '#' key binding. - contrib.shortcuts.get_input got a get_bottom_toolbar_tokens argument. - Separate key bindings for "open in editor." KeyBindingManager got a enable_open_in_editor argument. 0.28: 2015-01-25 ---------------- Fixed: - syntax error in 0.27 0.27: 2015-01-25 ---------------- Backwards-incompatible changes: - Complete refactoring of the layout system. (HSplit, VSplit, FloatContainer) as well as a list of controls (TokenListControl, BufferControl) in order to design much more complex layouts. - ptpython code has been moved to a separate repository. New features: - prompt_toolkit.contrib.shortcuts.get_input has been extended. Fixed: - Behavior of Control+left/right/up/down. - Backspace in incremental search. - Hide completion menu correctly when the cursor position changes. 0.26: 2015-01-08 ---------------- Backwards-incompatible changes: - Refactoring of the key input system. (The registry which contains the key bindings, the focus stack, key binding manager.) Overall much better API. - Renamed `Line` to `Buffer`. New features: - Added filters as a way of disabling/enabling parts of the runtime according to certain conditions. - Global clipboard, shared between all buffers. - Added (experimental) "merge history" feature to ptpython. - Added 'C-x r k' and 'C-x r y' emacs key bindings for cut and paste. - Added g_, ge and gE vi key bindings. - Added support for handling control + arrows keys. Fixed: - Correctly handle f1-f4 in rxvt-unicode. 0.25: 2014-12-11 ---------------- Fixed: - Package did not install on Python 2.6/2.7. 0.24: 2014-12-10 ---------------- Backwards-incompatible changes: - Completer.get_completions now gets a complete_event argument. New features: - For ptpython: filename completion inside Python strings. - prompt_toolkit.contrib.regular_languages added. - prompt_toolkit.contrib.pdb added. (Experimental PDB front-end.) - Support for multiline toolbars. - asyncio support added. (Integration with asyncio event loop.) - WORD parameter added to Document.word_before_cursor. Fixed: - Small fixes in Win32 terminal output. - Bug fix in parsing of CPR response. 0.23: 2014-11-28 ---------------- New features: - contrib.completers added. Fixed: - Improved j/k key bindings in Vi mode. - Don't leak internal variables into ptipython shell. - Initialize IPython extensions. - Use IPython's prompt. - Workarounds for Jedi crashes. 0.22: 2014-11-09 ---------------- Fixed: - Fixed missing import which caused Ctrl-Z to crash. - Show error message for ptipython when IPython is not installed. 0.21: 2014-10-25 ---------------- New features: - Using entry_points in setup.py - Experimental Win32 support added. Fixed: - Behavior of 'r' and 'R' key bindings in Vi mode. - Detect multiline correctly for ptpython when there are triple quoted strings. - Some other small improvements. 0.20: 2014-10-04 ---------------- Fixed: - Workarounds for Jedi bugs. - Better handling of window resize events. - Fixed counter in ptipython prompt. - Use IPythonInputSplitter.transform_cell for IPython syntax validation. - Only insert newlines for open brackets if the cursor is at the end of the input string. New features: - More Vi key bindings: 'B', 'W', 'E', 'aW', 'aw' and 'iW' - ControlZ now suspends the process 0.19: 2014-09-30 ---------------- Fixed: - Handle Jedi crashes. - Autocompletion in `ptipython` - Input validation in `ptipython` - Execution of system commands (in `ptpython`) in Python 3 - Add current directory to sys.path for `ptpython`. - Minimal jedi and six version in setup.py New features - Python 2.6 support - C-C> and C-C< indent and unindent emacs key bindings. - `ptpython` can now also run python scripts, so aliasing of `ptpython` as `python` will work better. 0.18: 2014-09-29 ---------------- - First official (beta) release. Jan 25, 2014 ------------ first commit ================================================ FILE: LICENSE ================================================ Copyright (c) 2014, Jonathan Slenders All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: MANIFEST.in ================================================ include *rst LICENSE CHANGELOG MANIFEST.in recursive-include examples *.py recursive-include tests *.py prune examples/sample?/build ================================================ FILE: PROJECTS.rst ================================================ Projects using `prompt_toolkit` =============================== Shells: - `ptpython `_: Python REPL - `ptpdb `_: Python debugger (pdb replacement) - `pgcli `_: Postgres client. - `mycli `_: MySql client. - `litecli `_: SQLite client. - `wharfee `_: A Docker command line. - `xonsh `_: A Python-ish, BASHwards-compatible shell. - `saws `_: A Supercharged AWS Command Line Interface. - `cycli `_: A Command Line Interface for Cypher. - `crash `_: Crate command line client. - `vcli `_: Vertica client. - `aws-shell `_: An integrated shell for working with the AWS CLI. - `softlayer-python `_: A command-line interface to manage various SoftLayer products and services. - `ipython `_: The IPython REPL - `click-repl `_: Subcommand REPL for click apps. - `haxor-news `_: A Hacker News CLI. - `gitsome `_: A Git/Shell Autocompleter with GitHub Integration. - `http-prompt `_: An interactive command-line HTTP client. - `coconut `_: Functional programming in Python. - `Ergonomica `_: A Bash alternative written in Python. - `Kube-shell `_: Kubernetes shell: An integrated shell for working with the Kubernetes CLI - `mssql-cli `_: A command-line client for Microsoft SQL Server. - `robotframework-debuglibrary `_: A debug library and REPL for RobotFramework. - `ptrepl `_: Run any command as REPL - `clipwdmgr `_: Command Line Password Manager. - `slacker `_: Easy access to the Slack API and admin of workspaces via REPL. - `EdgeDB `_: The next generation object-relational database. - `pywit `_: Python library for Wit.ai. - `objection `_: Runtime Mobile Exploration. - `habu `_: Python Network Hacking Toolkit. - `nawano `_: Nano cryptocurrency wallet - `athenacli `_: A CLI for AWS Athena. - `vulcano `_: A framework for creating command-line applications that also runs in REPL mode. - `kafka-shell `_: A supercharged shell for Apache Kafka. - `starterTree `_: A command launcher organized in a tree structure with fuzzy autocompletion - `git-delete-merged-branches `_: Command-line tool to delete merged Git branches - `radian `_: A 21 century R console Full screen applications: - `pymux `_: A terminal multiplexer (like tmux) in pure Python. - `pyvim `_: A Vim clone in pure Python. - `freud `_: REST client backed by SQLite for storing servers - `pypager `_: A $PAGER in pure Python (like "less"). - `kubeterminal `_: Kubectl helper tool. - `pydoro `_: Pomodoro timer. - `sanctuary-zero `_: A secure chatroom with zero logging and total transience. - `Hummingbot `_: A Cryptocurrency Algorithmic Trading Platform - `git-bbb `_: A `git blame` browser. - `ass `_: An OpenAI Assistants API client. Libraries: - `ptterm `_: A terminal emulator widget for prompt_toolkit. - `PyInquirer `_: A Python library that wants to make it easy for existing Inquirer.js users to write immersive command line applications in Python. - `clintermission `_: Non-fullscreen command-line selection menu Other libraries and implementations in other languages ****************************************************** - `go-prompt `_: building a powerful interactive prompt in Go, inspired by python-prompt-toolkit. - `urwid `_: Console user interface library for Python. (Want your own project to be listed here? Please create a GitHub issue.) ================================================ FILE: README.rst ================================================ Python Prompt Toolkit ===================== |AppVeyor| |PyPI| |RTD| |License| |Codecov| .. image :: /docs/images/logo_400px.png ``prompt_toolkit`` *is a library for building powerful interactive command line applications in Python.* Read the `documentation on readthedocs `_. Gallery ******* `ptpython `_ is an interactive Python Shell, build on top of ``prompt_toolkit``. .. image :: /docs/images/ptpython.png `More examples `_ prompt_toolkit features *********************** ``prompt_toolkit`` could be a replacement for `GNU readline `_, but it can be much more than that. Some features: - **Pure Python**. - Syntax highlighting of the input while typing. (For instance, with a Pygments lexer.) - Multi-line input editing. - Advanced code completion. - Both Emacs and Vi key bindings. (Similar to readline.) - Even some advanced Vi functionality, like named registers and digraphs. - Reverse and forward incremental search. - Works well with Unicode double width characters. (Chinese input.) - Selecting text for copy/paste. (Both Emacs and Vi style.) - Support for `bracketed paste `_. - Mouse support for cursor positioning and scrolling. - Auto suggestions. (Like `fish shell `_.) - Multiple input buffers. - No global state. - Lightweight, the only dependencies are Pygments and wcwidth. - Runs on Linux, OS X, FreeBSD, OpenBSD and Windows systems. - And much more... Feel free to create tickets for bugs and feature requests, and create pull requests if you have nice patches that you would like to share with others. Installation ************ :: pip install prompt_toolkit For Conda, do: :: conda install -c https://conda.anaconda.org/conda-forge prompt_toolkit About Windows support ********************* ``prompt_toolkit`` is cross platform, and everything that you build on top should run fine on both Unix and Windows systems. Windows support is best on recent Windows 10 builds, for which the command line window supports vt100 escape sequences. (If not supported, we fall back to using Win32 APIs for color and cursor movements). It's worth noting that the implementation is a "best effort of what is possible". Both Unix and Windows terminals have their limitations. But in general, the Unix experience will still be a little better. Getting started *************** The most simple example of the library would look like this: .. code:: python from prompt_toolkit import prompt if __name__ == '__main__': answer = prompt('Give me some input: ') print('You said: %s' % answer) For more complex examples, have a look in the ``examples`` directory. All examples are chosen to demonstrate only one thing. Also, don't be afraid to look at the source code. The implementation of the ``prompt`` function could be a good start. Philosophy ********** The source code of ``prompt_toolkit`` should be **readable**, **concise** and **efficient**. We prefer short functions focusing each on one task and for which the input and output types are clearly specified. We mostly prefer composition over inheritance, because inheritance can result in too much functionality in the same object. We prefer immutable objects where possible (objects don't change after initialization). Reusability is important. We absolutely refrain from having a changing global state, it should be possible to have multiple independent instances of the same code in the same process. The architecture should be layered: the lower levels operate on primitive operations and data structures giving -- when correctly combined -- all the possible flexibility; while at the higher level, there should be a simpler API, ready-to-use and sufficient for most use cases. Thinking about algorithms and efficiency is important, but avoid premature optimization. `Projects using prompt_toolkit `_ *********************************************** Special thanks to ***************** - `Pygments `_: Syntax highlighter. - `wcwidth `_: Determine columns needed for a wide characters. .. |PyPI| image:: https://img.shields.io/pypi/v/prompt_toolkit.svg :target: https://pypi.python.org/pypi/prompt-toolkit/ :alt: Latest Version .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/32r7s2skrgm9ubva?svg=true :target: https://ci.appveyor.com/project/prompt-toolkit/python-prompt-toolkit/ .. |RTD| image:: https://readthedocs.org/projects/python-prompt-toolkit/badge/ :target: https://python-prompt-toolkit.readthedocs.io/en/master/ .. |License| image:: https://img.shields.io/github/license/prompt-toolkit/python-prompt-toolkit.svg :target: https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/LICENSE .. |Codecov| image:: https://codecov.io/gh/prompt-toolkit/python-prompt-toolkit/branch/master/graphs/badge.svg?style=flat :target: https://codecov.io/gh/prompt-toolkit/python-prompt-toolkit/ ================================================ FILE: appveyor.yml ================================================ environment: matrix: - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5" - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5" - PYTHON: "C:\\Python34" PYTHON_VERSION: "3.4" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4" - PYTHON: "C:\\Python33" PYTHON_VERSION: "3.3" - PYTHON: "C:\\Python33-x64" PYTHON_VERSION: "3.3" - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7" - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7" - PYTHON: "C:\\Python26" PYTHON_VERSION: "2.6" - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.6" install: - "set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - pip install . pytest coverage codecov flake8 - pip list build: false test_script: - If not ($env:PYTHON_VERSION==2.6) flake8 prompt_toolkit - coverage run -m pytest after_test: - codecov ================================================ FILE: docs/conf.py ================================================ # # prompt_toolkit documentation build configuration file, created by # sphinx-quickstart on Thu Jul 31 14:17:08 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.graphviz", "sphinx_copybutton", ] # Add any paths that contain templates here, relative to this directory. # templates_path = ["_templates"] # The suffix of source filenames. source_suffix = {".rst": "restructuredtext"} # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "prompt_toolkit" copyright = "2014-2024, Jonathan Slenders" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # --------------------------------------------------------------------- # Versions. # The short X.Y version. version = "3.0.52" # The full version, including alpha/beta/rc tags. release = "3.0.52" # The URL pattern to match releases to ReadTheDocs URLs. docs_fmt_url = "https://python-prompt-toolkit.readthedocs.io/en/{release}/" # The list of releases to include in the dropdown. releases = [ "latest", release, "2.0.9", "1.0.15", ] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = "pastie" # Provided as a theme option below. # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # autodoc configuration # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html autodoc_inherit_docstrings = False autodoc_mock_imports = [ "prompt_toolkit.eventloop.win32", "prompt_toolkit.input.win32", "prompt_toolkit.output.win32", ] # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # on_rtd = os.environ.get("READTHEDOCS", None) == "True" html_theme = "sphinx_nefertiti" # html_theme_path = [sphinx_nefertiti.get_html_theme_path()] html_theme_options = { "monospace_font": "Ubuntu Sans Mono", "monospace_font_size": "0.9rem", # "style" can take the following values: "blue", "indigo", "purple", # "pink", "red", "orange", "yellow", "green", "tail", and "default". "style": "blue", "pygments_light_style": "pastie", "pygments_dark_style": "dracula", # Fonts are customizable (and are not retrieved online). # https://sphinx-nefertiti.readthedocs.io/en/latest/users-guide/customization/fonts.html "logo": "logo_400px.png", "logo_location": "sidebar", "logo_alt": "python-prompt-toolkit", "logo_width": "270", "logo_height": "270", "repository_url": "https://github.com/prompt-toolkit/python-prompt-toolkit", "repository_name": "python-prompt-toolkit", "current_version": "latest", "versions": [(item, docs_fmt_url.format(release=item)) for item in releases], "header_links": [ {"text": "Getting started", "link": "pages/getting_started"}, { "text": "Tutorials", "match": "/tutorials/*", "dropdown": ( {"text": "Build an SQLite REPL", "link": "pages/tutorials/repl"}, ), }, { "text": "Advanced", "link": "pages/advanced_topics/index", "match": "/advanced_topics/*", "dropdown": ( { "text": "More about key bindings", "link": "pages/advanced_topics/key_bindings", }, { "text": "More about styling", "link": "pages/advanced_topics/styling", }, { "text": "Filters", "link": "pages/advanced_topics/filters", }, { "text": "The rendering flow", "link": "pages/advanced_topics/rendering_flow", }, { "text": "Running on top of the asyncio event loop", "link": "pages/advanced_topics/asyncio", }, { "text": "Unit testing", "link": "pages/advanced_topics/unit_testing", }, { "text": "Input hooks", "link": "pages/advanced_topics/input_hooks", }, { "text": "Architecture", "link": "pages/advanced_topics/architecture", }, { "text": "The rendering pipeline", "link": "pages/advanced_topics/rendering_pipeline", }, ), }, { "text": "Reference", "link": "pages/reference", }, ], "footer_links": [ { "text": "Documentation", "link": "https://python-prompt-toolkit.readthedocs.io/", }, { "text": "Package", "link": "https://pypi.org/project/prompt-toolkit/", }, { "text": "Repository", "link": "https://github.com/prompt-toolkit/python-prompt-toolkit", }, { "text": "Issues", "link": "https://github.com/prompt-toolkit/python-prompt-toolkit/issues", }, ], } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "images/logo_400px.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "prompt_toolkitdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( "index", "prompt_toolkit.tex", "prompt_toolkit Documentation", "Jonathan Slenders", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "prompt_toolkit", "prompt_toolkit Documentation", ["Jonathan Slenders"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "prompt_toolkit", "prompt_toolkit Documentation", "Jonathan Slenders", "prompt_toolkit", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False ================================================ FILE: docs/index.rst ================================================ Python Prompt Toolkit 3.0 ========================= `prompt_toolkit` is a library for building powerful interactive command line and terminal applications in Python. It can be a very advanced pure Python replacement for `GNU readline `_, but it can also be used for building full screen applications. .. image:: images/ptpython-2.png Some features: - Syntax highlighting of the input while typing. (For instance, with a Pygments lexer.) - Multi-line input editing. - Advanced code completion. - Selecting text for copy/paste. (Both Emacs and Vi style.) - Mouse support for cursor positioning and scrolling. - Auto suggestions. (Like `fish shell `_.) - No global state. Like readline: - Both Emacs and Vi key bindings. - Reverse and forward incremental search. - Works well with Unicode double width characters. (Chinese input.) Works everywhere: - Pure Python. Runs on all Python versions starting at Python 3.6. (Python 2.6 - 3.x is supported in prompt_toolkit 2.0; not 3.0). - Runs on Linux, OS X, OpenBSD and Windows systems. - Lightweight, the only dependencies are Pygments and wcwidth. - No assumptions about I/O are made. Every prompt_toolkit application should also run in a telnet/ssh server or an `asyncio `_ process. Have a look at :ref:`the gallery ` to get an idea of what is possible. Getting started --------------- Go to :ref:`getting started ` and build your first prompt. Issues are tracked `on the Github project `_. Thanks to: ---------- A special thanks to `all the contributors `_ for making prompt_toolkit possible. Also, a special thanks to the `Pygments `_ and `wcwidth `_ libraries. Table of contents ----------------- .. toctree:: :maxdepth: 2 pages/gallery pages/getting_started pages/upgrading/index pages/printing_text pages/asking_for_input pages/asking_for_a_choice pages/dialogs pages/progress_bars pages/full_screen_apps pages/tutorials/index pages/advanced_topics/index pages/reference pages/related_projects Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` Prompt_toolkit was created by `Jonathan Slenders `_. ================================================ FILE: docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\xline.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\xline.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ================================================ FILE: docs/pages/advanced_topics/architecture.rst ================================================ .. _architecture: Architecture ============ TODO: this is a little outdated. :: +---------------------------------------------------------------+ | InputStream | | =========== | | - Parses the input stream coming from a VT100 | | compatible terminal. Translates it into data input | | and control characters. Calls the corresponding | | handlers of the `InputStreamHandler` instance. | | | | e.g. Translate '\x1b[6~' into "Keys.PageDown", call | | the `feed_key` method of `InputProcessor`. | +---------------------------------------------------------------+ | v +---------------------------------------------------------------+ | InputStreamHandler | | ================== | | - Has a `Registry` of key bindings, it calls the | | bindings according to the received keys and the | | input mode. | | | | We have Vi and Emacs bindings. +---------------------------------------------------------------+ | v +---------------------------------------------------------------+ | Key bindings | | ============ | | - Every key binding consists of a function that | | receives an `Event` and usually it operates on | | the `Buffer` object. (It could insert data or | | move the cursor for example.) | +---------------------------------------------------------------+ | | Most of the key bindings operate on a `Buffer` object, but | they don't have to. They could also change the visibility | of a menu for instance, or change the color scheme. | v +---------------------------------------------------------------+ | Buffer | | ====== | | - Contains a data structure to hold the current | | input (text and cursor position). This class | | implements all text manipulations and cursor | | movements (Like e.g. cursor_forward, insert_char | | or delete_word.) | | | | +-----------------------------------------------+ | | | Document (text, cursor_position) | | | | ================================ | | | | Accessed as the `document` property of the | | | | `Buffer` class. This is a wrapper around the | | | | text and cursor position, and contains | | | | methods for querying this data , e.g. to give | | | | the text before the cursor. | | | +-----------------------------------------------+ | +---------------------------------------------------------------+ | | Normally after every key press, the output will be | rendered again. This happens in the event loop of | the `Application` where `Renderer.render` is called. v +---------------------------------------------------------------+ | Layout | | ====== | | - When the renderer should redraw, the renderer | | asks the layout what the output should look like. | | - The layout operates on a `Screen` object that he | | received from the `Renderer` and will put the | | toolbars, menus, highlighted content and prompt | | in place. | | | | +-----------------------------------------------+ | | | Menus, toolbars, prompt | | | | ======================= | | | | | | | +-----------------------------------------------+ | +---------------------------------------------------------------+ | v +---------------------------------------------------------------+ | Renderer | | ======== | | - Calculates the difference between the last output | | and the new one and writes it to the terminal | | output. | +---------------------------------------------------------------+ ================================================ FILE: docs/pages/advanced_topics/asyncio.rst ================================================ .. _asyncio: Running on top of the `asyncio` event loop ========================================== .. note:: New in prompt_toolkit 3.0. (In prompt_toolkit 2.0 this was possible using a work-around). Prompt_toolkit 3.0 uses asyncio natively. Calling ``Application.run()`` will automatically run the asyncio event loop. If however you want to run a prompt_toolkit ``Application`` within an asyncio environment, you have to call the ``run_async`` method, like this: .. code:: python from prompt_toolkit.application import Application async def main(): # Define application. application = Application( ... ) result = await application.run_async() print(result) asyncio.get_event_loop().run_until_complete(main()) ================================================ FILE: docs/pages/advanced_topics/filters.rst ================================================ .. _filters: Filters ======= Many places in `prompt_toolkit` require a boolean value that can change over time. For instance: - to specify whether a part of the layout needs to be visible or not; - or to decide whether a certain key binding needs to be active or not; - or the ``wrap_lines`` option of :class:`~prompt_toolkit.layout.BufferControl`; - etcetera. These booleans are often dynamic and can change at runtime. For instance, the search toolbar should only be visible when the user is actually searching (when the search buffer has the focus). The ``wrap_lines`` option could be changed with a certain key binding. And that key binding could only work when the default buffer got the focus. In `prompt_toolkit`, we decided to reduce the amount of state in the whole framework, and apply a simple kind of reactive programming to describe the flow of these booleans as expressions. (It's one-way only: if a key binding needs to know whether it's active or not, it can follow this flow by evaluating an expression.) The (abstract) base class is :class:`~prompt_toolkit.filters.Filter`, which wraps an expression that takes no input and evaluates to a boolean. Getting the state of a filter is done by simply calling it. An example ---------- The most obvious way to create such a :class:`~prompt_toolkit.filters.Filter` instance is by creating a :class:`~prompt_toolkit.filters.Condition` instance from a function. For instance, the following condition will evaluate to ``True`` when the user is searching: .. code:: python from prompt_toolkit.application.current import get_app from prompt_toolkit.filters import Condition is_searching = Condition(lambda: get_app().is_searching) A different way of writing this, is by using the decorator syntax: .. code:: python from prompt_toolkit.application.current import get_app from prompt_toolkit.filters import Condition @Condition def is_searching(): return get_app().is_searching This filter can then be used in a key binding, like in the following snippet: .. code:: python from prompt_toolkit.key_binding import KeyBindings kb = KeyBindings() @kb.add('c-t', filter=is_searching) def _(event): # Do, something, but only when searching. pass If we want to know the boolean value of this filter, we have to call it like a function: .. code:: python print(is_searching()) Built-in filters ---------------- There are many built-in filters, ready to use. All of them have a lowercase name, because they represent the wrapped function underneath, and can be called as a function. - :class:`~prompt_toolkit.filters.app.has_arg` - :class:`~prompt_toolkit.filters.app.has_completions` - :class:`~prompt_toolkit.filters.app.has_focus` - :class:`~prompt_toolkit.filters.app.buffer_has_focus` - :class:`~prompt_toolkit.filters.app.has_selection` - :class:`~prompt_toolkit.filters.app.has_validation_error` - :class:`~prompt_toolkit.filters.app.is_aborting` - :class:`~prompt_toolkit.filters.app.is_done` - :class:`~prompt_toolkit.filters.app.is_read_only` - :class:`~prompt_toolkit.filters.app.is_multiline` - :class:`~prompt_toolkit.filters.app.renderer_height_is_known` - :class:`~prompt_toolkit.filters.app.in_editing_mode` - :class:`~prompt_toolkit.filters.app.in_paste_mode` - :class:`~prompt_toolkit.filters.app.vi_mode` - :class:`~prompt_toolkit.filters.app.vi_navigation_mode` - :class:`~prompt_toolkit.filters.app.vi_insert_mode` - :class:`~prompt_toolkit.filters.app.vi_insert_multiple_mode` - :class:`~prompt_toolkit.filters.app.vi_replace_mode` - :class:`~prompt_toolkit.filters.app.vi_selection_mode` - :class:`~prompt_toolkit.filters.app.vi_waiting_for_text_object_mode` - :class:`~prompt_toolkit.filters.app.vi_digraph_mode` - :class:`~prompt_toolkit.filters.app.emacs_mode` - :class:`~prompt_toolkit.filters.app.emacs_insert_mode` - :class:`~prompt_toolkit.filters.app.emacs_selection_mode` - :class:`~prompt_toolkit.filters.app.is_searching` - :class:`~prompt_toolkit.filters.app.control_is_searchable` - :class:`~prompt_toolkit.filters.app.vi_search_direction_reversed` Combining filters ----------------- Filters can be chained with the ``&`` (AND) and ``|`` (OR) operators and negated with the ``~`` (negation) operator. Some examples: .. code:: python from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.filters import has_selection, has_selection kb = KeyBindings() @kb.add('c-t', filter=~is_searching) def _(event): " Do something, but not while searching. " pass @kb.add('c-t', filter=has_search | has_selection) def _(event): " Do something, but only when searching or when there is a selection. " pass to_filter --------- Finally, in many situations you want your code to expose an API that is able to deal with both booleans as well as filters. For instance, when for most users a boolean works fine because they don't need to change the value over time, while some advanced users want to be able this value to a certain setting or event that does changes over time. In order to handle both use cases, there is a utility called :func:`~prompt_toolkit.filters.utils.to_filter`. This is a function that takes either a boolean or an actual :class:`~prompt_toolkit.filters.Filter` instance, and always returns a :class:`~prompt_toolkit.filters.Filter`. .. code:: python from prompt_toolkit.filters.utils import to_filter # In each of the following three examples, 'f' will be a `Filter` # instance. f = to_filter(True) f = to_filter(False) f = to_filter(Condition(lambda: True)) f = to_filter(has_search | has_selection) ================================================ FILE: docs/pages/advanced_topics/index.rst ================================================ .. _advanced_topics: Advanced topics =============== .. toctree:: :caption: Contents: :maxdepth: 1 key_bindings styling filters rendering_flow asyncio unit_testing input_hooks architecture rendering_pipeline ================================================ FILE: docs/pages/advanced_topics/input_hooks.rst ================================================ .. _input_hooks: Input hooks =========== Input hooks are a tool for inserting an external event loop into the prompt_toolkit event loop, so that the other loop can run as long as prompt_toolkit (actually asyncio) is idle. This is used in applications like `IPython `_, so that GUI toolkits can display their windows while we wait at the prompt for user input. As a consequence, we will "trampoline" back and forth between two event loops. .. note:: This will use a :class:`~asyncio.SelectorEventLoop`, not the :class: :class:`~asyncio.ProactorEventLoop` (on Windows) due to the way the implementation works (contributions are welcome to make that work). .. code:: python from prompt_toolkit.eventloop.inputhook import set_eventloop_with_inputhook def inputhook(inputhook_context): # At this point, we run the other loop. This loop is supposed to run # until either `inputhook_context.fileno` becomes ready for reading or # `inputhook_context.input_is_ready()` returns True. # A good way is to register this file descriptor in this other event # loop with a callback that stops this loop when this FD becomes ready. # There is no need to actually read anything from the FD. while True: ... set_eventloop_with_inputhook(inputhook) # Any asyncio code at this point will now use this new loop, with input # hook installed. ================================================ FILE: docs/pages/advanced_topics/key_bindings.rst ================================================ .. _key_bindings: More about key bindings ======================= This page contains a few additional notes about key bindings. Key bindings can be defined as follows by creating a :class:`~prompt_toolkit.key_binding.KeyBindings` instance: .. code:: python from prompt_toolkit.key_binding import KeyBindings bindings = KeyBindings() @bindings.add('a') def _(event): " Do something if 'a' has been pressed. " ... @bindings.add('c-t') def _(event): " Do something if Control-T has been pressed. " ... .. note:: :kbd:`c-q` (control-q) and :kbd:`c-s` (control-s) are often captured by the terminal, because they were used traditionally for software flow control. When this is enabled, the application will automatically freeze when :kbd:`c-s` is pressed, until :kbd:`c-q` is pressed. It won't be possible to bind these keys. In order to disable this, execute the following command in your shell, or even add it to your `.bashrc`. .. code:: stty -ixon Key bindings can even consist of a sequence of multiple keys. The binding is only triggered when all the keys in this sequence are pressed. .. code:: python @bindings.add('a', 'b') def _(event): " Do something if 'a' is pressed and then 'b' is pressed. " ... If the user presses only `a`, then nothing will happen until either a second key (like `b`) has been pressed or until the timeout expires (see later). List of special keys -------------------- Besides literal characters, any of the following keys can be used in a key binding: +-------------------+-----------------------------------------+ | Name + Possible keys | +===================+=========================================+ | Escape | :kbd:`escape` | | Shift + escape | :kbd:`s-escape` | +-------------------+-----------------------------------------+ | Arrows | :kbd:`left`, | | | :kbd:`right`, | | | :kbd:`up`, | | | :kbd:`down` | +-------------------+-----------------------------------------+ | Navigation | :kbd:`home`, | | | :kbd:`end`, | | | :kbd:`delete`, | | | :kbd:`pageup`, | | | :kbd:`pagedown`, | | | :kbd:`insert` | +-------------------+-----------------------------------------+ | Control+letter | :kbd:`c-a`, :kbd:`c-b`, :kbd:`c-c`, | | | :kbd:`c-d`, :kbd:`c-e`, :kbd:`c-f`, | | | :kbd:`c-g`, :kbd:`c-h`, :kbd:`c-i`, | | | :kbd:`c-j`, :kbd:`c-k`, :kbd:`c-l`, | | | | | | :kbd:`c-m`, :kbd:`c-n`, :kbd:`c-o`, | | | :kbd:`c-p`, :kbd:`c-q`, :kbd:`c-r`, | | | :kbd:`c-s`, :kbd:`c-t`, :kbd:`c-u`, | | | :kbd:`c-v`, :kbd:`c-w`, :kbd:`c-x`, | | | | | | :kbd:`c-y`, :kbd:`c-z` | +-------------------+-----------------------------------------+ | Control + number | :kbd:`c-1`, :kbd:`c-2`, :kbd:`c-3`, | | | :kbd:`c-4`, :kbd:`c-5`, :kbd:`c-6`, | | | :kbd:`c-7`, :kbd:`c-8`, :kbd:`c-9`, | | | :kbd:`c-0` | +-------------------+-----------------------------------------+ | Control + arrow | :kbd:`c-left`, | | | :kbd:`c-right`, | | | :kbd:`c-up`, | | | :kbd:`c-down` | +-------------------+-----------------------------------------+ | Other control | :kbd:`c-@`, | | keys | :kbd:`c-\\`, | | | :kbd:`c-]`, | | | :kbd:`c-^`, | | | :kbd:`c-_`, | | | :kbd:`c-delete` | +-------------------+-----------------------------------------+ | Shift + arrow | :kbd:`s-left`, | | | :kbd:`s-right`, | | | :kbd:`s-up`, | | | :kbd:`s-down` | +-------------------+-----------------------------------------+ | Control + Shift + | :kbd:`c-s-left`, | | arrow | :kbd:`c-s-right`, | | | :kbd:`c-s-up`, | | | :kbd:`c-s-down` | +-------------------+-----------------------------------------+ | Other shift | :kbd:`s-delete`, | | keys | :kbd:`s-tab` | +-------------------+-----------------------------------------+ | F-keys | :kbd:`f1`, :kbd:`f2`, :kbd:`f3`, | | | :kbd:`f4`, :kbd:`f5`, :kbd:`f6`, | | | :kbd:`f7`, :kbd:`f8`, :kbd:`f9`, | | | :kbd:`f10`, :kbd:`f11`, :kbd:`f12`, | | | | | | :kbd:`f13`, :kbd:`f14`, :kbd:`f15`, | | | :kbd:`f16`, :kbd:`f17`, :kbd:`f18`, | | | :kbd:`f19`, :kbd:`f20`, :kbd:`f21`, | | | :kbd:`f22`, :kbd:`f23`, :kbd:`f24` | +-------------------+-----------------------------------------+ There are a couple of useful aliases as well: +-------------------+-------------------+ | :kbd:`c-h` | :kbd:`backspace` | +-------------------+-------------------+ | :kbd:`c-@` | :kbd:`c-space` | +-------------------+-------------------+ | :kbd:`c-m` | :kbd:`enter` | +-------------------+-------------------+ | :kbd:`c-i` | :kbd:`tab` | +-------------------+-------------------+ .. note:: Note that the supported keys are limited to what typical VT100 terminals offer. Binding :kbd:`c-7` (control + number 7) for instance is not supported. Binding alt+something, option+something or meta+something --------------------------------------------------------- Vt100 terminals translate the alt key into a leading :kbd:`escape` key. For instance, in order to handle :kbd:`alt-f`, we have to handle :kbd:`escape` + :kbd:`f`. Notice that we receive this as two individual keys. This means that it's exactly the same as first typing :kbd:`escape` and then typing :kbd:`f`. Something this alt-key is also known as option or meta. In code that looks as follows: .. code:: python @bindings.add('escape', 'f') def _(event): " Do something if alt-f or meta-f have been pressed. " Wildcards --------- Sometimes you want to catch any key that follows after a certain key stroke. This is possible by binding the '' key: .. code:: python @bindings.add('a', '') def _(event): ... This will handle `aa`, `ab`, `ac`, etcetera. The key binding can check the `event` object for which keys exactly have been pressed. Attaching a filter (condition) ------------------------------ In order to enable a key binding according to a certain condition, we have to pass it a :class:`~prompt_toolkit.filters.Filter`, usually a :class:`~prompt_toolkit.filters.Condition` instance. (:ref:`Read more about filters `.) .. code:: python from prompt_toolkit.filters import Condition @Condition def is_active(): " Only activate key binding on the second half of each minute. " return datetime.datetime.now().second > 30 @bindings.add('c-t', filter=is_active) def _(event): # ... pass The key binding will be ignored when this condition is not satisfied. ConditionalKeyBindings: Disabling a set of key bindings ------------------------------------------------------- Sometimes you want to enable or disable a whole set of key bindings according to a certain condition. This is possible by wrapping it in a :class:`~prompt_toolkit.key_binding.ConditionalKeyBindings` object. .. code:: python from prompt_toolkit.key_binding import ConditionalKeyBindings @Condition def is_active(): " Only activate key binding on the second half of each minute. " return datetime.datetime.now().second > 30 bindings = ConditionalKeyBindings( key_bindings=my_bindings, filter=is_active) If the condition is not satisfied, all the key bindings in `my_bindings` above will be ignored. Merging key bindings -------------------- Sometimes you have different parts of your application generate a collection of key bindings. It is possible to merge them together through the :func:`~prompt_toolkit.key_binding.merge_key_bindings` function. This is preferred above passing a :class:`~prompt_toolkit.key_binding.KeyBindings` object around and having everyone populate it. .. code:: python from prompt_toolkit.key_binding import merge_key_bindings bindings = merge_key_bindings([ bindings1, bindings2, ]) Eager ----- Usually not required, but if ever you have to override an existing key binding, the `eager` flag can be useful. Suppose that there is already an active binding for `ab` and you'd like to add a second binding that only handles `a`. When the user presses only `a`, prompt_toolkit has to wait for the next key press in order to know which handler to call. By passing the `eager` flag to this second binding, we are actually saying that prompt_toolkit shouldn't wait for longer matches when all the keys in this key binding are matched. So, if `a` has been pressed, this second binding will be called, even if there's an active `ab` binding. .. code:: python @bindings.add('a', 'b') def binding_1(event): ... @bindings.add('a', eager=True) def binding_2(event): ... This is mainly useful in order to conditionally override another binding. Asyncio coroutines ------------------ Key binding handlers can be asyncio coroutines. .. code:: python from prompt_toolkit.application import in_terminal @bindings.add('x') async def print_hello(event): """ Pressing 'x' will print 5 times "hello" in the background above the prompt. """ for i in range(5): # Print hello above the current prompt. async with in_terminal(): print('hello') # Sleep, but allow further input editing in the meantime. await asyncio.sleep(1) If the user accepts the input on the prompt, while this coroutine is not yet finished , an `asyncio.CancelledError` exception will be thrown in this coroutine. Timeouts -------- There are two timeout settings that effect the handling of keys. - ``Application.ttimeoutlen``: Like Vim's `ttimeoutlen` option. When to flush the input (For flushing escape keys.) This is important on terminals that use vt100 input. We can't distinguish the escape key from for instance the left-arrow key, if we don't know what follows after "\x1b". This little timer will consider "\x1b" to be escape if nothing did follow in this time span. This seems to work like the `ttimeoutlen` option in Vim. - ``KeyProcessor.timeoutlen``: like Vim's `timeoutlen` option. This can be `None` or a float. For instance, suppose that we have a key binding AB and a second key binding A. If the uses presses A and then waits, we don't handle this binding yet (unless it was marked 'eager'), because we don't know what will follow. This timeout is the maximum amount of time that we wait until we call the handlers anyway. Pass `None` to disable this timeout. Recording macros ---------------- Both Emacs and Vi mode allow macro recording. By default, all key presses are recorded during a macro, but it is possible to exclude certain keys by setting the `record_in_macro` parameter to `False`: .. code:: python @bindings.add('c-t', record_in_macro=False) def _(event): # ... pass Creating new Vi text objects and operators ------------------------------------------ We tried very hard to ship prompt_toolkit with as many as possible Vi text objects and operators, so that text editing feels as natural as possible to Vi users. If you wish to create a new text object or key binding, that is actually possible. Check the `custom-vi-operator-and-text-object.py` example for more information. Handling SIGINT --------------- The SIGINT Unix signal can be handled by binding ````. For instance: .. code:: python @bindings.add('') def _(event): # ... pass This will handle a SIGINT that was sent by an external application into the process. Handling control-c should be done by binding ``c-c``. (The terminal input is set to raw mode, which means that a ``c-c`` won't be translated into a SIGINT.) For a ``PromptSession``, there is a default binding for ```` that corresponds to ``c-c``: it will exit the prompt, raising a ``KeyboardInterrupt`` exception. Processing `.inputrc` --------------------- GNU readline can be configured using an `.inputrc` configuration file. This file contains key bindings as well as certain settings. Right now, prompt_toolkit doesn't support `.inputrc`, but it should be possible in the future. ================================================ FILE: docs/pages/advanced_topics/rendering_flow.rst ================================================ .. _rendering_flow: The rendering flow ================== Understanding the rendering flow is important for understanding how :class:`~prompt_toolkit.layout.Container` and :class:`~prompt_toolkit.layout.UIControl` objects interact. We will demonstrate it by explaining the flow around a :class:`~prompt_toolkit.layout.BufferControl`. .. note:: A :class:`~prompt_toolkit.layout.BufferControl` is a :class:`~prompt_toolkit.layout.UIControl` for displaying the content of a :class:`~prompt_toolkit.buffer.Buffer`. A buffer is the object that holds any editable region of text. Like all controls, it has to be wrapped into a :class:`~prompt_toolkit.layout.Window`. Let's take the following code: .. code:: python from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import BufferControl from prompt_toolkit.buffer import Buffer b = Buffer(name=DEFAULT_BUFFER) Window(content=BufferControl(buffer=b)) What happens when a :class:`~prompt_toolkit.renderer.Renderer` objects wants a :class:`~prompt_toolkit.layout.Container` to be rendered on a certain :class:`~prompt_toolkit.layout.screen.Screen`? The visualization happens in several steps: 1. The :class:`~prompt_toolkit.renderer.Renderer` calls the :meth:`~prompt_toolkit.layout.Container.write_to_screen` method of a :class:`~prompt_toolkit.layout.Container`. This is a request to paint the layout in a rectangle of a certain size. The :class:`~prompt_toolkit.layout.Window` object then requests the :class:`~prompt_toolkit.layout.UIControl` to create a :class:`~prompt_toolkit.layout.UIContent` instance (by calling :meth:`~prompt_toolkit.layout.UIControl.create_content`). The user control receives the dimensions of the window, but can still decide to create more or less content. Inside the :meth:`~prompt_toolkit.layout.UIControl.create_content` method of :class:`~prompt_toolkit.layout.UIControl`, there are several steps: 2. First, the buffer's text is passed to the :meth:`~prompt_toolkit.lexers.Lexer.lex_document` method of a :class:`~prompt_toolkit.lexers.Lexer`. This returns a function which for a given line number, returns a "formatted text list" for that line (that's a list of ``(style_string, text)`` tuples). 3. This list is passed through a list of :class:`~prompt_toolkit.layout.processors.Processor` objects. Each processor can do a transformation for each line. (For instance, they can insert or replace some text, highlight the selection or search string, etc...) 4. The :class:`~prompt_toolkit.layout.UIControl` returns a :class:`~prompt_toolkit.layout.UIContent` instance which generates such a token lists for each lines. The :class:`~prompt_toolkit.layout.Window` receives the :class:`~prompt_toolkit.layout.UIContent` and then: 5. It calculates the horizontal and vertical scrolling, if applicable (if the content would take more space than what is available). 6. The content is copied to the correct absolute position :class:`~prompt_toolkit.layout.screen.Screen`, as requested by the :class:`~prompt_toolkit.renderer.Renderer`. While doing this, the :class:`~prompt_toolkit.layout.Window` can possible wrap the lines, if line wrapping was configured. Note that this process is lazy: if a certain line is not displayed in the :class:`~prompt_toolkit.layout.Window`, then it is not requested from the :class:`~prompt_toolkit.layout.UIContent`. And from there, the line is not passed through the processors or even asked from the :class:`~prompt_toolkit.lexers.Lexer`. ================================================ FILE: docs/pages/advanced_topics/rendering_pipeline.rst ================================================ The rendering pipeline ====================== This document is an attempt to describe how prompt_toolkit applications are rendered. It's a complex but logical process that happens more or less after every key stroke. We'll go through all the steps from the point where the user hits a key, until the character appears on the screen. Waiting for user input ---------------------- Most of the time when a prompt_toolkit application is running, it is idle. It's sitting in the event loop, waiting for some I/O to happen. The most important kind of I/O we're waiting for is user input. So, within the event loop, we have one file descriptor that represents the input device from where we receive key presses. The details are a little different between operating systems, but it comes down to a selector (like select or epoll) which waits for one or more file descriptor. The event loop is then responsible for calling the appropriate feedback when one of the file descriptors becomes ready. It is like that when the user presses a key: the input device becomes ready for reading, and the appropriate callback is called. This is the `read_from_input` function somewhere in `application.py`. It will read the input from the :class:`~prompt_toolkit.input.Input` object, by calling :meth:`~prompt_toolkit.input.Input.read_keys`. Reading the user input ---------------------- The actual reading is also operating system dependent. For instance, on a Linux machine with a vt100 terminal, we read the input from the pseudo terminal device, by calling `os.read`. This however returns a sequence of bytes. There are two difficulties: - The input could be UTF-8 encoded, and there is always the possibility that we receive only a portion of a multi-byte character. - vt100 key presses consist of multiple characters. For instance the "left arrow" would generate something like ``\x1b[D``. It could be that when we read this input stream, that at some point we only get the first part of such a key press, and we have to wait for the rest to arrive. Both problems are implemented using state machines. - The UTF-8 problem is solved using `codecs.getincrementaldecoder`, which is an object in which we can feed the incoming bytes, and it will only return the complete UTF-8 characters that we have so far. The rest is buffered for the next read operation. - Vt100 parsing is solved by the :class:`~prompt_toolkit.input.vt100_parser.Vt100Parser` state machine. The state machine itself is implemented using a generator. We feed the incoming characters to the generator, and it will call the appropriate callback for key presses once they arrive. One thing here to keep in mind is that the characters for some key presses are a prefix of other key presses, like for instance, escape (``\x1b``) is a prefix of the left arrow key (``\x1b[D``). So for those, we don't know what key is pressed until more data arrives or when the input is flushed because of a timeout. For Windows systems, it's a little different. Here we use Win32 syscalls for reading the console input. Processing the key presses -------------------------- The ``Key`` objects that we receive are then passed to the :class:`~prompt_toolkit.key_binding.key_processor.KeyProcessor` for matching against the currently registered and active key bindings. This is another state machine, because key bindings are linked to a sequence of key presses. We cannot call the handler until all of these key presses arrive and until we're sure that this combination is not a prefix of another combination. For instance, sometimes people bind ``jj`` (a double ``j`` key press) to ``esc`` in Vi mode. This is convenient, but we want to make sure that pressing ``j`` once only, followed by a different key will still insert the ``j`` character as usual. Now, there are hundreds of key bindings in prompt_toolkit (in ptpython, right now we have 585 bindings). This is mainly caused by the way that Vi key bindings are generated. In order to make this efficient, we keep a cache of handlers which match certain sequences of keys. Of course, key bindings also have filters attached for enabling/disabling them. So, if at some point, we get a list of handlers from that cache, we still have to discard the inactive bindings. Luckily, many bindings share exactly the same filter, and we have to check every filter only once. :ref:`Read more about key bindings ...` The key handlers ---------------- Once a key sequence is matched, the handler is called. This can do things like text manipulation, changing the focus or anything else. After the handler is called, the user interface is invalidated and rendered again. Rendering the user interface ---------------------------- The rendering is pretty complex for several reasons: - We have to compute the dimensions of all user interface elements. Sometimes they are given, but sometimes this requires calculating the size of :class:`~prompt_toolkit.layout.UIControl` objects. - It needs to be very efficient, because it's something that happens on every single key stroke. - We should output as little as possible on stdout in order to reduce latency on slow network connections and older terminals. Calculating the total UI height ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unless the application is a full screen application, we have to know how much vertical space is going to be consumed. The total available width is given, but the vertical space is more dynamic. We do this by asking the root :class:`~prompt_toolkit.layout.Container` object to calculate its preferred height. If this is a :class:`~prompt_toolkit.layout.VSplit` or :class:`~prompt_toolkit.layout.HSplit` then this involves recursively querying the child objects for their preferred widths and heights and either summing it up, or taking maximum values depending on the actual layout. In the end, we get the preferred height, for which we make sure it's at least the distance from the cursor position to the bottom of the screen. Painting to the screen ^^^^^^^^^^^^^^^^^^^^^^ Then we create a :class:`~prompt_toolkit.layout.screen.Screen` object. This is like a canvas on which user controls can paint their content. The :meth:`~prompt_toolkit.layout.Container.write_to_screen` method of the root `Container` is called with the screen dimensions. This will call recursively :meth:`~prompt_toolkit.layout.Container.write_to_screen` methods of nested child containers, each time passing smaller dimensions while we traverse what is a tree of `Container` objects. The most inner containers are :class:`~prompt_toolkit.layout.Window` objects, they will do the actual painting of the :class:`~prompt_toolkit.layout.UIControl` to the screen. This involves line wrapping the `UIControl`'s text and maybe scrolling the content horizontally or vertically. Rendering to stdout ^^^^^^^^^^^^^^^^^^^ Finally, when we have painted the screen, this needs to be rendered to stdout. This is done by taking the difference of the previously rendered screen and the new one. The algorithm that we have is heavily optimized to compute this difference as quickly as possible, and call the appropriate output functions of the :class:`~prompt_toolkit.output.Output` back-end. At the end, it will position the cursor in the right place. ================================================ FILE: docs/pages/advanced_topics/styling.rst ================================================ .. _styling: More about styling ================== This page will attempt to explain in more detail how to use styling in prompt_toolkit. To some extent, it is very similar to how `Pygments `_ styling works. Style strings ------------- Many user interface controls, like :class:`~prompt_toolkit.layout.Window` accept a ``style`` argument which can be used to pass the formatting as a string. For instance, we can select a foreground color: - ``"fg:ansired"`` (ANSI color palette) - ``"fg:ansiblue"`` (ANSI color palette) - ``"fg:#ffaa33"`` (hexadecimal notation) - ``"fg:darkred"`` (named color) Or a background color: - ``"bg:ansired"`` (ANSI color palette) - ``"bg:#ffaa33"`` (hexadecimal notation) Or we can add one of the following flags: - ``"bold"`` - ``"italic"`` - ``"underline"`` - ``"blink"`` - ``"reverse"`` (reverse foreground and background on the terminal.) - ``"hidden"`` Or their negative variants: - ``"nobold"`` - ``"noitalic"`` - ``"nounderline"`` - ``"noblink"`` - ``"noreverse"`` - ``"nohidden"`` All of these formatting options can be combined as well: - ``"fg:ansiyellow bg:black bold underline"`` The style string can be given to any user control directly, or to a :class:`~prompt_toolkit.layout.Container` object from where it will propagate to all its children. A style defined by a parent user control can be overridden by any of its children. The parent can for instance say ``style="bold underline"`` where a child overrides this style partly by specifying ``style="nobold bg:ansired"``. .. note:: These styles are actually compatible with `Pygments `_ styles, with additional support for `reverse` and `blink`. Further, we ignore flags like `roman`, `sans`, `mono` and `border`. The following ANSI colors are available (both for foreground and background): .. code:: # Low intensity, dark. (One or two components 0x80, the other 0x00.) ansiblack, ansired, ansigreen, ansiyellow, ansiblue ansimagenta, ansicyan, ansigray # High intensity, bright. ansibrightblack, ansibrightred, ansibrightgreen, ansibrightyellow ansibrightblue, ansibrightmagenta, ansibrightcyan, ansiwhite In order to know which styles are actually used in an application, it is possible to call :meth:`~Application.get_used_style_strings`, when the application is done. Class names ----------- Like we do for web design, it is not a good habit to specify all styling inline. Instead, we can attach class names to UI controls and have a style sheet that refers to these class names. The :class:`~prompt_toolkit.styles.Style` can be passed as an argument to the :class:`~prompt_toolkit.application.Application`. .. code:: python from prompt_toolkit.layout import VSplit, Window from prompt_toolkit.styles import Style layout = VSplit([ Window(BufferControl(...), style='class:left'), HSplit([ Window(BufferControl(...), style='class:top'), Window(BufferControl(...), style='class:bottom'), ], style='class:right') ]) style = Style([ ('left', 'bg:ansired'), ('top', 'fg:#00aaaa'), ('bottom', 'underline bold'), ]) It is possible to add multiple class names to an element. That way we'll combine the styling for these class names. Multiple classes can be passed by using a comma separated list, or by using the ``class:`` prefix twice. .. code:: python Window(BufferControl(...), style='class:left,bottom'), Window(BufferControl(...), style='class:left class:bottom'), It is possible to combine class names and inline styling. The order in which the class names and inline styling is specified determines the order of priority. In the following example for instance, we'll take first the style of the "header" class, and then override that with a red background color. .. code:: python Window(BufferControl(...), style='class:header bg:red'), Dot notation in class names --------------------------- The dot operator has a special meaning in a class name. If we write: ``style="class:a.b.c"``, then this will actually expand to the following: ``style="class:a class:a.b class:a.b.c"``. This is mainly added for `Pygments `_ lexers, which specify "Tokens" like this, but it's useful in other situations as well. Multiple classes in a style sheet --------------------------------- A style sheet can be more complex as well. We can for instance specify two class names. The following will underline the left part within the header, or whatever has both the class "left" and the class "header" (the order doesn't matter). .. code:: python style = Style([ ('header left', 'underline'), ]) If you have a dotted class, then it's required to specify the whole path in the style sheet (just typing ``c`` or ``b.c`` doesn't work if the class is ``a.b.c``): .. code:: python style = Style([ ('a.b.c', 'underline'), ]) It is possible to combine this: .. code:: python style = Style([ ('header body left.text', 'underline'), ]) Evaluation order of rules in a style sheet ------------------------------------------ The style is determined as follows: - First, we concatenate all the style strings from the root control through all the parents to the child in one big string. (Things at the right take precedence anyway.) E.g: ``class:body bg:#aaaaaa #000000 class:header.focused class:left.text.highlighted underline`` - Then we go through this style from left to right, starting from the default style. Inline styling is applied directly. If we come across a class name, then we generate all combinations of the class names that we collected so far (this one and all class names to the left), and for each combination which includes the new class name, we look for matching rules in our style sheet. All these rules are then applied (later rules have higher priority). If we find a dotted class name, this will be expanded in the individual names (like ``class:left class:left.text class:left.text.highlighted``), and all these are applied like any class names. - Then this final style is applied to this user interface element. Using a dictionary as a style sheet ----------------------------------- The order of the rules in a style sheet is meaningful, so typically, we use a list of tuples to specify the style. But is also possible to use a dictionary as a style sheet. This makes sense for Python 3.6, where dictionaries remember their ordering. An ``OrderedDict`` works as well. .. code:: python from prompt_toolkit.styles import Style style = Style.from_dict({ 'header body left.text': 'underline', }) Loading a style from Pygments ----------------------------- `Pygments `_ has a slightly different notation for specifying styles, because it maps styling to Pygments "Tokens". A Pygments style can however be loaded and used as follows: .. code:: python from prompt_toolkit.styles.pygments import style_from_pygments_cls from pygments.styles import get_style_by_name style = style_from_pygments_cls(get_style_by_name('monokai')) Merging styles together ----------------------- Multiple :class:`~prompt_toolkit.styles.Style` objects can be merged together as follows: .. code:: python from prompt_toolkit.styles import merge_styles style = merge_styles([ style1, style2, style3 ]) Color depths ------------ There are four different levels of color depths available: +--------+-----------------+-----------------------------+---------------------------------+ | 1 bit | Black and white | ``ColorDepth.DEPTH_1_BIT`` | ``ColorDepth.MONOCHROME`` | +--------+-----------------+-----------------------------+---------------------------------+ | 4 bit | ANSI colors | ``ColorDepth.DEPTH_4_BIT`` | ``ColorDepth.ANSI_COLORS_ONLY`` | +--------+-----------------+-----------------------------+---------------------------------+ | 8 bit | 256 colors | ``ColorDepth.DEPTH_8_BIT`` | ``ColorDepth.DEFAULT`` | +--------+-----------------+-----------------------------+---------------------------------+ | 24 bit | True colors | ``ColorDepth.DEPTH_24_BIT`` | ``ColorDepth.TRUE_COLOR`` | +--------+-----------------+-----------------------------+---------------------------------+ By default, 256 colors are used, because this is what most terminals support these days. If the ``TERM`` environment variable is set to ``linux`` or ``eterm-color``, then only ANSI colors are used, because of these terminals. The 24 bit true color output needs to be enabled explicitly. When 4 bit color output is chosen, all colors will be mapped to the closest ANSI color. Setting the default color depth for any prompt_toolkit application can be done by setting the ``PROMPT_TOOLKIT_COLOR_DEPTH`` environment variable. You could for instance copy the following into your `.bashrc` file. .. code:: shell # export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_1_BIT export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_4_BIT # export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_8_BIT # export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_24_BIT An application can also decide to set the color depth manually by passing a :class:`~prompt_toolkit.output.ColorDepth` value to the :class:`~prompt_toolkit.application.Application` object: .. code:: python from prompt_toolkit.output.color_depth import ColorDepth app = Application( color_depth=ColorDepth.ANSI_COLORS_ONLY, # ... ) Style transformations --------------------- Prompt_toolkit supports a way to apply certain transformations to the styles near the end of the rendering pipeline. This can be used for instance to change certain colors to improve the rendering in some terminals. One useful example is the :class:`~prompt_toolkit.styles.AdjustBrightnessStyleTransformation` class, which takes `min_brightness` and `max_brightness` as arguments which by default have 0.0 and 1.0 as values. In the following code snippet, we increase the minimum brightness to improve rendering on terminals with a dark background. .. code:: python from prompt_toolkit.styles import AdjustBrightnessStyleTransformation app = Application( style_transformation=AdjustBrightnessStyleTransformation( min_brightness=0.5, # Increase the minimum brightness. max_brightness=1.0, ) # ... ) ================================================ FILE: docs/pages/advanced_topics/unit_testing.rst ================================================ .. _unit_testing: Unit testing ============ Testing user interfaces is not always obvious. Here are a few tricks for testing prompt_toolkit applications. `PosixPipeInput` and `DummyOutput` ---------------------------------- During the creation of a prompt_toolkit :class:`~prompt_toolkit.application.Application`, we can specify what input and output device to be used. By default, these are output objects that correspond with `sys.stdin` and `sys.stdout`. In unit tests however, we want to replace these. - For the input, we want a "pipe input". This is an input device, in which we can programmatically send some input. It can be created with :func:`~prompt_toolkit.input.create_pipe_input`, and that return either a :class:`~prompt_toolkit.input.posix_pipe.PosixPipeInput` or a :class:`~prompt_toolkit.input.win32_pipe.Win32PipeInput` depending on the platform. - For the output, we want a :class:`~prompt_toolkit.output.DummyOutput`. This is an output device that doesn't render anything. We don't want to render anything to `sys.stdout` in the unit tests. .. note:: Typically, we don't want to test the bytes that are written to `sys.stdout`, because these can change any time when the rendering algorithm changes, and are not so meaningful anyway. Instead, we want to test the return value from the :class:`~prompt_toolkit.application.Application` or test how data structures (like text buffers) change over time. So we programmatically feed some input to the input pipe, have the key bindings process the input and then test what comes out of it. In the following example we use a :class:`~prompt_toolkit.shortcuts.PromptSession`, but the same works for any :class:`~prompt_toolkit.application.Application`. .. code:: python from prompt_toolkit.shortcuts import PromptSession from prompt_toolkit.input import create_pipe_input from prompt_toolkit.output import DummyOutput def test_prompt_session(): with create_pipe_input() as inp: inp.send_text("hello\n") session = PromptSession( input=inp, output=DummyOutput(), ) result = session.prompt() assert result == "hello" In the above example, don't forget to send the `\\n` character to accept the prompt, otherwise the :class:`~prompt_toolkit.application.Application` will wait forever for some more input to receive. Using an :class:`~prompt_toolkit.application.current.AppSession` ---------------------------------------------------------------- Sometimes it's not convenient to pass input or output objects to the :class:`~prompt_toolkit.application.Application`, and in some situations it's not even possible at all. This happens when these parameters are not passed down the call stack, through all function calls. An easy way to specify which input/output to use for all applications, is by creating an :class:`~prompt_toolkit.application.current.AppSession` with this input/output and running all code in that :class:`~prompt_toolkit.application.current.AppSession`. This way, we don't need to inject it into every :class:`~prompt_toolkit.application.Application` or :func:`~prompt_toolkit.shortcuts.print_formatted_text` call. Here is an example where we use :func:`~prompt_toolkit.application.create_app_session`: .. code:: python from prompt_toolkit.application import create_app_session from prompt_toolkit.shortcuts import print_formatted_text from prompt_toolkit.output import DummyOutput def test_something(): with create_app_session(output=DummyOutput()): ... print_formatted_text('Hello world') ... Pytest fixtures --------------- In order to get rid of the boilerplate of creating the input, the :class:`~prompt_toolkit.output.DummyOutput`, and the :class:`~prompt_toolkit.application.current.AppSession`, we create a single fixture that does it for every test. Something like this: .. code:: python import pytest from prompt_toolkit.application import create_app_session from prompt_toolkit.input import create_pipe_input from prompt_toolkit.output import DummyOutput @pytest.fixture(autouse=True, scope="function") def mock_input(): with create_pipe_input() as pipe_input: with create_app_session(input=pipe_input, output=DummyOutput()): yield pipe_input For compatibility with pytest's ``capsys`` fixture, we have to create a new :class:`~prompt_toolkit.application.current.AppSession` for every test. This can be done in an autouse fixture. Pytest replaces ``sys.stdout`` with a new object in every test that uses ``capsys`` and the following will ensure that the new :class:`~prompt_toolkit.application.current.AppSession` will each time refer to the latest output. .. code:: python from prompt_toolkit.application import create_app_session @fixture(autouse=True, scope="function") def _pt_app_session() with create_app_session(): yield Type checking ------------- Prompt_toolkit 3.0 is fully type annotated. This means that if a prompt_toolkit application is typed too, it can be verified with mypy. This is complementary to unit tests, but also great for testing for correctness. ================================================ FILE: docs/pages/asking_for_a_choice.rst ================================================ .. _asking_for_input: Asking for a choice =================== Similar to how the :func:`~prompt_toolkit.shortcuts.prompt` function allows for text input, prompt_toolkit has a :func:`~prompt_toolkit.shortcuts.choice` function to ask for a choice from a list of options: .. code:: python from prompt_toolkit.shortcuts import choice result = choice( message="Please choose a dish:", options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], default="salad", ) print(f"You have chosen: {result}") .. image:: ../images/choice-input.png Coloring the options -------------------- It is possible to customize the colors and styles. The ``message`` parameter takes any :ref:`formatted text `, and the labels (2nd argument from the options) can be :ref:`formatted text ` as well. Further, we can pass a :class:`~prompt_toolkit.styles.Style` instance using the :meth:`~prompt_toolkit.styles.Style.from_dict` function: .. code:: python from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice from prompt_toolkit.styles import Style style = Style.from_dict( { "input-selection": "fg:#ff0000", "number": "fg:#884444 bold", "selected-option": "underline", } ) result = choice( message=HTML("Please select a dish:"), options=[ ("pizza", "Pizza with mushrooms"), ( "salad", HTML("Salad with tomatoes"), ), ("sushi", "Sushi"), ], style=style, ) print(f"You have chosen: {result}") .. image:: ../images/colored-choice.png Adding a frame -------------- The :func:`~prompt_toolkit.shortcuts.choice` function takes a ``show_frame`` argument. When ``True``, the input is displayed within a frame. It is also possible to pass a :ref:`filter `, like ``~is_done``, so that the frame is only displayed when asking for input, but hidden once the input is accepted. .. code:: python from prompt_toolkit.shortcuts import choice from prompt_toolkit.filters import is_done from prompt_toolkit.styles import Style style = Style.from_dict( { "frame.border": "#884444", "selected-option": "bold", } ) result = choice( message="Please select a dish:", options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], style=style, show_frame=~is_done, ) print(f"You have chosen: {result}") .. image:: ../images/choice-with-frame.png Adding a bottom toolbar ----------------------- Adding a bottom toolbar can be done by passing a ``bottom_toolbar`` argument to :func:`~prompt_toolkit.shortcuts.choice`. This argument can be plain text, :ref:`formatted text ` or a callable that returns plain or formatted text. .. code:: python from prompt_toolkit.filters import is_done from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice from prompt_toolkit.styles import Style style = Style.from_dict( { "frame.border": "#ff4444", "selected-option": "bold", # ('noreverse' because the default toolbar style uses 'reverse') "bottom-toolbar": "#ffffff bg:#333333 noreverse", } ) result = choice( message=HTML("Please select a dish:"), options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], style=style, bottom_toolbar=HTML( " Press [Up]/[Down] to select, [Enter] to accept." ), show_frame=~is_done, ) print(f"You have chosen: {result}") .. image:: ../images/choice-with-frame-and-bottom-toolbar.png ================================================ FILE: docs/pages/asking_for_input.rst ================================================ .. _asking_for_input: Asking for input (prompts) ========================== This page is about building prompts. Pieces of code that we can embed in a program for asking the user for input. Even if you want to use `prompt_toolkit` for building full screen terminal applications, it is probably still a good idea to read this first, before heading to the :ref:`building full screen applications ` page. In this page, we will cover autocompletion, syntax highlighting, key bindings, and so on. Hello world ----------- The following snippet is the most simple example, it uses the :func:`~prompt_toolkit.shortcuts.prompt` function to ask the user for input and returns the text. Just like ``(raw_)input``. .. code:: python from prompt_toolkit import prompt text = prompt("Give me some input: ") print(f"You said: {text}") .. image:: ../images/hello-world-prompt.png What we get here is a simple prompt that supports the Emacs key bindings like readline, but further nothing special. However, :func:`~prompt_toolkit.shortcuts.prompt` has a lot of configuration options. In the following sections, we will discover all these parameters. The `PromptSession` object -------------------------- Instead of calling the :func:`~prompt_toolkit.shortcuts.prompt` function, it's also possible to create a :class:`~prompt_toolkit.shortcuts.PromptSession` instance followed by calling its :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` method for every input call. This creates a kind of an input session. .. code:: python from prompt_toolkit import PromptSession # Create prompt object. session = PromptSession() # Do multiple input calls. text1 = session.prompt() text2 = session.prompt() This has mainly two advantages: - The input history will be kept between consecutive :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` calls. - The :func:`~prompt_toolkit.shortcuts.PromptSession` instance and its :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` method take about the same arguments, like all the options described below (highlighting, completion, etc...). So if you want to ask for multiple inputs, but each input call needs about the same arguments, they can be passed to the :func:`~prompt_toolkit.shortcuts.PromptSession` instance as well, and they can be overridden by passing values to the :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` method. Syntax highlighting ------------------- Adding syntax highlighting is as simple as adding a lexer. All of the `Pygments `_ lexers can be used after wrapping them in a :class:`~prompt_toolkit.lexers.PygmentsLexer`. It is also possible to create a custom lexer by implementing the :class:`~prompt_toolkit.lexers.Lexer` abstract base class. .. code:: python from pygments.lexers.html import HtmlLexer from prompt_toolkit.shortcuts import prompt from prompt_toolkit.lexers import PygmentsLexer text = prompt("Enter HTML: ", lexer=PygmentsLexer(HtmlLexer)) print(f"You said: {text}") .. image:: ../images/html-input.png The default Pygments colorscheme is included as part of the default style in prompt_toolkit. If you want to use another Pygments style along with the lexer, you can do the following: .. code:: python from pygments.lexers.html import HtmlLexer from pygments.styles import get_style_by_name from prompt_toolkit.shortcuts import prompt from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles.pygments import style_from_pygments_cls style = style_from_pygments_cls(get_style_by_name("monokai")) text = prompt( "Enter HTML: ", lexer=PygmentsLexer(HtmlLexer), style=style, include_default_pygments_style=False ) print(f"You said: {text}") We pass ``include_default_pygments_style=False``, because otherwise, both styles will be merged, possibly giving slightly different colors in the outcome for cases where where our custom Pygments style doesn't specify a color. .. _colors: Colors ------ The colors for syntax highlighting are defined by a :class:`~prompt_toolkit.styles.Style` instance. By default, a neutral built-in style is used, but any style instance can be passed to the :func:`~prompt_toolkit.shortcuts.prompt` function. A simple way to create a style, is by using the :meth:`~prompt_toolkit.styles.Style.from_dict` function: .. code:: python from pygments.lexers.html import HtmlLexer from prompt_toolkit.shortcuts import prompt from prompt_toolkit.styles import Style from prompt_toolkit.lexers import PygmentsLexer our_style = Style.from_dict({ "pygments.comment": "#888888 bold", "pygments.keyword": "#ff88ff bold", }) text = prompt( "Enter HTML: ", lexer=PygmentsLexer(HtmlLexer), style=our_style ) The style dictionary is very similar to the Pygments ``styles`` dictionary, with a few differences: - The `roman`, `sans`, `mono` and `border` options are ignored. - The style has a few additions: ``blink``, ``noblink``, ``reverse`` and ``noreverse``. - Colors can be in the ``#ff0000`` format, but they can be one of the built-in ANSI color names as well. In that case, they map directly to the 16 color palette of the terminal. :ref:`Read more about styling `. Using a Pygments style ^^^^^^^^^^^^^^^^^^^^^^ All Pygments style classes can be used as well, when they are wrapped through :func:`~prompt_toolkit.styles.style_from_pygments_cls`. Suppose we'd like to use a Pygments style, for instance ``pygments.styles.tango.TangoStyle``, that is possible like this: .. code:: python from prompt_toolkit.shortcuts import prompt from prompt_toolkit.styles import style_from_pygments_cls from prompt_toolkit.lexers import PygmentsLexer from pygments.styles.tango import TangoStyle from pygments.lexers.html import HtmlLexer tango_style = style_from_pygments_cls(TangoStyle) text = prompt( "Enter HTML: ", lexer=PygmentsLexer(HtmlLexer), style=tango_style ) Creating a custom style could be done like this: .. code:: python from prompt_toolkit.shortcuts import prompt from prompt_toolkit.styles import Style, style_from_pygments_cls, merge_styles from prompt_toolkit.lexers import PygmentsLexer from pygments.styles.tango import TangoStyle from pygments.lexers.html import HtmlLexer our_style = merge_styles([ style_from_pygments_cls(TangoStyle), Style.from_dict({ "pygments.comment": "#888888 bold", "pygments.keyword": "#ff88ff bold", }) ]) text = prompt( "Enter HTML: ", lexer=PygmentsLexer(HtmlLexer), style=our_style ) Coloring the prompt itself ^^^^^^^^^^^^^^^^^^^^^^^^^^ It is possible to add some colors to the prompt itself. For this, we need to build some :ref:`formatted text `. One way of doing this is by creating a list of style/text tuples. In the following example, we use class names to refer to the style. .. code:: python from prompt_toolkit.shortcuts import prompt from prompt_toolkit.styles import Style style = Style.from_dict({ # User input (default text). "": "#ff0066", # Prompt. "username": "#884444", "at": "#00aa00", "colon": "#0000aa", "pound": "#00aa00", "host": "#00ffff bg:#444400", "path": "ansicyan underline", }) message = [ ("class:username", "john"), ("class:at", "@"), ("class:host", "localhost"), ("class:colon", ":"), ("class:path", "/user/john"), ("class:pound", "# "), ] text = prompt(message, style=style) .. image:: ../images/colored-prompt.png The `message` can be any kind of formatted text, as discussed :ref:`here `. It can also be a callable that returns some formatted text. By default, colors are taken from the 256 color palette. If you want to have 24bit true color, this is possible by adding the ``color_depth=ColorDepth.TRUE_COLOR`` option to the :func:`~prompt_toolkit.shortcuts.prompt.prompt` function. .. code:: python from prompt_toolkit.output import ColorDepth text = prompt(message, style=style, color_depth=ColorDepth.TRUE_COLOR) Autocompletion -------------- Autocompletion can be added by passing a ``completer`` parameter. This should be an instance of the :class:`~prompt_toolkit.completion.Completer` abstract base class. :class:`~prompt_toolkit.completion.WordCompleter` is an example of a completer that implements that interface. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter html_completer = WordCompleter(["", "", "", ""]) text = prompt("Enter HTML: ", completer=html_completer) print(f"You said: {text}") :class:`~prompt_toolkit.completion.WordCompleter` is a simple completer that completes the last word before the cursor with any of the given words. .. image:: ../images/html-completion.png .. note:: Note that in prompt_toolkit 2.0, the auto completion became synchronous. This means that if it takes a long time to compute the completions, that this will block the event loop and the input processing. For heavy completion algorithms, it is recommended to wrap the completer in a :class:`~prompt_toolkit.completion.ThreadedCompleter` in order to run it in a background thread. Nested completion ^^^^^^^^^^^^^^^^^ Sometimes you have a command line interface where the completion depends on the previous words from the input. Examples are the CLIs from routers and switches. A simple :class:`~prompt_toolkit.completion.WordCompleter` is not enough in that case. We want to to be able to define completions at multiple hierarchical levels. :class:`~prompt_toolkit.completion.NestedCompleter` solves this issue: .. code:: python from prompt_toolkit import prompt from prompt_toolkit.completion import NestedCompleter completer = NestedCompleter.from_nested_dict({ "show": { "version": None, "clock": None, "ip": { "interface": {"brief"} } }, "exit": None, }) text = prompt("# ", completer=completer) print(f"You said: {text}") Whenever there is a ``None`` value in the dictionary, it means that there is no further nested completion at that point. When all values of a dictionary would be ``None``, it can also be replaced with a set. A custom completer ^^^^^^^^^^^^^^^^^^ For more complex examples, it makes sense to create a custom completer. For instance: .. code:: python from prompt_toolkit import prompt from prompt_toolkit.completion import Completer, Completion class MyCustomCompleter(Completer): def get_completions(self, document, complete_event): yield Completion("completion", start_position=0) text = prompt("> ", completer=MyCustomCompleter()) A :class:`~prompt_toolkit.completion.Completer` class has to implement a generator named :meth:`~prompt_toolkit.completion.Completer.get_completions` that takes a :class:`~prompt_toolkit.document.Document` and yields the current :class:`~prompt_toolkit.completion.Completion` instances. Each completion contains a portion of text, and a position. The position is used for fixing text before the cursor. Pressing the tab key could for instance turn parts of the input from lowercase to uppercase. This makes sense for a case insensitive completer. Or in case of a fuzzy completion, it could fix typos. When ``start_position`` is something negative, this amount of characters will be deleted and replaced. Styling individual completions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Each completion can provide a custom style, which is used when it is rendered in the completion menu or toolbar. This is possible by passing a style to each :class:`~prompt_toolkit.completion.Completion` instance. .. code:: python from prompt_toolkit.completion import Completer, Completion class MyCustomCompleter(Completer): def get_completions(self, document, complete_event): # Display this completion, black on yellow. yield Completion( "completion1", start_position=0, style="bg:ansiyellow fg:ansiblack" ) # Underline completion. yield Completion( "completion2", start_position=0, style="underline" ) # Specify class name, which will be looked up in the style sheet. yield Completion( "completion3", start_position=0, style="class:special-completion" ) The "colorful-prompts.py" example uses completion styling: .. image:: ../images/colorful-completions.png Finally, it is possible to pass :ref:`formatted text <formatted_text>` for the ``display`` attribute of a :class:`~prompt_toolkit.completion.Completion`. This provides all the freedom you need to display the text in any possible way. It can also be combined with the ``style`` attribute. For instance: .. code:: python from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.formatted_text import HTML class MyCustomCompleter(Completer): def get_completions(self, document, complete_event): yield Completion( "completion1", start_position=0, display=HTML("<b>completion</b><ansired>1</ansired>"), style="bg:ansiyellow" ) Fuzzy completion ^^^^^^^^^^^^^^^^ If one possible completions is "django_migrations", a fuzzy completer would allow you to get this by typing "djm" only, a subset of characters for this string. Prompt_toolkit ships with a :class:`~prompt_toolkit.completion.FuzzyCompleter` and :class:`~prompt_toolkit.completion.FuzzyWordCompleter` class. These provide the means for doing this kind of "fuzzy completion". The first one can take any completer instance and wrap it so that it becomes a fuzzy completer. The second one behaves like a :class:`~prompt_toolkit.completion.WordCompleter` wrapped into a :class:`~prompt_toolkit.completion.FuzzyCompleter`. Complete while typing ^^^^^^^^^^^^^^^^^^^^^ Autcompletions can be generated automatically while typing or when the user presses the tab key. This can be configured with the ``complete_while_typing`` option: .. code:: python text = prompt( "Enter HTML: ", completer=my_completer, complete_while_typing=True ) Notice that this setting is incompatible with the ``enable_history_search`` option. The reason for this is that the up and down key bindings would conflict otherwise. So, make sure to disable history search for this. Asynchronous completion ^^^^^^^^^^^^^^^^^^^^^^^ When generating the completions takes a lot of time, it's better to do this in a background thread. This is possible by wrapping the completer in a :class:`~prompt_toolkit.completion.ThreadedCompleter`, but also by passing the `complete_in_thread=True` argument. .. code:: python text = prompt("> ", completer=MyCustomCompleter(), complete_in_thread=True) Input validation ---------------- A prompt can have a validator attached. This is some code that will check whether the given input is acceptable and it will only return it if that's the case. Otherwise it will show an error message and move the cursor to a given position. A validator should implements the :class:`~prompt_toolkit.validation.Validator` abstract base class. This requires only one method, named ``validate`` that takes a :class:`~prompt_toolkit.document.Document` as input and raises :class:`~prompt_toolkit.validation.ValidationError` when the validation fails. .. code:: python from prompt_toolkit.validation import Validator, ValidationError from prompt_toolkit import prompt class NumberValidator(Validator): def validate(self, document): text = document.text if text and not text.isdigit(): i = 0 # Get index of first non numeric character. # We want to move the cursor here. for i, c in enumerate(text): if not c.isdigit(): break raise ValidationError( message="This input contains non-numeric characters", cursor_position=i ) number = int(prompt("Give a number: ", validator=NumberValidator())) print(f"You said: {number}") .. image:: ../images/number-validator.png By default, the input is validated in real-time while the user is typing, but prompt_toolkit can also validate after the user presses the enter key: .. code:: python prompt( "Give a number: ", validator=NumberValidator(), validate_while_typing=False ) If the input validation contains some heavy CPU intensive code, but you don't want to block the event loop, then it's recommended to wrap the validator class in a :class:`~prompt_toolkit.validation.ThreadedValidator`. Validator from a callable ^^^^^^^^^^^^^^^^^^^^^^^^^ Instead of implementing the :class:`~prompt_toolkit.validation.Validator` abstract base class, it is also possible to start from a simple function and use the :meth:`~prompt_toolkit.validation.Validator.from_callable` classmethod. This is easier and sufficient for probably 90% of the validators. It looks as follows: .. code:: python from prompt_toolkit.validation import Validator from prompt_toolkit import prompt def is_number(text): return text.isdigit() validator = Validator.from_callable( is_number, error_message="This input contains non-numeric characters", move_cursor_to_end=True ) number = int(prompt("Give a number: ", validator=validator)) print(f"You said: {number}") We define a function that takes a string, and tells whether it's valid input or not by returning a boolean. :meth:`~prompt_toolkit.validation.Validator.from_callable` turns that into a :class:`~prompt_toolkit.validation.Validator` instance. Notice that setting the cursor position is not possible this way. History ------- A :class:`~prompt_toolkit.history.History` object keeps track of all the previously entered strings, so that the up-arrow can reveal previously entered items. The recommended way is to use a :class:`~prompt_toolkit.shortcuts.PromptSession`, which uses an :class:`~prompt_toolkit.history.InMemoryHistory` for the entire session by default. The following example has a history out of the box: .. code:: python from prompt_toolkit import PromptSession session = PromptSession() while True: session.prompt() To persist a history to disk, use a :class:`~prompt_toolkit.history.FileHistory` instead of the default :class:`~prompt_toolkit.history.InMemoryHistory`. This history object can be passed either to a :class:`~prompt_toolkit.shortcuts.PromptSession` or to the :meth:`~prompt_toolkit.shortcuts.prompt` function. For instance: .. code:: python from prompt_toolkit import PromptSession from prompt_toolkit.history import FileHistory session = PromptSession(history=FileHistory("~/.myhistory")) while True: session.prompt() Auto suggestion --------------- Auto suggestion is a way to propose some input completions to the user like the `fish shell <http://fishshell.com/>`_. Usually, the input is compared to the history and when there is another entry starting with the given text, the completion will be shown as gray text behind the current input. Pressing the right arrow :kbd:`→` or :kbd:`c-e` will insert this suggestion, :kbd:`alt-f` will insert the first word of the suggestion. .. note:: When suggestions are based on the history, don't forget to share one :class:`~prompt_toolkit.history.History` object between consecutive :func:`~prompt_toolkit.shortcuts.prompt` calls. Using a :class:`~prompt_toolkit.shortcuts.PromptSession` does this for you. Example: .. code:: python from prompt_toolkit import PromptSession from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory session = PromptSession() while True: text = session.prompt("> ", auto_suggest=AutoSuggestFromHistory()) print(f"You said: {text}") .. image:: ../images/auto-suggestion.png A suggestion does not have to come from the history. Any implementation of the :class:`~prompt_toolkit.auto_suggest.AutoSuggest` abstract base class can be passed as an argument. Adding a bottom toolbar ----------------------- Adding a bottom toolbar is as easy as passing a ``bottom_toolbar`` argument to :func:`~prompt_toolkit.shortcuts.prompt`. This argument be either plain text, :ref:`formatted text <formatted_text>` or a callable that returns plain or formatted text. When a function is given, it will be called every time the prompt is rendered, so the bottom toolbar can be used to display dynamic information. The toolbar is always erased when the prompt returns. Here we have an example of a callable that returns an :class:`~prompt_toolkit.formatted_text.HTML` object. By default, the toolbar has the **reversed style**, which is why we are setting the background instead of the foreground. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.formatted_text import HTML def bottom_toolbar(): return HTML('This is a <b><style bg="ansired">Toolbar</style></b>!') text = prompt("> ", bottom_toolbar=bottom_toolbar) print(f"You said: {text}") .. image:: ../images/bottom-toolbar.png Similar, we could use a list of style/text tuples. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.styles import Style def bottom_toolbar(): return [("class:bottom-toolbar", " This is a toolbar. ")] style = Style.from_dict({ "bottom-toolbar": "#ffffff bg:#333333", }) text = prompt("> ", bottom_toolbar=bottom_toolbar, style=style) print(f"You said: {text}") The default class name is ``bottom-toolbar`` and that will also be used to fill the background of the toolbar. Adding a right prompt --------------------- The :func:`~prompt_toolkit.shortcuts.prompt` function has out of the box support for right prompts as well. People familiar to ZSH could recognize this as the `RPROMPT` option. So, similar to adding a bottom toolbar, we can pass an ``rprompt`` argument. This can be either plain text, :ref:`formatted text <formatted_text>` or a callable which returns either. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.styles import Style example_style = Style.from_dict({ "rprompt": "bg:#ff0066 #ffffff", }) def get_rprompt(): return "<rprompt>" answer = prompt("> ", rprompt=get_rprompt, style=example_style) .. image:: ../images/rprompt.png The ``get_rprompt`` function can return any kind of formatted text such as :class:`~prompt_toolkit.formatted_text.HTML`. it is also possible to pass text directly to the ``rprompt`` argument of the :func:`~prompt_toolkit.shortcuts.prompt` function. It does not have to be a callable. Vi input mode ------------- Prompt-toolkit supports both Emacs and Vi key bindings, similar to Readline. The :func:`~prompt_toolkit.shortcuts.prompt` function will use Emacs bindings by default. This is done because on most operating systems, also the Bash shell uses Emacs bindings by default, and that is more intuitive. If however, Vi binding are required, just pass ``vi_mode=True``. .. code:: python from prompt_toolkit import prompt prompt("> ", vi_mode=True) Adding custom key bindings -------------------------- By default, every prompt already has a set of key bindings which implements the usual Vi or Emacs behavior. We can extend this by passing another :class:`~prompt_toolkit.key_binding.KeyBindings` instance to the ``key_bindings`` argument of the :func:`~prompt_toolkit.shortcuts.prompt` function or the :class:`~prompt_toolkit.shortcuts.PromptSession` class. An example of a prompt that prints ``'hello world'`` when :kbd:`Control-T` is pressed. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.application import run_in_terminal from prompt_toolkit.key_binding import KeyBindings bindings = KeyBindings() @bindings.add("c-t") def _(event): " Say "hello" when `c-t` is pressed. " def print_hello(): print("hello world") run_in_terminal(print_hello) @bindings.add("c-x") def _(event): " Exit when `c-x` is pressed. " event.app.exit() text = prompt("> ", key_bindings=bindings) print(f"You said: {text}") Note that we use :meth:`~prompt_toolkit.application.run_in_terminal` for the first key binding. This ensures that the output of the print-statement and the prompt don't mix up. If the key bindings doesn't print anything, then it can be handled directly without nesting functions. Enable key bindings according to a condition ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Often, some key bindings can be enabled or disabled according to a certain condition. For instance, the Emacs and Vi bindings will never be active at the same time, but it is possible to switch between Emacs and Vi bindings at run time. In order to enable a key binding according to a certain condition, we have to pass it a :class:`~prompt_toolkit.filters.Filter`, usually a :class:`~prompt_toolkit.filters.Condition` instance. (:ref:`Read more about filters <filters>`.) .. code:: python from prompt_toolkit import prompt from prompt_toolkit.filters import Condition from prompt_toolkit.key_binding import KeyBindings bindings = KeyBindings() @Condition def is_active(): " Only activate key binding on the second half of each minute. " return datetime.datetime.now().second > 30 @bindings.add("c-t", filter=is_active) def _(event): # ... pass prompt("> ", key_bindings=bindings) Dynamically switch between Emacs and Vi mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :class:`~prompt_toolkit.application.Application` has an ``editing_mode`` attribute. We can change the key bindings by changing this attribute from ``EditingMode.VI`` to ``EditingMode.EMACS``. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import EditingMode from prompt_toolkit.key_binding import KeyBindings def run(): # Create a set of key bindings. bindings = KeyBindings() # Add an additional key binding for toggling this flag. @bindings.add("f4") def _(event): " Toggle between Emacs and Vi mode. " app = event.app if app.editing_mode == EditingMode.VI: app.editing_mode = EditingMode.EMACS else: app.editing_mode = EditingMode.VI # Add a toolbar at the bottom to display the current input mode. def bottom_toolbar(): " Display the current input mode. " text = "Vi" if get_app().editing_mode == EditingMode.VI else "Emacs" return [ ("class:toolbar", " [F4] %s " % text) ] prompt("> ", key_bindings=bindings, bottom_toolbar=bottom_toolbar) run() :ref:`Read more about key bindings ...<key_bindings>` Using control-space for completion ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ An popular short cut that people sometimes use it to use control-space for opening the autocompletion menu instead of the tab key. This can be done with the following key binding. .. code:: python kb = KeyBindings() @kb.add("c-space") def _(event): " Initialize autocompletion, or select the next completion. " buff = event.app.current_buffer if buff.complete_state: buff.complete_next() else: buff.start_completion(select_first=False) Other prompt options -------------------- Multiline input ^^^^^^^^^^^^^^^ Reading multiline input is as easy as passing the ``multiline=True`` parameter. .. code:: python from prompt_toolkit import prompt prompt("> ", multiline=True) A side effect of this is that the enter key will now insert a newline instead of accepting and returning the input. The user will now have to press :kbd:`Meta+Enter` in order to accept the input. (Or :kbd:`Escape` followed by :kbd:`Enter`.) It is possible to specify a continuation prompt. This works by passing a ``prompt_continuation`` callable to :func:`~prompt_toolkit.shortcuts.prompt`. This function is supposed to return :ref:`formatted text <formatted_text>`, or a list of ``(style, text)`` tuples. The width of the returned text should not exceed the given width. (The width of the prompt margin is defined by the prompt.) .. code:: python from prompt_toolkit import prompt def prompt_continuation(width, line_number, is_soft_wrap): return "." * width # Or: return [("", "." * width)] prompt( "multiline input> ", multiline=True, prompt_continuation=prompt_continuation ) .. image:: ../images/multiline-input.png Passing a default ^^^^^^^^^^^^^^^^^ A default value can be given: .. code:: python from prompt_toolkit import prompt import getpass prompt("What is your name: ", default=f"{getpass.getuser()}") Mouse support ^^^^^^^^^^^^^ There is limited mouse support for positioning the cursor, for scrolling (in case of large multiline inputs) and for clicking in the autocompletion menu. Enabling can be done by passing the ``mouse_support=True`` option. .. code:: python from prompt_toolkit import prompt prompt("What is your name: ", mouse_support=True) Line wrapping ^^^^^^^^^^^^^ Line wrapping is enabled by default. This is what most people are used to and this is what GNU Readline does. When it is disabled, the input string will scroll horizontally. .. code:: python from prompt_toolkit import prompt prompt("What is your name: ", wrap_lines=False) Password input ^^^^^^^^^^^^^^ When the ``is_password=True`` flag has been given, the input is replaced by asterisks (``*`` characters). .. code:: python from prompt_toolkit import prompt prompt("Enter password: ", is_password=True) Cursor shapes ------------- Many terminals support displaying different types of cursor shapes. The most common are block, beam or underscore. Either blinking or not. It is possible to decide which cursor to display while asking for input, or in case of Vi input mode, have a modal prompt for which its cursor shape changes according to the input mode. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.cursor_shapes import CursorShape, ModalCursorShapeConfig # Several possible values for the `cursor_shape_config` parameter: prompt(">", cursor=CursorShape.BLOCK) prompt(">", cursor=CursorShape.UNDERLINE) prompt(">", cursor=CursorShape.BEAM) prompt(">", cursor=CursorShape.BLINKING_BLOCK) prompt(">", cursor=CursorShape.BLINKING_UNDERLINE) prompt(">", cursor=CursorShape.BLINKING_BEAM) prompt(">", cursor=ModalCursorShapeConfig()) Adding a frame -------------- A frame can be displayed around the input by passing ``show_frame=True`` as a parameter. The color of the frame can be chosen by styling the ``frame.border`` element: .. code:: python from prompt_toolkit import prompt from prompt_toolkit.styles import Style style = Style.from_dict( { "frame.border": "#884444", } ) answer = prompt("Say something > ", style=style, show_frame=True) print(f"You said: {answer}") .. image:: ../images/prompt-with-frame.png It is also possible to pass a :ref:`filter <filters>`, for instance ``show_frame=~is_done``, so that the frame is only displayed when asking for input, but hidden once the input is accepted. .. code:: python from prompt_toolkit import prompt from prompt_toolkit.filters import is_done answer = prompt("Say something > ", show_frame=~is_done) print(f"You said: {answer}") Prompt in an `asyncio` application ---------------------------------- .. note:: New in prompt_toolkit 3.0. (In prompt_toolkit 2.0 this was possible using a work-around). For `asyncio <https://docs.python.org/3/library/asyncio.html>`_ applications, it's very important to never block the eventloop. However, :func:`~prompt_toolkit.shortcuts.prompt` is blocking, and calling this would freeze the whole application. Asyncio actually won't even allow us to run that function within a coroutine. The answer is to call :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt_async` instead of :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt`. The async variation returns a coroutines and is awaitable. .. code:: python from prompt_toolkit import PromptSession from prompt_toolkit.patch_stdout import patch_stdout async def my_coroutine(): session = PromptSession() while True: with patch_stdout(): result = await session.prompt_async("Say something: ") print(f"You said: {result}") The :func:`~prompt_toolkit.patch_stdout.patch_stdout` context manager is optional, but it's recommended, because other coroutines could print to stdout. This ensures that other output won't destroy the prompt. Reading keys from stdin, one key at a time, but without a prompt ---------------------------------------------------------------- Suppose that you want to use prompt_toolkit to read the keys from stdin, one key at a time, but not render a prompt to the output, that is also possible: .. code:: python import asyncio from prompt_toolkit.input import create_input from prompt_toolkit.keys import Keys async def main() -> None: done = asyncio.Event() input = create_input() def keys_ready(): for key_press in input.read_keys(): print(key_press) if key_press.key == Keys.ControlC: done.set() with input.raw_mode(): with input.attach(keys_ready): await done.wait() if __name__ == "__main__": asyncio.run(main()) The above snippet will print the `KeyPress` object whenever a key is pressed. This is also cross platform, and should work on Windows. ================================================ FILE: docs/pages/dialogs.rst ================================================ .. _dialogs: Dialogs ======= Prompt_toolkit ships with a high level API for displaying dialogs, similar to the Whiptail program, but in pure Python. Message box ----------- Use the :func:`~prompt_toolkit.shortcuts.message_dialog` function to display a simple message box. For instance: .. code:: python from prompt_toolkit.shortcuts import message_dialog message_dialog( title='Example dialog window', text='Do you want to continue?\nPress ENTER to quit.').run() .. image:: ../images/dialogs/messagebox.png Input box --------- The :func:`~prompt_toolkit.shortcuts.input_dialog` function can display an input box. It will return the user input as a string. .. code:: python from prompt_toolkit.shortcuts import input_dialog text = input_dialog( title='Input dialog example', text='Please type your name:').run() .. image:: ../images/dialogs/inputbox.png The ``password=True`` option can be passed to the :func:`~prompt_toolkit.shortcuts.input_dialog` function to turn this into a password input box. Yes/No confirmation dialog -------------------------- The :func:`~prompt_toolkit.shortcuts.yes_no_dialog` function displays a yes/no confirmation dialog. It will return a boolean according to the selection. .. code:: python from prompt_toolkit.shortcuts import yes_no_dialog result = yes_no_dialog( title='Yes/No dialog example', text='Do you want to confirm?').run() .. image:: ../images/dialogs/confirm.png Button dialog ------------- The :func:`~prompt_toolkit.shortcuts.button_dialog` function displays a dialog with choices offered as buttons. Buttons are indicated as a list of tuples, each providing the label (first) and return value if clicked (second). .. code:: python from prompt_toolkit.shortcuts import button_dialog result = button_dialog( title='Button dialog example', text='Do you want to confirm?', buttons=[ ('Yes', True), ('No', False), ('Maybe...', None) ], ).run() .. image:: ../images/dialogs/button.png Radio list dialog ----------------- The :func:`~prompt_toolkit.shortcuts.radiolist_dialog` function displays a dialog with choices offered as a radio list. The values are provided as a list of tuples, each providing the return value (first element) and the displayed value (second element). .. code:: python from prompt_toolkit.shortcuts import radiolist_dialog result = radiolist_dialog( title="RadioList dialog", text="Which breakfast would you like ?", values=[ ("breakfast1", "Eggs and beacon"), ("breakfast2", "French breakfast"), ("breakfast3", "Equestrian breakfast") ] ).run() Checkbox list dialog -------------------- The :func:`~prompt_toolkit.shortcuts.checkboxlist_dialog` has the same usage and purpose than the Radiolist dialog, but allows several values to be selected and therefore returned. .. code:: python from prompt_toolkit.shortcuts import checkboxlist_dialog results_array = checkboxlist_dialog( title="CheckboxList dialog", text="What would you like in your breakfast ?", values=[ ("eggs", "Eggs"), ("bacon", "Bacon"), ("croissants", "20 Croissants"), ("daily", "The breakfast of the day") ] ).run() Styling of dialogs ------------------ A custom :class:`~prompt_toolkit.styles.Style` instance can be passed to all dialogs to override the default style. Also, text can be styled by passing an :class:`~prompt_toolkit.formatted_text.HTML` object. .. code:: python from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import message_dialog from prompt_toolkit.styles import Style example_style = Style.from_dict({ 'dialog': 'bg:#88ff88', 'dialog frame.label': 'bg:#ffffff #000000', 'dialog.body': 'bg:#000000 #00ff00', 'dialog shadow': 'bg:#00aa00', }) message_dialog( title=HTML('<style bg="blue" fg="white">Styled</style> ' '<style fg="ansired">dialog</style> window'), text='Do you want to continue?\nPress ENTER to quit.', style=example_style).run() .. image:: ../images/dialogs/styled.png Styling reference sheet ----------------------- In reality, the shortcut commands presented above build a full-screen frame by using a list of components. The two tables below allow you to get the classnames available for each shortcut, therefore you will be able to provide a custom style for every element that is displayed, using the method provided above. .. note:: All the shortcuts use the ``Dialog`` component, therefore it isn't specified explicitly below. +--------------------------+-------------------------+ | Shortcut | Components used | +==========================+=========================+ | ``yes_no_dialog`` | - ``Label`` | | | - ``Button`` (x2) | +--------------------------+-------------------------+ | ``button_dialog`` | - ``Label`` | | | - ``Button`` | +--------------------------+-------------------------+ | ``input_dialog`` | - ``TextArea`` | | | - ``Button`` (x2) | +--------------------------+-------------------------+ | ``message_dialog`` | - ``Label`` | | | - ``Button`` | +--------------------------+-------------------------+ | ``radiolist_dialog`` | - ``Label`` | | | - ``RadioList`` | | | - ``Button`` (x2) | +--------------------------+-------------------------+ | ``checkboxlist_dialog`` | - ``Label`` | | | - ``CheckboxList`` | | | - ``Button`` (x2) | +--------------------------+-------------------------+ | ``progress_dialog`` | - ``Label`` | | | - ``TextArea`` (locked) | | | - ``ProgressBar`` | +--------------------------+-------------------------+ +----------------+-----------------------------+ | Components | Available classnames | +================+=============================+ | Dialog | - ``dialog`` | | | - ``dialog.body`` | +----------------+-----------------------------+ | TextArea | - ``text-area`` | | | - ``text-area.prompt`` | +----------------+-----------------------------+ | Label | - ``label`` | +----------------+-----------------------------+ | Button | - ``button`` | | | - ``button.focused`` | | | - ``button.arrow`` | | | - ``button.text`` | +----------------+-----------------------------+ | Frame | - ``frame`` | | | - ``frame.border`` | | | - ``frame.label`` | +----------------+-----------------------------+ | Shadow | - ``shadow`` | +----------------+-----------------------------+ | RadioList | - ``radio-list`` | | | - ``radio`` | | | - ``radio-checked`` | | | - ``radio-selected`` | +----------------+-----------------------------+ | CheckboxList | - ``checkbox-list`` | | | - ``checkbox`` | | | - ``checkbox-checked`` | | | - ``checkbox-selected`` | +----------------+-----------------------------+ | VerticalLine | - ``line`` | | | - ``vertical-line`` | +----------------+-----------------------------+ | HorizontalLine | - ``line`` | | | - ``horizontal-line`` | +----------------+-----------------------------+ | ProgressBar | - ``progress-bar`` | | | - ``progress-bar.used`` | +----------------+-----------------------------+ Example _______ Let's customize the example of the ``checkboxlist_dialog``. It uses 2 ``Button``, a ``CheckboxList`` and a ``Label``, packed inside a ``Dialog``. Therefore we can customize each of these elements separately, using for instance: .. code:: python from prompt_toolkit.shortcuts import checkboxlist_dialog from prompt_toolkit.styles import Style results = checkboxlist_dialog( title="CheckboxList dialog", text="What would you like in your breakfast ?", values=[ ("eggs", "Eggs"), ("bacon", "Bacon"), ("croissants", "20 Croissants"), ("daily", "The breakfast of the day") ], style=Style.from_dict({ 'dialog': 'bg:#cdbbb3', 'button': 'bg:#bf99a4', 'checkbox': '#e8612c', 'dialog.body': 'bg:#a9cfd0', 'dialog shadow': 'bg:#c98982', 'frame.label': '#fcaca3', 'dialog.body label': '#fd8bb6', }) ).run() ================================================ FILE: docs/pages/full_screen_apps.rst ================================================ .. _full_screen_applications: Building full screen applications ================================= `prompt_toolkit` can be used to create complex full screen terminal applications. Typically, an application consists of a layout (to describe the graphical part) and a set of key bindings. The sections below describe the components required for full screen applications (or custom, non full screen applications), and how to assemble them together. Before going through this page, it could be helpful to go through :ref:`asking for input <asking_for_input>` (prompts) first. Many things that apply to an input prompt, like styling, key bindings and so on, also apply to full screen applications. .. note:: Also remember that the ``examples`` directory of the prompt_toolkit repository contains plenty of examples. Each example is supposed to explain one idea. So, this as well should help you get started. Don't hesitate to open a GitHub issue if you feel that a certain example is missing. A simple application -------------------- Every prompt_toolkit application is an instance of an :class:`~prompt_toolkit.application.Application` object. The simplest full screen example would look like this: .. code:: python from prompt_toolkit import Application app = Application(full_screen=True) app.run() This will display a dummy application that says "No layout specified. Press ENTER to quit.". .. note:: If we wouldn't set the ``full_screen`` option, the application would not run in the alternate screen buffer, and only consume the least amount of space required for the layout. An application consists of several components. The most important are: - I/O objects: the input and output device. - The layout: this defines the graphical structure of the application. For instance, a text box on the left side, and a button on the right side. You can also think of the layout as a collection of 'widgets'. - A style: this defines what colors and underline/bold/italic styles are used everywhere. - A set of key bindings. We will discuss all of these in more detail below. I/O objects ----------- Every :class:`~prompt_toolkit.application.Application` instance requires an I/O object for input and output: - An :class:`~prompt_toolkit.input.Input` instance, which is an abstraction of the input stream (stdin). - An :class:`~prompt_toolkit.output.Output` instance, which is an abstraction of the output stream, and is called by the renderer. Both are optional and normally not needed to pass explicitly. Usually, the default works fine. There is a third I/O object which is also required by the application, but not passed inside. This is the event loop, an :class:`~prompt_toolkit.eventloop` instance. This is basically a while-true loop that waits for user input, and when it receives something (like a key press), it will send that to the the appropriate handler, like for instance, a key binding. When :func:`~prompt_toolkit.application.Application.run()` is called, the event loop will run until the application is done. An application will quit when :func:`~prompt_toolkit.application.Application.exit()` is called. The layout ---------- A layered layout architecture ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There are several ways to create a prompt_toolkit layout, depending on how customizable you want things to be. In fact, there are several layers of abstraction. - The most low-level way of creating a layout is by combining :class:`~prompt_toolkit.layout.Container` and :class:`~prompt_toolkit.layout.UIControl` objects. Examples of :class:`~prompt_toolkit.layout.Container` objects are :class:`~prompt_toolkit.layout.VSplit` (vertical split), :class:`~prompt_toolkit.layout.HSplit` (horizontal split) and :class:`~prompt_toolkit.layout.FloatContainer`. These containers arrange the layout and can split it in multiple regions. Each container can recursively contain multiple other containers. They can be combined in any way to define the "shape" of the layout. The :class:`~prompt_toolkit.layout.Window` object is a special kind of container that can contain a :class:`~prompt_toolkit.layout.UIControl` object. The :class:`~prompt_toolkit.layout.UIControl` object is responsible for the generation of the actual content. The :class:`~prompt_toolkit.layout.Window` object acts as an adaptor between the :class:`~prompt_toolkit.layout.UIControl` and other containers, but it's also responsible for the scrolling and line wrapping of the content. Examples of :class:`~prompt_toolkit.layout.UIControl` objects are :class:`~prompt_toolkit.layout.BufferControl` for showing the content of an editable/scrollable buffer, and :class:`~prompt_toolkit.layout.FormattedTextControl` for displaying (:ref:`formatted <formatted_text>`) text. Normally, it is never needed to create new :class:`~prompt_toolkit.layout.UIControl` or :class:`~prompt_toolkit.layout.Container` classes, but instead you would create the layout by composing instances of the existing built-ins. - A higher level abstraction of building a layout is by using "widgets". A widget is a reusable layout component that can contain multiple containers and controls. Widgets have a ``__pt_container__`` function, which returns the root container for this widget. Prompt_toolkit contains a couple of widgets like :class:`~prompt_toolkit.widgets.TextArea`, :class:`~prompt_toolkit.widgets.Button`, :class:`~prompt_toolkit.widgets.Frame`, :class:`~prompt_toolkit.widgets.VerticalLine` and so on. - The highest level abstractions can be found in the ``shortcuts`` module. There we don't have to think about the layout, controls and containers at all. This is the simplest way to use prompt_toolkit, but is only meant for specific use cases, like a prompt or a simple dialog window. Containers and controls ^^^^^^^^^^^^^^^^^^^^^^^ The biggest difference between containers and controls is that containers arrange the layout by splitting the screen in many regions, while controls are responsible for generating the actual content. .. note:: Under the hood, the difference is: - containers use *absolute coordinates*, and paint on a :class:`~prompt_toolkit.layout.screen.Screen` instance. - user controls create a :class:`~prompt_toolkit.layout.controls.UIContent` instance. This is a collection of lines that represent the actual content. A :class:`~prompt_toolkit.layout.controls.UIControl` is not aware of the screen. +---------------------------------------------+------------------------------------------------------+ | Abstract base class | Examples | +=============================================+======================================================+ | :class:`~prompt_toolkit.layout.Container` | :class:`~prompt_toolkit.layout.HSplit` | | | :class:`~prompt_toolkit.layout.VSplit` | | | :class:`~prompt_toolkit.layout.FloatContainer` | | | :class:`~prompt_toolkit.layout.Window` | | | :class:`~prompt_toolkit.layout.ScrollablePane` | +---------------------------------------------+------------------------------------------------------+ | :class:`~prompt_toolkit.layout.UIControl` | :class:`~prompt_toolkit.layout.BufferControl` | | | :class:`~prompt_toolkit.layout.FormattedTextControl` | +---------------------------------------------+------------------------------------------------------+ The :class:`~prompt_toolkit.layout.Window` class itself is particular: it is a :class:`~prompt_toolkit.layout.Container` that can contain a :class:`~prompt_toolkit.layout.UIControl`. Thus, it's the adaptor between the two. The :class:`~prompt_toolkit.layout.Window` class also takes care of scrolling the content and wrapping the lines if needed. Finally, there is the :class:`~prompt_toolkit.layout.Layout` class which wraps the whole layout. This is responsible for keeping track of which window has the focus. Here is an example of a layout that displays the content of the default buffer on the left, and displays ``"Hello world"`` on the right. In between it shows a vertical line: .. code:: python from prompt_toolkit import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.layout.containers import VSplit, Window from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout buffer1 = Buffer() # Editable buffer. root_container = VSplit([ # One window that holds the BufferControl with the default buffer on # the left. Window(content=BufferControl(buffer=buffer1)), # A vertical line in the middle. We explicitly specify the width, to # make sure that the layout engine will not try to divide the whole # width by three for all these windows. The window will simply fill its # content by repeating this character. Window(width=1, char='|'), # Display the text 'Hello world' on the right. Window(content=FormattedTextControl(text='Hello world')), ]) layout = Layout(root_container) app = Application(layout=layout, full_screen=True) app.run() # You won't be able to Exit this app Notice that if you execute this right now, there is no way to quit this application yet. This is something we explain in the next section below. More complex layouts can be achieved by nesting multiple :class:`~prompt_toolkit.layout.VSplit`, :class:`~prompt_toolkit.layout.HSplit` and :class:`~prompt_toolkit.layout.FloatContainer` objects. If you want to make some part of the layout only visible when a certain condition is satisfied, use a :class:`~prompt_toolkit.layout.ConditionalContainer`. Finally, there is :class:`~prompt_toolkit.layout.ScrollablePane`, a container class that can be used to create long forms or nested layouts that are scrollable as a whole. Focusing windows ^^^^^^^^^^^^^^^^^ Focusing something can be done by calling the :meth:`~prompt_toolkit.layout.Layout.focus` method. This method is very flexible and accepts a :class:`~prompt_toolkit.layout.Window`, a :class:`~prompt_toolkit.buffer.Buffer`, a :class:`~prompt_toolkit.layout.controls.UIControl` and more. In the following example, we use :func:`~prompt_toolkit.application.get_app` for getting the active application. .. code:: python from prompt_toolkit.application import get_app # This window was created earlier. w = Window() # ... # Now focus it. get_app().layout.focus(w) Changing the focus is something which is typically done in a key binding, so read on to see how to define key bindings. Key bindings ------------ In order to react to user actions, we need to create a :class:`~prompt_toolkit.key_binding.KeyBindings` object and pass that to our :class:`~prompt_toolkit.application.Application`. There are two kinds of key bindings: - Global key bindings, which are always active. - Key bindings that belong to a certain :class:`~prompt_toolkit.layout.controls.UIControl` and are only active when this control is focused. Both :class:`~prompt_toolkit.layout.BufferControl` :class:`~prompt_toolkit.layout.FormattedTextControl` take a ``key_bindings`` argument. Global key bindings ^^^^^^^^^^^^^^^^^^^ Key bindings can be passed to the application as follows: .. code:: python from prompt_toolkit import Application from prompt_toolkit.key_binding import KeyBindings kb = KeyBindings() app = Application(key_bindings=kb) app.run() To register a new keyboard shortcut, we can use the :meth:`~prompt_toolkit.key_binding.KeyBindings.add` method as a decorator of the key handler: .. code:: python from prompt_toolkit import Application from prompt_toolkit.key_binding import KeyBindings kb = KeyBindings() @kb.add('c-q') def exit_(event): """ Pressing Ctrl-Q will exit the user interface. Setting a return value means: quit the event loop that drives the user interface and return this value from the `Application.run()` call. """ event.app.exit() app = Application(key_bindings=kb, full_screen=True) app.run() The callback function is named ``exit_`` for clarity, but it could have been named ``_`` (underscore) as well, because we won't refer to this name. :ref:`Read more about key bindings ...<key_bindings>` Modal containers ^^^^^^^^^^^^^^^^ The following container objects take a ``modal`` argument :class:`~prompt_toolkit.layout.VSplit`, :class:`~prompt_toolkit.layout.HSplit`, and :class:`~prompt_toolkit.layout.FloatContainer`. Setting ``modal=True`` makes what is called a **modal** container. Normally, a child container would inherit its parent key bindings. This does not apply to **modal** containers. Consider a **modal** container (e.g. :class:`~prompt_toolkit.layout.VSplit`) is child of another container, its parent. Any key bindings from the parent are not taken into account if the **modal** container (child) has the focus. This is useful in a complex layout, where many controls have their own key bindings, but you only want to enable the key bindings for a certain region of the layout. The global key bindings are always active. More about the Window class --------------------------- As said earlier, a :class:`~prompt_toolkit.layout.Window` is a :class:`~prompt_toolkit.layout.Container` that wraps a :class:`~prompt_toolkit.layout.UIControl`, like a :class:`~prompt_toolkit.layout.BufferControl` or :class:`~prompt_toolkit.layout.FormattedTextControl`. .. note:: Basically, windows are the leaves in the tree structure that represent the UI. A :class:`~prompt_toolkit.layout.Window` provides a "view" on the :class:`~prompt_toolkit.layout.UIControl`, which provides lines of content. The window is in the first place responsible for the line wrapping and scrolling of the content, but there are much more options. - Adding left or right margins. These are used for displaying scroll bars or line numbers. - There are the `cursorline` and `cursorcolumn` options. These allow highlighting the line or column of the cursor position. - Alignment of the content. The content can be left aligned, right aligned or centered. - Finally, the background can be filled with a default character. More about buffers and `BufferControl` -------------------------------------- Input processors ^^^^^^^^^^^^^^^^ A :class:`~prompt_toolkit.layout.processors.Processor` is used to postprocess the content of a :class:`~prompt_toolkit.layout.BufferControl` before it's displayed. It can for instance highlight matching brackets or change the visualization of tabs and so on. A :class:`~prompt_toolkit.layout.processors.Processor` operates on individual lines. Basically, it takes a (formatted) line and produces a new (formatted) line. Some built-in processors: +----------------------------------------------------------------------------+-----------------------------------------------------------+ | Processor | Usage: | +============================================================================+===========================================================+ | :class:`~prompt_toolkit.layout.processors.HighlightSearchProcessor` | Highlight the current search results. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.HighlightSelectionProcessor` | Highlight the selection. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.PasswordProcessor` | Display input as asterisks. (``*`` characters). | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.BracketsMismatchProcessor` | Highlight open/close mismatches for brackets. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.BeforeInput` | Insert some text before. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.AfterInput` | Insert some text after. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.AppendAutoSuggestion` | Append auto suggestion text. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.ShowLeadingWhiteSpaceProcessor` | Visualize leading whitespace. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.ShowTrailingWhiteSpaceProcessor` | Visualize trailing whitespace. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ | :class:`~prompt_toolkit.layout.processors.TabsProcessor` | Visualize tabs as `n` spaces, or some symbols. | +----------------------------------------------------------------------------+-----------------------------------------------------------+ A :class:`~prompt_toolkit.layout.BufferControl` takes only one processor as input, but it is possible to "merge" multiple processors into one with the :func:`~prompt_toolkit.layout.processors.merge_processors` function. ================================================ FILE: docs/pages/gallery.rst ================================================ .. _gallery: Gallery ======= Showcase, demonstrating the possibilities of prompt_toolkit. Ptpython, a Python REPL ^^^^^^^^^^^^^^^^^^^^^^^ The prompt: .. image:: ../images/ptpython.png The configuration menu of ptpython. .. image:: ../images/ptpython-menu.png The history page with its help. (This is a full-screen layout.) .. image:: ../images/ptpython-history-help.png Pyvim, a Vim clone ^^^^^^^^^^^^^^^^^^ .. image:: ../images/pyvim.png Pymux, a terminal multiplexer (like tmux) in Python ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. image:: ../images/pymux.png ================================================ FILE: docs/pages/getting_started.rst ================================================ .. _getting_started: Getting started =============== Installation ------------ :: pip install prompt_toolkit For Conda, do: :: conda install -c https://conda.anaconda.org/conda-forge prompt_toolkit Several use cases: prompts versus full screen terminal applications -------------------------------------------------------------------- `prompt_toolkit` was in the first place meant to be a replacement for readline. However, when it became more mature, we realized that all the components for full screen applications are there and `prompt_toolkit` is very capable of handling many use situations. `Pyvim <http://github.com/prompt-toolkit/pyvim>`_ and `pymux <http://github.com/prompt-toolkit/pymux>`_ are examples of full screen applications. .. image:: ../images/pyvim.png Basically, at the core, `prompt_toolkit` has a layout engine, that supports horizontal and vertical splits as well as floats, where each "window" can display a user control. The API for user controls is simple yet powerful. When `prompt_toolkit` is used as a readline replacement, (to simply read some input from the user), it uses a rather simple built-in layout. One that displays the default input buffer and the prompt, a float for the autocompletions and a toolbar for input validation which is hidden by default. For full screen applications, usually we build a custom layout ourselves. Further, there is a very flexible key binding system that can be programmed for all the needs of full screen applications. A simple prompt --------------- The following snippet is the most simple example, it uses the :func:`~prompt_toolkit.shortcuts.prompt` function to asks the user for input and returns the text. Just like ``(raw_)input``. .. code:: python from prompt_toolkit import prompt text = prompt("Give me some input: ") print(f"You said: {text}") Learning `prompt_toolkit` ------------------------- In order to learn and understand `prompt_toolkit`, it is best to go through the all sections in the order below. Also don't forget to have a look at all the `examples <https://github.com/prompt-toolkit/python-prompt-toolkit/tree/master/examples>`_ in the repository. - First, :ref:`learn how to print text <printing_text>`. This is important, because it covers how to use "formatted text", which is something you'll use whenever you want to use colors anywhere. - Secondly, go through the :ref:`asking for input <asking_for_input>` section. This is useful for almost any use case, even for full screen applications. It covers autocompletions, syntax highlighting, key bindings, and so on. - Then, learn about :ref:`dialogs`, which is easy and fun. - Finally, learn about :ref:`full screen applications <full_screen_applications>` and read through :ref:`the advanced topics <advanced_topics>`. ================================================ FILE: docs/pages/printing_text.rst ================================================ .. _printing_text: Printing (and using) formatted text =================================== Prompt_toolkit ships with a :func:`~prompt_toolkit.shortcuts.print_formatted_text` function that's meant to be (as much as possible) compatible with the built-in print function, but on top of that, also supports colors and formatting. On Linux systems, this will output VT100 escape sequences, while on Windows it will use Win32 API calls or VT100 sequences, depending on what is available. .. note:: This page is also useful if you'd like to learn how to use formatting in other places, like in a prompt or a toolbar. Just like :func:`~prompt_toolkit.shortcuts.print_formatted_text` takes any kind of "formatted text" as input, prompts and toolbars also accept "formatted text". Printing plain text ------------------- The print function can be imported as follows: .. code:: python from prompt_toolkit import print_formatted_text print_formatted_text('Hello world') You can replace the built in ``print`` function as follows, if you want to. .. code:: python from prompt_toolkit import print_formatted_text as print print('Hello world') .. note:: If you're using Python 2, make sure to add ``from __future__ import print_function``. Otherwise, it will not be possible to import a function named ``print``. .. _formatted_text: Formatted text -------------- There are several ways to display colors: - By creating an :class:`~prompt_toolkit.formatted_text.HTML` object. - By creating an :class:`~prompt_toolkit.formatted_text.ANSI` object that contains ANSI escape sequences. - By creating a list of ``(style, text)`` tuples. - By creating a list of ``(pygments.Token, text)`` tuples, and wrapping it in :class:`~prompt_toolkit.formatted_text.PygmentsTokens`. An instance of any of these four kinds of objects is called "formatted text". There are various places in prompt toolkit, where we accept not just plain text (as a string), but also formatted text. HTML ^^^^ :class:`~prompt_toolkit.formatted_text.HTML` can be used to indicate that a string contains HTML-like formatting. It recognizes the basic tags for bold, italic and underline: ``<b>``, ``<i>`` and ``<u>``. .. code:: python from prompt_toolkit import print_formatted_text, HTML print_formatted_text(HTML('<b>This is bold</b>')) print_formatted_text(HTML('<i>This is italic</i>')) print_formatted_text(HTML('<u>This is underlined</u>')) Further, it's possible to use tags for foreground colors: .. code:: python # Colors from the ANSI palette. print_formatted_text(HTML('<ansired>This is red</ansired>')) print_formatted_text(HTML('<ansigreen>This is green</ansigreen>')) # Named colors (256 color palette, or true color, depending on the output). print_formatted_text(HTML('<skyblue>This is sky blue</skyblue>')) print_formatted_text(HTML('<seagreen>This is sea green</seagreen>')) print_formatted_text(HTML('<violet>This is violet</violet>')) Both foreground and background colors can also be specified setting the `fg` and `bg` attributes of any HTML tag: .. code:: python # Colors from the ANSI palette. print_formatted_text(HTML('<aaa fg="ansiwhite" bg="ansigreen">White on green</aaa>')) Underneath, all HTML tags are mapped to classes from a stylesheet, so you can assign a style for a custom tag. .. code:: python from prompt_toolkit import print_formatted_text, HTML from prompt_toolkit.styles import Style style = Style.from_dict({ 'aaa': '#ff0066', 'bbb': '#44ff00 italic', }) print_formatted_text(HTML('<aaa>Hello</aaa> <bbb>world</bbb>!'), style=style) ANSI ^^^^ Some people like to use the VT100 ANSI escape sequences to generate output. Natively, this is however only supported on VT100 terminals, but prompt_toolkit can parse these, and map them to formatted text instances. This means that they will work on Windows as well. The :class:`~prompt_toolkit.formatted_text.ANSI` class takes care of that. .. code:: python from prompt_toolkit import print_formatted_text, ANSI print_formatted_text(ANSI('\x1b[31mhello \x1b[32mworld')) Keep in mind that even on a Linux VT100 terminal, the final output produced by prompt_toolkit, is not necessarily exactly the same. Depending on the color depth, it is possible that colors are mapped to different colors, and unknown tags will be removed. (style, text) tuples ^^^^^^^^^^^^^^^^^^^^ Internally, both :class:`~prompt_toolkit.formatted_text.HTML` and :class:`~prompt_toolkit.formatted_text.ANSI` objects are mapped to a list of ``(style, text)`` tuples. It is however also possible to create such a list manually with :class:`~prompt_toolkit.formatted_text.FormattedText` class. This is a little more verbose, but it's probably the most powerful way of expressing formatted text. .. code:: python from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import FormattedText text = FormattedText([ ('#ff0066', 'Hello'), ('', ' '), ('#44ff00 italic', 'World'), ]) print_formatted_text(text) Similar to the :class:`~prompt_toolkit.formatted_text.HTML` example, it is also possible to use class names, and separate the styling in a style sheet. .. code:: python from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.styles import Style # The text. text = FormattedText([ ('class:aaa', 'Hello'), ('', ' '), ('class:bbb', 'World'), ]) # The style sheet. style = Style.from_dict({ 'aaa': '#ff0066', 'bbb': '#44ff00 italic', }) print_formatted_text(text, style=style) Pygments ``(Token, text)`` tuples ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When you have a list of `Pygments <http://pygments.org/>`_ ``(Token, text)`` tuples, then these can be printed by wrapping them in a :class:`~prompt_toolkit.formatted_text.PygmentsTokens` object. .. code:: python from pygments.token import Token from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import PygmentsTokens text = [ (Token.Keyword, 'print'), (Token.Punctuation, '('), (Token.Literal.String.Double, '"'), (Token.Literal.String.Double, 'hello'), (Token.Literal.String.Double, '"'), (Token.Punctuation, ')'), (Token.Text, '\n'), ] print_formatted_text(PygmentsTokens(text)) Similarly, it is also possible to print the output of a Pygments lexer: .. code:: python import pygments from pygments.token import Token from pygments.lexers.python import PythonLexer from prompt_toolkit.formatted_text import PygmentsTokens from prompt_toolkit import print_formatted_text # Printing the output of a pygments lexer. tokens = list(pygments.lex('print("Hello")', lexer=PythonLexer())) print_formatted_text(PygmentsTokens(tokens)) Prompt_toolkit ships with a default colorscheme which styles it just like Pygments would do, but if you'd like to change the colors, keep in mind that Pygments tokens map to classnames like this: +-----------------------------------+---------------------------------------------+ | pygments.Token | prompt_toolkit classname | +===================================+=============================================+ | - ``Token.Keyword`` | - ``"class:pygments.keyword"`` | | - ``Token.Punctuation`` | - ``"class:pygments.punctuation"`` | | - ``Token.Literal.String.Double`` | - ``"class:pygments.literal.string.double"``| | - ``Token.Text`` | - ``"class:pygments.text"`` | | - ``Token`` | - ``"class:pygments"`` | +-----------------------------------+---------------------------------------------+ A classname like ``pygments.literal.string.double`` is actually decomposed in the following four classnames: ``pygments``, ``pygments.literal``, ``pygments.literal.string`` and ``pygments.literal.string.double``. The final style is computed by combining the style for these four classnames. So, changing the style from these Pygments tokens can be done as follows: .. code:: python from prompt_toolkit.styles import Style style = Style.from_dict({ 'pygments.keyword': 'underline', 'pygments.literal.string': 'bg:#00ff00 #ffffff', }) print_formatted_text(PygmentsTokens(tokens), style=style) to_formatted_text ^^^^^^^^^^^^^^^^^ A useful function to know about is :func:`~prompt_toolkit.formatted_text.to_formatted_text`. This ensures that the given input is valid formatted text. While doing so, an additional style can be applied as well. .. code:: python from prompt_toolkit.formatted_text import to_formatted_text, HTML from prompt_toolkit import print_formatted_text html = HTML('<aaa>Hello</aaa> <bbb>world</bbb>!') text = to_formatted_text(html, style='class:my_html bg:#00ff00 italic') print_formatted_text(text) ================================================ FILE: docs/pages/progress_bars.rst ================================================ .. _progress_bars: Progress bars ============= Prompt_toolkit ships with a high level API for displaying progress bars, inspired by `tqdm <https://github.com/tqdm/tqdm>`_ .. warning:: The API for the prompt_toolkit progress bars is still very new and can possibly change in the future. It is usable and tested, but keep this in mind when upgrading. Remember that the `examples directory <https://github.com/prompt-toolkit/python-prompt-toolkit/tree/master/examples>`_ of the prompt_toolkit repository ships with many progress bar examples as well. Simple progress bar ------------------- Creating a new progress bar can be done by calling the :class:`~prompt_toolkit.shortcuts.ProgressBar` context manager. The progress can be displayed for any iterable. This works by wrapping the iterable (like ``range``) with the :class:`~prompt_toolkit.shortcuts.ProgressBar` context manager itself. This way, the progress bar knows when the next item is consumed by the forloop and when progress happens. .. code:: python from prompt_toolkit.shortcuts import ProgressBar import time with ProgressBar() as pb: for i in pb(range(800)): time.sleep(.01) .. image:: ../images/progress-bars/simple-progress-bar.png Keep in mind that not all iterables can report their total length. This happens with a typical generator. In that case, you can still pass the total as follows in order to make displaying the progress possible: .. code:: python def some_iterable(): yield ... with ProgressBar() as pb: for i in pb(some_iterable(), total=1000): time.sleep(.01) Multiple parallel tasks ----------------------- A prompt_toolkit :class:`~prompt_toolkit.shortcuts.ProgressBar` can display the progress of multiple tasks running in parallel. Each task can run in a separate thread and the :class:`~prompt_toolkit.shortcuts.ProgressBar` user interface runs in its own thread. Notice that we set the "daemon" flag for both threads that run the tasks. This is because control-c will stop the progress and quit our application. We don't want the application to wait for the background threads to finish. Whether you want this depends on the application. .. code:: python from prompt_toolkit.shortcuts import ProgressBar import time import threading with ProgressBar() as pb: # Two parallel tasks. def task_1(): for i in pb(range(100)): time.sleep(.05) def task_2(): for i in pb(range(150)): time.sleep(.08) # Start threads. t1 = threading.Thread(target=task_1) t2 = threading.Thread(target=task_2) t1.daemon = True t2.daemon = True t1.start() t2.start() # Wait for the threads to finish. We use a timeout for the join() call, # because on Windows, join cannot be interrupted by Control-C or any other # signal. for t in [t1, t2]: while t.is_alive(): t.join(timeout=.5) .. image:: ../images/progress-bars/two-tasks.png Adding a title and label ------------------------ Each progress bar can have one title, and for each task an individual label. Both the title and the labels can be :ref:`formatted text <formatted_text>`. .. code:: python from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.formatted_text import HTML import time title = HTML('Downloading <style bg="yellow" fg="black">4 files...</style>') label = HTML('<ansired>some file</ansired>: ') with ProgressBar(title=title) as pb: for i in pb(range(800), label=label): time.sleep(.01) .. image:: ../images/progress-bars/colored-title-and-label.png Formatting the progress bar --------------------------- The visualization of a :class:`~prompt_toolkit.shortcuts.ProgressBar` can be customized by using a different sequence of formatters. The default formatting looks something like this: .. code:: python from prompt_toolkit.shortcuts.progress_bar.formatters import * default_formatting = [ Label(), Text(' '), Percentage(), Text(' '), Bar(), Text(' '), Progress(), Text(' '), Text('eta [', style='class:time-left'), TimeLeft(), Text(']', style='class:time-left'), Text(' '), ] That sequence of :class:`~prompt_toolkit.shortcuts.progress_bar.formatters.Formatter` can be passed to the `formatter` argument of :class:`~prompt_toolkit.shortcuts.ProgressBar`. So, we could change this and modify the progress bar to look like an apt-get style progress bar: .. code:: python from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.styles import Style from prompt_toolkit.shortcuts.progress_bar import formatters import time style = Style.from_dict({ 'label': 'bg:#ffff00 #000000', 'percentage': 'bg:#ffff00 #000000', 'current': '#448844', 'bar': '', }) custom_formatters = [ formatters.Label(), formatters.Text(': [', style='class:percentage'), formatters.Percentage(), formatters.Text(']', style='class:percentage'), formatters.Text(' '), formatters.Bar(sym_a='#', sym_b='#', sym_c='.'), formatters.Text(' '), ] with ProgressBar(style=style, formatters=custom_formatters) as pb: for i in pb(range(1600), label='Installing'): time.sleep(.01) .. image:: ../images/progress-bars/apt-get.png Adding key bindings and toolbar ------------------------------- Like other prompt_toolkit applications, we can add custom key bindings, by passing a :class:`~prompt_toolkit.key_binding.KeyBindings` object: .. code:: python from prompt_toolkit import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.shortcuts import ProgressBar import os import time import signal bottom_toolbar = HTML(' <b>[f]</b> Print "f" <b>[x]</b> Abort.') # Create custom key bindings first. kb = KeyBindings() cancel = [False] @kb.add('f') def _(event): print('You pressed `f`.') @kb.add('x') def _(event): " Send Abort (control-c) signal. " cancel[0] = True os.kill(os.getpid(), signal.SIGINT) # Use `patch_stdout`, to make sure that prints go above the # application. with patch_stdout(): with ProgressBar(key_bindings=kb, bottom_toolbar=bottom_toolbar) as pb: for i in pb(range(800)): time.sleep(.01) # Stop when the cancel flag has been set. if cancel[0]: break Notice that we use :func:`~prompt_toolkit.patch_stdout.patch_stdout` to make printing text possible while the progress bar is displayed. This ensures that printing happens above the progress bar. Further, when "x" is pressed, we set a cancel flag, which stops the progress. It would also be possible to send `SIGINT` to the main thread, but that's not always considered a clean way of cancelling something. In the example above, we also display a toolbar at the bottom which shows the key bindings. .. image:: ../images/progress-bars/custom-key-bindings.png :ref:`Read more about key bindings ...<key_bindings>` ================================================ FILE: docs/pages/reference.rst ================================================ Reference ========= Application ----------- .. automodule:: prompt_toolkit.application :members: Application, get_app, get_app_or_none, set_app, create_app_session, AppSession, get_app_session, DummyApplication, in_terminal, run_in_terminal, Formatted text -------------- .. automodule:: prompt_toolkit.formatted_text :members: Buffer ------ .. automodule:: prompt_toolkit.buffer :members: Selection --------- .. automodule:: prompt_toolkit.selection :members: Clipboard --------- .. automodule:: prompt_toolkit.clipboard :members: Clipboard, ClipboardData, DummyClipboard, DynamicClipboard, InMemoryClipboard .. automodule:: prompt_toolkit.clipboard.pyperclip :members: Auto completion --------------- .. automodule:: prompt_toolkit.completion :members: Document -------- .. automodule:: prompt_toolkit.document :members: Enums ----- .. automodule:: prompt_toolkit.enums :members: History ------- .. automodule:: prompt_toolkit.history :members: Keys ---- .. automodule:: prompt_toolkit.keys :members: Style ----- .. automodule:: prompt_toolkit.styles :members: Attrs, ANSI_COLOR_NAMES, BaseStyle, DummyStyle, DynamicStyle, Style, Priority, merge_styles, style_from_pygments_cls, style_from_pygments_dict, pygments_token_to_classname, NAMED_COLORS, StyleTransformation, SwapLightAndDarkStyleTransformation, AdjustBrightnessStyleTransformation, merge_style_transformations, DummyStyleTransformation, ConditionalStyleTransformation, DynamicStyleTransformation Shortcuts --------- .. automodule:: prompt_toolkit.shortcuts :members: prompt, PromptSession, confirm, CompleteStyle, create_confirm_session, clear, clear_title, print_formatted_text, set_title, ProgressBar, input_dialog, message_dialog, progress_dialog, radiolist_dialog, yes_no_dialog, button_dialog, choice .. automodule:: prompt_toolkit.shortcuts.progress_bar.formatters :members: Validation ---------- .. automodule:: prompt_toolkit.validation :members: Auto suggestion --------------- .. automodule:: prompt_toolkit.auto_suggest :members: Renderer -------- .. automodule:: prompt_toolkit.renderer :members: Lexers ------ .. automodule:: prompt_toolkit.lexers :members: Layout ------ .. automodule:: prompt_toolkit.layout The layout class itself ^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: prompt_toolkit.layout.Layout :members: .. autoclass:: prompt_toolkit.layout.InvalidLayoutError :members: .. autoclass:: prompt_toolkit.layout.walk :members: Containers ^^^^^^^^^^ .. autoclass:: prompt_toolkit.layout.Container :members: .. autoclass:: prompt_toolkit.layout.HSplit :members: .. autoclass:: prompt_toolkit.layout.VSplit :members: .. autoclass:: prompt_toolkit.layout.FloatContainer :members: .. autoclass:: prompt_toolkit.layout.Float :members: .. autoclass:: prompt_toolkit.layout.Window :members: .. autoclass:: prompt_toolkit.layout.WindowAlign :members: .. autoclass:: prompt_toolkit.layout.ConditionalContainer :members: .. autoclass:: prompt_toolkit.layout.DynamicContainer :members: .. autoclass:: prompt_toolkit.layout.ScrollablePane :members: .. autoclass:: prompt_toolkit.layout.ScrollOffsets :members: .. autoclass:: prompt_toolkit.layout.ColorColumn :members: .. autoclass:: prompt_toolkit.layout.to_container :members: .. autoclass:: prompt_toolkit.layout.to_window :members: .. autoclass:: prompt_toolkit.layout.is_container :members: .. autoclass:: prompt_toolkit.layout.HorizontalAlign :members: .. autoclass:: prompt_toolkit.layout.VerticalAlign :members: Controls ^^^^^^^^ .. autoclass:: prompt_toolkit.layout.BufferControl :members: .. autoclass:: prompt_toolkit.layout.SearchBufferControl :members: .. autoclass:: prompt_toolkit.layout.DummyControl :members: .. autoclass:: prompt_toolkit.layout.FormattedTextControl :members: .. autoclass:: prompt_toolkit.layout.UIControl :members: .. autoclass:: prompt_toolkit.layout.UIContent :members: Other ^^^^^ Sizing """""" .. autoclass:: prompt_toolkit.layout.Dimension :members: Margins """"""" .. autoclass:: prompt_toolkit.layout.Margin :members: .. autoclass:: prompt_toolkit.layout.NumberedMargin :members: .. autoclass:: prompt_toolkit.layout.ScrollbarMargin :members: .. autoclass:: prompt_toolkit.layout.ConditionalMargin :members: .. autoclass:: prompt_toolkit.layout.PromptMargin :members: Completion Menus """""""""""""""" .. autoclass:: prompt_toolkit.layout.CompletionsMenu :members: .. autoclass:: prompt_toolkit.layout.MultiColumnCompletionsMenu :members: Processors """""""""" .. automodule:: prompt_toolkit.layout.processors :members: Utils """"" .. automodule:: prompt_toolkit.layout.utils :members: Screen """""" .. automodule:: prompt_toolkit.layout.screen :members: Widgets ------- .. automodule:: prompt_toolkit.widgets :members: TextArea, Label, Button, Frame, Shadow, Box, VerticalLine, HorizontalLine, RadioList, Checkbox, ProgressBar, CompletionsToolbar, FormattedTextToolbar, SearchToolbar, SystemToolbar, ValidationToolbar, MenuContainer, MenuItem Filters ------- .. automodule:: prompt_toolkit.filters :members: .. autoclass:: prompt_toolkit.filters.Filter :members: :no-index: .. autoclass:: prompt_toolkit.filters.Condition :members: :no-index: .. automodule:: prompt_toolkit.filters.utils :members: .. automodule:: prompt_toolkit.filters.app :members: Key binding ----------- .. automodule:: prompt_toolkit.key_binding :members: KeyBindingsBase, KeyBindings, ConditionalKeyBindings, merge_key_bindings, DynamicKeyBindings .. automodule:: prompt_toolkit.key_binding.defaults :members: .. automodule:: prompt_toolkit.key_binding.vi_state :members: .. automodule:: prompt_toolkit.key_binding.key_processor :members: Eventloop --------- .. automodule:: prompt_toolkit.eventloop :members: run_in_executor_with_context, call_soon_threadsafe, get_traceback_from_context .. automodule:: prompt_toolkit.eventloop.inputhook :members: .. automodule:: prompt_toolkit.eventloop.utils :members: Input ----- .. automodule:: prompt_toolkit.input :members: Input, DummyInput, create_input, create_pipe_input .. automodule:: prompt_toolkit.input.vt100 :members: .. automodule:: prompt_toolkit.input.vt100_parser :members: .. automodule:: prompt_toolkit.input.ansi_escape_sequences :members: .. automodule:: prompt_toolkit.input.win32 :members: Output ------ .. automodule:: prompt_toolkit.output :members: Output, DummyOutput, ColorDepth, create_output .. automodule:: prompt_toolkit.output.vt100 :members: .. automodule:: prompt_toolkit.output.win32 :members: Data structures --------------- .. autoclass:: prompt_toolkit.layout.WindowRenderInfo :members: .. autoclass:: prompt_toolkit.data_structures.Point :members: .. autoclass:: prompt_toolkit.data_structures.Size :members: Patch stdout ------------ .. automodule:: prompt_toolkit.patch_stdout :members: patch_stdout, StdoutProxy ================================================ FILE: docs/pages/related_projects.rst ================================================ .. _related_projects: Related projects ================ There are some other Python libraries that provide similar functionality that are also worth checking out: - `Urwid <http://urwid.org/>`_ - `Textual <https://textual.textualize.io/>`_ - `Rich <https://rich.readthedocs.io/>`_ ================================================ FILE: docs/pages/tutorials/index.rst ================================================ .. _tutorials: Tutorials ========= .. toctree:: :caption: Contents: :maxdepth: 1 repl ================================================ FILE: docs/pages/tutorials/repl.rst ================================================ .. _tutorial_repl: Tutorial: Build an SQLite REPL ============================== The aim of this tutorial is to build an interactive command line interface for an SQLite database using prompt_toolkit_. First, install the library using pip, if you haven't done this already. .. code:: pip install prompt_toolkit Read User Input --------------- Let's start accepting input using the :func:`~prompt_toolkit.shortcuts.prompt()` function. This will ask the user for input, and echo back whatever the user typed. We wrap it in a ``main()`` function as a good practice. .. code:: python from prompt_toolkit import prompt def main(): text = prompt('> ') print('You entered:', text) if __name__ == '__main__': main() .. image:: ../../images/repl/sqlite-1.png Loop The REPL ------------- Now we want to call the :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` method in a loop. In order to keep the history, the easiest way to do it is to use a :class:`~prompt_toolkit.shortcuts.PromptSession`. This uses an :class:`~prompt_toolkit.history.InMemoryHistory` underneath that keeps track of the history, so that if the user presses the up-arrow, they'll see the previous entries. The :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` method raises ``KeyboardInterrupt`` when ControlC has been pressed and ``EOFError`` when ControlD has been pressed. This is what people use for cancelling commands and exiting in a REPL. The try/except below handles these error conditions and make sure that we go to the next iteration of the loop or quit the loop respectively. .. code:: python from prompt_toolkit import PromptSession def main(): session = PromptSession() while True: try: text = session.prompt('> ') except KeyboardInterrupt: continue except EOFError: break else: print('You entered:', text) print('GoodBye!') if __name__ == '__main__': main() .. image:: ../../images/repl/sqlite-2.png Syntax Highlighting ------------------- This is where things get really interesting. Let's step it up a notch by adding syntax highlighting to the user input. We know that users will be entering SQL statements, so we can leverage the Pygments_ library for coloring the input. The ``lexer`` parameter allows us to set the syntax lexer. We're going to use the ``SqlLexer`` from the Pygments_ library for highlighting. Notice that in order to pass a Pygments lexer to prompt_toolkit, it needs to be wrapped into a :class:`~prompt_toolkit.lexers.PygmentsLexer`. .. code:: python from prompt_toolkit import PromptSession from prompt_toolkit.lexers import PygmentsLexer from pygments.lexers.sql import SqlLexer def main(): session = PromptSession(lexer=PygmentsLexer(SqlLexer)) while True: try: text = session.prompt('> ') except KeyboardInterrupt: continue except EOFError: break else: print('You entered:', text) print('GoodBye!') if __name__ == '__main__': main() .. image:: ../../images/repl/sqlite-3.png Auto-completion --------------- Now we are going to add auto completion. We'd like to display a drop down menu of `possible keywords <https://www.sqlite.org/lang_keywords.html>`_ when the user starts typing. We can do this by creating an `sql_completer` object from the :class:`~prompt_toolkit.completion.WordCompleter` class, defining a set of `keywords` for the auto-completion. Like the lexer, this ``sql_completer`` instance can be passed to either the :class:`~prompt_toolkit.shortcuts.PromptSession` class or the :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` method. .. code:: python from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit.lexers import PygmentsLexer from pygments.lexers.sql import SqlLexer sql_completer = WordCompleter([ 'abort', 'action', 'add', 'after', 'all', 'alter', 'analyze', 'and', 'as', 'asc', 'attach', 'autoincrement', 'before', 'begin', 'between', 'by', 'cascade', 'case', 'cast', 'check', 'collate', 'column', 'commit', 'conflict', 'constraint', 'create', 'cross', 'current_date', 'current_time', 'current_timestamp', 'database', 'default', 'deferrable', 'deferred', 'delete', 'desc', 'detach', 'distinct', 'drop', 'each', 'else', 'end', 'escape', 'except', 'exclusive', 'exists', 'explain', 'fail', 'for', 'foreign', 'from', 'full', 'glob', 'group', 'having', 'if', 'ignore', 'immediate', 'in', 'index', 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect', 'into', 'is', 'isnull', 'join', 'key', 'left', 'like', 'limit', 'match', 'natural', 'no', 'not', 'notnull', 'null', 'of', 'offset', 'on', 'or', 'order', 'outer', 'plan', 'pragma', 'primary', 'query', 'raise', 'recursive', 'references', 'regexp', 'reindex', 'release', 'rename', 'replace', 'restrict', 'right', 'rollback', 'row', 'savepoint', 'select', 'set', 'table', 'temp', 'temporary', 'then', 'to', 'transaction', 'trigger', 'union', 'unique', 'update', 'using', 'vacuum', 'values', 'view', 'virtual', 'when', 'where', 'with', 'without'], ignore_case=True) def main(): session = PromptSession( lexer=PygmentsLexer(SqlLexer), completer=sql_completer) while True: try: text = session.prompt('> ') except KeyboardInterrupt: continue except EOFError: break else: print('You entered:', text) print('GoodBye!') if __name__ == '__main__': main() .. image:: ../../images/repl/sqlite-4.png In about 30 lines of code we got ourselves an auto completing, syntax highlighting REPL. Let's make it even better. Styling the menus ----------------- If we want, we can now change the colors of the completion menu. This is possible by creating a :class:`~prompt_toolkit.styles.Style` instance and passing it to the :meth:`~prompt_toolkit.shortcuts.PromptSession.prompt` function. .. code:: python from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles import Style from pygments.lexers.sql import SqlLexer sql_completer = WordCompleter([ 'abort', 'action', 'add', 'after', 'all', 'alter', 'analyze', 'and', 'as', 'asc', 'attach', 'autoincrement', 'before', 'begin', 'between', 'by', 'cascade', 'case', 'cast', 'check', 'collate', 'column', 'commit', 'conflict', 'constraint', 'create', 'cross', 'current_date', 'current_time', 'current_timestamp', 'database', 'default', 'deferrable', 'deferred', 'delete', 'desc', 'detach', 'distinct', 'drop', 'each', 'else', 'end', 'escape', 'except', 'exclusive', 'exists', 'explain', 'fail', 'for', 'foreign', 'from', 'full', 'glob', 'group', 'having', 'if', 'ignore', 'immediate', 'in', 'index', 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect', 'into', 'is', 'isnull', 'join', 'key', 'left', 'like', 'limit', 'match', 'natural', 'no', 'not', 'notnull', 'null', 'of', 'offset', 'on', 'or', 'order', 'outer', 'plan', 'pragma', 'primary', 'query', 'raise', 'recursive', 'references', 'regexp', 'reindex', 'release', 'rename', 'replace', 'restrict', 'right', 'rollback', 'row', 'savepoint', 'select', 'set', 'table', 'temp', 'temporary', 'then', 'to', 'transaction', 'trigger', 'union', 'unique', 'update', 'using', 'vacuum', 'values', 'view', 'virtual', 'when', 'where', 'with', 'without'], ignore_case=True) style = Style.from_dict({ 'completion-menu.completion': 'bg:#008888 #ffffff', 'completion-menu.completion.current': 'bg:#00aaaa #000000', 'scrollbar.background': 'bg:#88aaaa', 'scrollbar.button': 'bg:#222222', }) def main(): session = PromptSession( lexer=PygmentsLexer(SqlLexer), completer=sql_completer, style=style) while True: try: text = session.prompt('> ') except KeyboardInterrupt: continue except EOFError: break else: print('You entered:', text) print('GoodBye!') if __name__ == '__main__': main() .. image:: ../../images/repl/sqlite-5.png All that's left is hooking up the sqlite backend, which is left as an exercise for the reader. Just kidding... Keep reading. Hook up Sqlite -------------- This step is the final step to make the SQLite REPL actually work. It's time to relay the input to SQLite. Obviously I haven't done the due diligence to deal with the errors. But it gives a good idea of how to get started. .. code:: python #!/usr/bin/env python import sys import sqlite3 from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles import Style from pygments.lexers.sql import SqlLexer sql_completer = WordCompleter([ 'abort', 'action', 'add', 'after', 'all', 'alter', 'analyze', 'and', 'as', 'asc', 'attach', 'autoincrement', 'before', 'begin', 'between', 'by', 'cascade', 'case', 'cast', 'check', 'collate', 'column', 'commit', 'conflict', 'constraint', 'create', 'cross', 'current_date', 'current_time', 'current_timestamp', 'database', 'default', 'deferrable', 'deferred', 'delete', 'desc', 'detach', 'distinct', 'drop', 'each', 'else', 'end', 'escape', 'except', 'exclusive', 'exists', 'explain', 'fail', 'for', 'foreign', 'from', 'full', 'glob', 'group', 'having', 'if', 'ignore', 'immediate', 'in', 'index', 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect', 'into', 'is', 'isnull', 'join', 'key', 'left', 'like', 'limit', 'match', 'natural', 'no', 'not', 'notnull', 'null', 'of', 'offset', 'on', 'or', 'order', 'outer', 'plan', 'pragma', 'primary', 'query', 'raise', 'recursive', 'references', 'regexp', 'reindex', 'release', 'rename', 'replace', 'restrict', 'right', 'rollback', 'row', 'savepoint', 'select', 'set', 'table', 'temp', 'temporary', 'then', 'to', 'transaction', 'trigger', 'union', 'unique', 'update', 'using', 'vacuum', 'values', 'view', 'virtual', 'when', 'where', 'with', 'without'], ignore_case=True) style = Style.from_dict({ 'completion-menu.completion': 'bg:#008888 #ffffff', 'completion-menu.completion.current': 'bg:#00aaaa #000000', 'scrollbar.background': 'bg:#88aaaa', 'scrollbar.button': 'bg:#222222', }) def main(database): connection = sqlite3.connect(database) session = PromptSession( lexer=PygmentsLexer(SqlLexer), completer=sql_completer, style=style) while True: try: text = session.prompt('> ') except KeyboardInterrupt: continue # Control-C pressed. Try again. except EOFError: break # Control-D pressed. with connection: try: messages = connection.execute(text) except Exception as e: print(repr(e)) else: for message in messages: print(message) print('GoodBye!') if __name__ == '__main__': if len(sys.argv) < 2: db = ':memory:' else: db = sys.argv[1] main(db) .. image:: ../../images/repl/sqlite-6.png I hope that gives an idea of how to get started on building command line interfaces. The End. .. _prompt_toolkit: https://github.com/prompt-toolkit/python-prompt-toolkit .. _Pygments: http://pygments.org/ ================================================ FILE: docs/pages/upgrading/2.0.rst ================================================ .. _upgrading_2_0: Upgrading to prompt_toolkit 2.0 =============================== Prompt_toolkit 2.0 is not compatible with 1.0, however you probably want to upgrade your applications. This page explains why we have these differences and how to upgrade. If you experience some difficulties or you feel that some information is missing from this page, don't hesitate to open a GitHub issue for help. Why all these breaking changes? ------------------------------- After more and more custom prompt_toolkit applications were developed, it became clear that prompt_toolkit 1.0 was not flexible enough for certain use cases. Mostly, the development of full screen applications was not really natural. All the important components, like the rendering, key bindings, input and output handling were present, but the API was in the first place designed for simple command line prompts. This was mostly notably in the following two places: - First, there was the focus which was always pointing to a :class:`~prompt_toolkit.buffer.Buffer` (or text input widget), but in full screen applications there are other widgets, like menus and buttons which can be focused. - And secondly, it was impossible to make reusable UI components. All the key bindings for the entire applications were stored together in one ``KeyBindings`` object, and similar, all :class:`~prompt_toolkit.buffer.Buffer` objects were stored together in one dictionary. This didn't work well. You want reusable components to define their own key bindings and everything. It's the idea of encapsulation. For simple prompts, the changes wouldn't be that invasive, but given that there would be some, I took the opportunity to fix a couple of other things. For instance: - In prompt_toolkit 1.0, we translated `\\r` into `\\n` during the input processing. This was not a good idea, because some people wanted to handle these keys individually. This makes sense if you keep in mind that they correspond to `Control-M` and `Control-J`. However, we couldn't fix this without breaking everyone's enter key, which happens to be the most important key in prompts. Given that we were going to break compatibility anyway, we changed a couple of other important things that effect both simple prompt applications and full screen applications. These are the most important: - We no longer depend on Pygments for styling. While we like Pygments, it was not flexible enough to provide all the styling options that we need, and the Pygments tokens were not ideal for styling anything besides tokenized text. Instead we created something similar to CSS. All UI components can attach classnames to themselves, as well as define an inline style. The final style is then computed by combining the inline styles, the classnames and the style sheet. There are still adaptors available for using Pygments lexers as well as for Pygments styles. - The way that key bindings were defined was too complex. ``KeyBindingsManager`` was too complex and no longer exists. Every set of key bindings is now a :class:`~prompt_toolkit.key_binding.KeyBindings` object and multiple of these can be merged together at any time. The runtime performance remains the same, but it's now easier for users. - The separation between the ``CommandLineInterface`` and :class:`~prompt_toolkit.application.Application` class was confusing and in the end, didn't really had an advantage. These two are now merged together in one :class:`~prompt_toolkit.application.Application` class. - We no longer pass around the active ``CommandLineInterface``. This was one of the most annoying things. Key bindings need it in order to change anything and filters need it in order to evaluate their state. It was pretty annoying, especially because there was usually only one application active at a time. So, :class:`~prompt_toolkit.application.Application` became a ``TaskLocal``. That is like a global variable, but scoped in the current coroutine or context. The way this works is still not 100% correct, but good enough for the projects that need it (like Pymux), and hopefully Python will get support for this in the future thanks to PEP521, PEP550 or PEP555. All of these changes have been tested for many months, and I can say with confidence that prompt_toolkit 2.0 is a better prompt_toolkit. Some new features ----------------- Apart from the breaking changes above, there are also some exciting new features. - We now support vt100 escape codes for Windows consoles on Windows 10. This means much faster rendering, and full color support. - We have a concept of formatted text. This is an object that evaluates to styled text. Every input that expects some text, like the message in a prompt, or the text in a toolbar, can take any kind of formatted text as input. This means you can pass in a plain string, but also a list of `(style, text)` tuples (similar to a Pygments tokenized string), or an :class:`~prompt_toolkit.formatted_text.HTML` object. This simplifies many APIs. - New utilities were added. We now have function for printing formatted text and an experimental module for displaying progress bars. - Autocompletion, input validation, and auto suggestion can now either be asynchronous or synchronous. By default they are synchronous, but by wrapping them in :class:`~prompt_toolkit.completion.ThreadedCompleter`, :class:`~prompt_toolkit.validation.ThreadedValidator` or :class:`~prompt_toolkit.auto_suggest.ThreadedAutoSuggest`, they will become asynchronous by running in a background thread. Further, if the autocompletion code runs in a background thread, we will show the completions as soon as they arrive. This means that the autocompletion algorithm could for instance first yield the most trivial completions and then take time to produce the completions that take more time. Upgrading --------- More guidelines on how to upgrade will follow. `AbortAction` has been removed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prompt_toolkit 1.0 had an argument ``abort_action`` for both the ``Application`` class as well as for the ``prompt`` function. This has been removed. The recommended way to handle this now is by capturing ``KeyboardInterrupt`` and ``EOFError`` manually. Calling `create_eventloop` usually not required anymore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prompt_toolkit 2.0 will automatically create the appropriate event loop when it's needed for the first time. There is no need to create one and pass it around. If you want to run an application on top of asyncio (without using an executor), it still needs to be activated by calling :func:`~prompt_toolkit.eventloop.use_asyncio_event_loop` at the beginning. Pygments styles and tokens ^^^^^^^^^^^^^^^^^^^^^^^^^^ prompt_toolkit 2.0 no longer depends on `Pygments <http://pygments.org/>`_, but that definitely doesn't mean that you can't use any Pygments functionality anymore. The only difference is that Pygments stuff needs to be wrapped in an adaptor to make it compatible with the native prompt_toolkit objects. - For instance, if you have a list of ``(pygments.Token, text)`` tuples for formatting, then this needs to be wrapped in a :class:`~prompt_toolkit.formatted_text.PygmentsTokens` object. This is an adaptor that turns it into prompt_toolkit "formatted text". Feel free to keep using this. - Pygments lexers need to be wrapped in a :class:`~prompt_toolkit.lexers.PygmentsLexer`. This will convert the list of Pygments tokens into prompt_toolkit formatted text. - If you have a Pygments style, then this needs to be converted as well. A Pygments style class can be converted in a prompt_toolkit :class:`~prompt_toolkit.styles.Style` with the :func:`~prompt_toolkit.styles.pygments.style_from_pygments_cls` function (which used to be called ``style_from_pygments``). A Pygments style dictionary can be converted using :func:`~prompt_toolkit.styles.pygments.style_from_pygments_dict`. Multiple styles can be merged together using :func:`~prompt_toolkit.styles.merge_styles`. Wordcompleter ^^^^^^^^^^^^^ `WordCompleter` was moved from :class:`prompt_toolkit.contrib.completers.base.WordCompleter` to :class:`prompt_toolkit.completion.word_completer.WordCompleter`. Asynchronous autocompletion ^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default, prompt_toolkit 2.0 completion is now synchronous. If you still want asynchronous auto completion (which is often good thing), then you have to wrap the completer in a :class:`~prompt_toolkit.completion.ThreadedCompleter`. Filters ^^^^^^^ We don't distinguish anymore between `CLIFilter` and `SimpleFilter`, because the application object is no longer passed around. This means that all filters are a `Filter` from now on. All filters have been turned into functions. For instance, `IsDone` became `is_done` and `HasCompletions` became `has_completions`. This was done because almost all classes were called without any arguments in the `__init__` causing additional braces everywhere. This means that `HasCompletions()` has to be replaced by `has_completions` (without parenthesis). The few filters that took arguments as input, became functions, but still have to be called with the given arguments. For new filters, it is recommended to use the `@Condition` decorator, rather then inheriting from `Filter`. For instance: .. code:: python from prompt_toolkit.filters import Condition @Condition def my_filter(); return True # Or False ================================================ FILE: docs/pages/upgrading/3.0.rst ================================================ .. _upgrading_3_0: Upgrading to prompt_toolkit 3.0 =============================== There are two major changes in 3.0 to be aware of: - First, prompt_toolkit uses the asyncio event loop natively, rather then using its own implementations of event loops. This means that all coroutines are now asyncio coroutines, and all Futures are asyncio futures. Asynchronous generators became real asynchronous generators as well. - Prompt_toolkit uses type annotations (almost) everywhere. This should not break any code, but its very helpful in many ways. There are some minor breaking changes: - The dialogs API had to change (see below). Detecting the prompt_toolkit version ------------------------------------ Detecting whether version 3 is being used can be done as follows: .. code:: python from prompt_toolkit import __version__ as ptk_version PTK3 = ptk_version.startswith('3.') Fixing calls to `get_event_loop` -------------------------------- Every usage of ``get_event_loop`` has to be fixed. An easy way to do this is by changing the imports like this: .. code:: python if PTK3: from asyncio import get_event_loop else: from prompt_toolkit.eventloop import get_event_loop Notice that for prompt_toolkit 2.0, ``get_event_loop`` returns a prompt_toolkit ``EventLoop`` object. This is not an asyncio eventloop, but the API is similar. There are some changes to the eventloop API: +-----------------------------------+--------------------------------------+ | version 2.0 | version 3.0 (asyncio) | +===================================+======================================+ | loop.run_in_executor(callback) | loop.run_in_executor(None, callback) | +-----------------------------------+--------------------------------------+ | loop.call_from_executor(callback) | loop.call_soon_threadsafe(callback) | +-----------------------------------+--------------------------------------+ Running on top of asyncio ------------------------- For 2.0, you had tell prompt_toolkit to run on top of the asyncio event loop. Now it's the default. So, you can simply remove the following two lines: .. code:: from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop use_asyncio_event_loop() There is a few little breaking changes though. The following: .. code:: # For 2.0 result = await PromptSession().prompt('Say something: ', async_=True) has to be changed into: .. code:: # For 3.0 result = await PromptSession().prompt_async('Say something: ') Further, it's impossible to call the `prompt()` function within an asyncio application (within a coroutine), because it will try to run the event loop again. In that case, always use `prompt_async()`. Changes to the dialog functions ------------------------------- The original way of using dialog boxes looked like this: .. code:: python from prompt_toolkit.shortcuts import input_dialog result = input_dialog(title='...', text='...') Now, the dialog functions return a prompt_toolkit Application object. You have to call either its ``run`` or ``run_async`` method to display the dialog. The ``async_`` parameter has been removed everywhere. .. code:: python if PTK3: result = input_dialog(title='...', text='...').run() else: result = input_dialog(title='...', text='...') # Or if PTK3: result = await input_dialog(title='...', text='...').run_async() else: result = await input_dialog(title='...', text='...', async_=True) ================================================ FILE: docs/pages/upgrading/index.rst ================================================ .. _upgrading: Upgrading ========= .. toctree:: :caption: Contents: :maxdepth: 1 2.0 3.0 ================================================ FILE: docs/requirements.txt ================================================ Sphinx>=8,<9 wcwidth<1 pyperclip<2 sphinx_copybutton>=0.5.2,<1.0.0 sphinx-nefertiti>=0.8.8 ================================================ FILE: examples/choices/color.py ================================================ from __future__ import annotations from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice from prompt_toolkit.styles import Style def main() -> None: style = Style.from_dict( { "input-selection": "fg:#ff0000", "number": "fg:#884444 bold", "selected-option": "underline", "frame.border": "#884444", } ) result = choice( message=HTML("<u>Please select a dish</u>:"), options=[ ("pizza", "Pizza with mushrooms"), ( "salad", HTML("<ansigreen>Salad</ansigreen> with <ansired>tomatoes</ansired>"), ), ("sushi", "Sushi"), ], style=style, ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/choices/default.py ================================================ from __future__ import annotations from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice def main() -> None: result = choice( message=HTML("<u>Please select a dish</u>:"), options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], default="salad", ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/choices/frame-and-bottom-toolbar.py ================================================ from __future__ import annotations from prompt_toolkit.filters import is_done from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice from prompt_toolkit.styles import Style def main() -> None: style = Style.from_dict( { "frame.border": "#ff4444", "selected-option": "bold", # We use 'noreverse' because the default style for 'bottom-toolbar' # uses 'reverse'. "bottom-toolbar": "#ffffff bg:#333333 noreverse", } ) result = choice( message=HTML("<u>Please select a dish</u>:"), options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], style=style, bottom_toolbar=HTML( " Press <b>[Up]</b>/<b>[Down]</b> to select, <b>[Enter]</b> to accept." ), # Use `~is_done`, if you only want to show the frame while editing and # hide it when the input is accepted. # Use `True`, if you always want to show the frame. show_frame=~is_done, ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/choices/gray-frame-on-accept.py ================================================ from __future__ import annotations from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice from prompt_toolkit.styles import Style def main() -> None: style = Style.from_dict( { "selected-option": "bold", "frame.border": "#ff4444", "accepted frame.border": "#888888", } ) result = choice( message=HTML("<u>Please select a dish</u>:"), options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], style=style, show_frame=True, ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/choices/many-choices.py ================================================ from __future__ import annotations from prompt_toolkit.shortcuts import choice def main() -> None: result = choice( message="Please select an option:", options=[(i, f"Option {i}") for i in range(1, 100)], ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/choices/mouse-support.py ================================================ from __future__ import annotations from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice def main() -> None: result = choice( message=HTML("<u>Please select a dish</u>:"), options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], mouse_support=True, ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/choices/simple-selection.py ================================================ from __future__ import annotations from prompt_toolkit.shortcuts import choice def main() -> None: result = choice( message="Please select a dish:", options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/choices/with-frame.py ================================================ from __future__ import annotations from prompt_toolkit.filters import is_done from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import choice from prompt_toolkit.styles import Style def main() -> None: style = Style.from_dict( { "frame.border": "#884444", "selected-option": "bold underline", } ) result = choice( message=HTML("<u>Please select a dish</u>:"), options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], style=style, # Use `~is_done`, if you only want to show the frame while editing and # hide it when the input is accepted. # Use `True`, if you always want to show the frame. show_frame=~is_done, ) print(result) if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/button_dialog.py ================================================ #!/usr/bin/env python """ Example of button dialog window. """ from prompt_toolkit.shortcuts import button_dialog def main(): result = button_dialog( title="Button dialog example", text="Are you sure?", buttons=[("Yes", True), ("No", False), ("Maybe...", None)], ).run() print(f"Result = {result}") if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/checkbox_dialog.py ================================================ #!/usr/bin/env python """ Example of a checkbox-list-based dialog. """ from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import checkboxlist_dialog, message_dialog from prompt_toolkit.styles import Style results = checkboxlist_dialog( title="CheckboxList dialog", text="What would you like in your breakfast ?", values=[ ("eggs", "Eggs"), ("bacon", HTML("<blue>Bacon</blue>")), ("croissants", "20 Croissants"), ("daily", "The breakfast of the day"), ], style=Style.from_dict( { "dialog": "bg:#cdbbb3", "button": "bg:#bf99a4", "checkbox": "#e8612c", "dialog.body": "bg:#a9cfd0", "dialog shadow": "bg:#c98982", "frame.label": "#fcaca3", "dialog.body label": "#fd8bb6", } ), ).run() if results: message_dialog( title="Room service", text="You selected: {}\nGreat choice sir !".format(",".join(results)), ).run() else: message_dialog("*starves*").run() ================================================ FILE: examples/dialogs/input_dialog.py ================================================ #!/usr/bin/env python """ Example of an input box dialog. """ from prompt_toolkit.shortcuts import input_dialog def main(): result = input_dialog( title="Input dialog example", text="Please type your name:" ).run() print(f"Result = {result}") if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/messagebox.py ================================================ #!/usr/bin/env python """ Example of a message box window. """ from prompt_toolkit.shortcuts import message_dialog def main(): message_dialog( title="Example dialog window", text="Do you want to continue?\nPress ENTER to quit.", ).run() if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/password_dialog.py ================================================ #!/usr/bin/env python """ Example of an password input dialog. """ from prompt_toolkit.shortcuts import input_dialog def main(): result = input_dialog( title="Password dialog example", text="Please type your password:", password=True, ).run() print(f"Result = {result}") if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/progress_dialog.py ================================================ #!/usr/bin/env python """ Example of a progress bar dialog. """ import os import time from prompt_toolkit.shortcuts import progress_dialog def worker(set_percentage, log_text): """ This worker function is called by `progress_dialog`. It will run in a background thread. The `set_percentage` function can be used to update the progress bar, while the `log_text` function can be used to log text in the logging window. """ percentage = 0 for dirpath, dirnames, filenames in os.walk("../.."): for f in filenames: log_text(f"{dirpath} / {f}\n") set_percentage(percentage + 1) percentage += 2 time.sleep(0.1) if percentage == 100: break if percentage == 100: break # Show 100% for a second, before quitting. set_percentage(100) time.sleep(1) def main(): progress_dialog( title="Progress dialog example", text="As an examples, we walk through the filesystem and print all directories", run_callback=worker, ).run() if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/radio_dialog.py ================================================ #!/usr/bin/env python """ Example of a radio list box dialog. """ from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import radiolist_dialog def main(): result = radiolist_dialog( values=[ ("red", "Red"), ("green", "Green"), ("blue", "Blue"), ("orange", "Orange"), ], title="Radiolist dialog example", text="Please select a color:", ).run() print(f"Result = {result}") # With HTML. result = radiolist_dialog( values=[ ("red", HTML('<style bg="red" fg="white">Red</style>')), ("green", HTML('<style bg="green" fg="white">Green</style>')), ("blue", HTML('<style bg="blue" fg="white">Blue</style>')), ("orange", HTML('<style bg="orange" fg="white">Orange</style>')), ], title=HTML("Radiolist dialog example <reverse>with colors</reverse>"), text="Please select a color:", ).run() print(f"Result = {result}") if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/styled_messagebox.py ================================================ #!/usr/bin/env python """ Example of a style dialog window. All dialog shortcuts take a `style` argument in order to apply a custom styling. This also demonstrates that the `title` argument can be any kind of formatted text. """ from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import message_dialog from prompt_toolkit.styles import Style # Custom color scheme. example_style = Style.from_dict( { "dialog": "bg:#88ff88", "dialog frame-label": "bg:#ffffff #000000", "dialog.body": "bg:#000000 #00ff00", "dialog shadow": "bg:#00aa00", } ) def main(): message_dialog( title=HTML( '<style bg="blue" fg="white">Styled</style> ' '<style fg="ansired">dialog</style> window' ), text="Do you want to continue?\nPress ENTER to quit.", style=example_style, ).run() if __name__ == "__main__": main() ================================================ FILE: examples/dialogs/yes_no_dialog.py ================================================ #!/usr/bin/env python """ Example of confirmation (yes/no) dialog window. """ from prompt_toolkit.shortcuts import yes_no_dialog def main(): result = yes_no_dialog( title="Yes/No dialog example", text="Do you want to confirm?" ).run() print(f"Result = {result}") if __name__ == "__main__": main() ================================================ FILE: examples/full-screen/ansi-art-and-textarea.py ================================================ #!/usr/bin/env python from prompt_toolkit.application import Application from prompt_toolkit.formatted_text import ANSI, HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout import HSplit, Layout, VSplit, WindowAlign from prompt_toolkit.widgets import Dialog, Label, TextArea def main(): # Key bindings. kb = KeyBindings() @kb.add("c-c") def _(event): "Quit when control-c is pressed." event.app.exit() text_area = TextArea(text="You can type here...") dialog_body = HSplit( [ Label( HTML("Press <reverse>control-c</reverse> to quit."), align=WindowAlign.CENTER, ), VSplit( [ Label(PROMPT_TOOLKIT_LOGO, align=WindowAlign.CENTER), text_area, ], ), ] ) application = Application( layout=Layout( container=Dialog( title="ANSI Art demo - Art on the left, text area on the right", body=dialog_body, with_background=True, ), focused_element=text_area, ), full_screen=True, mouse_support=True, key_bindings=kb, ) application.run() PROMPT_TOOLKIT_LOGO = ANSI( """ \x1b[48;2;0;0;0m \x1b[m \x1b[48;2;0;0;0m \x1b[48;2;0;249;0m\x1b[38;2;0;0;0m▀\x1b[48;2;0;209;0m▀\x1b[48;2;0;207;0m\x1b[38;2;6;34;6m▀\x1b[48;2;0;66;0m\x1b[38;2;30;171;30m▀\x1b[48;2;0;169;0m\x1b[38;2;51;35;51m▀\x1b[48;2;0;248;0m\x1b[38;2;49;194;49m▀\x1b[48;2;0;111;0m\x1b[38;2;25;57;25m▀\x1b[48;2;140;195;140m\x1b[38;2;3;17;3m▀\x1b[48;2;30;171;30m\x1b[38;2;0;0;0m▀\x1b[48;2;0;0;0m \x1b[m \x1b[48;2;0;0;0m \x1b[48;2;77;127;78m\x1b[38;2;118;227;108m▀\x1b[48;2;216;1;13m\x1b[38;2;49;221;57m▀\x1b[48;2;26;142;76m\x1b[38;2;108;146;165m▀\x1b[48;2;26;142;90m\x1b[38;2;209;197;114m▀▀\x1b[38;2;209;146;114m▀\x1b[48;2;26;128;90m\x1b[38;2;158;197;114m▀\x1b[48;2;58;210;70m\x1b[38;2;223;152;89m▀\x1b[48;2;232;139;44m\x1b[38;2;97;121;146m▀\x1b[48;2;233;139;45m\x1b[38;2;140;188;183m▀\x1b[48;2;231;139;44m\x1b[38;2;40;168;8m▀\x1b[48;2;228;140;44m\x1b[38;2;37;169;7m▀\x1b[48;2;227;140;44m\x1b[38;2;36;169;7m▀\x1b[48;2;211;142;41m\x1b[38;2;23;171;5m▀\x1b[48;2;86;161;17m\x1b[38;2;2;174;1m▀\x1b[48;2;0;175;0m \x1b[48;2;0;254;0m\x1b[38;2;190;119;190m▀\x1b[48;2;92;39;23m\x1b[38;2;125;50;114m▀\x1b[48;2;43;246;41m\x1b[38;2;49;10;165m▀\x1b[48;2;12;128;90m\x1b[38;2;209;197;114m▀\x1b[48;2;26;128;90m▀▀▀▀\x1b[48;2;26;128;76m▀\x1b[48;2;26;128;90m\x1b[38;2;209;247;114m▀▀\x1b[38;2;209;197;114m▀\x1b[48;2;26;128;76m\x1b[38;2;209;247;114m▀\x1b[48;2;26;128;90m▀▀▀\x1b[48;2;26;128;76m▀\x1b[48;2;26;128;90m▀▀\x1b[48;2;12;128;76m▀\x1b[48;2;12;113;90m\x1b[38;2;209;247;64m▀\x1b[38;2;209;247;114m▀\x1b[48;2;12;128;90m▀\x1b[48;2;12;113;90m▀\x1b[48;2;12;113;76m\x1b[38;2;209;247;64m▀\x1b[48;2;12;128;90m▀\x1b[48;2;12;113;90m▀\x1b[48;2;12;113;76m\x1b[38;2;209;247;114m▀\x1b[48;2;12;113;90m\x1b[38;2;209;247;64m▀\x1b[48;2;26;128;90m\x1b[38;2;151;129;163m▀\x1b[48;2;115;120;103m\x1b[38;2;62;83;227m▀\x1b[48;2;138;14;25m\x1b[38;2;104;106;160m▀\x1b[48;2;0;0;57m\x1b[38;2;0;0;0m▀\x1b[m \x1b[48;2;249;147;8m\x1b[38;2;172;69;38m▀\x1b[48;2;197;202;10m\x1b[38;2;82;192;58m▀\x1b[48;2;248;124;45m\x1b[38;2;251;131;47m▀\x1b[48;2;248;124;44m▀\x1b[48;2;248;124;45m▀▀\x1b[48;2;248;124;44m▀\x1b[48;2;248;124;45m▀\x1b[48;2;248;125;45m\x1b[38;2;251;130;47m▀\x1b[48;2;248;124;45m\x1b[38;2;252;130;47m▀\x1b[48;2;248;125;45m\x1b[38;2;252;131;47m▀\x1b[38;2;252;130;47m▀\x1b[38;2;252;131;47m▀▀\x1b[48;2;249;125;45m\x1b[38;2;255;130;48m▀\x1b[48;2;233;127;42m\x1b[38;2;190;141;35m▀\x1b[48;2;57;163;10m\x1b[38;2;13;172;3m▀\x1b[48;2;0;176;0m\x1b[38;2;0;175;0m▀\x1b[48;2;7;174;1m\x1b[38;2;35;169;7m▀\x1b[48;2;178;139;32m\x1b[38;2;220;136;41m▀\x1b[48;2;252;124;45m\x1b[38;2;253;131;47m▀\x1b[48;2;248;125;45m\x1b[38;2;251;131;47m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;248;125;44m▀\x1b[48;2;248;135;61m\x1b[38;2;251;132;48m▀\x1b[48;2;250;173;122m\x1b[38;2;251;133;50m▀\x1b[48;2;249;155;93m\x1b[38;2;251;132;49m▀\x1b[48;2;248;132;55m\x1b[38;2;251;132;48m▀\x1b[48;2;250;173;122m\x1b[38;2;251;134;51m▀\x1b[48;2;250;163;106m\x1b[38;2;251;134;50m▀\x1b[48;2;248;128;49m\x1b[38;2;251;132;47m▀\x1b[48;2;250;166;110m\x1b[38;2;251;135;52m▀\x1b[48;2;250;175;125m\x1b[38;2;251;136;54m▀\x1b[48;2;248;132;56m\x1b[38;2;251;132;48m▀\x1b[48;2;248;220;160m\x1b[38;2;105;247;172m▀\x1b[48;2;62;101;236m\x1b[38;2;11;207;160m▀\x1b[m \x1b[48;2;138;181;197m\x1b[38;2;205;36;219m▀\x1b[48;2;177;211;200m\x1b[38;2;83;231;105m▀\x1b[48;2;242;113;40m\x1b[38;2;245;119;42m▀\x1b[48;2;243;113;41m▀\x1b[48;2;245;114;41m▀▀▀▀▀▀▀▀\x1b[38;2;245;119;43m▀▀▀\x1b[48;2;247;114;41m\x1b[38;2;246;119;43m▀\x1b[48;2;202;125;34m\x1b[38;2;143;141;25m▀\x1b[48;2;84;154;14m\x1b[38;2;97;152;17m▀\x1b[48;2;36;166;6m▀\x1b[48;2;139;140;23m\x1b[38;2;183;133;32m▀\x1b[48;2;248;114;41m\x1b[38;2;248;118;43m▀\x1b[48;2;245;115;41m\x1b[38;2;245;119;43m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[38;2;245;119;42m▀\x1b[48;2;246;117;44m\x1b[38;2;246;132;62m▀\x1b[48;2;246;123;54m\x1b[38;2;249;180;138m▀\x1b[48;2;246;120;49m\x1b[38;2;247;157;102m▀\x1b[48;2;246;116;42m\x1b[38;2;246;127;54m▀\x1b[48;2;246;121;50m\x1b[38;2;248;174;128m▀\x1b[48;2;246;120;48m\x1b[38;2;248;162;110m▀\x1b[48;2;246;116;41m\x1b[38;2;245;122;47m▀\x1b[48;2;246;118;46m\x1b[38;2;248;161;108m▀\x1b[48;2;244;118;47m\x1b[38;2;248;171;123m▀\x1b[48;2;243;115;42m\x1b[38;2;246;127;54m▀\x1b[48;2;179;52;29m\x1b[38;2;86;152;223m▀\x1b[48;2;141;225;95m\x1b[38;2;247;146;130m▀\x1b[m \x1b[48;2;50;237;108m\x1b[38;2;94;70;153m▀\x1b[48;2;206;221;133m\x1b[38;2;64;240;39m▀\x1b[48;2;233;100;36m\x1b[38;2;240;107;38m▀\x1b[48;2;114;56;22m\x1b[38;2;230;104;37m▀\x1b[48;2;24;20;10m\x1b[38;2;193;90;33m▀\x1b[48;2;21;19;9m\x1b[38;2;186;87;32m▀▀▀▀▀▀▀\x1b[38;2;186;87;33m▀▀▀\x1b[48;2;22;18;10m\x1b[38;2;189;86;33m▀\x1b[48;2;18;36;8m\x1b[38;2;135;107;24m▀\x1b[48;2;3;153;2m\x1b[38;2;5;171;1m▀\x1b[48;2;0;177;0m \x1b[48;2;4;158;2m\x1b[38;2;69;147;12m▀\x1b[48;2;19;45;8m\x1b[38;2;185;89;32m▀\x1b[48;2;22;17;10m\x1b[38;2;186;87;33m▀\x1b[48;2;21;19;9m▀▀▀▀▀▀▀▀\x1b[48;2;21;19;10m▀▀\x1b[48;2;21;19;9m▀▀▀▀\x1b[48;2;21;19;10m▀▀▀\x1b[38;2;186;87;32m▀▀\x1b[48;2;21;19;9m\x1b[38;2;186;87;33m▀\x1b[48;2;21;19;10m\x1b[38;2;186;87;32m▀▀\x1b[48;2;21;19;9m\x1b[38;2;186;87;33m▀\x1b[48;2;22;19;10m\x1b[38;2;191;89;33m▀\x1b[48;2;95;49;20m\x1b[38;2;226;103;37m▀\x1b[48;2;227;99;36m\x1b[38;2;241;109;39m▀\x1b[48;2;80;140;154m\x1b[38;2;17;240;92m▀\x1b[48;2;221;58;175m\x1b[38;2;71;14;245m▀\x1b[m \x1b[48;2;195;38;42m\x1b[38;2;5;126;86m▀\x1b[48;2;139;230;67m\x1b[38;2;253;201;228m▀\x1b[48;2;208;82;30m\x1b[38;2;213;89;32m▀\x1b[48;2;42;26;12m\x1b[38;2;44;27;12m▀\x1b[48;2;9;14;7m\x1b[38;2;8;13;7m▀\x1b[48;2;11;15;8m\x1b[38;2;10;14;7m▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;11;12;8m\x1b[38;2;10;17;7m▀\x1b[48;2;7;71;5m\x1b[38;2;4;120;3m▀\x1b[48;2;1;164;1m\x1b[38;2;0;178;0m▀\x1b[48;2;4;118;3m\x1b[38;2;0;177;0m▀\x1b[48;2;5;108;3m\x1b[38;2;4;116;3m▀\x1b[48;2;7;75;5m\x1b[38;2;10;23;7m▀\x1b[48;2;10;33;7m\x1b[38;2;10;12;7m▀\x1b[48;2;11;13;8m\x1b[38;2;10;14;7m▀\x1b[48;2;11;14;8m▀\x1b[48;2;11;15;8m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;10;14;7m\x1b[38;2;9;14;7m▀\x1b[48;2;30;21;10m\x1b[38;2;30;22;10m▀\x1b[48;2;195;79;29m\x1b[38;2;200;84;31m▀\x1b[48;2;205;228;23m\x1b[38;2;111;40;217m▀\x1b[48;2;9;217;69m\x1b[38;2;115;137;104m▀\x1b[m \x1b[48;2;106;72;209m\x1b[38;2;151;183;253m▀\x1b[48;2;120;239;0m\x1b[38;2;25;2;162m▀\x1b[48;2;203;72;26m\x1b[38;2;206;77;28m▀\x1b[48;2;42;24;11m\x1b[38;2;42;25;11m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;13;8m\x1b[38;2;10;28;7m▀\x1b[48;2;9;36;6m\x1b[38;2;7;78;5m▀\x1b[48;2;2;153;1m\x1b[38;2;6;94;4m▀\x1b[48;2;0;178;0m\x1b[38;2;2;156;1m▀\x1b[48;2;0;175;0m\x1b[38;2;1;167;1m▀\x1b[48;2;0;177;0m\x1b[38;2;2;145;2m▀\x1b[48;2;2;147;2m\x1b[38;2;8;54;6m▀\x1b[48;2;9;38;6m\x1b[38;2;11;13;8m▀\x1b[48;2;11;13;8m\x1b[38;2;11;14;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;29;20;10m\x1b[38;2;29;21;10m▀\x1b[48;2;190;69;25m\x1b[38;2;193;74;27m▀\x1b[48;2;136;91;148m\x1b[38;2;42;159;86m▀\x1b[48;2;89;85;149m\x1b[38;2;160;5;219m▀\x1b[m \x1b[48;2;229;106;143m\x1b[38;2;40;239;187m▀\x1b[48;2;196;134;237m\x1b[38;2;6;11;95m▀\x1b[48;2;197;60;22m\x1b[38;2;201;67;24m▀\x1b[48;2;41;22;10m\x1b[38;2;41;23;11m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[48;2;10;14;7m\x1b[38;2;11;15;8m▀▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;16;7m▀\x1b[48;2;11;15;7m\x1b[38;2;7;79;5m▀\x1b[48;2;7;68;5m\x1b[38;2;1;164;1m▀\x1b[48;2;2;153;1m\x1b[38;2;0;176;0m▀\x1b[48;2;2;154;1m\x1b[38;2;0;175;0m▀\x1b[48;2;5;107;3m\x1b[38;2;1;171;1m▀\x1b[48;2;4;115;3m\x1b[38;2;5;105;3m▀\x1b[48;2;6;84;4m\x1b[38;2;11;18;7m▀\x1b[48;2;10;30;7m\x1b[38;2;11;13;8m▀\x1b[48;2;11;13;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;14;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;29;19;9m\x1b[38;2;29;20;10m▀\x1b[48;2;185;58;22m\x1b[38;2;188;64;24m▀\x1b[48;2;68;241;49m\x1b[38;2;199;22;211m▀\x1b[48;2;133;139;8m\x1b[38;2;239;129;78m▀\x1b[m \x1b[48;2;74;30;32m\x1b[38;2;163;185;76m▀\x1b[48;2;110;172;9m\x1b[38;2;177;1;123m▀\x1b[48;2;189;43;16m\x1b[38;2;193;52;19m▀\x1b[48;2;39;20;9m\x1b[38;2;40;21;10m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;9;14;7m\x1b[38;2;11;14;8m▀\x1b[48;2;106;54;38m\x1b[38;2;31;24;15m▀\x1b[48;2;164;71;49m\x1b[38;2;24;20;12m▀\x1b[48;2;94;46;31m\x1b[38;2;8;14;7m▀\x1b[48;2;36;24;15m\x1b[38;2;9;14;7m▀\x1b[48;2;11;15;8m\x1b[38;2;11;14;7m▀\x1b[48;2;8;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;10;14;7m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;13;8m▀\x1b[48;2;11;13;8m\x1b[38;2;9;45;6m▀\x1b[48;2;10;19;7m\x1b[38;2;7;75;5m▀\x1b[48;2;6;83;4m\x1b[38;2;2;143;2m▀\x1b[48;2;2;156;1m\x1b[38;2;0;176;0m▀\x1b[48;2;0;177;0m\x1b[38;2;0;175;0m▀\x1b[38;2;3;134;2m▀\x1b[48;2;2;152;1m\x1b[38;2;9;46;6m▀\x1b[48;2;8;60;5m\x1b[38;2;11;13;8m▀\x1b[48;2;11;14;7m\x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;28;18;9m \x1b[48;2;177;43;16m\x1b[38;2;181;51;19m▀\x1b[48;2;93;35;236m\x1b[38;2;224;10;142m▀\x1b[48;2;72;51;52m\x1b[38;2;213;112;158m▀\x1b[m \x1b[48;2;175;209;155m\x1b[38;2;7;131;221m▀\x1b[48;2;24;0;85m\x1b[38;2;44;86;152m▀\x1b[48;2;181;27;10m\x1b[38;2;185;35;13m▀\x1b[48;2;38;17;8m\x1b[38;2;39;18;9m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m \x1b[48;2;9;14;7m \x1b[48;2;87;43;32m\x1b[38;2;114;54;39m▀\x1b[48;2;188;71;54m\x1b[38;2;211;82;59m▀\x1b[48;2;203;73;55m\x1b[38;2;204;80;57m▀\x1b[48;2;205;73;55m\x1b[38;2;178;71;51m▀\x1b[48;2;204;74;55m\x1b[38;2;119;52;37m▀\x1b[48;2;188;69;52m\x1b[38;2;54;29;19m▀\x1b[48;2;141;55;41m\x1b[38;2;16;17;9m▀\x1b[48;2;75;35;24m\x1b[38;2;8;14;7m▀\x1b[48;2;26;20;12m\x1b[38;2;10;14;7m▀\x1b[48;2;9;14;7m\x1b[38;2;11;14;7m▀\x1b[38;2;11;15;8m▀\x1b[48;2;11;14;7m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m \x1b[48;2;11;13;8m\x1b[38;2;9;45;6m▀\x1b[48;2;10;23;7m\x1b[38;2;4;123;3m▀\x1b[48;2;7;75;5m\x1b[38;2;1;172;1m▀\x1b[48;2;6;84;4m\x1b[38;2;2;154;1m▀\x1b[48;2;4;114;3m\x1b[38;2;5;107;3m▀\x1b[48;2;5;103;4m\x1b[38;2;10;29;7m▀\x1b[48;2;10;23;7m\x1b[38;2;11;13;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;27;16;8m\x1b[38;2;27;17;9m▀\x1b[48;2;170;27;10m\x1b[38;2;174;35;13m▀\x1b[48;2;118;117;199m\x1b[38;2;249;61;74m▀\x1b[48;2;10;219;61m\x1b[38;2;187;245;202m▀\x1b[m \x1b[48;2;20;155;44m\x1b[38;2;86;54;110m▀\x1b[48;2;195;85;113m\x1b[38;2;214;171;227m▀\x1b[48;2;173;10;4m\x1b[38;2;177;19;7m▀\x1b[48;2;37;14;7m\x1b[38;2;37;16;8m▀\x1b[48;2;9;15;8m\x1b[38;2;9;14;7m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;7m▀\x1b[48;2;11;14;7m\x1b[38;2;15;17;9m▀\x1b[48;2;9;14;7m\x1b[38;2;50;29;20m▀\x1b[48;2;10;15;8m\x1b[38;2;112;47;36m▀\x1b[48;2;33;22;15m\x1b[38;2;170;61;48m▀\x1b[48;2;88;38;29m\x1b[38;2;197;66;53m▀\x1b[48;2;151;53;43m\x1b[38;2;201;67;53m▀\x1b[48;2;189;60;50m▀\x1b[48;2;198;60;51m\x1b[38;2;194;65;52m▀\x1b[38;2;160;56;44m▀\x1b[48;2;196;60;50m\x1b[38;2;99;40;30m▀\x1b[48;2;174;55;47m\x1b[38;2;41;24;16m▀\x1b[48;2;122;43;35m\x1b[38;2;12;15;8m▀\x1b[48;2;59;27;20m\x1b[38;2;8;14;7m▀\x1b[48;2;16;16;9m\x1b[38;2;10;14;7m▀\x1b[48;2;10;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;12;8m▀\x1b[48;2;10;25;7m\x1b[38;2;7;79;5m▀\x1b[48;2;3;141;2m\x1b[38;2;1;174;1m▀\x1b[48;2;0;178;0m\x1b[38;2;1;169;1m▀\x1b[48;2;6;88;4m\x1b[38;2;8;56;6m▀\x1b[48;2;11;12;8m \x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;26;15;8m\x1b[38;2;27;15;8m▀\x1b[48;2;162;12;5m\x1b[38;2;166;20;8m▀\x1b[48;2;143;168;130m\x1b[38;2;18;142;37m▀\x1b[48;2;240;96;105m\x1b[38;2;125;158;211m▀\x1b[m \x1b[48;2;54;0;0m\x1b[38;2;187;22;0m▀\x1b[48;2;204;0;0m\x1b[38;2;128;208;0m▀\x1b[48;2;162;1;1m\x1b[38;2;168;3;1m▀\x1b[48;2;35;13;7m\x1b[38;2;36;13;7m▀\x1b[48;2;9;15;8m \x1b[48;2;11;15;8m \x1b[38;2;11;14;7m▀\x1b[38;2;9;14;7m▀\x1b[38;2;8;14;7m▀\x1b[48;2;10;14;7m\x1b[38;2;21;18;11m▀\x1b[48;2;7;13;6m\x1b[38;2;65;30;23m▀\x1b[48;2;12;16;9m\x1b[38;2;129;45;38m▀\x1b[48;2;57;29;23m\x1b[38;2;176;53;47m▀\x1b[48;2;148;49;44m\x1b[38;2;191;53;48m▀\x1b[48;2;187;52;48m\x1b[38;2;192;53;48m▀\x1b[48;2;186;51;47m\x1b[38;2;194;54;49m▀\x1b[48;2;182;52;47m\x1b[38;2;178;52;46m▀\x1b[48;2;59;27;21m\x1b[38;2;53;26;19m▀\x1b[48;2;8;14;7m \x1b[48;2;11;15;8m \x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;12;8m\x1b[38;2;11;14;8m▀\x1b[48;2;10;30;7m\x1b[38;2;10;23;7m▀\x1b[48;2;5;110;3m\x1b[38;2;3;138;2m▀\x1b[48;2;2;149;2m\x1b[38;2;0;181;0m▀\x1b[48;2;6;92;4m\x1b[38;2;5;100;4m▀\x1b[48;2;11;13;8m \x1b[48;2;11;14;8m \x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;25;14;7m\x1b[38;2;26;14;7m▀\x1b[48;2;152;2;1m\x1b[38;2;158;5;2m▀\x1b[48;2;6;0;0m\x1b[38;2;44;193;0m▀\x1b[48;2;108;0;0m\x1b[38;2;64;70;0m▀\x1b[m \x1b[48;2;44;0;0m\x1b[38;2;177;0;0m▀\x1b[48;2;147;0;0m\x1b[38;2;71;0;0m▀\x1b[48;2;148;1;1m\x1b[38;2;155;1;1m▀\x1b[48;2;33;13;7m\x1b[38;2;34;13;7m▀\x1b[48;2;9;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;10;14;7m▀\x1b[48;2;9;14;7m▀\x1b[48;2;13;16;9m\x1b[38;2;11;14;7m▀\x1b[48;2;42;24;17m\x1b[38;2;9;14;7m▀\x1b[48;2;97;38;32m\x1b[38;2;10;15;8m▀\x1b[48;2;149;49;44m\x1b[38;2;30;21;14m▀\x1b[48;2;174;52;48m\x1b[38;2;79;34;28m▀\x1b[48;2;178;52;48m\x1b[38;2;136;45;40m▀\x1b[38;2;172;51;47m▀\x1b[48;2;173;52;48m\x1b[38;2;181;52;48m▀\x1b[48;2;147;47;42m\x1b[38;2;183;52;48m▀\x1b[48;2;94;35;30m\x1b[38;2;177;52;48m▀\x1b[48;2;25;19;12m\x1b[38;2;56;27;20m▀\x1b[48;2;10;14;7m\x1b[38;2;8;14;7m▀\x1b[48;2;11;12;8m\x1b[38;2;11;15;8m▀\x1b[48;2;10;23;7m\x1b[38;2;11;14;8m▀\x1b[48;2;7;76;5m\x1b[38;2;11;13;8m▀\x1b[48;2;2;152;1m\x1b[38;2;9;45;6m▀\x1b[48;2;0;177;0m\x1b[38;2;5;106;3m▀\x1b[48;2;0;178;0m\x1b[38;2;4;123;3m▀\x1b[48;2;1;168;1m\x1b[38;2;5;104;3m▀\x1b[48;2;8;53;6m\x1b[38;2;9;47;6m▀\x1b[48;2;11;12;8m\x1b[38;2;11;13;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;24;14;7m\x1b[38;2;25;14;7m▀\x1b[48;2;140;2;1m\x1b[38;2;146;2;1m▀\x1b[48;2;219;0;0m\x1b[38;2;225;0;0m▀\x1b[48;2;126;0;0m\x1b[38;2;117;0;0m▀\x1b[m \x1b[48;2;34;0;0m\x1b[38;2;167;0;0m▀\x1b[48;2;89;0;0m\x1b[38;2;14;0;0m▀\x1b[48;2;134;1;1m\x1b[38;2;141;1;1m▀\x1b[48;2;31;13;7m\x1b[38;2;32;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;10;14;7m\x1b[38;2;11;14;7m▀\x1b[48;2;53;29;22m\x1b[38;2;10;14;7m▀\x1b[48;2;127;46;41m\x1b[38;2;20;18;11m▀\x1b[48;2;158;51;47m\x1b[38;2;57;28;22m▀\x1b[48;2;166;52;48m\x1b[38;2;113;42;36m▀\x1b[48;2;167;52;48m\x1b[38;2;156;50;46m▀\x1b[48;2;164;52;48m\x1b[38;2;171;52;48m▀\x1b[48;2;146;48;44m\x1b[38;2;172;52;48m▀\x1b[48;2;102;38;33m▀\x1b[48;2;50;26;19m\x1b[38;2;161;51;46m▀\x1b[48;2;17;17;10m\x1b[38;2;126;44;38m▀\x1b[48;2;8;14;7m\x1b[38;2;71;31;25m▀\x1b[48;2;10;14;7m\x1b[38;2;27;19;13m▀\x1b[48;2;11;13;8m\x1b[38;2;10;14;7m▀\x1b[48;2;9;40;6m\x1b[38;2;10;13;7m▀\x1b[48;2;4;119;3m\x1b[38;2;11;20;7m▀\x1b[48;2;1;168;1m\x1b[38;2;8;63;5m▀\x1b[48;2;0;177;0m\x1b[38;2;3;130;2m▀\x1b[48;2;0;175;0m\x1b[38;2;1;171;1m▀\x1b[48;2;1;174;1m\x1b[38;2;0;176;0m▀\x1b[48;2;1;175;1m\x1b[38;2;1;174;1m▀\x1b[48;2;0;177;0m\x1b[38;2;0;176;0m▀\x1b[48;2;3;134;2m\x1b[38;2;2;158;1m▀\x1b[48;2;10;21;7m\x1b[38;2;9;38;6m▀\x1b[48;2;11;14;8m\x1b[38;2;11;13;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;23;14;7m \x1b[48;2;127;2;1m\x1b[38;2;133;2;1m▀\x1b[48;2;176;0;0m\x1b[38;2;213;0;0m▀\x1b[48;2;109;0;0m\x1b[38;2;100;0;0m▀\x1b[m \x1b[48;2;24;0;0m\x1b[38;2;157;0;0m▀\x1b[48;2;32;0;0m\x1b[38;2;165;0;0m▀\x1b[48;2;121;1;1m\x1b[38;2;128;1;1m▀\x1b[48;2;28;13;7m\x1b[38;2;30;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m \x1b[48;2;9;15;7m \x1b[48;2;88;41;34m\x1b[38;2;91;41;34m▀\x1b[48;2;145;51;47m\x1b[38;2;163;53;49m▀\x1b[48;2;107;42;36m\x1b[38;2;161;52;48m▀\x1b[48;2;58;29;22m\x1b[38;2;155;51;47m▀\x1b[48;2;21;18;11m\x1b[38;2;128;45;40m▀\x1b[48;2;9;14;7m\x1b[38;2;79;33;27m▀\x1b[38;2;33;21;15m▀\x1b[48;2;11;14;7m\x1b[38;2;12;15;8m▀\x1b[48;2;11;15;8m\x1b[38;2;9;14;7m▀\x1b[38;2;10;14;7m▀ \x1b[48;2;11;12;8m\x1b[38;2;11;14;8m▀\x1b[48;2;8;54;6m\x1b[38;2;10;28;7m▀\x1b[48;2;6;93;4m\x1b[38;2;4;125;3m▀\x1b[48;2;2;152;1m\x1b[38;2;0;175;0m▀\x1b[48;2;0;176;0m▀\x1b[48;2;0;175;0m\x1b[38;2;1;174;1m▀\x1b[48;2;0;177;0m\x1b[38;2;1;175;1m▀\x1b[48;2;0;175;0m▀▀\x1b[48;2;1;162;1m\x1b[38;2;0;176;0m▀\x1b[48;2;9;47;6m\x1b[38;2;6;95;4m▀\x1b[48;2;11;13;8m \x1b[48;2;11;15;8m\x1b[38;2;11;14;8m▀ \x1b[48;2;10;15;8m \x1b[48;2;21;13;7m\x1b[38;2;22;13;7m▀\x1b[48;2;114;2;1m\x1b[38;2;121;2;1m▀\x1b[48;2;164;0;0m\x1b[38;2;170;0;0m▀\x1b[48;2;127;0;0m\x1b[38;2;118;0;0m▀\x1b[m \x1b[48;2;14;0;0m\x1b[38;2;147;0;0m▀\x1b[48;2;183;0;0m\x1b[38;2;108;0;0m▀\x1b[48;2;107;1;1m\x1b[38;2;114;1;1m▀\x1b[48;2;26;13;7m\x1b[38;2;27;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[38;2;11;14;7m▀ \x1b[48;2;10;14;7m\x1b[38;2;43;27;20m▀\x1b[48;2;9;14;7m\x1b[38;2;42;25;18m▀\x1b[48;2;11;14;7m\x1b[38;2;14;16;9m▀\x1b[48;2;11;15;8m\x1b[38;2;9;14;7m▀\x1b[38;2;10;14;7m▀\x1b[38;2;11;14;7m▀ \x1b[48;2;11;12;8m \x1b[48;2;9;49;6m\x1b[38;2;8;64;5m▀\x1b[48;2;1;166;1m\x1b[38;2;1;159;1m▀\x1b[48;2;0;175;0m\x1b[38;2;1;171;1m▀ \x1b[48;2;1;159;1m\x1b[38;2;1;167;1m▀\x1b[48;2;7;79;5m\x1b[38;2;4;122;3m▀\x1b[48;2;2;144;2m\x1b[38;2;2;158;1m▀\x1b[48;2;0;158;1m\x1b[38;2;0;177;0m▀\x1b[48;2;7;44;6m\x1b[38;2;4;112;3m▀\x1b[48;2;9;12;7m\x1b[38;2;11;17;7m▀\x1b[48;2;9;14;7m\x1b[38;2;11;14;8m▀\x1b[38;2;11;15;8m▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;11;14;7m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;20;13;7m\x1b[38;2;21;13;7m▀\x1b[48;2;102;2;1m\x1b[38;2;108;2;1m▀\x1b[48;2;121;0;0m\x1b[38;2;127;0;0m▀\x1b[48;2;146;0;0m\x1b[38;2;136;0;0m▀\x1b[m \x1b[48;2;3;0;0m\x1b[38;2;137;0;0m▀\x1b[48;2;173;0;0m\x1b[38;2;50;0;0m▀\x1b[48;2;93;1;1m\x1b[38;2;100;1;1m▀\x1b[48;2;24;13;7m\x1b[38;2;25;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀▀\x1b[48;2;17;14;7m\x1b[38;2;11;14;8m▀\x1b[48;2;49;12;7m\x1b[38;2;9;24;7m▀\x1b[48;2;62;54;4m\x1b[38;2;8;133;2m▀\x1b[48;2;7;159;1m\x1b[38;2;2;176;0m▀\x1b[48;2;0;175;0m \x1b[48;2;1;172;1m\x1b[38;2;0;175;0m▀\x1b[48;2;1;159;1m\x1b[38;2;0;173;1m▀\x1b[48;2;46;122;19m\x1b[38;2;1;176;0m▀\x1b[48;2;122;63;45m\x1b[38;2;45;111;18m▀\x1b[48;2;135;52;49m\x1b[38;2;75;36;31m▀\x1b[48;2;135;53;49m\x1b[38;2;74;36;30m▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;136;53;49m\x1b[38;2;75;37;31m▀\x1b[48;2;119;49;45m\x1b[38;2;66;34;28m▀\x1b[48;2;25;20;13m\x1b[38;2;18;18;11m▀\x1b[48;2;10;14;7m \x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;19;13;7m \x1b[48;2;89;2;1m\x1b[38;2;95;2;1m▀\x1b[48;2;77;0;0m\x1b[38;2;83;0;0m▀\x1b[48;2;128;0;0m\x1b[38;2;119;0;0m▀\x1b[m \x1b[48;2;60;0;0m\x1b[38;2;126;0;0m▀\x1b[48;2;182;0;0m\x1b[38;2;249;0;0m▀\x1b[48;2;83;1;1m\x1b[38;2;87;1;1m▀\x1b[48;2;22;13;7m\x1b[38;2;23;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;16;14;7m▀\x1b[48;2;14;14;7m\x1b[38;2;42;13;7m▀\x1b[48;2;58;13;6m\x1b[38;2;95;11;5m▀\x1b[48;2;34;13;7m\x1b[38;2;100;11;5m▀\x1b[48;2;9;14;7m\x1b[38;2;21;17;7m▀\x1b[48;2;11;12;8m\x1b[38;2;8;55;6m▀\x1b[38;2;7;75;5m▀\x1b[38;2;8;65;5m▀\x1b[48;2;11;13;8m\x1b[38;2;9;41;6m▀\x1b[48;2;12;15;8m\x1b[38;2;60;37;28m▀\x1b[38;2;90;42;37m▀\x1b[38;2;88;42;36m▀▀▀▀▀▀▀▀▀▀▀▀\x1b[38;2;89;42;37m▀\x1b[38;2;78;39;33m▀\x1b[48;2;11;15;8m\x1b[38;2;20;18;11m▀\x1b[48;2;11;14;7m\x1b[38;2;10;14;7m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;18;13;7m \x1b[48;2;78;2;1m\x1b[38;2;83;2;1m▀\x1b[48;2;196;0;0m\x1b[38;2;40;0;0m▀\x1b[48;2;217;0;0m\x1b[38;2;137;0;0m▀\x1b[m \x1b[48;2;227;0;0m\x1b[38;2;16;0;0m▀\x1b[48;2;116;0;0m\x1b[38;2;21;0;0m▀\x1b[48;2;79;1;1m\x1b[38;2;81;1;1m▀\x1b[48;2;22;13;7m \x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[38;2;10;15;8m▀\x1b[48;2;10;15;8m\x1b[38;2;21;14;7m▀\x1b[48;2;11;15;8m\x1b[38;2;14;14;7m▀\x1b[38;2;11;14;7m▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \x1b[48;2;10;15;8m \x1b[48;2;17;13;7m\x1b[38;2;18;13;7m▀\x1b[48;2;75;2;1m\x1b[38;2;76;2;1m▀\x1b[48;2;97;0;0m\x1b[38;2;34;0;0m▀\x1b[48;2;76;0;0m\x1b[38;2;147;0;0m▀\x1b[m \x1b[48;2;161;0;0m\x1b[38;2;183;0;0m▀\x1b[48;2;49;0;0m\x1b[38;2;211;0;0m▀\x1b[48;2;75;1;1m\x1b[38;2;77;1;1m▀\x1b[48;2;21;13;7m \x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;17;13;7m \x1b[48;2;71;2;1m\x1b[38;2;73;2;1m▀\x1b[48;2;253;0;0m\x1b[38;2;159;0;0m▀\x1b[48;2;191;0;0m\x1b[38;2;5;0;0m▀\x1b[m \x1b[48;2;110;161;100m\x1b[38;2;116;0;0m▀\x1b[48;2;9;205;205m\x1b[38;2;192;0;0m▀\x1b[48;2;78;0;0m\x1b[38;2;77;1;0m▀\x1b[48;2;66;3;1m\x1b[38;2;30;11;6m▀\x1b[48;2;42;8;4m\x1b[38;2;9;15;8m▀\x1b[48;2;39;8;4m\x1b[38;2;10;15;8m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;40;8;4m▀\x1b[48;2;39;8;4m▀▀▀▀▀▀▀\x1b[48;2;40;8;4m▀▀▀\x1b[48;2;39;8;4m▀\x1b[48;2;40;8;4m▀\x1b[48;2;39;8;4m▀\x1b[48;2;41;8;4m\x1b[38;2;9;15;8m▀\x1b[48;2;62;4;2m\x1b[38;2;24;13;7m▀\x1b[48;2;78;0;0m\x1b[38;2;74;1;1m▀\x1b[48;2;221;222;0m\x1b[38;2;59;0;0m▀\x1b[48;2;67;199;133m\x1b[38;2;85;0;0m▀\x1b[m \x1b[48;2;0;0;0m\x1b[38;2;143;233;149m▀\x1b[48;2;108;184;254m\x1b[38;2;213;6;76m▀\x1b[48;2;197;183;82m\x1b[38;2;76;0;0m▀\x1b[48;2;154;157;0m▀\x1b[48;2;96;0;0m▀\x1b[48;2;253;0;0m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;226;0;0m▀\x1b[48;2;255;127;255m▀\x1b[48;2;84;36;66m\x1b[38;2;64;247;251m▀\x1b[48;2;0;0;0m\x1b[38;2;18;76;210m▀\x1b[m \x1b[48;2;0;0;0m \x1b[m \x1b[48;2;0;0;0m \x1b[m """ ) if __name__ == "__main__": main() ================================================ FILE: examples/full-screen/buttons.py ================================================ #!/usr/bin/env python """ A simple example of a few buttons and click handlers. """ from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.layout import HSplit, Layout, VSplit from prompt_toolkit.styles import Style from prompt_toolkit.widgets import Box, Button, Frame, Label, TextArea # Event handlers for all the buttons. def button1_clicked(): text_area.text = "Button 1 clicked" def button2_clicked(): text_area.text = "Button 2 clicked" def button3_clicked(): text_area.text = "Button 3 clicked" def exit_clicked(): get_app().exit() # All the widgets for the UI. button1 = Button("Button 1", handler=button1_clicked) button2 = Button("Button 2", handler=button2_clicked) button3 = Button("Button 3", handler=button3_clicked) button4 = Button("Exit", handler=exit_clicked) text_area = TextArea(focusable=True) # Combine all the widgets in a UI. # The `Box` object ensures that padding will be inserted around the containing # widget. It adapts automatically, unless an explicit `padding` amount is given. root_container = Box( HSplit( [ Label(text="Press `Tab` to move the focus."), VSplit( [ Box( body=HSplit([button1, button2, button3, button4], padding=1), padding=1, style="class:left-pane", ), Box(body=Frame(text_area), padding=1, style="class:right-pane"), ] ), ] ), ) layout = Layout(container=root_container, focused_element=button1) # Key bindings. kb = KeyBindings() kb.add("tab")(focus_next) kb.add("s-tab")(focus_previous) # Styling. style = Style( [ ("left-pane", "bg:#888800 #000000"), ("right-pane", "bg:#00aa00 #000000"), ("button", "#000000"), ("button-arrow", "#000000"), ("button focused", "bg:#ff0000"), ("text-area focused", "bg:#ff0000"), ] ) # Build a main application object. application = Application(layout=layout, key_bindings=kb, style=style, full_screen=True) def main(): application.run() if __name__ == "__main__": main() ================================================ FILE: examples/full-screen/calculator.py ================================================ #!/usr/bin/env python """ A simple example of a calculator program. This could be used as inspiration for a REPL. """ from prompt_toolkit.application import Application from prompt_toolkit.document import Document from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, Window from prompt_toolkit.layout.layout import Layout from prompt_toolkit.styles import Style from prompt_toolkit.widgets import SearchToolbar, TextArea help_text = """ Type any expression (e.g. "4 + 4") followed by enter to execute. Press Control-C to exit. """ def main(): # The layout. search_field = SearchToolbar() # For reverse search. output_field = TextArea(style="class:output-field", text=help_text) input_field = TextArea( height=1, prompt=">>> ", style="class:input-field", multiline=False, wrap_lines=False, search_field=search_field, ) container = HSplit( [ output_field, Window(height=1, char="-", style="class:line"), input_field, search_field, ] ) # Attach accept handler to the input field. We do this by assigning the # handler to the `TextArea` that we created earlier. it is also possible to # pass it to the constructor of `TextArea`. # NOTE: It's better to assign an `accept_handler`, rather then adding a # custom ENTER key binding. This will automatically reset the input # field and add the strings to the history. def accept(buff): # Evaluate "calculator" expression. try: output = f"\n\nIn: {input_field.text}\nOut: {eval(input_field.text)}" # Don't do 'eval' in real code! except BaseException as e: output = f"\n\n{e}" new_text = output_field.text + output # Add text to output buffer. output_field.buffer.document = Document( text=new_text, cursor_position=len(new_text) ) input_field.accept_handler = accept # The key bindings. kb = KeyBindings() @kb.add("c-c") @kb.add("c-q") def _(event): "Pressing Ctrl-Q or Ctrl-C will exit the user interface." event.app.exit() # Style. style = Style( [ ("output-field", "bg:#000044 #ffffff"), ("input-field", "bg:#000000 #ffffff"), ("line", "#004400"), ] ) # Run application. application = Application( layout=Layout(container, focused_element=input_field), key_bindings=kb, style=style, mouse_support=True, full_screen=True, ) application.run() if __name__ == "__main__": main() ================================================ FILE: examples/full-screen/dummy-app.py ================================================ #!/usr/bin/env python """ This is the most simple example possible. """ from prompt_toolkit import Application app = Application(full_screen=False) app.run() ================================================ FILE: examples/full-screen/full-screen-demo.py ================================================ #!/usr/bin/env python """ """ from pygments.lexers.html import HtmlLexer from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.completion import WordCompleter from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.layout.containers import Float, HSplit, VSplit from prompt_toolkit.layout.dimension import D from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.menus import CompletionsMenu from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles import Style from prompt_toolkit.widgets import ( Box, Button, Checkbox, Dialog, Frame, Label, MenuContainer, MenuItem, ProgressBar, RadioList, TextArea, ) def accept_yes(): get_app().exit(result=True) def accept_no(): get_app().exit(result=False) def do_exit(): get_app().exit(result=False) yes_button = Button(text="Yes", handler=accept_yes) no_button = Button(text="No", handler=accept_no) textfield = TextArea(lexer=PygmentsLexer(HtmlLexer)) checkbox1 = Checkbox(text="Checkbox") checkbox2 = Checkbox(text="Checkbox") radios = RadioList( values=[ ("Red", "red"), ("Green", "green"), ("Blue", "blue"), ("Orange", "orange"), ("Yellow", "yellow"), ("Purple", "Purple"), ("Brown", "Brown"), ] ) animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) root_container = HSplit( [ VSplit( [ Frame(body=Label(text="Left frame\ncontent")), Dialog(title="The custom window", body=Label("hello\ntest")), textfield, ], height=D(), ), VSplit( [ Frame(body=ProgressBar(), title="Progress bar"), Frame( title="Checkbox list", body=HSplit([checkbox1, checkbox2]), ), Frame(title="Radio list", body=radios), ], padding=1, ), Box( body=VSplit([yes_button, no_button], align="CENTER", padding=3), style="class:button-bar", height=3, ), ] ) root_container = MenuContainer( body=root_container, menu_items=[ MenuItem( "File", children=[ MenuItem("New"), MenuItem( "Open", children=[ MenuItem("From file..."), MenuItem("From URL..."), MenuItem( "Something else..", children=[ MenuItem("A"), MenuItem("B"), MenuItem("C"), MenuItem("D"), MenuItem("E"), ], ), ], ), MenuItem("Save"), MenuItem("Save as..."), MenuItem("-", disabled=True), MenuItem("Exit", handler=do_exit), ], ), MenuItem( "Edit", children=[ MenuItem("Undo"), MenuItem("Cut"), MenuItem("Copy"), MenuItem("Paste"), MenuItem("Delete"), MenuItem("-", disabled=True), MenuItem("Find"), MenuItem("Find next"), MenuItem("Replace"), MenuItem("Go To"), MenuItem("Select All"), MenuItem("Time/Date"), ], ), MenuItem("View", children=[MenuItem("Status Bar")]), MenuItem("Info", children=[MenuItem("About")]), ], floats=[ Float( xcursor=True, ycursor=True, content=CompletionsMenu(max_height=16, scroll_offset=1), ), ], ) # Global key bindings. bindings = KeyBindings() bindings.add("tab")(focus_next) bindings.add("s-tab")(focus_previous) style = Style.from_dict( { "window.border": "#888888", "shadow": "bg:#222222", "menu-bar": "bg:#aaaaaa #888888", "menu-bar.selected-item": "bg:#ffffff #000000", "menu": "bg:#888888 #ffffff", "menu.border": "#aaaaaa", "window.border shadow": "#444444", "focused button": "bg:#880000 #ffffff noinherit", # Styling for Dialog widgets. "button-bar": "bg:#aaaaff", } ) application = Application( layout=Layout(root_container, focused_element=yes_button), key_bindings=bindings, style=style, mouse_support=True, full_screen=True, ) def run(): result = application.run() print(f"You said: {result!r}") if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/hello-world.py ================================================ #!/usr/bin/env python """ A simple example of a a text area displaying "Hello World!". """ from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout import Layout from prompt_toolkit.widgets import Box, Frame, TextArea # Layout for displaying hello world. # (The frame creates the border, the box takes care of the margin/padding.) root_container = Box( Frame( TextArea( text="Hello world!\nPress control-c to quit.", width=40, height=10, ) ), ) layout = Layout(container=root_container) # Key bindings. kb = KeyBindings() @kb.add("c-c") def _(event): "Quit when control-c is pressed." event.app.exit() # Build a main application object. application = Application(layout=layout, key_bindings=kb, full_screen=True) def main(): application.run() if __name__ == "__main__": main() ================================================ FILE: examples/full-screen/no-layout.py ================================================ #!/usr/bin/env python """ An empty full screen application without layout. """ from prompt_toolkit import Application Application(full_screen=True).run() ================================================ FILE: examples/full-screen/pager.py ================================================ #!/usr/bin/env python """ A simple application that shows a Pager application. """ from pygments.lexers.python import PythonLexer from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.dimension import LayoutDimension as D from prompt_toolkit.layout.layout import Layout from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles import Style from prompt_toolkit.widgets import SearchToolbar, TextArea # Create one text buffer for the main content. _pager_py_path = __file__ with open(_pager_py_path, "rb") as f: text = f.read().decode("utf-8") def get_statusbar_text(): return [ ("class:status", _pager_py_path + " - "), ( "class:status.position", f"{text_area.document.cursor_position_row + 1}:{text_area.document.cursor_position_col + 1}", ), ("class:status", " - Press "), ("class:status.key", "Ctrl-C"), ("class:status", " to exit, "), ("class:status.key", "/"), ("class:status", " for searching."), ] search_field = SearchToolbar( text_if_not_searching=[("class:not-searching", "Press '/' to start searching.")] ) text_area = TextArea( text=text, read_only=True, scrollbar=True, line_numbers=True, search_field=search_field, lexer=PygmentsLexer(PythonLexer), ) root_container = HSplit( [ # The top toolbar. Window( content=FormattedTextControl(get_statusbar_text), height=D.exact(1), style="class:status", ), # The main content. text_area, search_field, ] ) # Key bindings. bindings = KeyBindings() @bindings.add("c-c") @bindings.add("q") def _(event): "Quit." event.app.exit() style = Style.from_dict( { "status": "reverse", "status.position": "#aaaa00", "status.key": "#ffaa00", "not-searching": "#888888", } ) # create application. application = Application( layout=Layout(root_container, focused_element=text_area), key_bindings=bindings, enable_page_navigation_bindings=True, mouse_support=True, style=style, full_screen=True, ) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/scrollable-panes/simple-example.py ================================================ #!/usr/bin/env python """ A simple example of a scrollable pane. """ from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.layout import Dimension, HSplit, Layout, ScrollablePane from prompt_toolkit.widgets import Frame, TextArea def main(): # Create a big layout of many text areas, then wrap them in a `ScrollablePane`. root_container = Frame( ScrollablePane( HSplit( [ Frame(TextArea(text=f"label-{i}"), width=Dimension()) for i in range(20) ] ) ) # ScrollablePane(HSplit([TextArea(text=f"label-{i}") for i in range(20)])) ) layout = Layout(container=root_container) # Key bindings. kb = KeyBindings() @kb.add("c-c") def exit(event) -> None: get_app().exit() kb.add("tab")(focus_next) kb.add("s-tab")(focus_previous) # Create and run application. application = Application(layout=layout, key_bindings=kb, full_screen=True) application.run() if __name__ == "__main__": main() ================================================ FILE: examples/full-screen/scrollable-panes/with-completion-menu.py ================================================ #!/usr/bin/env python """ A simple example of a scrollable pane. """ from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.completion import WordCompleter from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.layout import ( CompletionsMenu, Float, FloatContainer, HSplit, Layout, ScrollablePane, VSplit, ) from prompt_toolkit.widgets import Frame, Label, TextArea def main(): # Create a big layout of many text areas, then wrap them in a `ScrollablePane`. root_container = VSplit( [ Label("<left column>"), HSplit( [ Label("ScrollContainer Demo"), Frame( ScrollablePane( HSplit( [ Frame( TextArea( text=f"label-{i}", completer=animal_completer, ) ) for i in range(20) ] ) ), ), ] ), ] ) root_container = FloatContainer( root_container, floats=[ Float( xcursor=True, ycursor=True, content=CompletionsMenu(max_height=16, scroll_offset=1), ), ], ) layout = Layout(container=root_container) # Key bindings. kb = KeyBindings() @kb.add("c-c") def exit(event) -> None: get_app().exit() kb.add("tab")(focus_next) kb.add("s-tab")(focus_previous) # Create and run application. application = Application( layout=layout, key_bindings=kb, full_screen=True, mouse_support=True ) application.run() animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) if __name__ == "__main__": main() ================================================ FILE: examples/full-screen/simple-demos/alignment.py ================================================ #!/usr/bin/env python """ Demo of the different Window alignment options. """ from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, Window, WindowAlign from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout LIPSUM = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus.""" # 1. The layout left_text = '\nLeft aligned text. - (Press "q" to quit)\n\n' + LIPSUM center_text = "Centered text.\n\n" + LIPSUM right_text = "Right aligned text.\n\n" + LIPSUM body = HSplit( [ Window(FormattedTextControl(left_text), align=WindowAlign.LEFT), Window(height=1, char="-"), Window(FormattedTextControl(center_text), align=WindowAlign.CENTER), Window(height=1, char="-"), Window(FormattedTextControl(right_text), align=WindowAlign.RIGHT), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/autocompletion.py ================================================ #!/usr/bin/env python """ An example of a BufferControl in a full screen layout that offers auto completion. Important is to make sure that there is a `CompletionsMenu` in the layout, otherwise the completions won't be visible. """ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.completion import WordCompleter from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import Float, FloatContainer, HSplit, Window from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.menus import CompletionsMenu # The completer. animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) # The layout buff = Buffer(completer=animal_completer, complete_while_typing=True) body = FloatContainer( content=HSplit( [ Window( FormattedTextControl('Press "q" to quit.'), height=1, style="reverse" ), Window(BufferControl(buffer=buff)), ] ), floats=[ Float( xcursor=True, ycursor=True, content=CompletionsMenu(max_height=16, scroll_offset=1), ) ], ) # Key bindings kb = KeyBindings() @kb.add("q") @kb.add("c-c") def _(event): "Quit application." event.app.exit() # The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/colorcolumn.py ================================================ #!/usr/bin/env python """ Colorcolumn example. """ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import ColorColumn, HSplit, Window from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout LIPSUM = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus.""" # Create text buffers. buff = Buffer() buff.text = LIPSUM # 1. The layout color_columns = [ ColorColumn(50), ColorColumn(80, style="bg:#ff0000"), ColorColumn(10, style="bg:#ff0000"), ] body = HSplit( [ Window(FormattedTextControl('Press "q" to quit.'), height=1, style="reverse"), Window(BufferControl(buffer=buff), colorcolumns=color_columns), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/cursorcolumn-cursorline.py ================================================ #!/usr/bin/env python """ Cursorcolumn / cursorline example. """ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, Window from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout LIPSUM = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus.""" # Create text buffers. Cursorcolumn/cursorline are mostly combined with an # (editable) text buffers, where the user can move the cursor. buff = Buffer() buff.text = LIPSUM # 1. The layout body = HSplit( [ Window(FormattedTextControl('Press "q" to quit.'), height=1, style="reverse"), Window(BufferControl(buffer=buff), cursorcolumn=True, cursorline=True), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/float-transparency.py ================================================ #!/usr/bin/env python """ Example of the 'transparency' attribute of `Window' when used in a Float. """ from prompt_toolkit.application import Application from prompt_toolkit.formatted_text import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import Float, FloatContainer, Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.widgets import Frame LIPSUM = " ".join( ( """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus. """ * 100 ).split() ) # 1. The layout left_text = HTML("<reverse>transparent=False</reverse>\n") right_text = HTML("<reverse>transparent=True</reverse>") quit_text = "Press 'q' to quit." body = FloatContainer( content=Window(FormattedTextControl(LIPSUM), wrap_lines=True), floats=[ # Important note: Wrapping the floating objects in a 'Frame' is # only required for drawing the border around the # floating text. We do it here to make the layout more # obvious. # Left float. Float( Frame(Window(FormattedTextControl(left_text), width=20, height=4)), transparent=False, left=0, ), # Right float. Float( Frame(Window(FormattedTextControl(right_text), width=20, height=4)), transparent=True, right=0, ), # Quit text. Float( Frame( Window(FormattedTextControl(quit_text), width=18, height=1), style="bg:#ff44ff #ffffff", ), top=1, ), ], ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/floats.py ================================================ #!/usr/bin/env python """ Floats example. """ from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import Float, FloatContainer, Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.widgets import Frame LIPSUM = " ".join( ( """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus. """ * 100 ).split() ) # 1. The layout left_text = "Floating\nleft" right_text = "Floating\nright" top_text = "Floating\ntop" bottom_text = "Floating\nbottom" center_text = "Floating\ncenter" quit_text = "Press 'q' to quit." body = FloatContainer( content=Window(FormattedTextControl(LIPSUM), wrap_lines=True), floats=[ # Important note: Wrapping the floating objects in a 'Frame' is # only required for drawing the border around the # floating text. We do it here to make the layout more # obvious. # Left float. Float( Frame( Window(FormattedTextControl(left_text), width=10, height=2), style="bg:#44ffff #ffffff", ), left=0, ), # Right float. Float( Frame( Window(FormattedTextControl(right_text), width=10, height=2), style="bg:#44ffff #ffffff", ), right=0, ), # Bottom float. Float( Frame( Window(FormattedTextControl(bottom_text), width=10, height=2), style="bg:#44ffff #ffffff", ), bottom=0, ), # Top float. Float( Frame( Window(FormattedTextControl(top_text), width=10, height=2), style="bg:#44ffff #ffffff", ), top=0, ), # Center float. Float( Frame( Window(FormattedTextControl(center_text), width=10, height=2), style="bg:#44ffff #ffffff", ) ), # Quit text. Float( Frame( Window(FormattedTextControl(quit_text), width=18, height=1), style="bg:#ff44ff #ffffff", ), top=6, ), ], ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/focus.py ================================================ #!/usr/bin/env python """ Demonstration of how to programmatically focus a certain widget. """ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, VSplit, Window from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout # 1. The layout top_text = ( "Focus example.\n" "[q] Quit [a] Focus left top [b] Right top [c] Left bottom [d] Right bottom." ) LIPSUM = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus. """ left_top = Window(BufferControl(Buffer(document=Document(LIPSUM)))) left_bottom = Window(BufferControl(Buffer(document=Document(LIPSUM)))) right_top = Window(BufferControl(Buffer(document=Document(LIPSUM)))) right_bottom = Window(BufferControl(Buffer(document=Document(LIPSUM)))) body = HSplit( [ Window(FormattedTextControl(top_text), height=2, style="reverse"), Window(height=1, char="-"), # Horizontal line in the middle. VSplit([left_top, Window(width=1, char="|"), right_top]), Window(height=1, char="-"), # Horizontal line in the middle. VSplit([left_bottom, Window(width=1, char="|"), right_bottom]), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() @kb.add("a") def _(event): event.app.layout.focus(left_top) @kb.add("b") def _(event): event.app.layout.focus(right_top) @kb.add("c") def _(event): event.app.layout.focus(left_bottom) @kb.add("d") def _(event): event.app.layout.focus(right_bottom) @kb.add("tab") def _(event): event.app.layout.focus_next() @kb.add("s-tab") def _(event): event.app.layout.focus_previous() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/horizontal-align.py ================================================ #!/usr/bin/env python """ Horizontal align demo with HSplit. """ from prompt_toolkit.application import Application from prompt_toolkit.formatted_text import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import ( HorizontalAlign, HSplit, VerticalAlign, VSplit, Window, WindowAlign, ) from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.widgets import Frame TITLE = HTML( """ <u>HSplit HorizontalAlign</u> example. Press <b>'q'</b> to quit.""" ) LIPSUM = """\ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim.""" # 1. The layout body = HSplit( [ Frame( Window(FormattedTextControl(TITLE), height=2), style="bg:#88ff88 #000000" ), HSplit( [ # Left alignment. VSplit( [ Window( FormattedTextControl(HTML("<u>LEFT</u>")), width=10, ignore_content_width=True, style="bg:#ff3333 ansiblack", align=WindowAlign.CENTER, ), VSplit( [ Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), ], padding=1, padding_style="bg:#888888", align=HorizontalAlign.LEFT, height=5, padding_char="|", ), ] ), # Center alignment. VSplit( [ Window( FormattedTextControl(HTML("<u>CENTER</u>")), width=10, ignore_content_width=True, style="bg:#ff3333 ansiblack", align=WindowAlign.CENTER, ), VSplit( [ Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), ], padding=1, padding_style="bg:#888888", align=HorizontalAlign.CENTER, height=5, padding_char="|", ), ] ), # Right alignment. VSplit( [ Window( FormattedTextControl(HTML("<u>RIGHT</u>")), width=10, ignore_content_width=True, style="bg:#ff3333 ansiblack", align=WindowAlign.CENTER, ), VSplit( [ Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488", ), ], padding=1, padding_style="bg:#888888", align=HorizontalAlign.RIGHT, height=5, padding_char="|", ), ] ), # Justify VSplit( [ Window( FormattedTextControl(HTML("<u>JUSTIFY</u>")), width=10, ignore_content_width=True, style="bg:#ff3333 ansiblack", align=WindowAlign.CENTER, ), VSplit( [ Window( FormattedTextControl(LIPSUM), style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), style="bg:#444488" ), ], padding=1, padding_style="bg:#888888", align=HorizontalAlign.JUSTIFY, height=5, padding_char="|", ), ] ), ], padding=1, padding_style="bg:#ff3333 #ffffff", padding_char=".", align=VerticalAlign.TOP, ), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/horizontal-split.py ================================================ #!/usr/bin/env python """ Horizontal split example. """ from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout # 1. The layout left_text = "\nVertical-split example. Press 'q' to quit.\n\n(top pane.)" right_text = "\n(bottom pane.)" body = HSplit( [ Window(FormattedTextControl(left_text)), Window(height=1, char="-"), # Horizontal line in the middle. Window(FormattedTextControl(right_text)), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/line-prefixes.py ================================================ #!/usr/bin/env python """ An example of a BufferControl in a full screen layout that offers auto completion. Important is to make sure that there is a `CompletionsMenu` in the layout, otherwise the completions won't be visible. """ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.filters import Condition from prompt_toolkit.formatted_text import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import Float, FloatContainer, HSplit, Window from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.menus import CompletionsMenu LIPSUM = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus.""" def get_line_prefix(lineno, wrap_count): if wrap_count == 0: return HTML('[%s] <style bg="orange" fg="black">--></style> ') % lineno text = str(lineno) + "-" + "*" * (lineno // 2) + ": " return HTML('[%s.%s] <style bg="ansigreen" fg="ansiblack">%s</style>') % ( lineno, wrap_count, text, ) # Global wrap lines flag. wrap_lines = True # The layout buff = Buffer(complete_while_typing=True) buff.text = LIPSUM body = FloatContainer( content=HSplit( [ Window( FormattedTextControl( 'Press "q" to quit. Press "w" to enable/disable wrapping.' ), height=1, style="reverse", ), Window( BufferControl(buffer=buff), get_line_prefix=get_line_prefix, wrap_lines=Condition(lambda: wrap_lines), ), ] ), floats=[ Float( xcursor=True, ycursor=True, content=CompletionsMenu(max_height=16, scroll_offset=1), ) ], ) # Key bindings kb = KeyBindings() @kb.add("q") @kb.add("c-c") def _(event): "Quit application." event.app.exit() @kb.add("w") def _(event): "Disable/enable wrapping." global wrap_lines wrap_lines = not wrap_lines # The `Application` application = Application( layout=Layout(body), key_bindings=kb, full_screen=True, mouse_support=True ) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/margins.py ================================================ #!/usr/bin/env python """ Example of Window margins. This is mainly used for displaying line numbers and scroll bars, but it could be used to display any other kind of information as well. """ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, Window from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.margins import NumberedMargin, ScrollbarMargin LIPSUM = ( """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex quis sodales maximus.""" * 40 ) # Create text buffers. The margins will update if you scroll up or down. buff = Buffer() buff.text = LIPSUM # 1. The layout body = HSplit( [ Window(FormattedTextControl('Press "q" to quit.'), height=1, style="reverse"), Window( BufferControl(buffer=buff), # Add margins. left_margins=[NumberedMargin(), ScrollbarMargin()], right_margins=[ScrollbarMargin(), ScrollbarMargin()], ), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") @kb.add("c-c") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/vertical-align.py ================================================ #!/usr/bin/env python """ Vertical align demo with VSplit. """ from prompt_toolkit.application import Application from prompt_toolkit.formatted_text import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import ( HSplit, VerticalAlign, VSplit, Window, WindowAlign, ) from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.widgets import Frame TITLE = HTML( """ <u>VSplit VerticalAlign</u> example. Press <b>'q'</b> to quit.""" ) LIPSUM = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at dignissim placerat.""" # 1. The layout body = HSplit( [ Frame( Window(FormattedTextControl(TITLE), height=2), style="bg:#88ff88 #000000" ), VSplit( [ Window( FormattedTextControl(HTML(" <u>VerticalAlign.TOP</u>")), height=4, ignore_content_width=True, style="bg:#ff3333 #000000 bold", align=WindowAlign.CENTER, ), Window( FormattedTextControl(HTML(" <u>VerticalAlign.CENTER</u>")), height=4, ignore_content_width=True, style="bg:#ff3333 #000000 bold", align=WindowAlign.CENTER, ), Window( FormattedTextControl(HTML(" <u>VerticalAlign.BOTTOM</u>")), height=4, ignore_content_width=True, style="bg:#ff3333 #000000 bold", align=WindowAlign.CENTER, ), Window( FormattedTextControl(HTML(" <u>VerticalAlign.JUSTIFY</u>")), height=4, ignore_content_width=True, style="bg:#ff3333 #000000 bold", align=WindowAlign.CENTER, ), ], height=1, padding=1, padding_style="bg:#ff3333", ), VSplit( [ # Top alignment. HSplit( [ Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), ], padding=1, padding_style="bg:#888888", align=VerticalAlign.TOP, padding_char="~", ), # Center alignment. HSplit( [ Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), ], padding=1, padding_style="bg:#888888", align=VerticalAlign.CENTER, padding_char="~", ), # Bottom alignment. HSplit( [ Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), Window( FormattedTextControl(LIPSUM), height=4, style="bg:#444488" ), ], padding=1, padding_style="bg:#888888", align=VerticalAlign.BOTTOM, padding_char="~", ), # Justify HSplit( [ Window(FormattedTextControl(LIPSUM), style="bg:#444488"), Window(FormattedTextControl(LIPSUM), style="bg:#444488"), Window(FormattedTextControl(LIPSUM), style="bg:#444488"), ], padding=1, padding_style="bg:#888888", align=VerticalAlign.JUSTIFY, padding_char="~", ), ], padding=1, padding_style="bg:#ff3333 #ffffff", padding_char=".", ), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/simple-demos/vertical-split.py ================================================ #!/usr/bin/env python """ Vertical split example. """ from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import VSplit, Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout # 1. The layout left_text = "\nVertical-split example. Press 'q' to quit.\n\n(left pane.)" right_text = "\n(right pane.)" body = VSplit( [ Window(FormattedTextControl(left_text)), Window(width=1, char="|"), # Vertical line in the middle. Window(FormattedTextControl(right_text)), ] ) # 2. Key bindings kb = KeyBindings() @kb.add("q") def _(event): "Quit application." event.app.exit() # 3. The `Application` application = Application(layout=Layout(body), key_bindings=kb, full_screen=True) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/split-screen.py ================================================ #!/usr/bin/env python """ Simple example of a full screen application with a vertical split. This will show a window on the left for user input. When the user types, the reversed input is shown on the right. Pressing Ctrl-Q will quit the application. """ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, VSplit, Window, WindowAlign from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.layout import Layout # 3. Create the buffers # ------------------ left_buffer = Buffer() right_buffer = Buffer() # 1. First we create the layout # -------------------------- left_window = Window(BufferControl(buffer=left_buffer)) right_window = Window(BufferControl(buffer=right_buffer)) body = VSplit( [ left_window, # A vertical line in the middle. We explicitly specify the width, to make # sure that the layout engine will not try to divide the whole width by # three for all these windows. Window(width=1, char="|", style="class:line"), # Display the Result buffer on the right. right_window, ] ) # As a demonstration. Let's add a title bar to the top, displaying "Hello world". # somewhere, because usually the default key bindings include searching. (Press # Ctrl-R.) It would be really annoying if the search key bindings are handled, # but the user doesn't see any feedback. We will add the search toolbar to the # bottom by using an HSplit. def get_titlebar_text(): return [ ("class:title", " Hello world "), ("class:title", " (Press [Ctrl-Q] to quit.)"), ] root_container = HSplit( [ # The titlebar. Window( height=1, content=FormattedTextControl(get_titlebar_text), align=WindowAlign.CENTER, ), # Horizontal separator. Window(height=1, char="-", style="class:line"), # The 'body', like defined above. body, ] ) # 2. Adding key bindings # -------------------- # As a demonstration, we will add just a ControlQ key binding to exit the # application. Key bindings are registered in a # `prompt_toolkit.key_bindings.registry.Registry` instance. We use the # `load_default_key_bindings` utility function to create a registry that # already contains the default key bindings. kb = KeyBindings() # Now add the Ctrl-Q binding. We have to pass `eager=True` here. The reason is # that there is another key *sequence* that starts with Ctrl-Q as well. Yes, a # key binding is linked to a sequence of keys, not necessarily one key. So, # what happens if there is a key binding for the letter 'a' and a key binding # for 'ab'. When 'a' has been pressed, nothing will happen yet. Because the # next key could be a 'b', but it could as well be anything else. If it's a 'c' # for instance, we'll handle the key binding for 'a' and then look for a key # binding for 'c'. So, when there's a common prefix in a key binding sequence, # prompt-toolkit will wait calling a handler, until we have enough information. # Now, There is an Emacs key binding for the [Ctrl-Q Any] sequence by default. # Pressing Ctrl-Q followed by any other key will do a quoted insert. So to be # sure that we won't wait for that key binding to match, but instead execute # Ctrl-Q immediately, we can pass eager=True. (Don't make a habit of adding # `eager=True` to all key bindings, but do it when it conflicts with another # existing key binding, and you definitely want to override that behavior. @kb.add("c-c", eager=True) @kb.add("c-q", eager=True) def _(event): """ Pressing Ctrl-Q or Ctrl-C will exit the user interface. Setting a return value means: quit the event loop that drives the user interface and return this value from the `Application.run()` call. Note that Ctrl-Q does not work on all terminals. Sometimes it requires executing `stty -ixon`. """ event.app.exit() # Now we add an event handler that captures change events to the buffer on the # left. If the text changes over there, we'll update the buffer on the right. def default_buffer_changed(_): """ When the buffer on the left changes, update the buffer on the right. We just reverse the text. """ right_buffer.text = left_buffer.text[::-1] left_buffer.on_text_changed += default_buffer_changed # 3. Creating an `Application` instance # ---------------------------------- # This glues everything together. application = Application( layout=Layout(root_container, focused_element=left_window), key_bindings=kb, # Let's add mouse support! mouse_support=True, # Using an alternate screen buffer means as much as: "run full screen". # It switches the terminal to an alternate screen. full_screen=True, ) # 4. Run the application # ------------------- def run(): # Run the interface. (This runs the event loop until Ctrl-Q is pressed.) application.run() if __name__ == "__main__": run() ================================================ FILE: examples/full-screen/text-editor.py ================================================ #!/usr/bin/env python """ A simple example of a Notepad-like text editor. """ import datetime from asyncio import Future, ensure_future from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.completion import PathCompleter from prompt_toolkit.filters import Condition from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import ( ConditionalContainer, Float, HSplit, VSplit, Window, WindowAlign, ) from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.dimension import D from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.menus import CompletionsMenu from prompt_toolkit.lexers import DynamicLexer, PygmentsLexer from prompt_toolkit.search import start_search from prompt_toolkit.styles import Style from prompt_toolkit.widgets import ( Button, Dialog, Label, MenuContainer, MenuItem, SearchToolbar, TextArea, ) class ApplicationState: """ Application state. For the simplicity, we store this as a global, but better would be to instantiate this as an object and pass at around. """ show_status_bar = True current_path = None def get_statusbar_text(): return " Press Ctrl-C to open menu. " def get_statusbar_right_text(): return f" {text_field.document.cursor_position_row + 1}:{text_field.document.cursor_position_col + 1} " search_toolbar = SearchToolbar() text_field = TextArea( lexer=DynamicLexer( lambda: PygmentsLexer.from_filename( ApplicationState.current_path or ".txt", sync_from_start=False ) ), scrollbar=True, line_numbers=True, search_field=search_toolbar, ) class TextInputDialog: def __init__(self, title="", label_text="", completer=None): self.future = Future() def accept_text(buf): get_app().layout.focus(ok_button) buf.complete_state = None return True def accept(): self.future.set_result(self.text_area.text) def cancel(): self.future.set_result(None) self.text_area = TextArea( completer=completer, multiline=False, width=D(preferred=40), accept_handler=accept_text, ) ok_button = Button(text="OK", handler=accept) cancel_button = Button(text="Cancel", handler=cancel) self.dialog = Dialog( title=title, body=HSplit([Label(text=label_text), self.text_area]), buttons=[ok_button, cancel_button], width=D(preferred=80), modal=True, ) def __pt_container__(self): return self.dialog class MessageDialog: def __init__(self, title, text): self.future = Future() def set_done(): self.future.set_result(None) ok_button = Button(text="OK", handler=(lambda: set_done())) self.dialog = Dialog( title=title, body=HSplit([Label(text=text)]), buttons=[ok_button], width=D(preferred=80), modal=True, ) def __pt_container__(self): return self.dialog body = HSplit( [ text_field, search_toolbar, ConditionalContainer( content=VSplit( [ Window( FormattedTextControl(get_statusbar_text), style="class:status" ), Window( FormattedTextControl(get_statusbar_right_text), style="class:status.right", width=9, align=WindowAlign.RIGHT, ), ], height=1, ), filter=Condition(lambda: ApplicationState.show_status_bar), ), ] ) # Global key bindings. bindings = KeyBindings() @bindings.add("c-c") def _(event): "Focus menu." event.app.layout.focus(root_container.window) # # Handlers for menu items. # def do_open_file(): async def coroutine(): open_dialog = TextInputDialog( title="Open file", label_text="Enter the path of a file:", completer=PathCompleter(), ) path = await show_dialog_as_float(open_dialog) ApplicationState.current_path = path if path is not None: try: with open(path, "rb") as f: text_field.text = f.read().decode("utf-8", errors="ignore") except OSError as e: show_message("Error", f"{e}") ensure_future(coroutine()) def do_about(): show_message("About", "Text editor demo.\nCreated by Jonathan Slenders.") def show_message(title, text): async def coroutine(): dialog = MessageDialog(title, text) await show_dialog_as_float(dialog) ensure_future(coroutine()) async def show_dialog_as_float(dialog): "Coroutine." float_ = Float(content=dialog) root_container.floats.insert(0, float_) app = get_app() focused_before = app.layout.current_window app.layout.focus(dialog) result = await dialog.future app.layout.focus(focused_before) if float_ in root_container.floats: root_container.floats.remove(float_) return result def do_new_file(): text_field.text = "" def do_exit(): get_app().exit() def do_time_date(): text = datetime.datetime.now().isoformat() text_field.buffer.insert_text(text) def do_go_to(): async def coroutine(): dialog = TextInputDialog(title="Go to line", label_text="Line number:") line_number = await show_dialog_as_float(dialog) try: line_number = int(line_number) except ValueError: show_message("Invalid line number") else: text_field.buffer.cursor_position = ( text_field.buffer.document.translate_row_col_to_index( line_number - 1, 0 ) ) ensure_future(coroutine()) def do_undo(): text_field.buffer.undo() def do_cut(): data = text_field.buffer.cut_selection() get_app().clipboard.set_data(data) def do_copy(): data = text_field.buffer.copy_selection() get_app().clipboard.set_data(data) def do_delete(): text_field.buffer.cut_selection() def do_find(): start_search(text_field.control) def do_find_next(): search_state = get_app().current_search_state cursor_position = text_field.buffer.get_search_position( search_state, include_current_position=False ) text_field.buffer.cursor_position = cursor_position def do_paste(): text_field.buffer.paste_clipboard_data(get_app().clipboard.get_data()) def do_select_all(): text_field.buffer.cursor_position = 0 text_field.buffer.start_selection() text_field.buffer.cursor_position = len(text_field.buffer.text) def do_status_bar(): ApplicationState.show_status_bar = not ApplicationState.show_status_bar # # The menu container. # root_container = MenuContainer( body=body, menu_items=[ MenuItem( "File", children=[ MenuItem("New...", handler=do_new_file), MenuItem("Open...", handler=do_open_file), MenuItem("Save"), MenuItem("Save as..."), MenuItem("-", disabled=True), MenuItem("Exit", handler=do_exit), ], ), MenuItem( "Edit", children=[ MenuItem("Undo", handler=do_undo), MenuItem("Cut", handler=do_cut), MenuItem("Copy", handler=do_copy), MenuItem("Paste", handler=do_paste), MenuItem("Delete", handler=do_delete), MenuItem("-", disabled=True), MenuItem("Find", handler=do_find), MenuItem("Find next", handler=do_find_next), MenuItem("Replace"), MenuItem("Go To", handler=do_go_to), MenuItem("Select All", handler=do_select_all), MenuItem("Time/Date", handler=do_time_date), ], ), MenuItem( "View", children=[MenuItem("Status Bar", handler=do_status_bar)], ), MenuItem( "Info", children=[MenuItem("About", handler=do_about)], ), ], floats=[ Float( xcursor=True, ycursor=True, content=CompletionsMenu(max_height=16, scroll_offset=1), ), ], key_bindings=bindings, ) style = Style.from_dict( { "status": "reverse", "shadow": "bg:#440044", } ) layout = Layout(root_container, focused_element=text_field) application = Application( layout=layout, enable_page_navigation_bindings=True, style=style, mouse_support=True, full_screen=True, ) def run(): application.run() if __name__ == "__main__": run() ================================================ FILE: examples/gevent-get-input.py ================================================ #!/usr/bin/env python """ For testing: test to make sure that everything still works when gevent monkey patches are applied. """ from gevent.monkey import patch_all from prompt_toolkit.eventloop.defaults import create_event_loop from prompt_toolkit.shortcuts import PromptSession if __name__ == "__main__": # Apply patches. patch_all() # There were some issues in the past when the event loop had an input hook. def dummy_inputhook(*a): pass eventloop = create_event_loop(inputhook=dummy_inputhook) # Ask for input. session = PromptSession("Give me some input: ", loop=eventloop) answer = session.prompt() print(f"You said: {answer}") ================================================ FILE: examples/print-text/ansi-colors.py ================================================ #!/usr/bin/env python """ Demonstration of all the ANSI colors. """ from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import HTML, FormattedText print = print_formatted_text def main(): wide_space = ("", " ") space = ("", " ") print(HTML("\n<u>Foreground colors</u>")) print( FormattedText( [ ("ansiblack", "ansiblack"), wide_space, ("ansired", "ansired"), wide_space, ("ansigreen", "ansigreen"), wide_space, ("ansiyellow", "ansiyellow"), wide_space, ("ansiblue", "ansiblue"), wide_space, ("ansimagenta", "ansimagenta"), wide_space, ("ansicyan", "ansicyan"), wide_space, ("ansigray", "ansigray"), wide_space, ("", "\n"), ("ansibrightblack", "ansibrightblack"), space, ("ansibrightred", "ansibrightred"), space, ("ansibrightgreen", "ansibrightgreen"), space, ("ansibrightyellow", "ansibrightyellow"), space, ("ansibrightblue", "ansibrightblue"), space, ("ansibrightmagenta", "ansibrightmagenta"), space, ("ansibrightcyan", "ansibrightcyan"), space, ("ansiwhite", "ansiwhite"), space, ] ) ) print(HTML("\n<u>Background colors</u>")) print( FormattedText( [ ("bg:ansiblack ansiwhite", "ansiblack"), wide_space, ("bg:ansired", "ansired"), wide_space, ("bg:ansigreen", "ansigreen"), wide_space, ("bg:ansiyellow", "ansiyellow"), wide_space, ("bg:ansiblue ansiwhite", "ansiblue"), wide_space, ("bg:ansimagenta", "ansimagenta"), wide_space, ("bg:ansicyan", "ansicyan"), wide_space, ("bg:ansigray", "ansigray"), wide_space, ("", "\n"), ("bg:ansibrightblack", "ansibrightblack"), space, ("bg:ansibrightred", "ansibrightred"), space, ("bg:ansibrightgreen", "ansibrightgreen"), space, ("bg:ansibrightyellow", "ansibrightyellow"), space, ("bg:ansibrightblue", "ansibrightblue"), space, ("bg:ansibrightmagenta", "ansibrightmagenta"), space, ("bg:ansibrightcyan", "ansibrightcyan"), space, ("bg:ansiwhite", "ansiwhite"), space, ] ) ) print() if __name__ == "__main__": main() ================================================ FILE: examples/print-text/ansi.py ================================================ #!/usr/bin/env python """ Demonstration of how to print using ANSI escape sequences. The advantage here is that this is cross platform. The escape sequences will be parsed and turned into appropriate Win32 API calls on Windows. """ from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import ANSI, HTML print = print_formatted_text def title(text): print(HTML("\n<u><b>{}</b></u>").format(text)) def main(): title("Special formatting") print(ANSI(" \x1b[1mBold")) print(ANSI(" \x1b[6mBlink")) print(ANSI(" \x1b[3mItalic")) print(ANSI(" \x1b[7mReverse")) print(ANSI(" \x1b[4mUnderline")) print(ANSI(" \x1b[9mStrike")) print(ANSI(" \x1b[8mHidden\x1b[0m (Hidden)")) # Ansi colors. title("ANSI colors") print(ANSI(" \x1b[91mANSI Red")) print(ANSI(" \x1b[94mANSI Blue")) # Other named colors. title("Named colors") print(ANSI(" \x1b[38;5;214morange")) print(ANSI(" \x1b[38;5;90mpurple")) # Background colors. title("Background colors") print(ANSI(" \x1b[97;101mANSI Red")) print(ANSI(" \x1b[97;104mANSI Blue")) print() if __name__ == "__main__": main() ================================================ FILE: examples/print-text/html.py ================================================ #!/usr/bin/env python """ Demonstration of how to print using the HTML class. """ from prompt_toolkit import HTML, print_formatted_text print = print_formatted_text def title(text): print(HTML("\n<u><b>{}</b></u>").format(text)) def main(): title("Special formatting") print(HTML(" <b>Bold</b>")) print(HTML(" <blink>Blink</blink>")) print(HTML(" <i>Italic</i>")) print(HTML(" <reverse>Reverse</reverse>")) print(HTML(" <u>Underline</u>")) print(HTML(" <s>Strike</s>")) print(HTML(" <hidden>Hidden</hidden> (hidden)")) # Ansi colors. title("ANSI colors") print(HTML(" <ansired>ANSI Red</ansired>")) print(HTML(" <ansiblue>ANSI Blue</ansiblue>")) # Other named colors. title("Named colors") print(HTML(" <orange>orange</orange>")) print(HTML(" <purple>purple</purple>")) # Background colors. title("Background colors") print(HTML(' <style fg="ansiwhite" bg="ansired">ANSI Red</style>')) print(HTML(' <style fg="ansiwhite" bg="ansiblue">ANSI Blue</style>')) # Interpolation. title("HTML interpolation (see source)") print(HTML(" <i>{}</i>").format("<test>")) print(HTML(" <b>{text}</b>").format(text="<test>")) print(HTML(" <u>%s</u>") % ("<text>",)) print() if __name__ == "__main__": main() ================================================ FILE: examples/print-text/named-colors.py ================================================ #!/usr/bin/env python """ Demonstration of all the ANSI colors. """ from prompt_toolkit import HTML, print_formatted_text from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.output import ColorDepth from prompt_toolkit.styles.named_colors import NAMED_COLORS print = print_formatted_text def main(): tokens = FormattedText([("fg:" + name, name + " ") for name in NAMED_COLORS]) print(HTML("\n<u>Named colors, using 16 color output.</u>")) print("(Note that it doesn't really make sense to use named colors ") print("with only 16 color output.)") print(tokens, color_depth=ColorDepth.DEPTH_4_BIT) print(HTML("\n<u>Named colors, use 256 colors.</u>")) print(tokens) print(HTML("\n<u>Named colors, using True color output.</u>")) print(tokens, color_depth=ColorDepth.TRUE_COLOR) if __name__ == "__main__": main() ================================================ FILE: examples/print-text/print-formatted-text.py ================================================ #!/usr/bin/env python """ Example of printing colored text to the output. """ from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import ANSI, HTML, FormattedText from prompt_toolkit.styles import Style print = print_formatted_text def main(): style = Style.from_dict( { "hello": "#ff0066", "world": "#44ff44 italic", } ) # Print using a a list of text fragments. text_fragments = FormattedText( [ ("class:hello", "Hello "), ("class:world", "World"), ("", "\n"), ] ) print(text_fragments, style=style) # Print using an HTML object. print(HTML("<hello>hello</hello> <world>world</world>\n"), style=style) # Print using an HTML object with inline styling. print( HTML( '<style fg="#ff0066">hello</style> ' '<style fg="#44ff44"><i>world</i></style>\n' ) ) # Print using ANSI escape sequences. print(ANSI("\x1b[31mhello \x1b[32mworld\n")) if __name__ == "__main__": main() ================================================ FILE: examples/print-text/print-frame.py ================================================ #!/usr/bin/env python """ Example usage of 'print_container', a tool to print any layout in a non-interactive way. """ from prompt_toolkit.shortcuts import print_container from prompt_toolkit.widgets import Frame, TextArea print_container( Frame( TextArea(text="Hello world!\n"), title="Stage: parse", ) ) ================================================ FILE: examples/print-text/prompt-toolkit-logo-ansi-art.py ================================================ #!/usr/bin/env python r""" This prints the prompt_toolkit logo at the terminal. The ANSI output was generated using "pngtoansi": https://github.com/crgimenes/pngtoansi (ESC still had to be replaced with \x1b """ from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import ANSI print_formatted_text( ANSI( """ \x1b[48;2;0;0;0m \x1b[m \x1b[48;2;0;0;0m \x1b[48;2;0;249;0m\x1b[38;2;0;0;0m▀\x1b[48;2;0;209;0m▀\x1b[48;2;0;207;0m\x1b[38;2;6;34;6m▀\x1b[48;2;0;66;0m\x1b[38;2;30;171;30m▀\x1b[48;2;0;169;0m\x1b[38;2;51;35;51m▀\x1b[48;2;0;248;0m\x1b[38;2;49;194;49m▀\x1b[48;2;0;111;0m\x1b[38;2;25;57;25m▀\x1b[48;2;140;195;140m\x1b[38;2;3;17;3m▀\x1b[48;2;30;171;30m\x1b[38;2;0;0;0m▀\x1b[48;2;0;0;0m \x1b[m \x1b[48;2;0;0;0m \x1b[48;2;77;127;78m\x1b[38;2;118;227;108m▀\x1b[48;2;216;1;13m\x1b[38;2;49;221;57m▀\x1b[48;2;26;142;76m\x1b[38;2;108;146;165m▀\x1b[48;2;26;142;90m\x1b[38;2;209;197;114m▀▀\x1b[38;2;209;146;114m▀\x1b[48;2;26;128;90m\x1b[38;2;158;197;114m▀\x1b[48;2;58;210;70m\x1b[38;2;223;152;89m▀\x1b[48;2;232;139;44m\x1b[38;2;97;121;146m▀\x1b[48;2;233;139;45m\x1b[38;2;140;188;183m▀\x1b[48;2;231;139;44m\x1b[38;2;40;168;8m▀\x1b[48;2;228;140;44m\x1b[38;2;37;169;7m▀\x1b[48;2;227;140;44m\x1b[38;2;36;169;7m▀\x1b[48;2;211;142;41m\x1b[38;2;23;171;5m▀\x1b[48;2;86;161;17m\x1b[38;2;2;174;1m▀\x1b[48;2;0;175;0m \x1b[48;2;0;254;0m\x1b[38;2;190;119;190m▀\x1b[48;2;92;39;23m\x1b[38;2;125;50;114m▀\x1b[48;2;43;246;41m\x1b[38;2;49;10;165m▀\x1b[48;2;12;128;90m\x1b[38;2;209;197;114m▀\x1b[48;2;26;128;90m▀▀▀▀\x1b[48;2;26;128;76m▀\x1b[48;2;26;128;90m\x1b[38;2;209;247;114m▀▀\x1b[38;2;209;197;114m▀\x1b[48;2;26;128;76m\x1b[38;2;209;247;114m▀\x1b[48;2;26;128;90m▀▀▀\x1b[48;2;26;128;76m▀\x1b[48;2;26;128;90m▀▀\x1b[48;2;12;128;76m▀\x1b[48;2;12;113;90m\x1b[38;2;209;247;64m▀\x1b[38;2;209;247;114m▀\x1b[48;2;12;128;90m▀\x1b[48;2;12;113;90m▀\x1b[48;2;12;113;76m\x1b[38;2;209;247;64m▀\x1b[48;2;12;128;90m▀\x1b[48;2;12;113;90m▀\x1b[48;2;12;113;76m\x1b[38;2;209;247;114m▀\x1b[48;2;12;113;90m\x1b[38;2;209;247;64m▀\x1b[48;2;26;128;90m\x1b[38;2;151;129;163m▀\x1b[48;2;115;120;103m\x1b[38;2;62;83;227m▀\x1b[48;2;138;14;25m\x1b[38;2;104;106;160m▀\x1b[48;2;0;0;57m\x1b[38;2;0;0;0m▀\x1b[m \x1b[48;2;249;147;8m\x1b[38;2;172;69;38m▀\x1b[48;2;197;202;10m\x1b[38;2;82;192;58m▀\x1b[48;2;248;124;45m\x1b[38;2;251;131;47m▀\x1b[48;2;248;124;44m▀\x1b[48;2;248;124;45m▀▀\x1b[48;2;248;124;44m▀\x1b[48;2;248;124;45m▀\x1b[48;2;248;125;45m\x1b[38;2;251;130;47m▀\x1b[48;2;248;124;45m\x1b[38;2;252;130;47m▀\x1b[48;2;248;125;45m\x1b[38;2;252;131;47m▀\x1b[38;2;252;130;47m▀\x1b[38;2;252;131;47m▀▀\x1b[48;2;249;125;45m\x1b[38;2;255;130;48m▀\x1b[48;2;233;127;42m\x1b[38;2;190;141;35m▀\x1b[48;2;57;163;10m\x1b[38;2;13;172;3m▀\x1b[48;2;0;176;0m\x1b[38;2;0;175;0m▀\x1b[48;2;7;174;1m\x1b[38;2;35;169;7m▀\x1b[48;2;178;139;32m\x1b[38;2;220;136;41m▀\x1b[48;2;252;124;45m\x1b[38;2;253;131;47m▀\x1b[48;2;248;125;45m\x1b[38;2;251;131;47m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;248;125;44m▀\x1b[48;2;248;135;61m\x1b[38;2;251;132;48m▀\x1b[48;2;250;173;122m\x1b[38;2;251;133;50m▀\x1b[48;2;249;155;93m\x1b[38;2;251;132;49m▀\x1b[48;2;248;132;55m\x1b[38;2;251;132;48m▀\x1b[48;2;250;173;122m\x1b[38;2;251;134;51m▀\x1b[48;2;250;163;106m\x1b[38;2;251;134;50m▀\x1b[48;2;248;128;49m\x1b[38;2;251;132;47m▀\x1b[48;2;250;166;110m\x1b[38;2;251;135;52m▀\x1b[48;2;250;175;125m\x1b[38;2;251;136;54m▀\x1b[48;2;248;132;56m\x1b[38;2;251;132;48m▀\x1b[48;2;248;220;160m\x1b[38;2;105;247;172m▀\x1b[48;2;62;101;236m\x1b[38;2;11;207;160m▀\x1b[m \x1b[48;2;138;181;197m\x1b[38;2;205;36;219m▀\x1b[48;2;177;211;200m\x1b[38;2;83;231;105m▀\x1b[48;2;242;113;40m\x1b[38;2;245;119;42m▀\x1b[48;2;243;113;41m▀\x1b[48;2;245;114;41m▀▀▀▀▀▀▀▀\x1b[38;2;245;119;43m▀▀▀\x1b[48;2;247;114;41m\x1b[38;2;246;119;43m▀\x1b[48;2;202;125;34m\x1b[38;2;143;141;25m▀\x1b[48;2;84;154;14m\x1b[38;2;97;152;17m▀\x1b[48;2;36;166;6m▀\x1b[48;2;139;140;23m\x1b[38;2;183;133;32m▀\x1b[48;2;248;114;41m\x1b[38;2;248;118;43m▀\x1b[48;2;245;115;41m\x1b[38;2;245;119;43m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[38;2;245;119;42m▀\x1b[48;2;246;117;44m\x1b[38;2;246;132;62m▀\x1b[48;2;246;123;54m\x1b[38;2;249;180;138m▀\x1b[48;2;246;120;49m\x1b[38;2;247;157;102m▀\x1b[48;2;246;116;42m\x1b[38;2;246;127;54m▀\x1b[48;2;246;121;50m\x1b[38;2;248;174;128m▀\x1b[48;2;246;120;48m\x1b[38;2;248;162;110m▀\x1b[48;2;246;116;41m\x1b[38;2;245;122;47m▀\x1b[48;2;246;118;46m\x1b[38;2;248;161;108m▀\x1b[48;2;244;118;47m\x1b[38;2;248;171;123m▀\x1b[48;2;243;115;42m\x1b[38;2;246;127;54m▀\x1b[48;2;179;52;29m\x1b[38;2;86;152;223m▀\x1b[48;2;141;225;95m\x1b[38;2;247;146;130m▀\x1b[m \x1b[48;2;50;237;108m\x1b[38;2;94;70;153m▀\x1b[48;2;206;221;133m\x1b[38;2;64;240;39m▀\x1b[48;2;233;100;36m\x1b[38;2;240;107;38m▀\x1b[48;2;114;56;22m\x1b[38;2;230;104;37m▀\x1b[48;2;24;20;10m\x1b[38;2;193;90;33m▀\x1b[48;2;21;19;9m\x1b[38;2;186;87;32m▀▀▀▀▀▀▀\x1b[38;2;186;87;33m▀▀▀\x1b[48;2;22;18;10m\x1b[38;2;189;86;33m▀\x1b[48;2;18;36;8m\x1b[38;2;135;107;24m▀\x1b[48;2;3;153;2m\x1b[38;2;5;171;1m▀\x1b[48;2;0;177;0m \x1b[48;2;4;158;2m\x1b[38;2;69;147;12m▀\x1b[48;2;19;45;8m\x1b[38;2;185;89;32m▀\x1b[48;2;22;17;10m\x1b[38;2;186;87;33m▀\x1b[48;2;21;19;9m▀▀▀▀▀▀▀▀\x1b[48;2;21;19;10m▀▀\x1b[48;2;21;19;9m▀▀▀▀\x1b[48;2;21;19;10m▀▀▀\x1b[38;2;186;87;32m▀▀\x1b[48;2;21;19;9m\x1b[38;2;186;87;33m▀\x1b[48;2;21;19;10m\x1b[38;2;186;87;32m▀▀\x1b[48;2;21;19;9m\x1b[38;2;186;87;33m▀\x1b[48;2;22;19;10m\x1b[38;2;191;89;33m▀\x1b[48;2;95;49;20m\x1b[38;2;226;103;37m▀\x1b[48;2;227;99;36m\x1b[38;2;241;109;39m▀\x1b[48;2;80;140;154m\x1b[38;2;17;240;92m▀\x1b[48;2;221;58;175m\x1b[38;2;71;14;245m▀\x1b[m \x1b[48;2;195;38;42m\x1b[38;2;5;126;86m▀\x1b[48;2;139;230;67m\x1b[38;2;253;201;228m▀\x1b[48;2;208;82;30m\x1b[38;2;213;89;32m▀\x1b[48;2;42;26;12m\x1b[38;2;44;27;12m▀\x1b[48;2;9;14;7m\x1b[38;2;8;13;7m▀\x1b[48;2;11;15;8m\x1b[38;2;10;14;7m▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;11;12;8m\x1b[38;2;10;17;7m▀\x1b[48;2;7;71;5m\x1b[38;2;4;120;3m▀\x1b[48;2;1;164;1m\x1b[38;2;0;178;0m▀\x1b[48;2;4;118;3m\x1b[38;2;0;177;0m▀\x1b[48;2;5;108;3m\x1b[38;2;4;116;3m▀\x1b[48;2;7;75;5m\x1b[38;2;10;23;7m▀\x1b[48;2;10;33;7m\x1b[38;2;10;12;7m▀\x1b[48;2;11;13;8m\x1b[38;2;10;14;7m▀\x1b[48;2;11;14;8m▀\x1b[48;2;11;15;8m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;10;14;7m\x1b[38;2;9;14;7m▀\x1b[48;2;30;21;10m\x1b[38;2;30;22;10m▀\x1b[48;2;195;79;29m\x1b[38;2;200;84;31m▀\x1b[48;2;205;228;23m\x1b[38;2;111;40;217m▀\x1b[48;2;9;217;69m\x1b[38;2;115;137;104m▀\x1b[m \x1b[48;2;106;72;209m\x1b[38;2;151;183;253m▀\x1b[48;2;120;239;0m\x1b[38;2;25;2;162m▀\x1b[48;2;203;72;26m\x1b[38;2;206;77;28m▀\x1b[48;2;42;24;11m\x1b[38;2;42;25;11m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;13;8m\x1b[38;2;10;28;7m▀\x1b[48;2;9;36;6m\x1b[38;2;7;78;5m▀\x1b[48;2;2;153;1m\x1b[38;2;6;94;4m▀\x1b[48;2;0;178;0m\x1b[38;2;2;156;1m▀\x1b[48;2;0;175;0m\x1b[38;2;1;167;1m▀\x1b[48;2;0;177;0m\x1b[38;2;2;145;2m▀\x1b[48;2;2;147;2m\x1b[38;2;8;54;6m▀\x1b[48;2;9;38;6m\x1b[38;2;11;13;8m▀\x1b[48;2;11;13;8m\x1b[38;2;11;14;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;29;20;10m\x1b[38;2;29;21;10m▀\x1b[48;2;190;69;25m\x1b[38;2;193;74;27m▀\x1b[48;2;136;91;148m\x1b[38;2;42;159;86m▀\x1b[48;2;89;85;149m\x1b[38;2;160;5;219m▀\x1b[m \x1b[48;2;229;106;143m\x1b[38;2;40;239;187m▀\x1b[48;2;196;134;237m\x1b[38;2;6;11;95m▀\x1b[48;2;197;60;22m\x1b[38;2;201;67;24m▀\x1b[48;2;41;22;10m\x1b[38;2;41;23;11m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[48;2;10;14;7m\x1b[38;2;11;15;8m▀▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;16;7m▀\x1b[48;2;11;15;7m\x1b[38;2;7;79;5m▀\x1b[48;2;7;68;5m\x1b[38;2;1;164;1m▀\x1b[48;2;2;153;1m\x1b[38;2;0;176;0m▀\x1b[48;2;2;154;1m\x1b[38;2;0;175;0m▀\x1b[48;2;5;107;3m\x1b[38;2;1;171;1m▀\x1b[48;2;4;115;3m\x1b[38;2;5;105;3m▀\x1b[48;2;6;84;4m\x1b[38;2;11;18;7m▀\x1b[48;2;10;30;7m\x1b[38;2;11;13;8m▀\x1b[48;2;11;13;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;14;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;29;19;9m\x1b[38;2;29;20;10m▀\x1b[48;2;185;58;22m\x1b[38;2;188;64;24m▀\x1b[48;2;68;241;49m\x1b[38;2;199;22;211m▀\x1b[48;2;133;139;8m\x1b[38;2;239;129;78m▀\x1b[m \x1b[48;2;74;30;32m\x1b[38;2;163;185;76m▀\x1b[48;2;110;172;9m\x1b[38;2;177;1;123m▀\x1b[48;2;189;43;16m\x1b[38;2;193;52;19m▀\x1b[48;2;39;20;9m\x1b[38;2;40;21;10m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;9;14;7m\x1b[38;2;11;14;8m▀\x1b[48;2;106;54;38m\x1b[38;2;31;24;15m▀\x1b[48;2;164;71;49m\x1b[38;2;24;20;12m▀\x1b[48;2;94;46;31m\x1b[38;2;8;14;7m▀\x1b[48;2;36;24;15m\x1b[38;2;9;14;7m▀\x1b[48;2;11;15;8m\x1b[38;2;11;14;7m▀\x1b[48;2;8;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;10;14;7m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;13;8m▀\x1b[48;2;11;13;8m\x1b[38;2;9;45;6m▀\x1b[48;2;10;19;7m\x1b[38;2;7;75;5m▀\x1b[48;2;6;83;4m\x1b[38;2;2;143;2m▀\x1b[48;2;2;156;1m\x1b[38;2;0;176;0m▀\x1b[48;2;0;177;0m\x1b[38;2;0;175;0m▀\x1b[38;2;3;134;2m▀\x1b[48;2;2;152;1m\x1b[38;2;9;46;6m▀\x1b[48;2;8;60;5m\x1b[38;2;11;13;8m▀\x1b[48;2;11;14;7m\x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;28;18;9m \x1b[48;2;177;43;16m\x1b[38;2;181;51;19m▀\x1b[48;2;93;35;236m\x1b[38;2;224;10;142m▀\x1b[48;2;72;51;52m\x1b[38;2;213;112;158m▀\x1b[m \x1b[48;2;175;209;155m\x1b[38;2;7;131;221m▀\x1b[48;2;24;0;85m\x1b[38;2;44;86;152m▀\x1b[48;2;181;27;10m\x1b[38;2;185;35;13m▀\x1b[48;2;38;17;8m\x1b[38;2;39;18;9m▀\x1b[48;2;9;14;7m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m \x1b[48;2;9;14;7m \x1b[48;2;87;43;32m\x1b[38;2;114;54;39m▀\x1b[48;2;188;71;54m\x1b[38;2;211;82;59m▀\x1b[48;2;203;73;55m\x1b[38;2;204;80;57m▀\x1b[48;2;205;73;55m\x1b[38;2;178;71;51m▀\x1b[48;2;204;74;55m\x1b[38;2;119;52;37m▀\x1b[48;2;188;69;52m\x1b[38;2;54;29;19m▀\x1b[48;2;141;55;41m\x1b[38;2;16;17;9m▀\x1b[48;2;75;35;24m\x1b[38;2;8;14;7m▀\x1b[48;2;26;20;12m\x1b[38;2;10;14;7m▀\x1b[48;2;9;14;7m\x1b[38;2;11;14;7m▀\x1b[38;2;11;15;8m▀\x1b[48;2;11;14;7m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m \x1b[48;2;11;13;8m\x1b[38;2;9;45;6m▀\x1b[48;2;10;23;7m\x1b[38;2;4;123;3m▀\x1b[48;2;7;75;5m\x1b[38;2;1;172;1m▀\x1b[48;2;6;84;4m\x1b[38;2;2;154;1m▀\x1b[48;2;4;114;3m\x1b[38;2;5;107;3m▀\x1b[48;2;5;103;4m\x1b[38;2;10;29;7m▀\x1b[48;2;10;23;7m\x1b[38;2;11;13;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;27;16;8m\x1b[38;2;27;17;9m▀\x1b[48;2;170;27;10m\x1b[38;2;174;35;13m▀\x1b[48;2;118;117;199m\x1b[38;2;249;61;74m▀\x1b[48;2;10;219;61m\x1b[38;2;187;245;202m▀\x1b[m \x1b[48;2;20;155;44m\x1b[38;2;86;54;110m▀\x1b[48;2;195;85;113m\x1b[38;2;214;171;227m▀\x1b[48;2;173;10;4m\x1b[38;2;177;19;7m▀\x1b[48;2;37;14;7m\x1b[38;2;37;16;8m▀\x1b[48;2;9;15;8m\x1b[38;2;9;14;7m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;7m▀\x1b[48;2;11;14;7m\x1b[38;2;15;17;9m▀\x1b[48;2;9;14;7m\x1b[38;2;50;29;20m▀\x1b[48;2;10;15;8m\x1b[38;2;112;47;36m▀\x1b[48;2;33;22;15m\x1b[38;2;170;61;48m▀\x1b[48;2;88;38;29m\x1b[38;2;197;66;53m▀\x1b[48;2;151;53;43m\x1b[38;2;201;67;53m▀\x1b[48;2;189;60;50m▀\x1b[48;2;198;60;51m\x1b[38;2;194;65;52m▀\x1b[38;2;160;56;44m▀\x1b[48;2;196;60;50m\x1b[38;2;99;40;30m▀\x1b[48;2;174;55;47m\x1b[38;2;41;24;16m▀\x1b[48;2;122;43;35m\x1b[38;2;12;15;8m▀\x1b[48;2;59;27;20m\x1b[38;2;8;14;7m▀\x1b[48;2;16;16;9m\x1b[38;2;10;14;7m▀\x1b[48;2;10;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[38;2;11;14;8m▀\x1b[48;2;11;14;8m\x1b[38;2;11;12;8m▀\x1b[48;2;10;25;7m\x1b[38;2;7;79;5m▀\x1b[48;2;3;141;2m\x1b[38;2;1;174;1m▀\x1b[48;2;0;178;0m\x1b[38;2;1;169;1m▀\x1b[48;2;6;88;4m\x1b[38;2;8;56;6m▀\x1b[48;2;11;12;8m \x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;14;7m \x1b[48;2;26;15;8m\x1b[38;2;27;15;8m▀\x1b[48;2;162;12;5m\x1b[38;2;166;20;8m▀\x1b[48;2;143;168;130m\x1b[38;2;18;142;37m▀\x1b[48;2;240;96;105m\x1b[38;2;125;158;211m▀\x1b[m \x1b[48;2;54;0;0m\x1b[38;2;187;22;0m▀\x1b[48;2;204;0;0m\x1b[38;2;128;208;0m▀\x1b[48;2;162;1;1m\x1b[38;2;168;3;1m▀\x1b[48;2;35;13;7m\x1b[38;2;36;13;7m▀\x1b[48;2;9;15;8m \x1b[48;2;11;15;8m \x1b[38;2;11;14;7m▀\x1b[38;2;9;14;7m▀\x1b[38;2;8;14;7m▀\x1b[48;2;10;14;7m\x1b[38;2;21;18;11m▀\x1b[48;2;7;13;6m\x1b[38;2;65;30;23m▀\x1b[48;2;12;16;9m\x1b[38;2;129;45;38m▀\x1b[48;2;57;29;23m\x1b[38;2;176;53;47m▀\x1b[48;2;148;49;44m\x1b[38;2;191;53;48m▀\x1b[48;2;187;52;48m\x1b[38;2;192;53;48m▀\x1b[48;2;186;51;47m\x1b[38;2;194;54;49m▀\x1b[48;2;182;52;47m\x1b[38;2;178;52;46m▀\x1b[48;2;59;27;21m\x1b[38;2;53;26;19m▀\x1b[48;2;8;14;7m \x1b[48;2;11;15;8m \x1b[48;2;11;14;8m\x1b[38;2;11;15;8m▀\x1b[48;2;11;12;8m\x1b[38;2;11;14;8m▀\x1b[48;2;10;30;7m\x1b[38;2;10;23;7m▀\x1b[48;2;5;110;3m\x1b[38;2;3;138;2m▀\x1b[48;2;2;149;2m\x1b[38;2;0;181;0m▀\x1b[48;2;6;92;4m\x1b[38;2;5;100;4m▀\x1b[48;2;11;13;8m \x1b[48;2;11;14;8m \x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;25;14;7m\x1b[38;2;26;14;7m▀\x1b[48;2;152;2;1m\x1b[38;2;158;5;2m▀\x1b[48;2;6;0;0m\x1b[38;2;44;193;0m▀\x1b[48;2;108;0;0m\x1b[38;2;64;70;0m▀\x1b[m \x1b[48;2;44;0;0m\x1b[38;2;177;0;0m▀\x1b[48;2;147;0;0m\x1b[38;2;71;0;0m▀\x1b[48;2;148;1;1m\x1b[38;2;155;1;1m▀\x1b[48;2;33;13;7m\x1b[38;2;34;13;7m▀\x1b[48;2;9;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;10;14;7m▀\x1b[48;2;9;14;7m▀\x1b[48;2;13;16;9m\x1b[38;2;11;14;7m▀\x1b[48;2;42;24;17m\x1b[38;2;9;14;7m▀\x1b[48;2;97;38;32m\x1b[38;2;10;15;8m▀\x1b[48;2;149;49;44m\x1b[38;2;30;21;14m▀\x1b[48;2;174;52;48m\x1b[38;2;79;34;28m▀\x1b[48;2;178;52;48m\x1b[38;2;136;45;40m▀\x1b[38;2;172;51;47m▀\x1b[48;2;173;52;48m\x1b[38;2;181;52;48m▀\x1b[48;2;147;47;42m\x1b[38;2;183;52;48m▀\x1b[48;2;94;35;30m\x1b[38;2;177;52;48m▀\x1b[48;2;25;19;12m\x1b[38;2;56;27;20m▀\x1b[48;2;10;14;7m\x1b[38;2;8;14;7m▀\x1b[48;2;11;12;8m\x1b[38;2;11;15;8m▀\x1b[48;2;10;23;7m\x1b[38;2;11;14;8m▀\x1b[48;2;7;76;5m\x1b[38;2;11;13;8m▀\x1b[48;2;2;152;1m\x1b[38;2;9;45;6m▀\x1b[48;2;0;177;0m\x1b[38;2;5;106;3m▀\x1b[48;2;0;178;0m\x1b[38;2;4;123;3m▀\x1b[48;2;1;168;1m\x1b[38;2;5;104;3m▀\x1b[48;2;8;53;6m\x1b[38;2;9;47;6m▀\x1b[48;2;11;12;8m\x1b[38;2;11;13;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;24;14;7m\x1b[38;2;25;14;7m▀\x1b[48;2;140;2;1m\x1b[38;2;146;2;1m▀\x1b[48;2;219;0;0m\x1b[38;2;225;0;0m▀\x1b[48;2;126;0;0m\x1b[38;2;117;0;0m▀\x1b[m \x1b[48;2;34;0;0m\x1b[38;2;167;0;0m▀\x1b[48;2;89;0;0m\x1b[38;2;14;0;0m▀\x1b[48;2;134;1;1m\x1b[38;2;141;1;1m▀\x1b[48;2;31;13;7m\x1b[38;2;32;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀\x1b[48;2;10;14;7m\x1b[38;2;11;14;7m▀\x1b[48;2;53;29;22m\x1b[38;2;10;14;7m▀\x1b[48;2;127;46;41m\x1b[38;2;20;18;11m▀\x1b[48;2;158;51;47m\x1b[38;2;57;28;22m▀\x1b[48;2;166;52;48m\x1b[38;2;113;42;36m▀\x1b[48;2;167;52;48m\x1b[38;2;156;50;46m▀\x1b[48;2;164;52;48m\x1b[38;2;171;52;48m▀\x1b[48;2;146;48;44m\x1b[38;2;172;52;48m▀\x1b[48;2;102;38;33m▀\x1b[48;2;50;26;19m\x1b[38;2;161;51;46m▀\x1b[48;2;17;17;10m\x1b[38;2;126;44;38m▀\x1b[48;2;8;14;7m\x1b[38;2;71;31;25m▀\x1b[48;2;10;14;7m\x1b[38;2;27;19;13m▀\x1b[48;2;11;13;8m\x1b[38;2;10;14;7m▀\x1b[48;2;9;40;6m\x1b[38;2;10;13;7m▀\x1b[48;2;4;119;3m\x1b[38;2;11;20;7m▀\x1b[48;2;1;168;1m\x1b[38;2;8;63;5m▀\x1b[48;2;0;177;0m\x1b[38;2;3;130;2m▀\x1b[48;2;0;175;0m\x1b[38;2;1;171;1m▀\x1b[48;2;1;174;1m\x1b[38;2;0;176;0m▀\x1b[48;2;1;175;1m\x1b[38;2;1;174;1m▀\x1b[48;2;0;177;0m\x1b[38;2;0;176;0m▀\x1b[48;2;3;134;2m\x1b[38;2;2;158;1m▀\x1b[48;2;10;21;7m\x1b[38;2;9;38;6m▀\x1b[48;2;11;14;8m\x1b[38;2;11;13;8m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;23;14;7m \x1b[48;2;127;2;1m\x1b[38;2;133;2;1m▀\x1b[48;2;176;0;0m\x1b[38;2;213;0;0m▀\x1b[48;2;109;0;0m\x1b[38;2;100;0;0m▀\x1b[m \x1b[48;2;24;0;0m\x1b[38;2;157;0;0m▀\x1b[48;2;32;0;0m\x1b[38;2;165;0;0m▀\x1b[48;2;121;1;1m\x1b[38;2;128;1;1m▀\x1b[48;2;28;13;7m\x1b[38;2;30;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m \x1b[48;2;9;15;7m \x1b[48;2;88;41;34m\x1b[38;2;91;41;34m▀\x1b[48;2;145;51;47m\x1b[38;2;163;53;49m▀\x1b[48;2;107;42;36m\x1b[38;2;161;52;48m▀\x1b[48;2;58;29;22m\x1b[38;2;155;51;47m▀\x1b[48;2;21;18;11m\x1b[38;2;128;45;40m▀\x1b[48;2;9;14;7m\x1b[38;2;79;33;27m▀\x1b[38;2;33;21;15m▀\x1b[48;2;11;14;7m\x1b[38;2;12;15;8m▀\x1b[48;2;11;15;8m\x1b[38;2;9;14;7m▀\x1b[38;2;10;14;7m▀ \x1b[48;2;11;12;8m\x1b[38;2;11;14;8m▀\x1b[48;2;8;54;6m\x1b[38;2;10;28;7m▀\x1b[48;2;6;93;4m\x1b[38;2;4;125;3m▀\x1b[48;2;2;152;1m\x1b[38;2;0;175;0m▀\x1b[48;2;0;176;0m▀\x1b[48;2;0;175;0m\x1b[38;2;1;174;1m▀\x1b[48;2;0;177;0m\x1b[38;2;1;175;1m▀\x1b[48;2;0;175;0m▀▀\x1b[48;2;1;162;1m\x1b[38;2;0;176;0m▀\x1b[48;2;9;47;6m\x1b[38;2;6;95;4m▀\x1b[48;2;11;13;8m \x1b[48;2;11;15;8m\x1b[38;2;11;14;8m▀ \x1b[48;2;10;15;8m \x1b[48;2;21;13;7m\x1b[38;2;22;13;7m▀\x1b[48;2;114;2;1m\x1b[38;2;121;2;1m▀\x1b[48;2;164;0;0m\x1b[38;2;170;0;0m▀\x1b[48;2;127;0;0m\x1b[38;2;118;0;0m▀\x1b[m \x1b[48;2;14;0;0m\x1b[38;2;147;0;0m▀\x1b[48;2;183;0;0m\x1b[38;2;108;0;0m▀\x1b[48;2;107;1;1m\x1b[38;2;114;1;1m▀\x1b[48;2;26;13;7m\x1b[38;2;27;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[38;2;11;14;7m▀ \x1b[48;2;10;14;7m\x1b[38;2;43;27;20m▀\x1b[48;2;9;14;7m\x1b[38;2;42;25;18m▀\x1b[48;2;11;14;7m\x1b[38;2;14;16;9m▀\x1b[48;2;11;15;8m\x1b[38;2;9;14;7m▀\x1b[38;2;10;14;7m▀\x1b[38;2;11;14;7m▀ \x1b[48;2;11;12;8m \x1b[48;2;9;49;6m\x1b[38;2;8;64;5m▀\x1b[48;2;1;166;1m\x1b[38;2;1;159;1m▀\x1b[48;2;0;175;0m\x1b[38;2;1;171;1m▀ \x1b[48;2;1;159;1m\x1b[38;2;1;167;1m▀\x1b[48;2;7;79;5m\x1b[38;2;4;122;3m▀\x1b[48;2;2;144;2m\x1b[38;2;2;158;1m▀\x1b[48;2;0;158;1m\x1b[38;2;0;177;0m▀\x1b[48;2;7;44;6m\x1b[38;2;4;112;3m▀\x1b[48;2;9;12;7m\x1b[38;2;11;17;7m▀\x1b[48;2;9;14;7m\x1b[38;2;11;14;8m▀\x1b[38;2;11;15;8m▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;11;14;7m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;20;13;7m\x1b[38;2;21;13;7m▀\x1b[48;2;102;2;1m\x1b[38;2;108;2;1m▀\x1b[48;2;121;0;0m\x1b[38;2;127;0;0m▀\x1b[48;2;146;0;0m\x1b[38;2;136;0;0m▀\x1b[m \x1b[48;2;3;0;0m\x1b[38;2;137;0;0m▀\x1b[48;2;173;0;0m\x1b[38;2;50;0;0m▀\x1b[48;2;93;1;1m\x1b[38;2;100;1;1m▀\x1b[48;2;24;13;7m\x1b[38;2;25;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;11;15;8m▀▀\x1b[48;2;17;14;7m\x1b[38;2;11;14;8m▀\x1b[48;2;49;12;7m\x1b[38;2;9;24;7m▀\x1b[48;2;62;54;4m\x1b[38;2;8;133;2m▀\x1b[48;2;7;159;1m\x1b[38;2;2;176;0m▀\x1b[48;2;0;175;0m \x1b[48;2;1;172;1m\x1b[38;2;0;175;0m▀\x1b[48;2;1;159;1m\x1b[38;2;0;173;1m▀\x1b[48;2;46;122;19m\x1b[38;2;1;176;0m▀\x1b[48;2;122;63;45m\x1b[38;2;45;111;18m▀\x1b[48;2;135;52;49m\x1b[38;2;75;36;31m▀\x1b[48;2;135;53;49m\x1b[38;2;74;36;30m▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;136;53;49m\x1b[38;2;75;37;31m▀\x1b[48;2;119;49;45m\x1b[38;2;66;34;28m▀\x1b[48;2;25;20;13m\x1b[38;2;18;18;11m▀\x1b[48;2;10;14;7m \x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;19;13;7m \x1b[48;2;89;2;1m\x1b[38;2;95;2;1m▀\x1b[48;2;77;0;0m\x1b[38;2;83;0;0m▀\x1b[48;2;128;0;0m\x1b[38;2;119;0;0m▀\x1b[m \x1b[48;2;60;0;0m\x1b[38;2;126;0;0m▀\x1b[48;2;182;0;0m\x1b[38;2;249;0;0m▀\x1b[48;2;83;1;1m\x1b[38;2;87;1;1m▀\x1b[48;2;22;13;7m\x1b[38;2;23;13;7m▀\x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;11;14;7m\x1b[38;2;16;14;7m▀\x1b[48;2;14;14;7m\x1b[38;2;42;13;7m▀\x1b[48;2;58;13;6m\x1b[38;2;95;11;5m▀\x1b[48;2;34;13;7m\x1b[38;2;100;11;5m▀\x1b[48;2;9;14;7m\x1b[38;2;21;17;7m▀\x1b[48;2;11;12;8m\x1b[38;2;8;55;6m▀\x1b[38;2;7;75;5m▀\x1b[38;2;8;65;5m▀\x1b[48;2;11;13;8m\x1b[38;2;9;41;6m▀\x1b[48;2;12;15;8m\x1b[38;2;60;37;28m▀\x1b[38;2;90;42;37m▀\x1b[38;2;88;42;36m▀▀▀▀▀▀▀▀▀▀▀▀\x1b[38;2;89;42;37m▀\x1b[38;2;78;39;33m▀\x1b[48;2;11;15;8m\x1b[38;2;20;18;11m▀\x1b[48;2;11;14;7m\x1b[38;2;10;14;7m▀\x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;18;13;7m \x1b[48;2;78;2;1m\x1b[38;2;83;2;1m▀\x1b[48;2;196;0;0m\x1b[38;2;40;0;0m▀\x1b[48;2;217;0;0m\x1b[38;2;137;0;0m▀\x1b[m \x1b[48;2;227;0;0m\x1b[38;2;16;0;0m▀\x1b[48;2;116;0;0m\x1b[38;2;21;0;0m▀\x1b[48;2;79;1;1m\x1b[38;2;81;1;1m▀\x1b[48;2;22;13;7m \x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[38;2;10;15;8m▀\x1b[48;2;10;15;8m\x1b[38;2;21;14;7m▀\x1b[48;2;11;15;8m\x1b[38;2;14;14;7m▀\x1b[38;2;11;14;7m▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \x1b[48;2;10;15;8m \x1b[48;2;17;13;7m\x1b[38;2;18;13;7m▀\x1b[48;2;75;2;1m\x1b[38;2;76;2;1m▀\x1b[48;2;97;0;0m\x1b[38;2;34;0;0m▀\x1b[48;2;76;0;0m\x1b[38;2;147;0;0m▀\x1b[m \x1b[48;2;161;0;0m\x1b[38;2;183;0;0m▀\x1b[48;2;49;0;0m\x1b[38;2;211;0;0m▀\x1b[48;2;75;1;1m\x1b[38;2;77;1;1m▀\x1b[48;2;21;13;7m \x1b[48;2;10;15;8m \x1b[48;2;11;15;8m \x1b[48;2;10;15;8m \x1b[48;2;17;13;7m \x1b[48;2;71;2;1m\x1b[38;2;73;2;1m▀\x1b[48;2;253;0;0m\x1b[38;2;159;0;0m▀\x1b[48;2;191;0;0m\x1b[38;2;5;0;0m▀\x1b[m \x1b[48;2;110;161;100m\x1b[38;2;116;0;0m▀\x1b[48;2;9;205;205m\x1b[38;2;192;0;0m▀\x1b[48;2;78;0;0m\x1b[38;2;77;1;0m▀\x1b[48;2;66;3;1m\x1b[38;2;30;11;6m▀\x1b[48;2;42;8;4m\x1b[38;2;9;15;8m▀\x1b[48;2;39;8;4m\x1b[38;2;10;15;8m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;40;8;4m▀\x1b[48;2;39;8;4m▀▀▀▀▀▀▀\x1b[48;2;40;8;4m▀▀▀\x1b[48;2;39;8;4m▀\x1b[48;2;40;8;4m▀\x1b[48;2;39;8;4m▀\x1b[48;2;41;8;4m\x1b[38;2;9;15;8m▀\x1b[48;2;62;4;2m\x1b[38;2;24;13;7m▀\x1b[48;2;78;0;0m\x1b[38;2;74;1;1m▀\x1b[48;2;221;222;0m\x1b[38;2;59;0;0m▀\x1b[48;2;67;199;133m\x1b[38;2;85;0;0m▀\x1b[m \x1b[48;2;0;0;0m\x1b[38;2;143;233;149m▀\x1b[48;2;108;184;254m\x1b[38;2;213;6;76m▀\x1b[48;2;197;183;82m\x1b[38;2;76;0;0m▀\x1b[48;2;154;157;0m▀\x1b[48;2;96;0;0m▀\x1b[48;2;253;0;0m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[48;2;226;0;0m▀\x1b[48;2;255;127;255m▀\x1b[48;2;84;36;66m\x1b[38;2;64;247;251m▀\x1b[48;2;0;0;0m\x1b[38;2;18;76;210m▀\x1b[m \x1b[48;2;0;0;0m \x1b[m \x1b[48;2;0;0;0m \x1b[m """ ) ) ================================================ FILE: examples/print-text/pygments-tokens.py ================================================ #!/usr/bin/env python """ Printing a list of Pygments (Token, text) tuples, or an output of a Pygments lexer. """ import pygments from pygments.lexers.python import PythonLexer from pygments.token import Token from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import PygmentsTokens from prompt_toolkit.styles import Style def main(): # Printing a manually constructed list of (Token, text) tuples. text = [ (Token.Keyword, "print"), (Token.Punctuation, "("), (Token.Literal.String.Double, '"'), (Token.Literal.String.Double, "hello"), (Token.Literal.String.Double, '"'), (Token.Punctuation, ")"), (Token.Text, "\n"), ] print_formatted_text(PygmentsTokens(text)) # Printing the output of a pygments lexer. tokens = list(pygments.lex('print("Hello")', lexer=PythonLexer())) print_formatted_text(PygmentsTokens(tokens)) # With a custom style. style = Style.from_dict( { "pygments.keyword": "underline", "pygments.literal.string": "bg:#00ff00 #ffffff", } ) print_formatted_text(PygmentsTokens(tokens), style=style) if __name__ == "__main__": main() ================================================ FILE: examples/print-text/true-color-demo.py ================================================ #!/usr/bin/env python """ Demonstration of all the ANSI colors. """ from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import HTML, FormattedText from prompt_toolkit.output import ColorDepth print = print_formatted_text def main(): print(HTML("\n<u>True color test.</u>")) for template in [ "bg:#{0:02x}0000", # Red. "bg:#00{0:02x}00", # Green. "bg:#0000{0:02x}", # Blue. "bg:#{0:02x}{0:02x}00", # Yellow. "bg:#{0:02x}00{0:02x}", # Magenta. "bg:#00{0:02x}{0:02x}", # Cyan. "bg:#{0:02x}{0:02x}{0:02x}", # Gray. ]: fragments = [] for i in range(0, 256, 4): fragments.append((template.format(i), " ")) print(FormattedText(fragments), color_depth=ColorDepth.DEPTH_4_BIT) print(FormattedText(fragments), color_depth=ColorDepth.DEPTH_8_BIT) print(FormattedText(fragments), color_depth=ColorDepth.DEPTH_24_BIT) print() if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/a-lot-of-parallel-tasks.py ================================================ #!/usr/bin/env python """ More complex demonstration of what's possible with the progress bar. """ import random import threading import time from prompt_toolkit import HTML from prompt_toolkit.shortcuts import ProgressBar def main(): with ProgressBar( title=HTML("<b>Example of many parallel tasks.</b>"), bottom_toolbar=HTML("<b>[Control-L]</b> clear <b>[Control-C]</b> abort"), ) as pb: def run_task(label, total, sleep_time): """Complete a normal run.""" for i in pb(range(total), label=label): time.sleep(sleep_time) def stop_task(label, total, sleep_time): """Stop at some random index. Breaking out of iteration at some stop index mimics how progress bars behave in cases where errors are raised. """ stop_i = random.randrange(total) bar = pb(range(total), label=label) for i in bar: if stop_i == i: bar.label = f"{label} BREAK" break time.sleep(sleep_time) threads = [] for i in range(160): label = f"Task {i}" total = random.randrange(50, 200) sleep_time = random.randrange(5, 20) / 100.0 threads.append( threading.Thread( target=random.choice((run_task, stop_task)), args=(label, total, sleep_time), ) ) for t in threads: t.daemon = True t.start() # Wait for the threads to finish. We use a timeout for the join() call, # because on Windows, join cannot be interrupted by Control-C or any other # signal. for t in threads: while t.is_alive(): t.join(timeout=0.5) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/colored-title-and-label.py ================================================ #!/usr/bin/env python """ A progress bar that displays a formatted title above the progress bar and has a colored label. """ import time from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import ProgressBar def main(): title = HTML('Downloading <style bg="yellow" fg="black">4 files...</style>') label = HTML("<ansired>some file</ansired>: ") with ProgressBar(title=title) as pb: for i in pb(range(800), label=label): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/custom-key-bindings.py ================================================ #!/usr/bin/env python """ A very simple progress bar which keep track of the progress as we consume an iterator. """ import os import signal import time from prompt_toolkit import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.shortcuts import ProgressBar def main(): bottom_toolbar = HTML( ' <b>[f]</b> Print "f" <b>[q]</b> Abort <b>[x]</b> Send Control-C.' ) # Create custom key bindings first. kb = KeyBindings() cancel = [False] @kb.add("f") def _(event): print("You pressed `f`.") @kb.add("q") def _(event): "Quit by setting cancel flag." cancel[0] = True @kb.add("x") def _(event): "Quit by sending SIGINT to the main thread." os.kill(os.getpid(), signal.SIGINT) # Use `patch_stdout`, to make sure that prints go above the # application. with patch_stdout(): with ProgressBar(key_bindings=kb, bottom_toolbar=bottom_toolbar) as pb: for i in pb(range(800)): time.sleep(0.01) if cancel[0]: break if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/many-parallel-tasks.py ================================================ #!/usr/bin/env python """ More complex demonstration of what's possible with the progress bar. """ import threading import time from prompt_toolkit import HTML from prompt_toolkit.shortcuts import ProgressBar def main(): with ProgressBar( title=HTML("<b>Example of many parallel tasks.</b>"), bottom_toolbar=HTML("<b>[Control-L]</b> clear <b>[Control-C]</b> abort"), ) as pb: def run_task(label, total, sleep_time): for i in pb(range(total), label=label): time.sleep(sleep_time) threads = [ threading.Thread(target=run_task, args=("First task", 50, 0.1)), threading.Thread(target=run_task, args=("Second task", 100, 0.1)), threading.Thread(target=run_task, args=("Third task", 8, 3)), threading.Thread(target=run_task, args=("Fourth task", 200, 0.1)), threading.Thread(target=run_task, args=("Fifth task", 40, 0.2)), threading.Thread(target=run_task, args=("Sixth task", 220, 0.1)), threading.Thread(target=run_task, args=("Seventh task", 85, 0.05)), threading.Thread(target=run_task, args=("Eight task", 200, 0.05)), ] for t in threads: t.daemon = True t.start() # Wait for the threads to finish. We use a timeout for the join() call, # because on Windows, join cannot be interrupted by Control-C or any other # signal. for t in threads: while t.is_alive(): t.join(timeout=0.5) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/nested-progress-bars.py ================================================ #!/usr/bin/env python """ Example of nested progress bars. """ import time from prompt_toolkit import HTML from prompt_toolkit.shortcuts import ProgressBar def main(): with ProgressBar( title=HTML('<b fg="#aa00ff">Nested progress bars</b>'), bottom_toolbar=HTML(" <b>[Control-L]</b> clear <b>[Control-C]</b> abort"), ) as pb: for i in pb(range(6), label="Main task"): for j in pb(range(200), label=f"Subtask <{i + 1}>", remove_when_done=True): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/scrolling-task-name.py ================================================ #!/usr/bin/env python """ A very simple progress bar where the name of the task scrolls, because it's too long. iterator. """ import time from prompt_toolkit.shortcuts import ProgressBar def main(): with ProgressBar( title="Scrolling task name (make sure the window is not too big)." ) as pb: for i in pb( range(800), label="This is a very very very long task that requires horizontal scrolling ...", ): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/simple-progress-bar.py ================================================ #!/usr/bin/env python """ A very simple progress bar which keep track of the progress as we consume an iterator. """ import time from prompt_toolkit.shortcuts import ProgressBar def main(): with ProgressBar() as pb: for i in pb(range(800)): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/styled-1.py ================================================ #!/usr/bin/env python """ A very simple progress bar which keep track of the progress as we consume an iterator. """ import time from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.styles import Style style = Style.from_dict( { "title": "#4444ff underline", "label": "#ff4400 bold", "percentage": "#00ff00", "bar-a": "bg:#00ff00 #004400", "bar-b": "bg:#00ff00 #000000", "bar-c": "#000000 underline", "current": "#448844", "total": "#448844", "time-elapsed": "#444488", "time-left": "bg:#88ff88 #000000", } ) def main(): with ProgressBar( style=style, title="Progress bar example with custom styling." ) as pb: for i in pb(range(1600), label="Downloading..."): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/styled-2.py ================================================ #!/usr/bin/env python """ A very simple progress bar which keep track of the progress as we consume an iterator. """ import time from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.shortcuts.progress_bar import formatters from prompt_toolkit.styles import Style style = Style.from_dict( { "progressbar title": "#0000ff", "item-title": "#ff4400 underline", "percentage": "#00ff00", "bar-a": "bg:#00ff00 #004400", "bar-b": "bg:#00ff00 #000000", "bar-c": "bg:#000000 #000000", "tildes": "#444488", "time-left": "bg:#88ff88 #ffffff", "spinning-wheel": "bg:#ffff00 #000000", } ) def main(): custom_formatters = [ formatters.Label(), formatters.Text(" "), formatters.SpinningWheel(), formatters.Text(" "), formatters.Text(HTML("<tildes>~~~</tildes>")), formatters.Bar(sym_a="#", sym_b="#", sym_c="."), formatters.Text(" left: "), formatters.TimeLeft(), ] with ProgressBar( title="Progress bar example with custom formatter.", formatters=custom_formatters, style=style, ) as pb: for i in pb(range(20), label="Downloading..."): time.sleep(1) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/styled-apt-get-install.py ================================================ #!/usr/bin/env python """ Styled just like an apt-get installation. """ import time from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.shortcuts.progress_bar import formatters from prompt_toolkit.styles import Style style = Style.from_dict( { "label": "bg:#ffff00 #000000", "percentage": "bg:#ffff00 #000000", "current": "#448844", "bar": "", } ) def main(): custom_formatters = [ formatters.Label(), formatters.Text(": [", style="class:percentage"), formatters.Percentage(), formatters.Text("]", style="class:percentage"), formatters.Text(" "), formatters.Bar(sym_a="#", sym_b="#", sym_c="."), formatters.Text(" "), ] with ProgressBar(style=style, formatters=custom_formatters) as pb: for i in pb(range(1600), label="Installing"): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/styled-rainbow.py ================================================ #!/usr/bin/env python """ A simple progress bar, visualized with rainbow colors (for fun). """ import time from prompt_toolkit.output import ColorDepth from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.shortcuts.progress_bar import formatters from prompt_toolkit.shortcuts.prompt import confirm def main(): true_color = confirm("Yes true colors? (y/n) ") custom_formatters = [ formatters.Label(), formatters.Text(" "), formatters.Rainbow(formatters.Bar()), formatters.Text(" left: "), formatters.Rainbow(formatters.TimeLeft()), ] if true_color: color_depth = ColorDepth.DEPTH_24_BIT else: color_depth = ColorDepth.DEPTH_8_BIT with ProgressBar(formatters=custom_formatters, color_depth=color_depth) as pb: for i in pb(range(20), label="Downloading..."): time.sleep(1) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/styled-tqdm-1.py ================================================ #!/usr/bin/env python """ Styled similar to tqdm, another progress bar implementation in Python. See: https://github.com/noamraph/tqdm """ import time from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.shortcuts.progress_bar import formatters from prompt_toolkit.styles import Style style = Style.from_dict({"": "cyan"}) def main(): custom_formatters = [ formatters.Label(suffix=": "), formatters.Bar(start="|", end="|", sym_a="#", sym_b="#", sym_c="-"), formatters.Text(" "), formatters.Progress(), formatters.Text(" "), formatters.Percentage(), formatters.Text(" [elapsed: "), formatters.TimeElapsed(), formatters.Text(" left: "), formatters.TimeLeft(), formatters.Text(", "), formatters.IterationsPerSecond(), formatters.Text(" iters/sec]"), formatters.Text(" "), ] with ProgressBar(style=style, formatters=custom_formatters) as pb: for i in pb(range(1600), label="Installing"): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/styled-tqdm-2.py ================================================ #!/usr/bin/env python """ Styled similar to tqdm, another progress bar implementation in Python. See: https://github.com/noamraph/tqdm """ import time from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.shortcuts.progress_bar import formatters from prompt_toolkit.styles import Style style = Style.from_dict({"bar-a": "reverse"}) def main(): custom_formatters = [ formatters.Label(suffix=": "), formatters.Percentage(), formatters.Bar(start="|", end="|", sym_a=" ", sym_b=" ", sym_c=" "), formatters.Text(" "), formatters.Progress(), formatters.Text(" ["), formatters.TimeElapsed(), formatters.Text("<"), formatters.TimeLeft(), formatters.Text(", "), formatters.IterationsPerSecond(), formatters.Text("it/s]"), ] with ProgressBar(style=style, formatters=custom_formatters) as pb: for i in pb(range(1600), label="Installing"): time.sleep(0.01) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/two-tasks.py ================================================ #!/usr/bin/env python """ Two progress bars that run in parallel. """ import threading import time from prompt_toolkit.shortcuts import ProgressBar def main(): with ProgressBar() as pb: # Two parallal tasks. def task_1(): for i in pb(range(100)): time.sleep(0.05) def task_2(): for i in pb(range(150)): time.sleep(0.08) # Start threads. t1 = threading.Thread(target=task_1) t2 = threading.Thread(target=task_2) t1.daemon = True t2.daemon = True t1.start() t2.start() # Wait for the threads to finish. We use a timeout for the join() call, # because on Windows, join cannot be interrupted by Control-C or any other # signal. for t in [t1, t2]: while t.is_alive(): t.join(timeout=0.5) if __name__ == "__main__": main() ================================================ FILE: examples/progress-bar/unknown-length.py ================================================ #!/usr/bin/env python """ A very simple progress bar which keep track of the progress as we consume an iterator. """ import time from prompt_toolkit.shortcuts import ProgressBar def data(): """ A generator that produces items. len() doesn't work here, so the progress bar can't estimate the time it will take. """ yield from range(1000) def main(): with ProgressBar() as pb: for i in pb(data()): time.sleep(0.1) if __name__ == "__main__": main() ================================================ FILE: examples/prompts/accept-default.py ================================================ #!/usr/bin/env python """ Example of `accept_default`, a way to automatically accept the input that the user typed without allowing him/her to edit it. This should display the prompt with all the formatting like usual, but not allow any editing. """ from prompt_toolkit import HTML, prompt if __name__ == "__main__": answer = prompt( HTML("<b>Type <u>some input</u>: </b>"), accept_default=True, default="test" ) print(f"You said: {answer}") ================================================ FILE: examples/prompts/asyncio-prompt.py ================================================ #!/usr/bin/env python """ This is an example of how to prompt inside an application that uses the asyncio eventloop. The ``prompt_toolkit`` library will make sure that when other coroutines are writing to stdout, they write above the prompt, not destroying the input line. This example does several things: 1. It starts a simple coroutine, printing a counter to stdout every second. 2. It starts a simple input/echo app loop which reads from stdin. Very important is the following patch. If you are passing stdin by reference to other parts of the code, make sure that this patch is applied as early as possible. :: sys.stdout = app.stdout_proxy() """ import asyncio from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.shortcuts import PromptSession async def print_counter(): """ Coroutine that prints counters. """ try: i = 0 while True: print(f"Counter: {i}") i += 1 await asyncio.sleep(3) except asyncio.CancelledError: print("Background task cancelled.") async def interactive_shell(): """ Like `interactive_shell`, but doing things manual. """ # Create Prompt. session = PromptSession("Say something: ") # Run echo loop. Read text from stdin, and reply it back. while True: try: result = await session.prompt_async() print(f'You said: "{result}"') except (EOFError, KeyboardInterrupt): return async def main(): with patch_stdout(): background_task = asyncio.create_task(print_counter()) try: await interactive_shell() finally: background_task.cancel() print("Quitting event loop. Bye.") if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: examples/prompts/auto-completion/autocomplete-with-control-space.py ================================================ #!/usr/bin/env python """ Example of using the control-space key binding for auto completion. """ from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter from prompt_toolkit.key_binding import KeyBindings animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) kb = KeyBindings() @kb.add("c-space") def _(event): """ Start auto completion. If the menu is showing already, select the next completion. """ b = event.app.current_buffer if b.complete_state: b.complete_next() else: b.start_completion(select_first=False) def main(): text = prompt( "Give some animals: ", completer=animal_completer, complete_while_typing=False, key_bindings=kb, ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/autocompletion-like-readline.py ================================================ #!/usr/bin/env python """ Autocompletion example that displays the autocompletions like readline does by binding a custom handler to the Tab key. """ from prompt_toolkit.completion import WordCompleter from prompt_toolkit.shortcuts import CompleteStyle, prompt animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) def main(): text = prompt( "Give some animals: ", completer=animal_completer, complete_style=CompleteStyle.READLINE_LIKE, ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/autocompletion.py ================================================ #!/usr/bin/env python """ Autocompletion example. Press [Tab] to complete the current word. - The first Tab press fills in the common part of all completions and shows all the completions. (In the menu) - Any following tab press cycles through all the possible completions. """ from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) def main(): text = prompt( "Give some animals: ", completer=animal_completer, complete_while_typing=False ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/colored-completions-with-formatted-text.py ================================================ #!/usr/bin/env python """ Demonstration of a custom completer class and the possibility of styling completions independently by passing formatted text objects to the "display" and "display_meta" arguments of "Completion". """ from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import CompleteStyle, prompt animals = [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", ] animal_family = { "alligator": "reptile", "ant": "insect", "ape": "mammal", "bat": "mammal", "bear": "mammal", "beaver": "mammal", "bee": "insect", "bison": "mammal", "butterfly": "insect", "cat": "mammal", "chicken": "bird", "crocodile": "reptile", "dinosaur": "reptile", "dog": "mammal", "dolphin": "mammal", "dove": "bird", "duck": "bird", "eagle": "bird", "elephant": "mammal", } family_colors = { "mammal": "ansimagenta", "insect": "ansigreen", "reptile": "ansired", "bird": "ansiyellow", } meta = { "alligator": HTML( "An <ansired>alligator</ansired> is a <u>crocodilian</u> in the genus Alligator of the family Alligatoridae." ), "ant": HTML( "<ansired>Ants</ansired> are eusocial <u>insects</u> of the family Formicidae." ), "ape": HTML( "<ansired>Apes</ansired> (Hominoidea) are a branch of Old World tailless anthropoid catarrhine <u>primates</u>." ), "bat": HTML("<ansired>Bats</ansired> are mammals of the order <u>Chiroptera</u>."), "bee": HTML( "<ansired>Bees</ansired> are flying <u>insects</u> closely related to wasps and ants." ), "beaver": HTML( "The <ansired>beaver</ansired> (genus Castor) is a large, primarily <u>nocturnal</u>, semiaquatic <u>rodent</u>." ), "bear": HTML( "<ansired>Bears</ansired> are carnivoran <u>mammals</u> of the family Ursidae." ), "butterfly": HTML( "<ansiblue>Butterflies</ansiblue> are <u>insects</u> in the macrolepidopteran clade Rhopalocera from the order Lepidoptera." ), # ... } class AnimalCompleter(Completer): def get_completions(self, document, complete_event): word = document.get_word_before_cursor() for animal in animals: if animal.startswith(word): if animal in animal_family: family = animal_family[animal] family_color = family_colors.get(family, "default") display = HTML( "%s<b>:</b> <ansired>(<" + family_color + ">%s</" + family_color + ">)</ansired>" ) % (animal, family) else: display = animal yield Completion( animal, start_position=-len(word), display=display, display_meta=meta.get(animal), ) def main(): # Simple completion menu. print("(The completion menu displays colors.)") prompt("Type an animal: ", completer=AnimalCompleter()) # Multi-column menu. prompt( "Type an animal: ", completer=AnimalCompleter(), complete_style=CompleteStyle.MULTI_COLUMN, ) # Readline-like prompt( "Type an animal: ", completer=AnimalCompleter(), complete_style=CompleteStyle.READLINE_LIKE, ) if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/colored-completions.py ================================================ #!/usr/bin/env python """ Demonstration of a custom completer class and the possibility of styling completions independently. """ from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.output.color_depth import ColorDepth from prompt_toolkit.shortcuts import CompleteStyle, prompt colors = [ "red", "blue", "green", "orange", "purple", "yellow", "cyan", "magenta", "pink", ] class ColorCompleter(Completer): def get_completions(self, document, complete_event): word = document.get_word_before_cursor() for color in colors: if color.startswith(word): yield Completion( color, start_position=-len(word), style="fg:" + color, selected_style="fg:white bg:" + color, ) def main(): # Simple completion menu. print("(The completion menu displays colors.)") prompt("Type a color: ", completer=ColorCompleter()) # Multi-column menu. prompt( "Type a color: ", completer=ColorCompleter(), complete_style=CompleteStyle.MULTI_COLUMN, ) # Readline-like prompt( "Type a color: ", completer=ColorCompleter(), complete_style=CompleteStyle.READLINE_LIKE, ) # Prompt with true color output. message = [ ("#cc2244", "T"), ("#bb4444", "r"), ("#996644", "u"), ("#cc8844", "e "), ("#ccaa44", "C"), ("#bbaa44", "o"), ("#99aa44", "l"), ("#778844", "o"), ("#55aa44", "r "), ("#33aa44", "p"), ("#11aa44", "r"), ("#11aa66", "o"), ("#11aa88", "m"), ("#11aaaa", "p"), ("#11aacc", "t"), ("#11aaee", ": "), ] prompt(message, completer=ColorCompleter(), color_depth=ColorDepth.TRUE_COLOR) if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/combine-multiple-completers.py ================================================ #!/usr/bin/env python """ Example of multiple individual completers that are combined into one. """ from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter, merge_completers animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) color_completer = WordCompleter( [ "red", "green", "blue", "yellow", "white", "black", "orange", "gray", "pink", "purple", "cyan", "magenta", "violet", ], ignore_case=True, ) def main(): completer = merge_completers([animal_completer, color_completer]) text = prompt( "Give some animals: ", completer=completer, complete_while_typing=False ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/fuzzy-custom-completer.py ================================================ #!/usr/bin/env python """ Demonstration of a custom completer wrapped in a `FuzzyCompleter` for fuzzy matching. """ from prompt_toolkit.completion import Completer, Completion, FuzzyCompleter from prompt_toolkit.shortcuts import CompleteStyle, prompt colors = [ "red", "blue", "green", "orange", "purple", "yellow", "cyan", "magenta", "pink", ] class ColorCompleter(Completer): def get_completions(self, document, complete_event): word = document.get_word_before_cursor() for color in colors: if color.startswith(word): yield Completion( color, start_position=-len(word), style="fg:" + color, selected_style="fg:white bg:" + color, ) def main(): # Simple completion menu. print("(The completion menu displays colors.)") prompt("Type a color: ", completer=FuzzyCompleter(ColorCompleter())) # Multi-column menu. prompt( "Type a color: ", completer=FuzzyCompleter(ColorCompleter()), complete_style=CompleteStyle.MULTI_COLUMN, ) # Readline-like prompt( "Type a color: ", completer=FuzzyCompleter(ColorCompleter()), complete_style=CompleteStyle.READLINE_LIKE, ) if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/fuzzy-word-completer.py ================================================ #!/usr/bin/env python """ Autocompletion example. Press [Tab] to complete the current word. - The first Tab press fills in the common part of all completions and shows all the completions. (In the menu) - Any following tab press cycles through all the possible completions. """ from prompt_toolkit.completion import FuzzyWordCompleter from prompt_toolkit.shortcuts import prompt animal_completer = FuzzyWordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ] ) def main(): text = prompt( "Give some animals: ", completer=animal_completer, complete_while_typing=True ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/multi-column-autocompletion-with-meta.py ================================================ #!/usr/bin/env python """ Autocompletion example that shows meta-information alongside the completions. """ from prompt_toolkit.completion import WordCompleter from prompt_toolkit.shortcuts import CompleteStyle, prompt animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", ], meta_dict={ "alligator": "An alligator is a crocodilian in the genus Alligator of the family Alligatoridae.", "ant": "Ants are eusocial insects of the family Formicidae", "ape": "Apes (Hominoidea) are a branch of Old World tailless anthropoid catarrhine primates ", "bat": "Bats are mammals of the order Chiroptera", }, ignore_case=True, ) def main(): text = prompt( "Give some animals: ", completer=animal_completer, complete_style=CompleteStyle.MULTI_COLUMN, ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/multi-column-autocompletion.py ================================================ #!/usr/bin/env python """ Similar to the autocompletion example. But display all the completions in multiple columns. """ from prompt_toolkit.completion import WordCompleter from prompt_toolkit.shortcuts import CompleteStyle, prompt animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) def main(): text = prompt( "Give some animals: ", completer=animal_completer, complete_style=CompleteStyle.MULTI_COLUMN, ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/nested-autocompletion.py ================================================ #!/usr/bin/env python """ Example of nested autocompletion. """ from prompt_toolkit import prompt from prompt_toolkit.completion import NestedCompleter completer = NestedCompleter.from_nested_dict( { "show": {"version": None, "clock": None, "ip": {"interface": {"brief": None}}}, "exit": None, } ) def main(): text = prompt("Type a command: ", completer=completer) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-completion/slow-completions.py ================================================ #!/usr/bin/env python """ An example of how to deal with slow auto completion code. - Running the completions in a thread is possible by wrapping the `Completer` object in a `ThreadedCompleter`. This makes sure that the ``get_completions`` generator is executed in a background thread. For the `prompt` shortcut, we don't have to wrap the completer ourselves. Passing `complete_in_thread=True` is sufficient. - We also set a `loading` boolean in the completer function to keep track of when the completer is running, and display this in the toolbar. """ import time from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.shortcuts import CompleteStyle, prompt WORDS = [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ] class SlowCompleter(Completer): """ This is a completer that's very slow. """ def __init__(self): self.loading = 0 def get_completions(self, document, complete_event): # Keep count of how many completion generators are running. self.loading += 1 word_before_cursor = document.get_word_before_cursor() try: for word in WORDS: if word.startswith(word_before_cursor): time.sleep(0.2) # Simulate slowness. yield Completion(word, -len(word_before_cursor)) finally: # We use try/finally because this generator can be closed if the # input text changes before all completions are generated. self.loading -= 1 def main(): # We wrap it in a ThreadedCompleter, to make sure it runs in a different # thread. That way, we don't block the UI while running the completions. slow_completer = SlowCompleter() # Add a bottom toolbar that display when completions are loading. def bottom_toolbar(): return " Loading completions... " if slow_completer.loading > 0 else "" # Display prompt. text = prompt( "Give some animals: ", completer=slow_completer, complete_in_thread=True, complete_while_typing=True, bottom_toolbar=bottom_toolbar, complete_style=CompleteStyle.MULTI_COLUMN, ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/auto-suggestion.py ================================================ #!/usr/bin/env python """ Simple example of a CLI that demonstrates fish-style auto suggestion. When you type some input, it will match the input against the history. If One entry of the history starts with the given input, then it will show the remaining part as a suggestion. Pressing the right arrow will insert this suggestion. """ from prompt_toolkit import PromptSession from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.history import InMemoryHistory def main(): # Create some history first. (Easy for testing.) history = InMemoryHistory() history.append_string("import os") history.append_string('print("hello")') history.append_string('print("world")') history.append_string("import path") # Print help. print("This CLI has fish-style auto-suggestion enable.") print('Type for instance "pri", then you\'ll see a suggestion.') print("Press the right arrow to insert the suggestion.") print("Press Control-C to retry. Control-D to exit.") print() session = PromptSession( history=history, auto_suggest=AutoSuggestFromHistory(), enable_history_search=True, ) while True: try: text = session.prompt("Say something: ") except KeyboardInterrupt: pass # Ctrl-C pressed. Try again. else: break print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/autocorrection.py ================================================ #!/usr/bin/env python """ Example of implementing auto correction while typing. The word "impotr" will be corrected when the user types a space afterwards. """ from prompt_toolkit import prompt from prompt_toolkit.key_binding import KeyBindings # Database of words to be replaced by typing. corrections = { "impotr": "import", "wolrd": "world", } def main(): # We start with a `KeyBindings` for our extra key bindings. bindings = KeyBindings() # We add a custom key binding to space. @bindings.add(" ") def _(event): """ When space is pressed, we check the word before the cursor, and autocorrect that. """ b = event.app.current_buffer w = b.document.get_word_before_cursor() if w is not None: if w in corrections: b.delete_before_cursor(count=len(w)) b.insert_text(corrections[w]) b.insert_text(" ") # Read input. text = prompt("Say something: ", key_bindings=bindings) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/bottom-toolbar.py ================================================ #!/usr/bin/env python """ A few examples of displaying a bottom toolbar. The ``prompt`` function takes a ``bottom_toolbar`` attribute. This can be any kind of formatted text (plain text, HTML or ANSI), or it can be a callable that takes an App and returns an of these. The bottom toolbar will always receive the style 'bottom-toolbar', and the text inside will get 'bottom-toolbar.text'. These can be used to change the default style. """ import time from prompt_toolkit import prompt from prompt_toolkit.formatted_text import ANSI, HTML from prompt_toolkit.styles import Style def main(): # Example 1: fixed text. text = prompt("Say something: ", bottom_toolbar="This is a toolbar") print(f"You said: {text}") # Example 2: fixed text from a callable: def get_toolbar(): return f"Bottom toolbar: time={time.time()!r}" text = prompt("Say something: ", bottom_toolbar=get_toolbar, refresh_interval=0.5) print(f"You said: {text}") # Example 3: Using HTML: text = prompt( "Say something: ", bottom_toolbar=HTML( '(html) <b>This</b> <u>is</u> a <style bg="ansired">toolbar</style>' ), ) print(f"You said: {text}") # Example 4: Using ANSI: text = prompt( "Say something: ", bottom_toolbar=ANSI( "(ansi): \x1b[1mThis\x1b[0m \x1b[4mis\x1b[0m a \x1b[91mtoolbar" ), ) print(f"You said: {text}") # Example 5: styling differently. style = Style.from_dict( { "bottom-toolbar": "#aaaa00 bg:#ff0000", "bottom-toolbar.text": "#aaaa44 bg:#aa4444", } ) text = prompt("Say something: ", bottom_toolbar="This is a toolbar", style=style) print(f"You said: {text}") # Example 6: Using a list of tokens. def get_bottom_toolbar(): return [ ("", " "), ("bg:#ff0000 fg:#000000", "This"), ("", " is a "), ("bg:#ff0000 fg:#000000", "toolbar"), ("", ". "), ] text = prompt("Say something: ", bottom_toolbar=get_bottom_toolbar) print(f"You said: {text}") # Example 7: multiline fixed text. text = prompt("Say something: ", bottom_toolbar="This is\na multiline toolbar") print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/clock-input.py ================================================ #!/usr/bin/env python """ Example of a 'dynamic' prompt. On that shows the current time in the prompt. """ import datetime from prompt_toolkit.shortcuts import prompt def get_prompt(): "Tokens to be shown before the prompt." now = datetime.datetime.now() return [ ("bg:#008800 #ffffff", f"{now.hour}:{now.minute}:{now.second}"), ("bg:cornsilk fg:maroon", " Enter something: "), ] def main(): result = prompt(get_prompt, refresh_interval=0.5) print(f"You said: {result}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/colored-prompt.py ================================================ #!/usr/bin/env python """ Example of a colored prompt. """ from prompt_toolkit import prompt from prompt_toolkit.formatted_text import ANSI, HTML from prompt_toolkit.styles import Style style = Style.from_dict( { # Default style. "": "#ff0066", # Prompt. "username": "#884444 italic", "at": "#00aa00", "colon": "#00aa00", "pound": "#00aa00", "host": "#000088 bg:#aaaaff", "path": "#884444 underline", # Make a selection reverse/underlined. # (Use Control-Space to select.) "selected-text": "reverse underline", } ) def example_1(): """ Style and list of (style, text) tuples. """ # Not that we can combine class names and inline styles. prompt_fragments = [ ("class:username", "john"), ("class:at", "@"), ("class:host", "localhost"), ("class:colon", ":"), ("class:path", "/user/john"), ("bg:#00aa00 #ffffff", "#"), ("", " "), ] answer = prompt(prompt_fragments, style=style) print(f"You said: {answer}") def example_2(): """ Using HTML for the formatting. """ answer = prompt( HTML( "<username>john</username><at>@</at>" "<host>localhost</host>" "<colon>:</colon>" "<path>/user/john</path>" '<style bg="#00aa00" fg="#ffffff">#</style> ' ), style=style, ) print(f"You said: {answer}") def example_3(): """ Using ANSI for the formatting. """ answer = prompt( ANSI("\x1b[31mjohn\x1b[0m@\x1b[44mlocalhost\x1b[0m:\x1b[4m/user/john\x1b[0m# ") ) print(f"You said: {answer}") if __name__ == "__main__": example_1() example_2() example_3() ================================================ FILE: examples/prompts/confirmation-prompt.py ================================================ #!/usr/bin/env python """ Example of a confirmation prompt. """ from prompt_toolkit.shortcuts import confirm if __name__ == "__main__": answer = confirm("Should we do that?") print(f"You said: {answer}") ================================================ FILE: examples/prompts/cursor-shapes.py ================================================ #!/usr/bin/env python """ Example of cursor shape configurations. """ from prompt_toolkit import prompt from prompt_toolkit.cursor_shapes import CursorShape, ModalCursorShapeConfig # NOTE: We pass `enable_suspend=True`, so that we can easily see what happens # to the cursor shapes when the application is suspended. prompt("(block): ", cursor=CursorShape.BLOCK, enable_suspend=True) prompt("(underline): ", cursor=CursorShape.UNDERLINE, enable_suspend=True) prompt("(beam): ", cursor=CursorShape.BEAM, enable_suspend=True) prompt( "(modal - according to vi input mode): ", cursor=ModalCursorShapeConfig(), vi_mode=True, enable_suspend=True, ) ================================================ FILE: examples/prompts/custom-key-binding.py ================================================ #!/usr/bin/env python """ Example of adding a custom key binding to a prompt. """ import asyncio from prompt_toolkit import prompt from prompt_toolkit.application import in_terminal, run_in_terminal from prompt_toolkit.key_binding import KeyBindings def main(): # We start with a `KeyBindings` of default key bindings. bindings = KeyBindings() # Add our own key binding. @bindings.add("f4") def _(event): """ When F4 has been pressed. Insert "hello world" as text. """ event.app.current_buffer.insert_text("hello world") @bindings.add("x", "y") def _(event): """ (Useless, but for demoing.) Typing 'xy' will insert 'z'. Note that when you type for instance 'xa', the insertion of 'x' is postponed until the 'a' is typed. because we don't know earlier whether or not a 'y' will follow. However, prompt-toolkit should already give some visual feedback of the typed character. """ event.app.current_buffer.insert_text("z") @bindings.add("a", "b", "c") def _(event): "Typing 'abc' should insert 'd'." event.app.current_buffer.insert_text("d") @bindings.add("c-t") def _(event): """ Print 'hello world' in the terminal when ControlT is pressed. We use ``run_in_terminal``, because that ensures that the prompt is hidden right before ``print_hello`` gets executed and it's drawn again after it. (Otherwise this would destroy the output.) """ def print_hello(): print("hello world") run_in_terminal(print_hello) @bindings.add("c-k") async def _(event): """ Example of asyncio coroutine as a key binding. """ try: for i in range(5): async with in_terminal(): print("hello") await asyncio.sleep(1) except asyncio.CancelledError: print("Prompt terminated before we completed.") # Read input. print('Press F4 to insert "hello world", type "xy" to insert "z":') text = prompt("> ", key_bindings=bindings) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/custom-lexer.py ================================================ #!/usr/bin/env python """ An example of a custom lexer that prints the input text in random colors. """ from prompt_toolkit.lexers import Lexer from prompt_toolkit.shortcuts import prompt from prompt_toolkit.styles.named_colors import NAMED_COLORS class RainbowLexer(Lexer): def lex_document(self, document): colors = sorted(NAMED_COLORS, key=NAMED_COLORS.get) def get_line(lineno): return [ (colors[i % len(colors)], c) for i, c in enumerate(document.lines[lineno]) ] return get_line def main(): answer = prompt("Give me some input: ", lexer=RainbowLexer()) print(f"You said: {answer}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/custom-vi-operator-and-text-object.py ================================================ #!/usr/bin/env python """ Example of adding a custom Vi operator and text object. (Note that this API is not guaranteed to remain stable.) """ from prompt_toolkit import prompt from prompt_toolkit.enums import EditingMode from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.vi import ( TextObject, create_operator_decorator, create_text_object_decorator, ) def main(): # We start with a `Registry` of default key bindings. bindings = KeyBindings() # Create the decorators to be used for registering text objects and # operators in this registry. operator = create_operator_decorator(bindings) text_object = create_text_object_decorator(bindings) # Create a custom operator. @operator("R") def _(event, text_object): "Custom operator that reverses text." buff = event.current_buffer # Get relative start/end coordinates. start, end = text_object.operator_range(buff.document) start += buff.cursor_position end += buff.cursor_position text = buff.text[start:end] text = "".join(reversed(text)) event.app.current_buffer.text = buff.text[:start] + text + buff.text[end:] # Create a text object. @text_object("A") def _(event): "A custom text object that involves everything." # Note that a `TextObject` has coordinates, relative to the cursor position. buff = event.current_buffer return TextObject( -buff.document.cursor_position, # The start. len(buff.text) - buff.document.cursor_position, ) # The end. # Read input. print('There is a custom text object "A" that applies to everything') print('and a custom operator "r" that reverses the text object.\n') print("Things that are possible:") print("- Riw - reverse inner word.") print("- yA - yank everything.") print("- RA - reverse everything.") text = prompt( "> ", default="hello world", key_bindings=bindings, editing_mode=EditingMode.VI ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/enforce-tty-input-output.py ================================================ #!/usr/bin/env python """ This will display a prompt that will always use the terminal for input and output, even if sys.stdin/stdout are connected to pipes. For testing, run as: cat /dev/null | python ./enforce-tty-input-output.py > /dev/null """ from prompt_toolkit.application import create_app_session_from_tty from prompt_toolkit.shortcuts import prompt with create_app_session_from_tty(): prompt(">") ================================================ FILE: examples/prompts/fancy-zsh-prompt.py ================================================ #!/usr/bin/env python """ Example of the fancy ZSH prompt that @anki-code was using. The theme is coming from the xonsh plugin from the xxh project: https://github.com/xxh/xxh-plugin-xonsh-theme-bar See: - https://github.com/xonsh/xonsh/issues/3356 - https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1111 """ import datetime from prompt_toolkit import prompt from prompt_toolkit.application import get_app from prompt_toolkit.formatted_text import ( HTML, fragment_list_width, merge_formatted_text, to_formatted_text, ) from prompt_toolkit.styles import Style style = Style.from_dict( { "username": "#aaaaaa italic", "path": "#ffffff bold", "branch": "bg:#666666", "branch exclamation-mark": "#ff0000", "env": "bg:#666666", "left-part": "bg:#444444", "right-part": "bg:#444444", "padding": "bg:#444444", } ) def get_prompt() -> HTML: """ Build the prompt dynamically every time its rendered. """ left_part = HTML( "<left-part>" " <username>root</username> " " abc " "<path>~/.oh-my-zsh/themes</path>" "</left-part>" ) right_part = HTML( "<right-part> " "<branch> master<exclamation-mark>!</exclamation-mark> </branch> " " <env> py36 </env> " " <time>%s</time> " "</right-part>" ) % (datetime.datetime.now().isoformat(),) used_width = sum( [ fragment_list_width(to_formatted_text(left_part)), fragment_list_width(to_formatted_text(right_part)), ] ) total_width = get_app().output.get_size().columns padding_size = total_width - used_width padding = HTML("<padding>%s</padding>") % (" " * padding_size,) return merge_formatted_text([left_part, padding, right_part, "\n", "# "]) def main() -> None: while True: answer = prompt(get_prompt, style=style, refresh_interval=1) print(f"You said: {answer}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/finalterm-shell-integration.py ================================================ #!/usr/bin/env python """ Mark the start and end of the prompt with Final term (iterm2) escape sequences. See: https://iterm2.com/finalterm.html """ import sys from prompt_toolkit import prompt from prompt_toolkit.formatted_text import ANSI BEFORE_PROMPT = "\033]133;A\a" AFTER_PROMPT = "\033]133;B\a" BEFORE_OUTPUT = "\033]133;C\a" AFTER_OUTPUT = ( "\033]133;D;{command_status}\a" # command_status is the command status, 0-255 ) def get_prompt_text(): # Generate the text fragments for the prompt. # Important: use the `ZeroWidthEscape` fragment only if you are sure that # writing this as raw text to the output will not introduce any # cursor movements. return [ ("[ZeroWidthEscape]", BEFORE_PROMPT), ("", "Say something: # "), ("[ZeroWidthEscape]", AFTER_PROMPT), ] if __name__ == "__main__": # Option 1: Using a `get_prompt_text` function: answer = prompt(get_prompt_text) # Option 2: Using ANSI escape sequences. before = "\001" + BEFORE_PROMPT + "\002" after = "\001" + AFTER_PROMPT + "\002" answer = prompt(ANSI(f"{before}Say something: # {after}")) # Output. sys.stdout.write(BEFORE_OUTPUT) print(f"You said: {answer}") sys.stdout.write(AFTER_OUTPUT.format(command_status=0)) ================================================ FILE: examples/prompts/get-input-vi-mode.py ================================================ #!/usr/bin/env python from prompt_toolkit import prompt if __name__ == "__main__": print("You have Vi keybindings here. Press [Esc] to go to navigation mode.") answer = prompt("Give me some input: ", multiline=False, vi_mode=True) print(f"You said: {answer}") ================================================ FILE: examples/prompts/get-input-with-default.py ================================================ #!/usr/bin/env python """ Example of a call to `prompt` with a default value. The input is pre-filled, but the user can still edit the default. """ import getpass from prompt_toolkit import prompt if __name__ == "__main__": answer = prompt("What is your name: ", default=f"{getpass.getuser()}") print(f"You said: {answer}") ================================================ FILE: examples/prompts/get-input.py ================================================ #!/usr/bin/env python """ The most simple prompt example. """ from prompt_toolkit import prompt if __name__ == "__main__": answer = prompt("Give me some input: ") print(f"You said: {answer}") ================================================ FILE: examples/prompts/get-multiline-input.py ================================================ #!/usr/bin/env python from prompt_toolkit import prompt from prompt_toolkit.formatted_text import HTML def prompt_continuation(width, line_number, wrap_count): """ The continuation: display line numbers and '->' before soft wraps. Notice that we can return any kind of formatted text from here. The prompt continuation doesn't have to be the same width as the prompt which is displayed before the first line, but in this example we choose to align them. The `width` input that we receive here represents the width of the prompt. """ if wrap_count > 0: return " " * (width - 3) + "-> " else: text = ("- %i - " % (line_number + 1)).rjust(width) return HTML("<strong>%s</strong>") % text if __name__ == "__main__": print("Press [Meta+Enter] or [Esc] followed by [Enter] to accept input.") answer = prompt( "Multiline input: ", multiline=True, prompt_continuation=prompt_continuation ) print(f"You said: {answer}") ================================================ FILE: examples/prompts/get-password-with-toggle-display-shortcut.py ================================================ #!/usr/bin/env python """ get_password function that displays asterisks instead of the actual characters. With the addition of a ControlT shortcut to hide/show the input. """ from prompt_toolkit import prompt from prompt_toolkit.filters import Condition from prompt_toolkit.key_binding import KeyBindings def main(): hidden = [True] # Nonlocal bindings = KeyBindings() @bindings.add("c-t") def _(event): "When ControlT has been pressed, toggle visibility." hidden[0] = not hidden[0] print("Type Control-T to toggle password visible.") password = prompt( "Password: ", is_password=Condition(lambda: hidden[0]), key_bindings=bindings ) print(f"You said: {password}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/get-password.py ================================================ #!/usr/bin/env python from prompt_toolkit import prompt if __name__ == "__main__": password = prompt("Password: ", is_password=True) print(f"You said: {password}") ================================================ FILE: examples/prompts/history/persistent-history.py ================================================ #!/usr/bin/env python """ Simple example of a CLI that keeps a persistent history of all the entered strings in a file. When you run this script for a second time, pressing arrow-up will go back in history. """ from prompt_toolkit import PromptSession from prompt_toolkit.history import FileHistory def main(): our_history = FileHistory(".example-history-file") # The history needs to be passed to the `PromptSession`. It can't be passed # to the `prompt` call because only one history can be used during a # session. session = PromptSession(history=our_history) while True: text = session.prompt("Say something: ") print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/history/slow-history.py ================================================ #!/usr/bin/env python """ Simple example of a custom, very slow history, that is loaded asynchronously. By wrapping it in `ThreadedHistory`, the history will load in the background without blocking any user interaction. """ import time from prompt_toolkit import PromptSession from prompt_toolkit.history import History, ThreadedHistory class SlowHistory(History): """ Example class that loads the history very slowly... """ def load_history_strings(self): for i in range(1000): time.sleep(1) # Emulate slowness. yield f"item-{i}" def store_string(self, string): pass # Don't store strings. def main(): print( "Asynchronous loading of history. Notice that the up-arrow will work " "for as far as the completions are loaded.\n" "Even when the input is accepted, loading will continue in the " "background and when the next prompt is displayed.\n" ) our_history = ThreadedHistory(SlowHistory()) # The history needs to be passed to the `PromptSession`. It can't be passed # to the `prompt` call because only one history can be used during a # session. session = PromptSession(history=our_history) while True: text = session.prompt("Say something: ") print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/html-input.py ================================================ #!/usr/bin/env python """ Simple example of a syntax-highlighted HTML input line. (This requires Pygments to be installed.) """ from pygments.lexers.html import HtmlLexer from prompt_toolkit import prompt from prompt_toolkit.lexers import PygmentsLexer def main(): text = prompt("Enter HTML: ", lexer=PygmentsLexer(HtmlLexer)) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/input-validation.py ================================================ #!/usr/bin/env python """ Simple example of input validation. """ from prompt_toolkit import prompt from prompt_toolkit.validation import Validator def is_valid_email(text): return "@" in text validator = Validator.from_callable( is_valid_email, error_message="Not a valid e-mail address (Does not contain an @).", move_cursor_to_end=True, ) def main(): # Validate when pressing ENTER. text = prompt( "Enter e-mail address: ", validator=validator, validate_while_typing=False ) print(f"You said: {text}") # While typing text = prompt( "Enter e-mail address: ", validator=validator, validate_while_typing=True ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/inputhook.py ================================================ #!/usr/bin/env python """ An example that demonstrates how inputhooks can be used in prompt-toolkit. An inputhook is a callback that an eventloop calls when it's idle. For instance, readline calls `PyOS_InputHook`. This allows us to do other work in the same thread, while waiting for input. Important however is that we give the control back to prompt-toolkit when some input is ready to be processed. There are two ways to know when input is ready. One way is to poll `InputHookContext.input_is_ready()`. Another way is to check for `InputHookContext.fileno()` to be ready. In this example we do the latter. """ import gobject import gtk from pygments.lexers.python import PythonLexer from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.shortcuts import PromptSession def hello_world_window(): """ Create a GTK window with one 'Hello world' button. """ # Create a new window. window = gtk.Window(gtk.WINDOW_TOPLEVEL) window.set_border_width(50) # Create a new button with the label "Hello World". button = gtk.Button("Hello World") window.add(button) # Clicking the button prints some text. def clicked(data): print("Button clicked!") button.connect("clicked", clicked) # Display the window. button.show() window.show() def inputhook(context): """ When the eventloop of prompt-toolkit is idle, call this inputhook. This will run the GTK main loop until the file descriptor `context.fileno()` becomes ready. :param context: An `InputHookContext` instance. """ def _main_quit(*a, **kw): gtk.main_quit() return False gobject.io_add_watch(context.fileno(), gobject.IO_IN, _main_quit) gtk.main() def main(): # Create user interface. hello_world_window() # Enable threading in GTK. (Otherwise, GTK will keep the GIL.) gtk.gdk.threads_init() # Read input from the command line, using an event loop with this hook. # We use `patch_stdout`, because clicking the button will print something; # and that should print nicely 'above' the input line. with patch_stdout(): session = PromptSession( "Python >>> ", inputhook=inputhook, lexer=PygmentsLexer(PythonLexer) ) result = session.prompt() print(f"You said: {result}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/mouse-support.py ================================================ #!/usr/bin/env python from prompt_toolkit import prompt if __name__ == "__main__": print( "This is multiline input. press [Meta+Enter] or [Esc] followed by [Enter] to accept input." ) print("You can click with the mouse in order to select text.") answer = prompt("Multiline input: ", multiline=True, mouse_support=True) print(f"You said: {answer}") ================================================ FILE: examples/prompts/multiline-autosuggest.py ================================================ #!/usr/bin/env python """ A more complex example of a CLI that demonstrates fish-style auto suggestion across multiple lines. This can typically be used for LLM that may return multi-line responses. Note that unlike simple autosuggest, using multiline autosuggest requires more care as it may shift the buffer layout, and care must taken ton consider the various case when the number iof suggestions lines is longer than the number of lines in the buffer, what happens to the existing text (is it pushed down, or hidden until the suggestion is accepted) Etc. So generally multiline autosuggest will require a custom processor to handle the different use case and user experience. We also have not hooked any keys to accept the suggestion, so it will be up to you to decide how and when to accept the suggestion, accept it as a whole, like by line, or token by token. """ from prompt_toolkit import PromptSession from prompt_toolkit.auto_suggest import AutoSuggest, Suggestion from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import HasFocus, IsDone from prompt_toolkit.layout.processors import ( ConditionalProcessor, Processor, Transformation, TransformationInput, ) universal_declaration_of_human_rights = """ All human beings are born free and equal in dignity and rights. They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood Everyone is entitled to all the rights and freedoms set forth in this Declaration, without distinction of any kind, such as race, colour, sex, language, religion, political or other opinion, national or social origin, property, birth or other status. Furthermore, no distinction shall be made on the basis of the political, jurisdictional or international status of the country or territory to which a person belongs, whether it be independent, trust, non-self-governing or under any other limitation of sovereignty.""".strip().splitlines() class FakeLLMAutoSuggest(AutoSuggest): def get_suggestion(self, buffer, document): if document.line_count == 1: return Suggestion(" (Add a few new lines to see multiline completion)") cursor_line = document.cursor_position_row text = document.text.split("\n")[cursor_line] if not text.strip(): return None index = None for i, l in enumerate(universal_declaration_of_human_rights): if l.startswith(text): index = i break if index is None: return None return Suggestion( universal_declaration_of_human_rights[index][len(text) :] + "\n" + "\n".join(universal_declaration_of_human_rights[index + 1 :]) ) class AppendMultilineAutoSuggestionInAnyLine(Processor): def __init__(self, style: str = "class:auto-suggestion") -> None: self.style = style def apply_transformation(self, ti: TransformationInput) -> Transformation: # a convenient noop transformation that does nothing. noop = Transformation(fragments=ti.fragments) # We get out of the way if the prompt is only one line, and let prompt_toolkit handle the rest. if ti.document.line_count == 1: return noop # first everything before the current line is unchanged. if ti.lineno < ti.document.cursor_position_row: return noop buffer = ti.buffer_control.buffer if not buffer.suggestion or not ti.document.is_cursor_at_the_end_of_line: return noop # compute the number delta between the current cursor line and line we are transforming # transformed line can either be suggestions, or an existing line that is shifted. delta = ti.lineno - ti.document.cursor_position_row # convert the suggestion into a list of lines suggestions = buffer.suggestion.text.splitlines() if not suggestions: return noop if delta == 0: # append suggestion to current line suggestion = suggestions[0] return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) elif delta < len(suggestions): # append a line with the nth line of the suggestion suggestion = suggestions[delta] assert "\n" not in suggestion return Transformation([(self.style, suggestion)]) else: # return the line that is by delta-1 suggestion (first suggestion does not shifts) shift = ti.lineno - len(suggestions) + 1 return Transformation(ti.get_line(shift)) def main(): # Create some history first. (Easy for testing.) autosuggest = FakeLLMAutoSuggest() # Print help. print("This CLI has fish-style auto-suggestion enabled across multiple lines.") print("This will try to complete the universal declaration of human rights.") print("") print(" " + "\n ".join(universal_declaration_of_human_rights)) print("") print("Add a few new lines to see multiline completion, and start typing.") print("Press Control-C to retry. Control-D to exit.") print() session = PromptSession( auto_suggest=autosuggest, enable_history_search=False, reserve_space_for_menu=5, multiline=True, prompt_continuation="... ", input_processors=[ ConditionalProcessor( processor=AppendMultilineAutoSuggestionInAnyLine(), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone(), ), ], ) while True: try: text = session.prompt( "Say something (Esc-enter : accept, enter : new line): " ) except KeyboardInterrupt: pass # Ctrl-C pressed. Try again. else: break print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/multiline-prompt.py ================================================ #!/usr/bin/env python """ Demonstration of how the input can be indented. """ from prompt_toolkit import prompt if __name__ == "__main__": answer = prompt( "Give me some input: (ESCAPE followed by ENTER to accept)\n > ", multiline=True ) print(f"You said: {answer}") ================================================ FILE: examples/prompts/no-wrapping.py ================================================ #!/usr/bin/env python from prompt_toolkit import prompt if __name__ == "__main__": answer = prompt("Give me some input: ", wrap_lines=False, multiline=True) print(f"You said: {answer}") ================================================ FILE: examples/prompts/operate-and-get-next.py ================================================ #!/usr/bin/env python """ Demo of "operate-and-get-next". (Actually, this creates one prompt application, and keeps running the same app over and over again. -- For now, this is the only way to get this working.) """ from prompt_toolkit.shortcuts import PromptSession def main(): session = PromptSession("prompt> ") while True: session.prompt() if __name__ == "__main__": main() ================================================ FILE: examples/prompts/patch-stdout.py ================================================ #!/usr/bin/env python """ An example that demonstrates how `patch_stdout` works. This makes sure that output from other threads doesn't disturb the rendering of the prompt, but instead is printed nicely above the prompt. """ import threading import time from prompt_toolkit import prompt from prompt_toolkit.patch_stdout import patch_stdout def main(): # Print a counter every second in another thread. running = True def thread(): i = 0 while running: i += 1 print(f"i={i}") time.sleep(1) t = threading.Thread(target=thread) t.daemon = True t.start() # Now read the input. The print statements of the other thread # should not disturb anything. with patch_stdout(): result = prompt("Say something: ") print(f"You said: {result}") # Stop thread. running = False if __name__ == "__main__": main() ================================================ FILE: examples/prompts/placeholder-text.py ================================================ #!/usr/bin/env python """ Example of a placeholder that's displayed as long as no input is given. """ from prompt_toolkit import prompt from prompt_toolkit.formatted_text import HTML if __name__ == "__main__": answer = prompt( "Give me some input: ", placeholder=HTML('<style color="#888888">(please type something)</style>'), ) print(f"You said: {answer}") ================================================ FILE: examples/prompts/regular-language.py ================================================ #!/usr/bin/env python """ This is an example of "prompt_toolkit.contrib.regular_languages" which implements a little calculator. Type for instance:: > add 4 4 > sub 4 4 > sin 3.14 This example shows how you can define the grammar of a regular language and how to use variables in this grammar with completers and tokens attached. """ import math from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter from prompt_toolkit.contrib.regular_languages.compiler import compile from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer from prompt_toolkit.lexers import SimpleLexer from prompt_toolkit.styles import Style operators1 = ["add", "sub", "div", "mul"] operators2 = ["cos", "sin"] def create_grammar(): return compile( r""" (\s* (?P<operator1>[a-z]+) \s+ (?P<var1>[0-9.]+) \s+ (?P<var2>[0-9.]+) \s*) | (\s* (?P<operator2>[a-z]+) \s+ (?P<var1>[0-9.]+) \s*) """ ) example_style = Style.from_dict( { "operator": "#33aa33 bold", "number": "#ff0000 bold", "trailing-input": "bg:#662222 #ffffff", } ) if __name__ == "__main__": g = create_grammar() lexer = GrammarLexer( g, lexers={ "operator1": SimpleLexer("class:operator"), "operator2": SimpleLexer("class:operator"), "var1": SimpleLexer("class:number"), "var2": SimpleLexer("class:number"), }, ) completer = GrammarCompleter( g, { "operator1": WordCompleter(operators1), "operator2": WordCompleter(operators2), }, ) try: # REPL loop. while True: # Read input and parse the result. text = prompt( "Calculate: ", lexer=lexer, completer=completer, style=example_style ) m = g.match(text) if m: vars = m.variables() else: print("Invalid command\n") continue print(vars) if vars.get("operator1") or vars.get("operator2"): try: var1 = float(vars.get("var1", 0)) var2 = float(vars.get("var2", 0)) except ValueError: print("Invalid command (2)\n") continue # Turn the operator string into a function. operator = { "add": (lambda a, b: a + b), "sub": (lambda a, b: a - b), "mul": (lambda a, b: a * b), "div": (lambda a, b: a / b), "sin": (lambda a, b: math.sin(a)), "cos": (lambda a, b: math.cos(a)), }[vars.get("operator1") or vars.get("operator2")] # Execute and print the result. print(f"Result: {operator(var1, var2)}\n") elif vars.get("operator2"): print("Operator 2") except EOFError: pass ================================================ FILE: examples/prompts/rprompt.py ================================================ #!/usr/bin/env python """ Example of a right prompt. This is an additional prompt that is displayed on the right side of the terminal. It will be hidden automatically when the input is long enough to cover the right side of the terminal. This is similar to RPROMPT is Zsh. """ from prompt_toolkit import prompt from prompt_toolkit.formatted_text import ANSI, HTML from prompt_toolkit.styles import Style example_style = Style.from_dict( { # The 'rprompt' gets by default the 'rprompt' class. We can use this # for the styling. "rprompt": "bg:#ff0066 #ffffff", } ) def get_rprompt_text(): return [ ("", " "), ("underline", "<rprompt>"), ("", " "), ] def main(): # Option 1: pass a string to 'rprompt': answer = prompt("> ", rprompt=" <rprompt> ", style=example_style) print(f"You said: {answer}") # Option 2: pass HTML: answer = prompt("> ", rprompt=HTML(" <u><rprompt></u> "), style=example_style) print(f"You said: {answer}") # Option 3: pass ANSI: answer = prompt( "> ", rprompt=ANSI(" \x1b[4m<rprompt>\x1b[0m "), style=example_style ) print(f"You said: {answer}") # Option 4: Pass a callable. (This callable can either return plain text, # an HTML object, an ANSI object or a list of (style, text) # tuples. answer = prompt("> ", rprompt=get_rprompt_text, style=example_style) print(f"You said: {answer}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/swap-light-and-dark-colors.py ================================================ #!/usr/bin/env python """ Demonstration of swapping light/dark colors in prompt_toolkit using the `swap_light_and_dark_colors` parameter. Notice that this doesn't swap foreground and background like "reverse" does. It turns light green into dark green and the other way around. Foreground and background are independent of each other. """ from pygments.lexers.html import HtmlLexer from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter from prompt_toolkit.filters import Condition from prompt_toolkit.formatted_text import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.lexers import PygmentsLexer html_completer = WordCompleter( [ "<body>", "<div>", "<head>", "<html>", "<img>", "<li>", "<link>", "<ol>", "<p>", "<span>", "<table>", "<td>", "<th>", "<tr>", "<ul>", ], ignore_case=True, ) def main(): swapped = [False] # Nonlocal bindings = KeyBindings() @bindings.add("c-t") def _(event): "When ControlT has been pressed, toggle light/dark colors." swapped[0] = not swapped[0] def bottom_toolbar(): if swapped[0]: on = "on=true" else: on = "on=false" return ( HTML( 'Press <style bg="#222222" fg="#ff8888">[control-t]</style> ' "to swap between dark/light colors. " '<style bg="ansiblack" fg="ansiwhite">[%s]</style>' ) % on ) text = prompt( HTML('<style fg="#aaaaaa">Give some animals</style>: '), completer=html_completer, complete_while_typing=True, bottom_toolbar=bottom_toolbar, key_bindings=bindings, lexer=PygmentsLexer(HtmlLexer), swap_light_and_dark_colors=Condition(lambda: swapped[0]), ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/switch-between-vi-emacs.py ================================================ #!/usr/bin/env python """ Example that displays how to switch between Emacs and Vi input mode. """ from prompt_toolkit import prompt from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import EditingMode from prompt_toolkit.key_binding import KeyBindings def run(): # Create a `KeyBindings` that contains the default key bindings. bindings = KeyBindings() # Add an additional key binding for toggling this flag. @bindings.add("f4") def _(event): "Toggle between Emacs and Vi mode." if event.app.editing_mode == EditingMode.VI: event.app.editing_mode = EditingMode.EMACS else: event.app.editing_mode = EditingMode.VI def bottom_toolbar(): "Display the current input mode." if get_app().editing_mode == EditingMode.VI: return " [F4] Vi " else: return " [F4] Emacs " prompt("> ", key_bindings=bindings, bottom_toolbar=bottom_toolbar) if __name__ == "__main__": run() ================================================ FILE: examples/prompts/system-clipboard-integration.py ================================================ #!/usr/bin/env python """ Demonstration of a custom clipboard class. This requires the 'pyperclip' library to be installed. """ from prompt_toolkit import prompt from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard if __name__ == "__main__": print("Emacs shortcuts:") print(" Press Control-Y to paste from the system clipboard.") print(" Press Control-Space or Control-@ to enter selection mode.") print(" Press Control-W to cut to clipboard.") print("") answer = prompt("Give me some input: ", clipboard=PyperclipClipboard()) print(f"You said: {answer}") ================================================ FILE: examples/prompts/system-prompt.py ================================================ #!/usr/bin/env python from prompt_toolkit import prompt if __name__ == "__main__": # System prompt. print( "(1/3) If you press meta-! or esc-! at the following prompt, you can enter system commands." ) answer = prompt("Give me some input: ", enable_system_prompt=True) print(f"You said: {answer}") # Enable suspend. print("(2/3) If you press Control-Z, the application will suspend.") answer = prompt("Give me some input: ", enable_suspend=True) print(f"You said: {answer}") # Enable open_in_editor print("(3/3) If you press Control-X Control-E, the prompt will open in $EDITOR.") answer = prompt("Give me some input: ", enable_open_in_editor=True) print(f"You said: {answer}") ================================================ FILE: examples/prompts/terminal-title.py ================================================ #!/usr/bin/env python from prompt_toolkit import prompt from prompt_toolkit.shortcuts import set_title if __name__ == "__main__": set_title("This is the terminal title") answer = prompt("Give me some input: ") set_title("") print(f"You said: {answer}") ================================================ FILE: examples/prompts/up-arrow-partial-string-matching.py ================================================ #!/usr/bin/env python """ Simple example of a CLI that demonstrates up-arrow partial string matching. When you type some input, it's possible to use the up arrow to filter the history on the items starting with the given input text. """ from prompt_toolkit import PromptSession from prompt_toolkit.history import InMemoryHistory def main(): # Create some history first. (Easy for testing.) history = InMemoryHistory() history.append_string("import os") history.append_string('print("hello")') history.append_string('print("world")') history.append_string("import path") # Print help. print("This CLI has up-arrow partial string matching enabled.") print('Type for instance "pri" followed by up-arrow and you') print('get the last items starting with "pri".') print("Press Control-C to retry. Control-D to exit.") print() session = PromptSession(history=history, enable_history_search=True) while True: try: text = session.prompt("Say something: ") except KeyboardInterrupt: pass # Ctrl-C pressed. Try again. else: break print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/with-frames/frame-and-autocompletion.py ================================================ #!/usr/bin/env python """ Example of a frame around a prompt input that has autocompletion and a bottom toolbar. """ from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter from prompt_toolkit.filters import is_done animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) def main(): text = prompt( "Give some animals: ", completer=animal_completer, complete_while_typing=False, # Only show the frame during editing. Hide when the input gets accepted. show_frame=~is_done, bottom_toolbar="Press [Tab] to complete the current word.", ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/with-frames/gray-frame-on-accept.py ================================================ #!/usr/bin/env python """ Example of a frame around a prompt input that has autocompletion and a bottom toolbar. """ from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter from prompt_toolkit.styles import Style animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) def main(): style = Style.from_dict( { "frame.border": "#ff4444", "accepted frame.border": "#444444", } ) text = prompt( "Give some animals: ", completer=animal_completer, complete_while_typing=False, show_frame=True, style=style, bottom_toolbar="Press [Tab] to complete the current word.", ) print(f"You said: {text}") if __name__ == "__main__": main() ================================================ FILE: examples/prompts/with-frames/with-frame.py ================================================ #!/usr/bin/env python """ Example of a frame around a prompt input. """ from prompt_toolkit import prompt from prompt_toolkit.styles import Style style = Style.from_dict( { "frame.border": "#884444", } ) def example(): """ Style and list of (style, text) tuples. """ answer = prompt("Say something > ", style=style, show_frame=True) print(f"You said: {answer}") if __name__ == "__main__": example() ================================================ FILE: examples/ssh/asyncssh-server.py ================================================ #!/usr/bin/env python """ Example of running a prompt_toolkit application in an asyncssh server. """ import asyncio import logging import asyncssh from pygments.lexers.html import HtmlLexer from prompt_toolkit.completion import WordCompleter from prompt_toolkit.contrib.ssh import PromptToolkitSSHServer, PromptToolkitSSHSession from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.shortcuts import ProgressBar, print_formatted_text from prompt_toolkit.shortcuts.dialogs import input_dialog, yes_no_dialog from prompt_toolkit.shortcuts.prompt import PromptSession animal_completer = WordCompleter( [ "alligator", "ant", "ape", "bat", "bear", "beaver", "bee", "bison", "butterfly", "cat", "chicken", "crocodile", "dinosaur", "dog", "dolphin", "dove", "duck", "eagle", "elephant", "fish", "goat", "gorilla", "kangaroo", "leopard", "lion", "mouse", "rabbit", "rat", "snake", "spider", "turkey", "turtle", ], ignore_case=True, ) async def interact(ssh_session: PromptToolkitSSHSession) -> None: """ The application interaction. This will run automatically in a prompt_toolkit AppSession, which means that any prompt_toolkit application (dialogs, prompts, etc...) will use the SSH channel for input and output. """ prompt_session = PromptSession() # Alias 'print_formatted_text', so that 'print' calls go to the SSH client. print = print_formatted_text print("We will be running a few prompt_toolkit applications through this ") print("SSH connection.\n") # Simple progress bar. with ProgressBar() as pb: for i in pb(range(50)): await asyncio.sleep(0.1) # Normal prompt. text = await prompt_session.prompt_async("(normal prompt) Type something: ") print("You typed", text) # Prompt with auto completion. text = await prompt_session.prompt_async( "(autocompletion) Type an animal: ", completer=animal_completer ) print("You typed", text) # prompt with syntax highlighting. text = await prompt_session.prompt_async( "(HTML syntax highlighting) Type something: ", lexer=PygmentsLexer(HtmlLexer) ) print("You typed", text) # Show yes/no dialog. await prompt_session.prompt_async("Showing yes/no dialog... [ENTER]") await yes_no_dialog("Yes/no dialog", "Running over asyncssh").run_async() # Show input dialog await prompt_session.prompt_async("Showing input dialog... [ENTER]") await input_dialog("Input dialog", "Running over asyncssh").run_async() async def main(port=8222): # Set up logging. logging.basicConfig() logging.getLogger().setLevel(logging.DEBUG) await asyncssh.create_server( lambda: PromptToolkitSSHServer(interact), "", port, server_host_keys=["/etc/ssh/ssh_host_ecdsa_key"], ) # Run forever. await asyncio.Future() if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: examples/telnet/chat-app.py ================================================ #!/usr/bin/env python """ A simple chat application over telnet. Everyone that connects is asked for his name, and then people can chat with each other. """ import logging import random from asyncio import Future, run from prompt_toolkit.contrib.telnet.server import TelnetServer from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import PromptSession, clear # Set up logging logging.basicConfig() logging.getLogger().setLevel(logging.INFO) # List of connections. _connections = [] _connection_to_color = {} COLORS = [ "ansired", "ansigreen", "ansiyellow", "ansiblue", "ansifuchsia", "ansiturquoise", "ansilightgray", "ansidarkgray", "ansidarkred", "ansidarkgreen", "ansibrown", "ansidarkblue", "ansipurple", "ansiteal", ] async def interact(connection): write = connection.send prompt_session = PromptSession() # When a client is connected, erase the screen from the client and say # Hello. clear() write("Welcome to our chat application!\n") write("All connected clients will receive what you say.\n") name = await prompt_session.prompt_async(message="Type your name: ") # Random color. color = random.choice(COLORS) _connection_to_color[connection] = color # Send 'connected' message. _send_to_everyone(connection, name, "(connected)", color) # Prompt. prompt_msg = HTML('<reverse fg="{}">[{}]</reverse> > ').format(color, name) _connections.append(connection) try: # Set Application. while True: try: result = await prompt_session.prompt_async(message=prompt_msg) _send_to_everyone(connection, name, result, color) except KeyboardInterrupt: pass except EOFError: _send_to_everyone(connection, name, "(leaving)", color) finally: _connections.remove(connection) def _send_to_everyone(sender_connection, name, message, color): """ Send a message to all the clients. """ for c in _connections: if c != sender_connection: c.send_above_prompt( [ ("fg:" + color, f"[{name}]"), ("", " "), ("fg:" + color, f"{message}\n"), ] ) async def main(): server = TelnetServer(interact=interact, port=2323) server.start() # Run forever. await Future() if __name__ == "__main__": run(main()) ================================================ FILE: examples/telnet/dialog.py ================================================ #!/usr/bin/env python """ Example of a telnet application that displays a dialog window. """ import logging from asyncio import Future, run from prompt_toolkit.contrib.telnet.server import TelnetServer from prompt_toolkit.shortcuts.dialogs import yes_no_dialog # Set up logging logging.basicConfig() logging.getLogger().setLevel(logging.INFO) async def interact(connection): result = await yes_no_dialog( title="Yes/no dialog demo", text="Press yes or no" ).run_async() connection.send(f"You said: {result}\n") connection.send("Bye.\n") async def main(): server = TelnetServer(interact=interact, port=2323) server.start() # Run forever. await Future() if __name__ == "__main__": run(main()) ================================================ FILE: examples/telnet/hello-world.py ================================================ #!/usr/bin/env python """ A simple Telnet application that asks for input and responds. The interaction function is a prompt_toolkit coroutine. Also see the `hello-world-asyncio.py` example which uses an asyncio coroutine. That is probably the preferred way if you only need Python 3 support. """ import logging from asyncio import run from prompt_toolkit.contrib.telnet.server import TelnetServer from prompt_toolkit.shortcuts import PromptSession, clear # Set up logging logging.basicConfig() logging.getLogger().setLevel(logging.INFO) async def interact(connection): clear() connection.send("Welcome!\n") # Ask for input. session = PromptSession() result = await session.prompt_async(message="Say something: ") # Send output. connection.send(f"You said: {result}\n") connection.send("Bye.\n") async def main(): server = TelnetServer(interact=interact, port=2323) await server.run() if __name__ == "__main__": run(main()) ================================================ FILE: examples/telnet/toolbar.py ================================================ #!/usr/bin/env python """ Example of a telnet application that displays a bottom toolbar and completions in the prompt. """ import logging from asyncio import run from prompt_toolkit.completion import WordCompleter from prompt_toolkit.contrib.telnet.server import TelnetServer from prompt_toolkit.shortcuts import PromptSession # Set up logging logging.basicConfig() logging.getLogger().setLevel(logging.INFO) async def interact(connection): # When a client is connected, erase the screen from the client and say # Hello. connection.send("Welcome!\n") # Display prompt with bottom toolbar. animal_completer = WordCompleter(["alligator", "ant"]) def get_toolbar(): return "Bottom toolbar..." session = PromptSession() result = await session.prompt_async( "Say something: ", bottom_toolbar=get_toolbar, completer=animal_completer ) connection.send(f"You said: {result}\n") connection.send("Bye.\n") async def main(): server = TelnetServer(interact=interact, port=2323) await server.run() if __name__ == "__main__": run(main()) ================================================ FILE: examples/tutorial/README.md ================================================ See http://python-prompt-toolkit.readthedocs.io/en/stable/pages/tutorials/repl.html ================================================ FILE: examples/tutorial/sqlite-cli.py ================================================ #!/usr/bin/env python import sqlite3 import sys from pygments.lexers.sql import SqlLexer from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles import Style sql_completer = WordCompleter( [ "abort", "action", "add", "after", "all", "alter", "analyze", "and", "as", "asc", "attach", "autoincrement", "before", "begin", "between", "by", "cascade", "case", "cast", "check", "collate", "column", "commit", "conflict", "constraint", "create", "cross", "current_date", "current_time", "current_timestamp", "database", "default", "deferrable", "deferred", "delete", "desc", "detach", "distinct", "drop", "each", "else", "end", "escape", "except", "exclusive", "exists", "explain", "fail", "for", "foreign", "from", "full", "glob", "group", "having", "if", "ignore", "immediate", "in", "index", "indexed", "initially", "inner", "insert", "instead", "intersect", "into", "is", "isnull", "join", "key", "left", "like", "limit", "match", "natural", "no", "not", "notnull", "null", "of", "offset", "on", "or", "order", "outer", "plan", "pragma", "primary", "query", "raise", "recursive", "references", "regexp", "reindex", "release", "rename", "replace", "restrict", "right", "rollback", "row", "savepoint", "select", "set", "table", "temp", "temporary", "then", "to", "transaction", "trigger", "union", "unique", "update", "using", "vacuum", "values", "view", "virtual", "when", "where", "with", "without", ], ignore_case=True, ) style = Style.from_dict( { "completion-menu.completion": "bg:#008888 #ffffff", "completion-menu.completion.current": "bg:#00aaaa #000000", "scrollbar.background": "bg:#88aaaa", "scrollbar.button": "bg:#222222", } ) def main(database): connection = sqlite3.connect(database) session = PromptSession( lexer=PygmentsLexer(SqlLexer), completer=sql_completer, style=style ) while True: try: text = session.prompt("> ") except KeyboardInterrupt: continue # Control-C pressed. Try again. except EOFError: break # Control-D pressed. with connection: try: messages = connection.execute(text) except Exception as e: print(repr(e)) else: for message in messages: print(message) print("GoodBye!") if __name__ == "__main__": if len(sys.argv) < 2: db = ":memory:" else: db = sys.argv[1] main(db) ================================================ FILE: pyproject.toml ================================================ [project] name = "prompt_toolkit" version = "3.0.52" # Also update in `docs/conf.py`. description = "Library for building powerful interactive command lines in Python" readme = "README.rst" authors = [{ name = "Jonathan Slenders" }] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Free Threading :: 3 - Stable", "Programming Language :: Python", "Topic :: Software Development", ] requires-python = ">=3.10" dependencies = ["wcwidth>=0.1.4"] [dependency-groups] build = ["build>=1", "setuptools>=68"] docs = [ "sphinx>=8,<9", "wcwidth", "pyperclip", "sphinx_copybutton>=0.5.2,<1.0.0", "sphinx-nefertiti>=0.8.8", ] dev = [ "asyncssh", "codecov>=2.1", "coverage>=7.11", "ipython>=8.23", "mypy>=1.13", "prek>=0.3.5", "pytest>=8.1.1", "pytest-cov>=5", "pytest-mock>=3.14.1", "ruff>=0.14.10", "typos", ] [project.urls] Homepage = "https://github.com/prompt-toolkit/python-prompt-toolkit" Documentation = "https://python-prompt-toolkit.readthedocs.io/en/stable/" [tool.ruff] target-version = "py310" lint.select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "C", # flake8-comprehensions "T", # Print. "I", # isort # "B", # flake8-bugbear "UP", # pyupgrade "RUF100", # unused-noqa "Q", # quotes ] lint.ignore = [ "E501", # Line too long, handled by black "C901", # Too complex "E731", # Assign lambda. "E402", # Module level import not at the top. "E741", # Ambiguous variable name. ] [tool.ruff.lint.per-file-ignores] "examples/*" = ["UP031", "T201"] # Print allowed in examples. "src/prompt_toolkit/application/application.py" = [ "T100", "T201", "F821", ] # pdb and print allowed. "src/prompt_toolkit/contrib/telnet/server.py" = ["T201"] # Print allowed. "src/prompt_toolkit/key_binding/bindings/named_commands.py" = [ "T201", ] # Print allowed. "src/prompt_toolkit/shortcuts/progress_bar/base.py" = ["T201"] # Print allowed. "tools/*" = ["T201"] # Print allowed. "src/prompt_toolkit/filters/__init__.py" = [ "F403", "F405", ] # Possibly undefined due to star import. "src/prompt_toolkit/filters/cli.py" = [ "F403", "F405", ] # Possibly undefined due to star import. "src/prompt_toolkit/shortcuts/progress_bar/formatters.py" = [ "UP031", ] # %-style formatting. "src/*" = ["UP031", "UP032"] # f-strings instead of format calls. [tool.ruff.lint.isort] known-first-party = ["prompt_toolkit"] known-third-party = ["pygments", "asyncssh"] [tool.typos.default] extend-ignore-re = [ "Formicidae", "Iterm", "goes", "iterm", "prepend", "prepended", "prev", "ret", "rouble", "x1b\\[4m", "Vertica", # Database. # Deliberate spelling mistakes in autocorrection.py "wolrd", "impotr", # Lorem ipsum. "Nam", "varius", ] locale = 'en-us' # US English. [tool.typos.files] extend-exclude = [ "tests/test_buffer.py", "tests/test_cli.py", "tests/test_regular_languages.py", # complains about some spelling in human right declaration. "examples/prompts/multiline-autosuggest.py", ] [tool.mypy] # --strict. check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true exclude = [ "^.git/", "^.venv/", "^build/", # .build directory "^docs/", # docs directory "^dist/", "^examples/", # examples directory "^tests/", # tests directory ] files = ['.'] ignore_missing_imports = true no_implicit_optional = true no_implicit_reexport = true strict_equality = true strict_optional = true warn_redundant_casts = true warn_return_any = true warn_unused_configs = true warn_unused_ignores = true [build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" [tool.uv] default-groups = ["build", "dev"] ================================================ FILE: src/prompt_toolkit/__init__.py ================================================ """ prompt_toolkit ============== Author: Jonathan Slenders Description: prompt_toolkit is a Library for building powerful interactive command lines in Python. It can be a replacement for GNU Readline, but it can be much more than that. See the examples directory to learn about the usage. Probably, to get started, you might also want to have a look at `prompt_toolkit.shortcuts.prompt`. """ from __future__ import annotations from typing import Any from .application import Application from .formatted_text import ANSI, HTML from .shortcuts import PromptSession, choice, print_formatted_text, prompt __version__: str VERSION: tuple[int, int, int] def _load_version() -> None: """ Load the package version from importlib.metadata and cache both __version__ and VERSION in the module globals. """ global __version__, VERSION import re from importlib import metadata # note: this is a bit more lax than the actual pep 440 to allow for a/b/rc/dev without a number pep440_pattern = ( r"^([1-9]\d*!)?(0|[1-9]\d*)(\.(0|[1-9]\d*))*" r"((a|b|rc)(0|[1-9]\d*)?)?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*)?)?$" ) version = metadata.version("prompt_toolkit") assert re.fullmatch(pep440_pattern, version) # Version string. __version__ = version # Version tuple. parts = [int(v.rstrip("abrc")) for v in version.split(".")] VERSION = (parts[0], parts[1], parts[2]) def __getattr__(name: str) -> Any: if name in {"__version__", "VERSION"}: _load_version() return globals()[name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def __dir__() -> list[str]: return sorted( { *globals().keys(), "__version__", "VERSION", } ) __all__ = [ # Application. "Application", # Shortcuts. "prompt", "choice", "PromptSession", "print_formatted_text", # Formatted text. "HTML", "ANSI", # Version info. "__version__", "VERSION", ] ================================================ FILE: src/prompt_toolkit/application/__init__.py ================================================ from __future__ import annotations from .application import Application from .current import ( AppSession, create_app_session, create_app_session_from_tty, get_app, get_app_or_none, get_app_session, set_app, ) from .dummy import DummyApplication from .run_in_terminal import in_terminal, run_in_terminal __all__ = [ # Application. "Application", # Current. "AppSession", "get_app_session", "create_app_session", "create_app_session_from_tty", "get_app", "get_app_or_none", "set_app", # Dummy. "DummyApplication", # Run_in_terminal "in_terminal", "run_in_terminal", ] ================================================ FILE: src/prompt_toolkit/application/application.py ================================================ from __future__ import annotations import asyncio import contextvars import os import re import signal import sys import threading import time from asyncio import ( AbstractEventLoop, Future, Task, ensure_future, get_running_loop, sleep, ) from collections.abc import Callable, Coroutine, Generator, Hashable, Iterable, Iterator from contextlib import ExitStack, contextmanager from subprocess import Popen from traceback import format_tb from typing import ( Any, Generic, TypeVar, cast, overload, ) from prompt_toolkit.buffer import Buffer from prompt_toolkit.cache import SimpleCache from prompt_toolkit.clipboard import Clipboard, InMemoryClipboard from prompt_toolkit.cursor_shapes import AnyCursorShapeConfig, to_cursor_shape_config from prompt_toolkit.data_structures import Size from prompt_toolkit.enums import EditingMode from prompt_toolkit.eventloop import ( InputHook, get_traceback_from_context, new_eventloop_with_inputhook, run_in_executor_with_context, ) from prompt_toolkit.eventloop.utils import call_soon_threadsafe from prompt_toolkit.filters import Condition, Filter, FilterOrBool, to_filter from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.input.base import Input from prompt_toolkit.input.typeahead import get_typeahead, store_typeahead from prompt_toolkit.key_binding.bindings.page_navigation import ( load_page_navigation_bindings, ) from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.key_binding.emacs_state import EmacsState from prompt_toolkit.key_binding.key_bindings import ( Binding, ConditionalKeyBindings, GlobalOnlyKeyBindings, KeyBindings, KeyBindingsBase, KeysTuple, merge_key_bindings, ) from prompt_toolkit.key_binding.key_processor import KeyPressEvent, KeyProcessor from prompt_toolkit.key_binding.vi_state import ViState from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import Container, Window from prompt_toolkit.layout.controls import BufferControl, UIControl from prompt_toolkit.layout.dummy import create_dummy_layout from prompt_toolkit.layout.layout import Layout, walk from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.renderer import Renderer, print_formatted_text from prompt_toolkit.search import SearchState from prompt_toolkit.styles import ( BaseStyle, DummyStyle, DummyStyleTransformation, DynamicStyle, StyleTransformation, default_pygments_style, default_ui_style, merge_styles, ) from prompt_toolkit.utils import Event, in_main_thread from .current import get_app_session, set_app from .run_in_terminal import in_terminal, run_in_terminal __all__ = [ "Application", ] E = KeyPressEvent _AppResult = TypeVar("_AppResult") ApplicationEventHandler = Callable[["Application[_AppResult]"], None] _SIGWINCH = getattr(signal, "SIGWINCH", None) _SIGTSTP = getattr(signal, "SIGTSTP", None) class Application(Generic[_AppResult]): """ The main Application class! This glues everything together. :param layout: A :class:`~prompt_toolkit.layout.Layout` instance. :param key_bindings: :class:`~prompt_toolkit.key_binding.KeyBindingsBase` instance for the key bindings. :param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` to use. :param full_screen: When True, run the application on the alternate screen buffer. :param color_depth: Any :class:`~.ColorDepth` value, a callable that returns a :class:`~.ColorDepth` or `None` for default. :param erase_when_done: (bool) Clear the application output when it finishes. :param reverse_vi_search_direction: Normally, in Vi mode, a '/' searches forward and a '?' searches backward. In Readline mode, this is usually reversed. :param min_redraw_interval: Number of seconds to wait between redraws. Use this for applications where `invalidate` is called a lot. This could cause a lot of terminal output, which some terminals are not able to process. `None` means that every `invalidate` will be scheduled right away (which is usually fine). When one `invalidate` is called, but a scheduled redraw of a previous `invalidate` call has not been executed yet, nothing will happen in any case. :param max_render_postpone_time: When there is high CPU (a lot of other scheduled calls), postpone the rendering max x seconds. '0' means: don't postpone. '.5' means: try to draw at least twice a second. :param refresh_interval: Automatically invalidate the UI every so many seconds. When `None` (the default), only invalidate when `invalidate` has been called. :param terminal_size_polling_interval: Poll the terminal size every so many seconds. Useful if the applications runs in a thread other then then main thread where SIGWINCH can't be handled, or on Windows. Filters: :param mouse_support: (:class:`~prompt_toolkit.filters.Filter` or boolean). When True, enable mouse support. :param paste_mode: :class:`~prompt_toolkit.filters.Filter` or boolean. :param editing_mode: :class:`~prompt_toolkit.enums.EditingMode`. :param enable_page_navigation_bindings: When `True`, enable the page navigation key bindings. These include both Emacs and Vi bindings like page-up, page-down and so on to scroll through pages. Mostly useful for creating an editor or other full screen applications. Probably, you don't want this for the implementation of a REPL. By default, this is enabled if `full_screen` is set. Callbacks (all of these should accept an :class:`~prompt_toolkit.application.Application` object as input.) :param on_reset: Called during reset. :param on_invalidate: Called when the UI has been invalidated. :param before_render: Called right before rendering. :param after_render: Called right after rendering. I/O: (Note that the preferred way to change the input/output is by creating an `AppSession` with the required input/output objects. If you need multiple applications running at the same time, you have to create a separate `AppSession` using a `with create_app_session():` block. :param input: :class:`~prompt_toolkit.input.Input` instance. :param output: :class:`~prompt_toolkit.output.Output` instance. (Probably Vt100_Output or Win32Output.) Usage: app = Application(...) app.run() # Or await app.run_async() """ def __init__( self, layout: Layout | None = None, style: BaseStyle | None = None, include_default_pygments_style: FilterOrBool = True, style_transformation: StyleTransformation | None = None, key_bindings: KeyBindingsBase | None = None, clipboard: Clipboard | None = None, full_screen: bool = False, color_depth: (ColorDepth | Callable[[], ColorDepth | None] | None) = None, mouse_support: FilterOrBool = False, enable_page_navigation_bindings: None | (FilterOrBool) = None, # Can be None, True or False. paste_mode: FilterOrBool = False, editing_mode: EditingMode = EditingMode.EMACS, erase_when_done: bool = False, reverse_vi_search_direction: FilterOrBool = False, min_redraw_interval: float | int | None = None, max_render_postpone_time: float | int | None = 0.01, refresh_interval: float | None = None, terminal_size_polling_interval: float | None = 0.5, cursor: AnyCursorShapeConfig = None, on_reset: ApplicationEventHandler[_AppResult] | None = None, on_invalidate: ApplicationEventHandler[_AppResult] | None = None, before_render: ApplicationEventHandler[_AppResult] | None = None, after_render: ApplicationEventHandler[_AppResult] | None = None, # I/O. input: Input | None = None, output: Output | None = None, ) -> None: # If `enable_page_navigation_bindings` is not specified, enable it in # case of full screen applications only. This can be overridden by the user. if enable_page_navigation_bindings is None: enable_page_navigation_bindings = Condition(lambda: self.full_screen) paste_mode = to_filter(paste_mode) mouse_support = to_filter(mouse_support) reverse_vi_search_direction = to_filter(reverse_vi_search_direction) enable_page_navigation_bindings = to_filter(enable_page_navigation_bindings) include_default_pygments_style = to_filter(include_default_pygments_style) if layout is None: layout = create_dummy_layout() if style_transformation is None: style_transformation = DummyStyleTransformation() self.style = style self.style_transformation = style_transformation # Key bindings. self.key_bindings = key_bindings self._default_bindings = load_key_bindings() self._page_navigation_bindings = load_page_navigation_bindings() self.layout = layout self.clipboard = clipboard or InMemoryClipboard() self.full_screen: bool = full_screen self._color_depth = color_depth self.mouse_support = mouse_support self.paste_mode = paste_mode self.editing_mode = editing_mode self.erase_when_done = erase_when_done self.reverse_vi_search_direction = reverse_vi_search_direction self.enable_page_navigation_bindings = enable_page_navigation_bindings self.min_redraw_interval = min_redraw_interval self.max_render_postpone_time = max_render_postpone_time self.refresh_interval = refresh_interval self.terminal_size_polling_interval = terminal_size_polling_interval self.cursor = to_cursor_shape_config(cursor) # Events. self.on_invalidate = Event(self, on_invalidate) self.on_reset = Event(self, on_reset) self.before_render = Event(self, before_render) self.after_render = Event(self, after_render) # I/O. session = get_app_session() self.output = output or session.output self.input = input or session.input # List of 'extra' functions to execute before a Application.run. self.pre_run_callables: list[Callable[[], None]] = [] self._is_running = False self.future: Future[_AppResult] | None = None self.loop: AbstractEventLoop | None = None self._loop_thread: threading.Thread | None = None self.context: contextvars.Context | None = None #: Quoted insert. This flag is set if we go into quoted insert mode. self.quoted_insert = False #: Vi state. (For Vi key bindings.) self.vi_state = ViState() self.emacs_state = EmacsState() #: When to flush the input (For flushing escape keys.) This is important #: on terminals that use vt100 input. We can't distinguish the escape #: key from for instance the left-arrow key, if we don't know what follows #: after "\x1b". This little timer will consider "\x1b" to be escape if #: nothing did follow in this time span. #: This seems to work like the `ttimeoutlen` option in Vim. self.ttimeoutlen = 0.5 # Seconds. #: Like Vim's `timeoutlen` option. This can be `None` or a float. For #: instance, suppose that we have a key binding AB and a second key #: binding A. If the uses presses A and then waits, we don't handle #: this binding yet (unless it was marked 'eager'), because we don't #: know what will follow. This timeout is the maximum amount of time #: that we wait until we call the handlers anyway. Pass `None` to #: disable this timeout. self.timeoutlen = 1.0 #: The `Renderer` instance. # Make sure that the same stdout is used, when a custom renderer has been passed. self._merged_style = self._create_merged_style(include_default_pygments_style) self.renderer = Renderer( self._merged_style, self.output, full_screen=full_screen, mouse_support=mouse_support, cpr_not_supported_callback=self.cpr_not_supported_callback, ) #: Render counter. This one is increased every time the UI is rendered. #: It can be used as a key for caching certain information during one #: rendering. self.render_counter = 0 # Invalidate flag. When 'True', a repaint has been scheduled. self._invalidated = False self._invalidate_events: list[ Event[object] ] = [] # Collection of 'invalidate' Event objects. self._last_redraw_time = 0.0 # Unix timestamp of last redraw. Used when # `min_redraw_interval` is given. #: The `InputProcessor` instance. self.key_processor = KeyProcessor(_CombinedRegistry(self)) # If `run_in_terminal` was called. This will point to a `Future` what will be # set at the point when the previous run finishes. self._running_in_terminal = False self._running_in_terminal_f: Future[None] | None = None # Trigger initialize callback. self.reset() def _create_merged_style(self, include_default_pygments_style: Filter) -> BaseStyle: """ Create a `Style` object that merges the default UI style, the default pygments style, and the custom user style. """ dummy_style = DummyStyle() pygments_style = default_pygments_style() @DynamicStyle def conditional_pygments_style() -> BaseStyle: if include_default_pygments_style(): return pygments_style else: return dummy_style return merge_styles( [ default_ui_style(), conditional_pygments_style, DynamicStyle(lambda: self.style), ] ) @property def color_depth(self) -> ColorDepth: """ The active :class:`.ColorDepth`. The current value is determined as follows: - If a color depth was given explicitly to this application, use that value. - Otherwise, fall back to the color depth that is reported by the :class:`.Output` implementation. If the :class:`.Output` class was created using `output.defaults.create_output`, then this value is coming from the $PROMPT_TOOLKIT_COLOR_DEPTH environment variable. """ depth = self._color_depth if callable(depth): depth = depth() if depth is None: depth = self.output.get_default_color_depth() return depth @property def current_buffer(self) -> Buffer: """ The currently focused :class:`~.Buffer`. (This returns a dummy :class:`.Buffer` when none of the actual buffers has the focus. In this case, it's really not practical to check for `None` values or catch exceptions every time.) """ return self.layout.current_buffer or Buffer( name="dummy-buffer" ) # Dummy buffer. @property def current_search_state(self) -> SearchState: """ Return the current :class:`.SearchState`. (The one for the focused :class:`.BufferControl`.) """ ui_control = self.layout.current_control if isinstance(ui_control, BufferControl): return ui_control.search_state else: return SearchState() # Dummy search state. (Don't return None!) def reset(self) -> None: """ Reset everything, for reading the next input. """ # Notice that we don't reset the buffers. (This happens just before # returning, and when we have multiple buffers, we clearly want the # content in the other buffers to remain unchanged between several # calls of `run`. (And the same is true for the focus stack.) self.exit_style = "" self._background_tasks: set[Task[None]] = set() self.renderer.reset() self.key_processor.reset() self.layout.reset() self.vi_state.reset() self.emacs_state.reset() # Trigger reset event. self.on_reset.fire() # Make sure that we have a 'focusable' widget focused. # (The `Layout` class can't determine this.) layout = self.layout if not layout.current_control.is_focusable(): for w in layout.find_all_windows(): if w.content.is_focusable(): layout.current_window = w break def invalidate(self) -> None: """ Thread safe way of sending a repaint trigger to the input event loop. """ if not self._is_running: # Don't schedule a redraw if we're not running. # Otherwise, `get_running_loop()` in `call_soon_threadsafe` can fail. # See: https://github.com/dbcli/mycli/issues/797 return # `invalidate()` called if we don't have a loop yet (not running?), or # after the event loop was closed. if self.loop is None or self.loop.is_closed(): return # Never schedule a second redraw, when a previous one has not yet been # executed. (This should protect against other threads calling # 'invalidate' many times, resulting in 100% CPU.) if self._invalidated: return else: self._invalidated = True # Trigger event. self.loop.call_soon_threadsafe(self.on_invalidate.fire) def redraw() -> None: self._invalidated = False self._redraw() def schedule_redraw() -> None: call_soon_threadsafe( redraw, max_postpone_time=self.max_render_postpone_time, loop=self.loop ) if self.min_redraw_interval: # When a minimum redraw interval is set, wait minimum this amount # of time between redraws. diff = time.time() - self._last_redraw_time if diff < self.min_redraw_interval: async def redraw_in_future() -> None: await sleep(cast(float, self.min_redraw_interval) - diff) schedule_redraw() self.loop.call_soon_threadsafe( lambda: self.create_background_task(redraw_in_future()) ) else: schedule_redraw() else: schedule_redraw() @property def invalidated(self) -> bool: "True when a redraw operation has been scheduled." return self._invalidated def _redraw(self, render_as_done: bool = False) -> None: """ Render the command line again. (Not thread safe!) (From other threads, or if unsure, use :meth:`.Application.invalidate`.) :param render_as_done: make sure to put the cursor after the UI. """ def run_in_context() -> None: # Only draw when no sub application was started. if self._is_running and not self._running_in_terminal: if self.min_redraw_interval: self._last_redraw_time = time.time() # Render self.render_counter += 1 self.before_render.fire() if render_as_done: if self.erase_when_done: self.renderer.erase() else: # Draw in 'done' state and reset renderer. self.renderer.render(self, self.layout, is_done=render_as_done) else: self.renderer.render(self, self.layout) self.layout.update_parents_relations() # Fire render event. self.after_render.fire() self._update_invalidate_events() # NOTE: We want to make sure this Application is the active one. The # invalidate function is often called from a context where this # application is not the active one. (Like the # `PromptSession._auto_refresh_context`). # We copy the context in case the context was already active, to # prevent RuntimeErrors. (The rendering is not supposed to change # any context variables.) if self.context is not None: self.context.copy().run(run_in_context) def _start_auto_refresh_task(self) -> None: """ Start a while/true loop in the background for automatic invalidation of the UI. """ if self.refresh_interval is not None and self.refresh_interval != 0: async def auto_refresh(refresh_interval: float) -> None: while True: await sleep(refresh_interval) self.invalidate() self.create_background_task(auto_refresh(self.refresh_interval)) def _update_invalidate_events(self) -> None: """ Make sure to attach 'invalidate' handlers to all invalidate events in the UI. """ # Remove all the original event handlers. (Components can be removed # from the UI.) for ev in self._invalidate_events: ev -= self._invalidate_handler # Gather all new events. # (All controls are able to invalidate themselves.) def gather_events() -> Iterable[Event[object]]: for c in self.layout.find_all_controls(): yield from c.get_invalidate_events() self._invalidate_events = list(gather_events()) for ev in self._invalidate_events: ev += self._invalidate_handler def _invalidate_handler(self, sender: object) -> None: """ Handler for invalidate events coming from UIControls. (This handles the difference in signature between event handler and `self.invalidate`. It also needs to be a method -not a nested function-, so that we can remove it again .) """ self.invalidate() def _on_resize(self) -> None: """ When the window size changes, we erase the current output and request again the cursor position. When the CPR answer arrives, the output is drawn again. """ # Erase, request position (when cursor is at the start position) # and redraw again. -- The order is important. self.renderer.erase(leave_alternate_screen=False) self._request_absolute_cursor_position() self._redraw() def _pre_run(self, pre_run: Callable[[], None] | None = None) -> None: """ Called during `run`. `self.future` should be set to the new future at the point where this is called in order to avoid data races. `pre_run` can be used to set a `threading.Event` to synchronize with UI termination code, running in another thread that would call `Application.exit`. (See the progress bar code for an example.) """ if pre_run: pre_run() # Process registered "pre_run_callables" and clear list. for c in self.pre_run_callables: c() del self.pre_run_callables[:] async def run_async( self, pre_run: Callable[[], None] | None = None, set_exception_handler: bool = True, handle_sigint: bool = True, slow_callback_duration: float = 0.5, ) -> _AppResult: """ Run the prompt_toolkit :class:`~prompt_toolkit.application.Application` until :meth:`~prompt_toolkit.application.Application.exit` has been called. Return the value that was passed to :meth:`~prompt_toolkit.application.Application.exit`. This is the main entry point for a prompt_toolkit :class:`~prompt_toolkit.application.Application` and usually the only place where the event loop is actually running. :param pre_run: Optional callable, which is called right after the "reset" of the application. :param set_exception_handler: When set, in case of an exception, go out of the alternate screen and hide the application, display the exception, and wait for the user to press ENTER. :param handle_sigint: Handle SIGINT signal if possible. This will call the `<sigint>` key binding when a SIGINT is received. (This only works in the main thread.) :param slow_callback_duration: Display warnings if code scheduled in the asyncio event loop takes more time than this. The asyncio default of `0.1` is sometimes not sufficient on a slow system, because exceptionally, the drawing of the app, which happens in the event loop, can take a bit longer from time to time. """ assert not self._is_running, "Application is already running." if not in_main_thread() or sys.platform == "win32": # Handling signals in other threads is not supported. # Also on Windows, `add_signal_handler(signal.SIGINT, ...)` raises # `NotImplementedError`. # See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1553 handle_sigint = False async def _run_async(f: asyncio.Future[_AppResult]) -> _AppResult: context = contextvars.copy_context() self.context = context # Counter for cancelling 'flush' timeouts. Every time when a key is # pressed, we start a 'flush' timer for flushing our escape key. But # when any subsequent input is received, a new timer is started and # the current timer will be ignored. flush_task: asyncio.Task[None] | None = None # Reset. # (`self.future` needs to be set when `pre_run` is called.) self.reset() self._pre_run(pre_run) # Feed type ahead input first. self.key_processor.feed_multiple(get_typeahead(self.input)) self.key_processor.process_keys() def read_from_input() -> None: nonlocal flush_task # Ignore when we aren't running anymore. This callback will # removed from the loop next time. (It could be that it was # still in the 'tasks' list of the loop.) # Except: if we need to process incoming CPRs. if not self._is_running and not self.renderer.waiting_for_cpr: return # Get keys from the input object. keys = self.input.read_keys() # Feed to key processor. self.key_processor.feed_multiple(keys) self.key_processor.process_keys() # Quit when the input stream was closed. if self.input.closed: if not f.done(): f.set_exception(EOFError) else: # Automatically flush keys. if flush_task: flush_task.cancel() flush_task = self.create_background_task(auto_flush_input()) def read_from_input_in_context() -> None: # Ensure that key bindings callbacks are always executed in the # current context. This is important when key bindings are # accessing contextvars. (These callbacks are currently being # called from a different context. Underneath, # `loop.add_reader` is used to register the stdin FD.) # (We copy the context to avoid a `RuntimeError` in case the # context is already active.) context.copy().run(read_from_input) async def auto_flush_input() -> None: # Flush input after timeout. # (Used for flushing the enter key.) # This sleep can be cancelled, in that case we won't flush yet. await sleep(self.ttimeoutlen) flush_input() def flush_input() -> None: if not self.is_done: # Get keys, and feed to key processor. keys = self.input.flush_keys() self.key_processor.feed_multiple(keys) self.key_processor.process_keys() if self.input.closed: f.set_exception(EOFError) # Enter raw mode, attach input and attach WINCH event handler. with ( self.input.raw_mode(), self.input.attach(read_from_input_in_context), attach_winch_signal_handler(self._on_resize), ): # Draw UI. self._request_absolute_cursor_position() self._redraw() self._start_auto_refresh_task() self.create_background_task(self._poll_output_size()) # Wait for UI to finish. try: result = await f finally: # In any case, when the application finishes. # (Successful, or because of an error.) try: self._redraw(render_as_done=True) finally: # _redraw has a good chance to fail if it calls widgets # with bad code. Make sure to reset the renderer # anyway. self.renderer.reset() # Unset `is_running`, this ensures that possibly # scheduled draws won't paint during the following # yield. self._is_running = False # Detach event handlers for invalidate events. # (Important when a UIControl is embedded in multiple # applications, like ptterm in pymux. An invalidate # should not trigger a repaint in terminated # applications.) for ev in self._invalidate_events: ev -= self._invalidate_handler self._invalidate_events = [] # Wait for CPR responses. if self.output.responds_to_cpr: await self.renderer.wait_for_cpr_responses() # Wait for the run-in-terminals to terminate. previous_run_in_terminal_f = self._running_in_terminal_f if previous_run_in_terminal_f: await previous_run_in_terminal_f # Store unprocessed input as typeahead for next time. store_typeahead(self.input, self.key_processor.empty_queue()) return result @contextmanager def set_loop() -> Iterator[AbstractEventLoop]: loop = get_running_loop() self.loop = loop self._loop_thread = threading.current_thread() try: yield loop finally: self.loop = None self._loop_thread = None @contextmanager def set_is_running() -> Iterator[None]: self._is_running = True try: yield finally: self._is_running = False @contextmanager def set_handle_sigint(loop: AbstractEventLoop) -> Iterator[None]: if handle_sigint: with _restore_sigint_from_ctypes(): # save sigint handlers (python and os level) # See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1576 loop.add_signal_handler( signal.SIGINT, lambda *_: loop.call_soon_threadsafe( self.key_processor.send_sigint ), ) try: yield finally: loop.remove_signal_handler(signal.SIGINT) else: yield @contextmanager def set_exception_handler_ctx(loop: AbstractEventLoop) -> Iterator[None]: if set_exception_handler: previous_exc_handler = loop.get_exception_handler() loop.set_exception_handler(self._handle_exception) try: yield finally: loop.set_exception_handler(previous_exc_handler) else: yield @contextmanager def set_callback_duration(loop: AbstractEventLoop) -> Iterator[None]: # Set slow_callback_duration. original_slow_callback_duration = loop.slow_callback_duration loop.slow_callback_duration = slow_callback_duration try: yield finally: # Reset slow_callback_duration. loop.slow_callback_duration = original_slow_callback_duration @contextmanager def create_future( loop: AbstractEventLoop, ) -> Iterator[asyncio.Future[_AppResult]]: f = loop.create_future() self.future = f # XXX: make sure to set this before calling '_redraw'. try: yield f finally: # Also remove the Future again. (This brings the # application back to its initial state, where it also # doesn't have a Future.) self.future = None with ExitStack() as stack: stack.enter_context(set_is_running()) # Make sure to set `_invalidated` to `False` to begin with, # otherwise we're not going to paint anything. This can happen if # this application had run before on a different event loop, and a # paint was scheduled using `call_soon_threadsafe` with # `max_postpone_time`. self._invalidated = False loop = stack.enter_context(set_loop()) stack.enter_context(set_handle_sigint(loop)) stack.enter_context(set_exception_handler_ctx(loop)) stack.enter_context(set_callback_duration(loop)) stack.enter_context(set_app(self)) stack.enter_context(self._enable_breakpointhook()) f = stack.enter_context(create_future(loop)) try: return await _run_async(f) finally: # Wait for the background tasks to be done. This needs to # go in the finally! If `_run_async` raises # `KeyboardInterrupt`, we still want to wait for the # background tasks. await self.cancel_and_wait_for_background_tasks() # The `ExitStack` above is defined in typeshed in a way that it can # swallow exceptions. Without next line, mypy would think that there's # a possibility we don't return here. See: # https://github.com/python/mypy/issues/7726 assert False, "unreachable" def run( self, pre_run: Callable[[], None] | None = None, set_exception_handler: bool = True, handle_sigint: bool = True, in_thread: bool = False, inputhook: InputHook | None = None, ) -> _AppResult: """ A blocking 'run' call that waits until the UI is finished. This will run the application in a fresh asyncio event loop. :param pre_run: Optional callable, which is called right after the "reset" of the application. :param set_exception_handler: When set, in case of an exception, go out of the alternate screen and hide the application, display the exception, and wait for the user to press ENTER. :param in_thread: When true, run the application in a background thread, and block the current thread until the application terminates. This is useful if we need to be sure the application won't use the current event loop (asyncio does not support nested event loops). A new event loop will be created in this background thread, and that loop will also be closed when the background thread terminates. When this is used, it's especially important to make sure that all asyncio background tasks are managed through `get_app().create_background_task()`, so that unfinished tasks are properly cancelled before the event loop is closed. This is used for instance in ptpython. :param handle_sigint: Handle SIGINT signal. Call the key binding for `Keys.SIGINT`. (This only works in the main thread.) """ if in_thread: result: _AppResult exception: BaseException | None = None def run_in_thread() -> None: nonlocal result, exception try: result = self.run( pre_run=pre_run, set_exception_handler=set_exception_handler, # Signal handling only works in the main thread. handle_sigint=False, inputhook=inputhook, ) except BaseException as e: exception = e thread = threading.Thread(target=run_in_thread) thread.start() thread.join() if exception is not None: raise exception return result coro = self.run_async( pre_run=pre_run, set_exception_handler=set_exception_handler, handle_sigint=handle_sigint, ) def _called_from_ipython() -> bool: try: return ( sys.modules["IPython"].version_info < (8, 18, 0, "") and "IPython/terminal/interactiveshell.py" in sys._getframe(3).f_code.co_filename ) except BaseException: return False if inputhook is not None: # Create new event loop with given input hook and run the app. # In Python 3.12, we can use asyncio.run(loop_factory=...) # For now, use `run_until_complete()`. loop = new_eventloop_with_inputhook(inputhook) result = loop.run_until_complete(coro) loop.run_until_complete(loop.shutdown_asyncgens()) loop.close() return result elif _called_from_ipython(): # workaround to make input hooks work for IPython until # https://github.com/ipython/ipython/pull/14241 is merged. # IPython was setting the input hook by installing an event loop # previously. try: # See whether a loop was installed already. If so, use that. # That's required for the input hooks to work, they are # installed using `set_event_loop`. loop = asyncio.get_event_loop() except RuntimeError: # No loop installed. Run like usual. return asyncio.run(coro) else: # Use existing loop. return loop.run_until_complete(coro) else: # No loop installed. Run like usual. return asyncio.run(coro) def _handle_exception( self, loop: AbstractEventLoop, context: dict[str, Any] ) -> None: """ Handler for event loop exceptions. This will print the exception, using run_in_terminal. """ # For Python 2: we have to get traceback at this point, because # we're still in the 'except:' block of the event loop where the # traceback is still available. Moving this code in the # 'print_exception' coroutine will loose the exception. tb = get_traceback_from_context(context) formatted_tb = "".join(format_tb(tb)) async def in_term() -> None: async with in_terminal(): # Print output. Similar to 'loop.default_exception_handler', # but don't use logger. (This works better on Python 2.) print("\nUnhandled exception in event loop:") print(formatted_tb) print("Exception {}".format(context.get("exception"))) await _do_wait_for_enter("Press ENTER to continue...") ensure_future(in_term()) @contextmanager def _enable_breakpointhook(self) -> Generator[None, None, None]: """ Install our custom breakpointhook for the duration of this context manager. (We will only install the hook if no other custom hook was set.) """ if sys.breakpointhook == sys.__breakpointhook__: sys.breakpointhook = self._breakpointhook try: yield finally: sys.breakpointhook = sys.__breakpointhook__ else: yield def _breakpointhook(self, *a: object, **kw: object) -> None: """ Breakpointhook which uses PDB, but ensures that the application is hidden and input echoing is restored during each debugger dispatch. This can be called from any thread. In any case, the application's event loop will be blocked while the PDB input is displayed. The event will continue after leaving the debugger. """ app = self # Inline import on purpose. We don't want to import pdb, if not needed. import pdb from types import FrameType TraceDispatch = Callable[[FrameType, str, Any], Any] @contextmanager def hide_app_from_eventloop_thread() -> Generator[None, None, None]: """Stop application if `__breakpointhook__` is called from within the App's event loop.""" # Hide application. app.renderer.erase() # Detach input and dispatch to debugger. with app.input.detach(): with app.input.cooked_mode(): yield # Note: we don't render the application again here, because # there's a good chance that there's a breakpoint on the next # line. This paint/erase cycle would move the PDB prompt back # to the middle of the screen. @contextmanager def hide_app_from_other_thread() -> Generator[None, None, None]: """Stop application if `__breakpointhook__` is called from a thread other than the App's event loop.""" ready = threading.Event() done = threading.Event() async def in_loop() -> None: # from .run_in_terminal import in_terminal # async with in_terminal(): # ready.set() # await asyncio.get_running_loop().run_in_executor(None, done.wait) # return # Hide application. app.renderer.erase() # Detach input and dispatch to debugger. with app.input.detach(): with app.input.cooked_mode(): ready.set() # Here we block the App's event loop thread until the # debugger resumes. We could have used `with # run_in_terminal.in_terminal():` like the commented # code above, but it seems to work better if we # completely stop the main event loop while debugging. done.wait() self.create_background_task(in_loop()) ready.wait() try: yield finally: done.set() class CustomPdb(pdb.Pdb): def trace_dispatch( self, frame: FrameType, event: str, arg: Any ) -> TraceDispatch: if app._loop_thread is None: return super().trace_dispatch(frame, event, arg) if app._loop_thread == threading.current_thread(): with hide_app_from_eventloop_thread(): return super().trace_dispatch(frame, event, arg) with hide_app_from_other_thread(): return super().trace_dispatch(frame, event, arg) frame = sys._getframe().f_back CustomPdb(stdout=sys.__stdout__).set_trace(frame) def create_background_task( self, coroutine: Coroutine[Any, Any, None] ) -> asyncio.Task[None]: """ Start a background task (coroutine) for the running application. When the `Application` terminates, unfinished background tasks will be cancelled. Given that we still support Python versions before 3.11, we can't use task groups (and exception groups), because of that, these background tasks are not allowed to raise exceptions. If they do, we'll call the default exception handler from the event loop. If at some point, we have Python 3.11 as the minimum supported Python version, then we can use a `TaskGroup` (with the lifetime of `Application.run_async()`, and run run the background tasks in there. This is not threadsafe. """ loop = self.loop or get_running_loop() task: asyncio.Task[None] = loop.create_task(coroutine) self._background_tasks.add(task) task.add_done_callback(self._on_background_task_done) return task def _on_background_task_done(self, task: asyncio.Task[None]) -> None: """ Called when a background task completes. Remove it from `_background_tasks`, and handle exceptions if any. """ self._background_tasks.discard(task) if task.cancelled(): return exc = task.exception() if exc is not None: get_running_loop().call_exception_handler( { "message": f"prompt_toolkit.Application background task {task!r} " "raised an unexpected exception.", "exception": exc, "task": task, } ) async def cancel_and_wait_for_background_tasks(self) -> None: """ Cancel all background tasks, and wait for the cancellation to complete. If any of the background tasks raised an exception, this will also propagate the exception. (If we had nurseries like Trio, this would be the `__aexit__` of a nursery.) """ for task in self._background_tasks: task.cancel() # Wait until the cancellation of the background tasks completes. # `asyncio.wait()` does not propagate exceptions raised within any of # these tasks, which is what we want. Otherwise, we can't distinguish # between a `CancelledError` raised in this task because it got # cancelled, and a `CancelledError` raised on this `await` checkpoint, # because *we* got cancelled during the teardown of the application. # (If we get cancelled here, then it's important to not suppress the # `CancelledError`, and have it propagate.) # NOTE: Currently, if we get cancelled at this point then we can't wait # for the cancellation to complete (in the future, we should be # using anyio or Python's 3.11 TaskGroup.) # Also, if we had exception groups, we could propagate an # `ExceptionGroup` if something went wrong here. Right now, we # don't propagate exceptions, but have them printed in # `_on_background_task_done`. if len(self._background_tasks) > 0: await asyncio.wait( self._background_tasks, timeout=None, return_when=asyncio.ALL_COMPLETED ) async def _poll_output_size(self) -> None: """ Coroutine for polling the terminal dimensions. Useful for situations where `attach_winch_signal_handler` is not sufficient: - If we are not running in the main thread. - On Windows. """ size: Size | None = None interval = self.terminal_size_polling_interval if interval is None: return while True: await asyncio.sleep(interval) new_size = self.output.get_size() if size is not None and new_size != size: self._on_resize() size = new_size def cpr_not_supported_callback(self) -> None: """ Called when we don't receive the cursor position response in time. """ if not self.output.responds_to_cpr: return # We know about this already. def in_terminal() -> None: self.output.write( "WARNING: your terminal doesn't support cursor position requests (CPR).\r\n" ) self.output.flush() run_in_terminal(in_terminal) @overload def exit(self) -> None: "Exit without arguments." @overload def exit(self, *, result: _AppResult, style: str = "") -> None: "Exit with `_AppResult`." @overload def exit( self, *, exception: BaseException | type[BaseException], style: str = "" ) -> None: "Exit with exception." def exit( self, result: _AppResult | None = None, exception: BaseException | type[BaseException] | None = None, style: str = "", ) -> None: """ Exit application. .. note:: If `Application.exit` is called before `Application.run()` is called, then the `Application` won't exit (because the `Application.future` doesn't correspond to the current run). Use a `pre_run` hook and an event to synchronize the closing if there's a chance this can happen. :param result: Set this result for the application. :param exception: Set this exception as the result for an application. For a prompt, this is often `EOFError` or `KeyboardInterrupt`. :param style: Apply this style on the whole content when quitting, often this is 'class:exiting' for a prompt. (Used when `erase_when_done` is not set.) """ assert result is None or exception is None if self.future is None: raise Exception("Application is not running. Application.exit() failed.") if self.future.done(): raise Exception("Return value already set. Application.exit() failed.") self.exit_style = style if exception is not None: self.future.set_exception(exception) else: self.future.set_result(cast(_AppResult, result)) def _request_absolute_cursor_position(self) -> None: """ Send CPR request. """ # Note: only do this if the input queue is not empty, and a return # value has not been set. Otherwise, we won't be able to read the # response anyway. if not self.key_processor.input_queue and not self.is_done: self.renderer.request_absolute_cursor_position() async def run_system_command( self, command: str, wait_for_enter: bool = True, display_before_text: AnyFormattedText = "", wait_text: str = "Press ENTER to continue...", ) -> None: """ Run system command (While hiding the prompt. When finished, all the output will scroll above the prompt.) :param command: Shell command to be executed. :param wait_for_enter: FWait for the user to press enter, when the command is finished. :param display_before_text: If given, text to be displayed before the command executes. :return: A `Future` object. """ async with in_terminal(): # Try to use the same input/output file descriptors as the one, # used to run this application. try: input_fd = self.input.fileno() except AttributeError: input_fd = sys.stdin.fileno() try: output_fd = self.output.fileno() except AttributeError: output_fd = sys.stdout.fileno() # Run sub process. def run_command() -> None: self.print_text(display_before_text) p = Popen(command, shell=True, stdin=input_fd, stdout=output_fd) p.wait() await run_in_executor_with_context(run_command) # Wait for the user to press enter. if wait_for_enter: await _do_wait_for_enter(wait_text) def suspend_to_background(self, suspend_group: bool = True) -> None: """ (Not thread safe -- to be called from inside the key bindings.) Suspend process. :param suspend_group: When true, suspend the whole process group. (This is the default, and probably what you want.) """ # Only suspend when the operating system supports it. # (Not on Windows.) if _SIGTSTP is not None: def run() -> None: signal = cast(int, _SIGTSTP) # Send `SIGTSTP` to own process. # This will cause it to suspend. # Usually we want the whole process group to be suspended. This # handles the case when input is piped from another process. if suspend_group: os.kill(0, signal) else: os.kill(os.getpid(), signal) run_in_terminal(run) def print_text( self, text: AnyFormattedText, style: BaseStyle | None = None ) -> None: """ Print a list of (style_str, text) tuples to the output. (When the UI is running, this method has to be called through `run_in_terminal`, otherwise it will destroy the UI.) :param text: List of ``(style_str, text)`` tuples. :param style: Style class to use. Defaults to the active style in the CLI. """ print_formatted_text( output=self.output, formatted_text=text, style=style or self._merged_style, color_depth=self.color_depth, style_transformation=self.style_transformation, ) @property def is_running(self) -> bool: "`True` when the application is currently active/running." return self._is_running @property def is_done(self) -> bool: if self.future: return self.future.done() return False def get_used_style_strings(self) -> list[str]: """ Return a list of used style strings. This is helpful for debugging, and for writing a new `Style`. """ attrs_for_style = self.renderer._attrs_for_style if attrs_for_style: return sorted( re.sub(r"\s+", " ", style_str).strip() for style_str in attrs_for_style.keys() ) return [] class _CombinedRegistry(KeyBindingsBase): """ The `KeyBindings` of key bindings for a `Application`. This merges the global key bindings with the one of the current user control. """ def __init__(self, app: Application[_AppResult]) -> None: self.app = app self._cache: SimpleCache[ tuple[Window, frozenset[UIControl]], KeyBindingsBase ] = SimpleCache() @property def _version(self) -> Hashable: """Not needed - this object is not going to be wrapped in another KeyBindings object.""" raise NotImplementedError @property def bindings(self) -> list[Binding]: """Not needed - this object is not going to be wrapped in another KeyBindings object.""" raise NotImplementedError def _create_key_bindings( self, current_window: Window, other_controls: list[UIControl] ) -> KeyBindingsBase: """ Create a `KeyBindings` object that merges the `KeyBindings` from the `UIControl` with all the parent controls and the global key bindings. """ key_bindings = [] collected_containers = set() # Collect key bindings from currently focused control and all parent # controls. Don't include key bindings of container parent controls. container: Container = current_window while True: collected_containers.add(container) kb = container.get_key_bindings() if kb is not None: key_bindings.append(kb) if container.is_modal(): break parent = self.app.layout.get_parent(container) if parent is None: break else: container = parent # Include global bindings (starting at the top-model container). for c in walk(container): if c not in collected_containers: kb = c.get_key_bindings() if kb is not None: key_bindings.append(GlobalOnlyKeyBindings(kb)) # Add App key bindings if self.app.key_bindings: key_bindings.append(self.app.key_bindings) # Add mouse bindings. key_bindings.append( ConditionalKeyBindings( self.app._page_navigation_bindings, self.app.enable_page_navigation_bindings, ) ) key_bindings.append(self.app._default_bindings) # Reverse this list. The current control's key bindings should come # last. They need priority. key_bindings = key_bindings[::-1] return merge_key_bindings(key_bindings) @property def _key_bindings(self) -> KeyBindingsBase: current_window = self.app.layout.current_window other_controls = list(self.app.layout.find_all_controls()) key = current_window, frozenset(other_controls) return self._cache.get( key, lambda: self._create_key_bindings(current_window, other_controls) ) def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]: return self._key_bindings.get_bindings_for_keys(keys) def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]: return self._key_bindings.get_bindings_starting_with_keys(keys) async def _do_wait_for_enter(wait_text: AnyFormattedText) -> None: """ Create a sub application to wait for the enter key press. This has two advantages over using 'input'/'raw_input': - This will share the same input/output I/O. - This doesn't block the event loop. """ from prompt_toolkit.shortcuts import PromptSession key_bindings = KeyBindings() @key_bindings.add("enter") def _ok(event: E) -> None: event.app.exit() @key_bindings.add(Keys.Any) def _ignore(event: E) -> None: "Disallow typing." pass session: PromptSession[None] = PromptSession( message=wait_text, key_bindings=key_bindings ) try: await session.app.run_async() except KeyboardInterrupt: pass # Control-c pressed. Don't propagate this error. @contextmanager def attach_winch_signal_handler( handler: Callable[[], None], ) -> Generator[None, None, None]: """ Attach the given callback as a WINCH signal handler within the context manager. Restore the original signal handler when done. The `Application.run` method will register SIGWINCH, so that it will properly repaint when the terminal window resizes. However, using `run_in_terminal`, we can temporarily send an application to the background, and run an other app in between, which will then overwrite the SIGWINCH. This is why it's important to restore the handler when the app terminates. """ # The tricky part here is that signals are registered in the Unix event # loop with a wakeup fd, but another application could have registered # signals using signal.signal directly. For now, the implementation is # hard-coded for the `asyncio.unix_events._UnixSelectorEventLoop`. # No WINCH? Then don't do anything. sigwinch = getattr(signal, "SIGWINCH", None) if sigwinch is None or not in_main_thread(): yield return # Keep track of the previous handler. # (Only UnixSelectorEventloop has `_signal_handlers`.) loop = get_running_loop() previous_winch_handler = getattr(loop, "_signal_handlers", {}).get(sigwinch) try: loop.add_signal_handler(sigwinch, handler) yield finally: # Restore the previous signal handler. loop.remove_signal_handler(sigwinch) if previous_winch_handler is not None: loop.add_signal_handler( sigwinch, previous_winch_handler._callback, *previous_winch_handler._args, ) @contextmanager def _restore_sigint_from_ctypes() -> Generator[None, None, None]: # The following functions are part of the stable ABI since python 3.2 # See: https://docs.python.org/3/c-api/sys.html#c.PyOS_getsig # Inline import: these are not available on Pypy. try: from ctypes import c_int, c_void_p, pythonapi except ImportError: have_ctypes_signal = False else: # GraalPy has the functions, but they don't work have_ctypes_signal = sys.implementation.name != "graalpy" if have_ctypes_signal: # PyOS_sighandler_t PyOS_getsig(int i) pythonapi.PyOS_getsig.restype = c_void_p pythonapi.PyOS_getsig.argtypes = (c_int,) # PyOS_sighandler_t PyOS_setsig(int i, PyOS_sighandler_t h) pythonapi.PyOS_setsig.restype = c_void_p pythonapi.PyOS_setsig.argtypes = ( c_int, c_void_p, ) sigint = signal.getsignal(signal.SIGINT) if have_ctypes_signal: sigint_os = pythonapi.PyOS_getsig(signal.SIGINT) try: yield finally: if sigint is not None: signal.signal(signal.SIGINT, sigint) if have_ctypes_signal: pythonapi.PyOS_setsig(signal.SIGINT, sigint_os) ================================================ FILE: src/prompt_toolkit/application/current.py ================================================ from __future__ import annotations from collections.abc import Generator from contextlib import contextmanager from contextvars import ContextVar from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from prompt_toolkit.input.base import Input from prompt_toolkit.output.base import Output from .application import Application __all__ = [ "AppSession", "get_app_session", "get_app", "get_app_or_none", "set_app", "create_app_session", "create_app_session_from_tty", ] class AppSession: """ An AppSession is an interactive session, usually connected to one terminal. Within one such session, interaction with many applications can happen, one after the other. The input/output device is not supposed to change during one session. Warning: Always use the `create_app_session` function to create an instance, so that it gets activated correctly. :param input: Use this as a default input for all applications running in this session, unless an input is passed to the `Application` explicitly. :param output: Use this as a default output. """ def __init__( self, input: Input | None = None, output: Output | None = None ) -> None: self._input = input self._output = output # The application will be set dynamically by the `set_app` context # manager. This is called in the application itself. self.app: Application[Any] | None = None def __repr__(self) -> str: return f"AppSession(app={self.app!r})" @property def input(self) -> Input: if self._input is None: from prompt_toolkit.input.defaults import create_input self._input = create_input() return self._input @property def output(self) -> Output: if self._output is None: from prompt_toolkit.output.defaults import create_output self._output = create_output() return self._output _current_app_session: ContextVar[AppSession] = ContextVar( "_current_app_session", default=AppSession() ) def get_app_session() -> AppSession: return _current_app_session.get() def get_app() -> Application[Any]: """ Get the current active (running) Application. An :class:`.Application` is active during the :meth:`.Application.run_async` call. We assume that there can only be one :class:`.Application` active at the same time. There is only one terminal window, with only one stdin and stdout. This makes the code significantly easier than passing around the :class:`.Application` everywhere. If no :class:`.Application` is running, then return by default a :class:`.DummyApplication`. For practical reasons, we prefer to not raise an exception. This way, we don't have to check all over the place whether an actual `Application` was returned. (For applications like pymux where we can have more than one `Application`, we'll use a work-around to handle that.) """ session = _current_app_session.get() if session.app is not None: return session.app from .dummy import DummyApplication return DummyApplication() def get_app_or_none() -> Application[Any] | None: """ Get the current active (running) Application, or return `None` if no application is running. """ session = _current_app_session.get() return session.app @contextmanager def set_app(app: Application[Any]) -> Generator[None, None, None]: """ Context manager that sets the given :class:`.Application` active in an `AppSession`. This should only be called by the `Application` itself. The application will automatically be active while its running. If you want the application to be active in other threads/coroutines, where that's not the case, use `contextvars.copy_context()`, or use `Application.context` to run it in the appropriate context. """ session = _current_app_session.get() previous_app = session.app session.app = app try: yield finally: session.app = previous_app @contextmanager def create_app_session( input: Input | None = None, output: Output | None = None ) -> Generator[AppSession, None, None]: """ Create a separate AppSession. This is useful if there can be multiple individual ``AppSession``'s going on. Like in the case of a Telnet/SSH server. """ # If no input/output is specified, fall back to the current input/output, # if there was one that was set/created for the current session. # (Note that we check `_input`/`_output` and not `input`/`output`. This is # because we don't want to accidentally create a new input/output objects # here and store it in the "parent" `AppSession`. Especially, when # combining pytest's `capsys` fixture and `create_app_session`, sys.stdin # and sys.stderr are patched for every test, so we don't want to leak # those outputs object across `AppSession`s.) if input is None: input = get_app_session()._input if output is None: output = get_app_session()._output # Create new `AppSession` and activate. session = AppSession(input=input, output=output) token = _current_app_session.set(session) try: yield session finally: _current_app_session.reset(token) @contextmanager def create_app_session_from_tty() -> Generator[AppSession, None, None]: """ Create `AppSession` that always prefers the TTY input/output. Even if `sys.stdin` and `sys.stdout` are connected to input/output pipes, this will still use the terminal for interaction (because `sys.stderr` is still connected to the terminal). Usage:: from prompt_toolkit.shortcuts import prompt with create_app_session_from_tty(): prompt('>') """ from prompt_toolkit.input.defaults import create_input from prompt_toolkit.output.defaults import create_output input = create_input(always_prefer_tty=True) output = create_output(always_prefer_tty=True) with create_app_session(input=input, output=output) as app_session: yield app_session ================================================ FILE: src/prompt_toolkit/application/dummy.py ================================================ from __future__ import annotations from collections.abc import Callable from prompt_toolkit.eventloop import InputHook from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.input import DummyInput from prompt_toolkit.output import DummyOutput from .application import Application __all__ = [ "DummyApplication", ] class DummyApplication(Application[None]): """ When no :class:`.Application` is running, :func:`.get_app` will run an instance of this :class:`.DummyApplication` instead. """ def __init__(self) -> None: super().__init__(output=DummyOutput(), input=DummyInput()) def run( self, pre_run: Callable[[], None] | None = None, set_exception_handler: bool = True, handle_sigint: bool = True, in_thread: bool = False, inputhook: InputHook | None = None, ) -> None: raise NotImplementedError("A DummyApplication is not supposed to run.") async def run_async( self, pre_run: Callable[[], None] | None = None, set_exception_handler: bool = True, handle_sigint: bool = True, slow_callback_duration: float = 0.5, ) -> None: raise NotImplementedError("A DummyApplication is not supposed to run.") async def run_system_command( self, command: str, wait_for_enter: bool = True, display_before_text: AnyFormattedText = "", wait_text: str = "", ) -> None: raise NotImplementedError def suspend_to_background(self, suspend_group: bool = True) -> None: raise NotImplementedError ================================================ FILE: src/prompt_toolkit/application/run_in_terminal.py ================================================ """ Tools for running functions on the terminal above the current application or prompt. """ from __future__ import annotations from asyncio import Future, ensure_future from collections.abc import AsyncGenerator, Awaitable, Callable from contextlib import asynccontextmanager from typing import TypeVar from prompt_toolkit.eventloop import run_in_executor_with_context from .current import get_app_or_none __all__ = [ "run_in_terminal", "in_terminal", ] _T = TypeVar("_T") def run_in_terminal( func: Callable[[], _T], render_cli_done: bool = False, in_executor: bool = False ) -> Awaitable[_T]: """ Run function on the terminal above the current application or prompt. What this does is first hiding the prompt, then running this callable (which can safely output to the terminal), and then again rendering the prompt which causes the output of this function to scroll above the prompt. ``func`` is supposed to be a synchronous function. If you need an asynchronous version of this function, use the ``in_terminal`` context manager directly. :param func: The callable to execute. :param render_cli_done: When True, render the interface in the 'Done' state first, then execute the function. If False, erase the interface first. :param in_executor: When True, run in executor. (Use this for long blocking functions, when you don't want to block the event loop.) :returns: A `Future`. """ async def run() -> _T: async with in_terminal(render_cli_done=render_cli_done): if in_executor: return await run_in_executor_with_context(func) else: return func() return ensure_future(run()) @asynccontextmanager async def in_terminal(render_cli_done: bool = False) -> AsyncGenerator[None, None]: """ Asynchronous context manager that suspends the current application and runs the body in the terminal. .. code:: async def f(): async with in_terminal(): call_some_function() await call_some_async_function() """ app = get_app_or_none() if app is None or not app._is_running: yield return # When a previous `run_in_terminal` call was in progress. Wait for that # to finish, before starting this one. Chain to previous call. previous_run_in_terminal_f = app._running_in_terminal_f new_run_in_terminal_f: Future[None] = Future() app._running_in_terminal_f = new_run_in_terminal_f # Wait for the previous `run_in_terminal` to finish. if previous_run_in_terminal_f is not None: await previous_run_in_terminal_f # Wait for all CPRs to arrive. We don't want to detach the input until # all cursor position responses have been arrived. Otherwise, the tty # will echo its input and can show stuff like ^[[39;1R. if app.output.responds_to_cpr: await app.renderer.wait_for_cpr_responses() # Draw interface in 'done' state, or erase. if render_cli_done: app._redraw(render_as_done=True) else: app.renderer.erase() # Disable rendering. app._running_in_terminal = True # Detach input. try: with app.input.detach(): with app.input.cooked_mode(): yield finally: # Redraw interface again. try: app._running_in_terminal = False app.renderer.reset() app._request_absolute_cursor_position() app._redraw() finally: # (Check for `.done()`, because it can be that this future was # cancelled.) if not new_run_in_terminal_f.done(): new_run_in_terminal_f.set_result(None) ================================================ FILE: src/prompt_toolkit/auto_suggest.py ================================================ """ `Fish-style <http://fishshell.com/>`_ like auto-suggestion. While a user types input in a certain buffer, suggestions are generated (asynchronously.) Usually, they are displayed after the input. When the cursor presses the right arrow and the cursor is at the end of the input, the suggestion will be inserted. If you want the auto suggestions to be asynchronous (in a background thread), because they take too much time, and could potentially block the event loop, then wrap the :class:`.AutoSuggest` instance into a :class:`.ThreadedAutoSuggest`. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable from typing import TYPE_CHECKING from prompt_toolkit.eventloop import run_in_executor_with_context from .document import Document from .filters import Filter, to_filter if TYPE_CHECKING: from .buffer import Buffer __all__ = [ "Suggestion", "AutoSuggest", "ThreadedAutoSuggest", "DummyAutoSuggest", "AutoSuggestFromHistory", "ConditionalAutoSuggest", "DynamicAutoSuggest", ] class Suggestion: """ Suggestion returned by an auto-suggest algorithm. :param text: The suggestion text. """ def __init__(self, text: str) -> None: self.text = text def __repr__(self) -> str: return f"Suggestion({self.text})" class AutoSuggest(metaclass=ABCMeta): """ Base class for auto suggestion implementations. """ @abstractmethod def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None: """ Return `None` or a :class:`.Suggestion` instance. We receive both :class:`~prompt_toolkit.buffer.Buffer` and :class:`~prompt_toolkit.document.Document`. The reason is that auto suggestions are retrieved asynchronously. (Like completions.) The buffer text could be changed in the meantime, but ``document`` contains the buffer document like it was at the start of the auto suggestion call. So, from here, don't access ``buffer.text``, but use ``document.text`` instead. :param buffer: The :class:`~prompt_toolkit.buffer.Buffer` instance. :param document: The :class:`~prompt_toolkit.document.Document` instance. """ async def get_suggestion_async( self, buff: Buffer, document: Document ) -> Suggestion | None: """ Return a :class:`.Future` which is set when the suggestions are ready. This function can be overloaded in order to provide an asynchronous implementation. """ return self.get_suggestion(buff, document) class ThreadedAutoSuggest(AutoSuggest): """ Wrapper that runs auto suggestions in a thread. (Use this to prevent the user interface from becoming unresponsive if the generation of suggestions takes too much time.) """ def __init__(self, auto_suggest: AutoSuggest) -> None: self.auto_suggest = auto_suggest def get_suggestion(self, buff: Buffer, document: Document) -> Suggestion | None: return self.auto_suggest.get_suggestion(buff, document) async def get_suggestion_async( self, buff: Buffer, document: Document ) -> Suggestion | None: """ Run the `get_suggestion` function in a thread. """ def run_get_suggestion_thread() -> Suggestion | None: return self.get_suggestion(buff, document) return await run_in_executor_with_context(run_get_suggestion_thread) class DummyAutoSuggest(AutoSuggest): """ AutoSuggest class that doesn't return any suggestion. """ def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None: return None # No suggestion class AutoSuggestFromHistory(AutoSuggest): """ Give suggestions based on the lines in the history. """ def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None: history = buffer.history # Consider only the last line for the suggestion. text = document.text.rsplit("\n", 1)[-1] # Only create a suggestion when this is not an empty line. if text.strip(): # Find first matching line in history. for string in reversed(list(history.get_strings())): for line in reversed(string.splitlines()): if line.startswith(text): return Suggestion(line[len(text) :]) return None class ConditionalAutoSuggest(AutoSuggest): """ Auto suggest that can be turned on and of according to a certain condition. """ def __init__(self, auto_suggest: AutoSuggest, filter: bool | Filter) -> None: self.auto_suggest = auto_suggest self.filter = to_filter(filter) def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None: if self.filter(): return self.auto_suggest.get_suggestion(buffer, document) return None class DynamicAutoSuggest(AutoSuggest): """ Validator class that can dynamically returns any Validator. :param get_validator: Callable that returns a :class:`.Validator` instance. """ def __init__(self, get_auto_suggest: Callable[[], AutoSuggest | None]) -> None: self.get_auto_suggest = get_auto_suggest def get_suggestion(self, buff: Buffer, document: Document) -> Suggestion | None: auto_suggest = self.get_auto_suggest() or DummyAutoSuggest() return auto_suggest.get_suggestion(buff, document) async def get_suggestion_async( self, buff: Buffer, document: Document ) -> Suggestion | None: auto_suggest = self.get_auto_suggest() or DummyAutoSuggest() return await auto_suggest.get_suggestion_async(buff, document) ================================================ FILE: src/prompt_toolkit/buffer.py ================================================ """ Data structures for the Buffer. It holds the text, cursor position, history, etc... """ from __future__ import annotations import asyncio import logging import os import re import shlex import shutil import subprocess import tempfile from collections import deque from collections.abc import Callable, Coroutine, Iterable from enum import Enum from functools import wraps from typing import Any, TypeVar, cast from .application.current import get_app from .application.run_in_terminal import run_in_terminal from .auto_suggest import AutoSuggest, Suggestion from .cache import FastDictCache from .clipboard import ClipboardData from .completion import ( CompleteEvent, Completer, Completion, DummyCompleter, get_common_complete_suffix, ) from .document import Document from .eventloop import aclosing from .filters import FilterOrBool, to_filter from .history import History, InMemoryHistory from .search import SearchDirection, SearchState from .selection import PasteMode, SelectionState, SelectionType from .utils import Event, to_str from .validation import ValidationError, Validator __all__ = [ "EditReadOnlyBuffer", "Buffer", "CompletionState", "indent", "unindent", "reshape_text", ] logger = logging.getLogger(__name__) class EditReadOnlyBuffer(Exception): "Attempt editing of read-only :class:`.Buffer`." class ValidationState(Enum): "The validation state of a buffer. This is set after the validation." VALID = "VALID" INVALID = "INVALID" UNKNOWN = "UNKNOWN" class CompletionState: """ Immutable class that contains a completion state. """ def __init__( self, original_document: Document, completions: list[Completion] | None = None, complete_index: int | None = None, ) -> None: #: Document as it was when the completion started. self.original_document = original_document #: List of all the current Completion instances which are possible at #: this point. self.completions = completions or [] #: Position in the `completions` array. #: This can be `None` to indicate "no completion", the original text. self.complete_index = complete_index # Position in the `_completions` array. def __repr__(self) -> str: return f"{self.__class__.__name__}({self.original_document!r}, <{len(self.completions)!r}> completions, index={self.complete_index!r})" def go_to_index(self, index: int | None) -> None: """ Create a new :class:`.CompletionState` object with the new index. When `index` is `None` deselect the completion. """ if self.completions: assert index is None or 0 <= index < len(self.completions) self.complete_index = index def new_text_and_position(self) -> tuple[str, int]: """ Return (new_text, new_cursor_position) for this completion. """ if self.complete_index is None: return self.original_document.text, self.original_document.cursor_position else: original_text_before_cursor = self.original_document.text_before_cursor original_text_after_cursor = self.original_document.text_after_cursor c = self.completions[self.complete_index] if c.start_position == 0: before = original_text_before_cursor else: before = original_text_before_cursor[: c.start_position] new_text = before + c.text + original_text_after_cursor new_cursor_position = len(before) + len(c.text) return new_text, new_cursor_position @property def current_completion(self) -> Completion | None: """ Return the current completion, or return `None` when no completion is selected. """ if self.complete_index is not None: return self.completions[self.complete_index] return None _QUOTED_WORDS_RE = re.compile(r"""(\s+|".*?"|'.*?')""") class YankNthArgState: """ For yank-last-arg/yank-nth-arg: Keep track of where we are in the history. """ def __init__( self, history_position: int = 0, n: int = -1, previous_inserted_word: str = "" ) -> None: self.history_position = history_position self.previous_inserted_word = previous_inserted_word self.n = n def __repr__(self) -> str: return f"{self.__class__.__name__}(history_position={self.history_position!r}, n={self.n!r}, previous_inserted_word={self.previous_inserted_word!r})" BufferEventHandler = Callable[["Buffer"], None] BufferAcceptHandler = Callable[["Buffer"], bool] class Buffer: """ The core data structure that holds the text and cursor position of the current input line and implements all text manipulations on top of it. It also implements the history, undo stack and the completion state. :param completer: :class:`~prompt_toolkit.completion.Completer` instance. :param history: :class:`~prompt_toolkit.history.History` instance. :param tempfile_suffix: The tempfile suffix (extension) to be used for the "open in editor" function. For a Python REPL, this would be ".py", so that the editor knows the syntax highlighting to use. This can also be a callable that returns a string. :param tempfile: For more advanced tempfile situations where you need control over the subdirectories and filename. For a Git Commit Message, this would be ".git/COMMIT_EDITMSG", so that the editor knows the syntax highlighting to use. This can also be a callable that returns a string. :param name: Name for this buffer. E.g. DEFAULT_BUFFER. This is mostly useful for key bindings where we sometimes prefer to refer to a buffer by their name instead of by reference. :param accept_handler: Called when the buffer input is accepted. (Usually when the user presses `enter`.) The accept handler receives this `Buffer` as input and should return True when the buffer text should be kept instead of calling reset. In case of a `PromptSession` for instance, we want to keep the text, because we will exit the application, and only reset it during the next run. :param max_number_of_completions: Never display more than this number of completions, even when the completer can produce more (limited by default to 10k for performance). Events: :param on_text_changed: When the buffer text changes. (Callable or None.) :param on_text_insert: When new text is inserted. (Callable or None.) :param on_cursor_position_changed: When the cursor moves. (Callable or None.) :param on_completions_changed: When the completions were changed. (Callable or None.) :param on_suggestion_set: When an auto-suggestion text has been set. (Callable or None.) Filters: :param complete_while_typing: :class:`~prompt_toolkit.filters.Filter` or `bool`. Decide whether or not to do asynchronous autocompleting while typing. :param validate_while_typing: :class:`~prompt_toolkit.filters.Filter` or `bool`. Decide whether or not to do asynchronous validation while typing. :param enable_history_search: :class:`~prompt_toolkit.filters.Filter` or `bool` to indicate when up-arrow partial string matching is enabled. It is advised to not enable this at the same time as `complete_while_typing`, because when there is an autocompletion found, the up arrows usually browse through the completions, rather than through the history. :param read_only: :class:`~prompt_toolkit.filters.Filter`. When True, changes will not be allowed. :param multiline: :class:`~prompt_toolkit.filters.Filter` or `bool`. When not set, pressing `Enter` will call the `accept_handler`. Otherwise, pressing `Esc-Enter` is required. """ def __init__( self, completer: Completer | None = None, auto_suggest: AutoSuggest | None = None, history: History | None = None, validator: Validator | None = None, tempfile_suffix: str | Callable[[], str] = "", tempfile: str | Callable[[], str] = "", name: str = "", complete_while_typing: FilterOrBool = False, validate_while_typing: FilterOrBool = False, enable_history_search: FilterOrBool = False, document: Document | None = None, accept_handler: BufferAcceptHandler | None = None, read_only: FilterOrBool = False, multiline: FilterOrBool = True, max_number_of_completions: int = 10000, on_text_changed: BufferEventHandler | None = None, on_text_insert: BufferEventHandler | None = None, on_cursor_position_changed: BufferEventHandler | None = None, on_completions_changed: BufferEventHandler | None = None, on_suggestion_set: BufferEventHandler | None = None, ) -> None: # Accept both filters and booleans as input. enable_history_search = to_filter(enable_history_search) complete_while_typing = to_filter(complete_while_typing) validate_while_typing = to_filter(validate_while_typing) read_only = to_filter(read_only) multiline = to_filter(multiline) self.completer = completer or DummyCompleter() self.auto_suggest = auto_suggest self.validator = validator self.tempfile_suffix = tempfile_suffix self.tempfile = tempfile self.name = name self.accept_handler = accept_handler # Filters. (Usually, used by the key bindings to drive the buffer.) self.complete_while_typing = complete_while_typing self.validate_while_typing = validate_while_typing self.enable_history_search = enable_history_search self.read_only = read_only self.multiline = multiline self.max_number_of_completions = max_number_of_completions # Text width. (For wrapping, used by the Vi 'gq' operator.) self.text_width = 0 #: The command buffer history. # Note that we shouldn't use a lazy 'or' here. bool(history) could be # False when empty. self.history = InMemoryHistory() if history is None else history self.__cursor_position = 0 # Events self.on_text_changed: Event[Buffer] = Event(self, on_text_changed) self.on_text_insert: Event[Buffer] = Event(self, on_text_insert) self.on_cursor_position_changed: Event[Buffer] = Event( self, on_cursor_position_changed ) self.on_completions_changed: Event[Buffer] = Event(self, on_completions_changed) self.on_suggestion_set: Event[Buffer] = Event(self, on_suggestion_set) # Document cache. (Avoid creating new Document instances.) self._document_cache: FastDictCache[ tuple[str, int, SelectionState | None], Document ] = FastDictCache(Document, size=10) # Create completer / auto suggestion / validation coroutines. self._async_suggester = self._create_auto_suggest_coroutine() self._async_completer = self._create_completer_coroutine() self._async_validator = self._create_auto_validate_coroutine() # Asyncio task for populating the history. self._load_history_task: asyncio.Future[None] | None = None # Reset other attributes. self.reset(document=document) def __repr__(self) -> str: if len(self.text) < 15: text = self.text else: text = self.text[:12] + "..." return f"<Buffer(name={self.name!r}, text={text!r}) at {id(self)!r}>" def reset( self, document: Document | None = None, append_to_history: bool = False ) -> None: """ :param append_to_history: Append current input to history first. """ if append_to_history: self.append_to_history() document = document or Document() self.__cursor_position = document.cursor_position # `ValidationError` instance. (Will be set when the input is wrong.) self.validation_error: ValidationError | None = None self.validation_state: ValidationState | None = ValidationState.UNKNOWN # State of the selection. self.selection_state: SelectionState | None = None # Multiple cursor mode. (When we press 'I' or 'A' in visual-block mode, # we can insert text on multiple lines at once. This is implemented by # using multiple cursors.) self.multiple_cursor_positions: list[int] = [] # When doing consecutive up/down movements, prefer to stay at this column. self.preferred_column: int | None = None # State of complete browser # For interactive completion through Ctrl-N/Ctrl-P. self.complete_state: CompletionState | None = None # State of Emacs yank-nth-arg completion. self.yank_nth_arg_state: YankNthArgState | None = None # for yank-nth-arg. # Remember the document that we had *right before* the last paste # operation. This is used for rotating through the kill ring. self.document_before_paste: Document | None = None # Current suggestion. self.suggestion: Suggestion | None = None # The history search text. (Used for filtering the history when we # browse through it.) self.history_search_text: str | None = None # Undo/redo stacks (stack of `(text, cursor_position)`). self._undo_stack: list[tuple[str, int]] = [] self._redo_stack: list[tuple[str, int]] = [] # Cancel history loader. If history loading was still ongoing. # Cancel the `_load_history_task`, so that next repaint of the # `BufferControl` we will repopulate it. if self._load_history_task is not None: self._load_history_task.cancel() self._load_history_task = None #: The working lines. Similar to history, except that this can be #: modified. The user can press arrow_up and edit previous entries. #: Ctrl-C should reset this, and copy the whole history back in here. #: Enter should process the current command and append to the real #: history. self._working_lines: deque[str] = deque([document.text]) self.__working_index = 0 def load_history_if_not_yet_loaded(self) -> None: """ Create task for populating the buffer history (if not yet done). Note:: This needs to be called from within the event loop of the application, because history loading is async, and we need to be sure the right event loop is active. Therefor, we call this method in the `BufferControl.create_content`. There are situations where prompt_toolkit applications are created in one thread, but will later run in a different thread (Ptpython is one example. The REPL runs in a separate thread, in order to prevent interfering with a potential different event loop in the main thread. The REPL UI however is still created in the main thread.) We could decide to not support creating prompt_toolkit objects in one thread and running the application in a different thread, but history loading is the only place where it matters, and this solves it. """ if self._load_history_task is None: async def load_history() -> None: async for item in self.history.load(): self._working_lines.appendleft(item) self.__working_index += 1 self._load_history_task = get_app().create_background_task(load_history()) def load_history_done(f: asyncio.Future[None]) -> None: """ Handle `load_history` result when either done, cancelled, or when an exception was raised. """ try: f.result() except asyncio.CancelledError: # Ignore cancellation. But handle it, so that we don't get # this traceback. pass except GeneratorExit: # Probably not needed, but we had situations where # `GeneratorExit` was raised in `load_history` during # cancellation. pass except BaseException: # Log error if something goes wrong. (We don't have a # caller to which we can propagate this exception.) logger.exception("Loading history failed") self._load_history_task.add_done_callback(load_history_done) # <getters/setters> def _set_text(self, value: str) -> bool: """set text at current working_index. Return whether it changed.""" working_index = self.working_index working_lines = self._working_lines original_value = working_lines[working_index] working_lines[working_index] = value # Return True when this text has been changed. if len(value) != len(original_value): # For Python 2, it seems that when two strings have a different # length and one is a prefix of the other, Python still scans # character by character to see whether the strings are different. # (Some benchmarking showed significant differences for big # documents. >100,000 of lines.) return True elif value != original_value: return True return False def _set_cursor_position(self, value: int) -> bool: """Set cursor position. Return whether it changed.""" original_position = self.__cursor_position self.__cursor_position = max(0, value) return self.__cursor_position != original_position @property def text(self) -> str: return self._working_lines[self.working_index] @text.setter def text(self, value: str) -> None: """ Setting text. (When doing this, make sure that the cursor_position is valid for this text. text/cursor_position should be consistent at any time, otherwise set a Document instead.) """ # Ensure cursor position remains within the size of the text. if self.cursor_position > len(value): self.cursor_position = len(value) # Don't allow editing of read-only buffers. if self.read_only(): raise EditReadOnlyBuffer() changed = self._set_text(value) if changed: self._text_changed() # Reset history search text. # (Note that this doesn't need to happen when working_index # changes, which is when we traverse the history. That's why we # don't do this in `self._text_changed`.) self.history_search_text = None @property def cursor_position(self) -> int: return self.__cursor_position @cursor_position.setter def cursor_position(self, value: int) -> None: """ Setting cursor position. """ assert isinstance(value, int) # Ensure cursor position is within the size of the text. if value > len(self.text): value = len(self.text) if value < 0: value = 0 changed = self._set_cursor_position(value) if changed: self._cursor_position_changed() @property def working_index(self) -> int: return self.__working_index @working_index.setter def working_index(self, value: int) -> None: if self.__working_index != value: self.__working_index = value # Make sure to reset the cursor position, otherwise we end up in # situations where the cursor position is out of the bounds of the # text. self.cursor_position = 0 self._text_changed() def _text_changed(self) -> None: # Remove any validation errors and complete state. self.validation_error = None self.validation_state = ValidationState.UNKNOWN self.complete_state = None self.yank_nth_arg_state = None self.document_before_paste = None self.selection_state = None self.suggestion = None self.preferred_column = None # fire 'on_text_changed' event. self.on_text_changed.fire() # Input validation. # (This happens on all change events, unlike auto completion, also when # deleting text.) if self.validator and self.validate_while_typing(): get_app().create_background_task(self._async_validator()) def _cursor_position_changed(self) -> None: # Remove any complete state. # (Input validation should only be undone when the cursor position # changes.) self.complete_state = None self.yank_nth_arg_state = None self.document_before_paste = None # Unset preferred_column. (Will be set after the cursor movement, if # required.) self.preferred_column = None # Note that the cursor position can change if we have a selection the # new position of the cursor determines the end of the selection. # fire 'on_cursor_position_changed' event. self.on_cursor_position_changed.fire() @property def document(self) -> Document: """ Return :class:`~prompt_toolkit.document.Document` instance from the current text, cursor position and selection state. """ return self._document_cache[ self.text, self.cursor_position, self.selection_state ] @document.setter def document(self, value: Document) -> None: """ Set :class:`~prompt_toolkit.document.Document` instance. This will set both the text and cursor position at the same time, but atomically. (Change events will be triggered only after both have been set.) """ self.set_document(value) def set_document(self, value: Document, bypass_readonly: bool = False) -> None: """ Set :class:`~prompt_toolkit.document.Document` instance. Like the ``document`` property, but accept an ``bypass_readonly`` argument. :param bypass_readonly: When True, don't raise an :class:`.EditReadOnlyBuffer` exception, even when the buffer is read-only. .. warning:: When this buffer is read-only and `bypass_readonly` was not passed, the `EditReadOnlyBuffer` exception will be caught by the `KeyProcessor` and is silently suppressed. This is important to keep in mind when writing key bindings, because it won't do what you expect, and there won't be a stack trace. Use try/finally around this function if you need some cleanup code. """ # Don't allow editing of read-only buffers. if not bypass_readonly and self.read_only(): raise EditReadOnlyBuffer() # Set text and cursor position first. text_changed = self._set_text(value.text) cursor_position_changed = self._set_cursor_position(value.cursor_position) # Now handle change events. (We do this when text/cursor position is # both set and consistent.) if text_changed: self._text_changed() self.history_search_text = None if cursor_position_changed: self._cursor_position_changed() @property def is_returnable(self) -> bool: """ True when there is something handling accept. """ return bool(self.accept_handler) # End of <getters/setters> def save_to_undo_stack(self, clear_redo_stack: bool = True) -> None: """ Safe current state (input text and cursor position), so that we can restore it by calling undo. """ # Safe if the text is different from the text at the top of the stack # is different. If the text is the same, just update the cursor position. if self._undo_stack and self._undo_stack[-1][0] == self.text: self._undo_stack[-1] = (self._undo_stack[-1][0], self.cursor_position) else: self._undo_stack.append((self.text, self.cursor_position)) # Saving anything to the undo stack, clears the redo stack. if clear_redo_stack: self._redo_stack = [] def transform_lines( self, line_index_iterator: Iterable[int], transform_callback: Callable[[str], str], ) -> str: """ Transforms the text on a range of lines. When the iterator yield an index not in the range of lines that the document contains, it skips them silently. To uppercase some lines:: new_text = transform_lines(range(5,10), lambda text: text.upper()) :param line_index_iterator: Iterator of line numbers (int) :param transform_callback: callable that takes the original text of a line, and return the new text for this line. :returns: The new text. """ # Split lines lines = self.text.split("\n") # Apply transformation for index in line_index_iterator: try: lines[index] = transform_callback(lines[index]) except IndexError: pass return "\n".join(lines) def transform_current_line(self, transform_callback: Callable[[str], str]) -> None: """ Apply the given transformation function to the current line. :param transform_callback: callable that takes a string and return a new string. """ document = self.document a = document.cursor_position + document.get_start_of_line_position() b = document.cursor_position + document.get_end_of_line_position() self.text = ( document.text[:a] + transform_callback(document.text[a:b]) + document.text[b:] ) def transform_region( self, from_: int, to: int, transform_callback: Callable[[str], str] ) -> None: """ Transform a part of the input string. :param from_: (int) start position. :param to: (int) end position. :param transform_callback: Callable which accepts a string and returns the transformed string. """ assert from_ < to self.text = "".join( [ self.text[:from_] + transform_callback(self.text[from_:to]) + self.text[to:] ] ) def cursor_left(self, count: int = 1) -> None: self.cursor_position += self.document.get_cursor_left_position(count=count) def cursor_right(self, count: int = 1) -> None: self.cursor_position += self.document.get_cursor_right_position(count=count) def cursor_up(self, count: int = 1) -> None: """(for multiline edit). Move cursor to the previous line.""" original_column = self.preferred_column or self.document.cursor_position_col self.cursor_position += self.document.get_cursor_up_position( count=count, preferred_column=original_column ) # Remember the original column for the next up/down movement. self.preferred_column = original_column def cursor_down(self, count: int = 1) -> None: """(for multiline edit). Move cursor to the next line.""" original_column = self.preferred_column or self.document.cursor_position_col self.cursor_position += self.document.get_cursor_down_position( count=count, preferred_column=original_column ) # Remember the original column for the next up/down movement. self.preferred_column = original_column def auto_up( self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False ) -> None: """ If we're not on the first line (of a multiline input) go a line up, otherwise go back in history. (If nothing is selected.) """ if self.complete_state: self.complete_previous(count=count) elif self.document.cursor_position_row > 0: self.cursor_up(count=count) elif not self.selection_state: self.history_backward(count=count) # Go to the start of the line? if go_to_start_of_line_if_history_changes: self.cursor_position += self.document.get_start_of_line_position() def auto_down( self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False ) -> None: """ If we're not on the last line (of a multiline input) go a line down, otherwise go forward in history. (If nothing is selected.) """ if self.complete_state: self.complete_next(count=count) elif self.document.cursor_position_row < self.document.line_count - 1: self.cursor_down(count=count) elif not self.selection_state: self.history_forward(count=count) # Go to the start of the line? if go_to_start_of_line_if_history_changes: self.cursor_position += self.document.get_start_of_line_position() def delete_before_cursor(self, count: int = 1) -> str: """ Delete specified number of characters before cursor and return the deleted text. """ assert count >= 0 deleted = "" if self.cursor_position > 0: deleted = self.text[self.cursor_position - count : self.cursor_position] new_text = ( self.text[: self.cursor_position - count] + self.text[self.cursor_position :] ) new_cursor_position = self.cursor_position - len(deleted) # Set new Document atomically. self.document = Document(new_text, new_cursor_position) return deleted def delete(self, count: int = 1) -> str: """ Delete specified number of characters and Return the deleted text. """ if self.cursor_position < len(self.text): deleted = self.document.text_after_cursor[:count] self.text = ( self.text[: self.cursor_position] + self.text[self.cursor_position + len(deleted) :] ) return deleted else: return "" def join_next_line(self, separator: str = " ") -> None: """ Join the next line to the current one by deleting the line ending after the current line. """ if not self.document.on_last_line: self.cursor_position += self.document.get_end_of_line_position() self.delete() # Remove spaces. self.text = ( self.document.text_before_cursor + separator + self.document.text_after_cursor.lstrip(" ") ) def join_selected_lines(self, separator: str = " ") -> None: """ Join the selected lines. """ assert self.selection_state # Get lines. from_, to = sorted( [self.cursor_position, self.selection_state.original_cursor_position] ) before = self.text[:from_] lines = self.text[from_:to].splitlines() after = self.text[to:] # Replace leading spaces with just one space. lines = [l.lstrip(" ") + separator for l in lines] # Set new document. self.document = Document( text=before + "".join(lines) + after, cursor_position=len(before + "".join(lines[:-1])) - 1, ) def swap_characters_before_cursor(self) -> None: """ Swap the last two characters before the cursor. """ pos = self.cursor_position if pos >= 2: a = self.text[pos - 2] b = self.text[pos - 1] self.text = self.text[: pos - 2] + b + a + self.text[pos:] def go_to_history(self, index: int) -> None: """ Go to this item in the history. """ if index < len(self._working_lines): self.working_index = index self.cursor_position = len(self.text) def complete_next(self, count: int = 1, disable_wrap_around: bool = False) -> None: """ Browse to the next completions. (Does nothing if there are no completion.) """ index: int | None if self.complete_state: completions_count = len(self.complete_state.completions) if self.complete_state.complete_index is None: index = 0 elif self.complete_state.complete_index == completions_count - 1: index = None if disable_wrap_around: return else: index = min( completions_count - 1, self.complete_state.complete_index + count ) self.go_to_completion(index) def complete_previous( self, count: int = 1, disable_wrap_around: bool = False ) -> None: """ Browse to the previous completions. (Does nothing if there are no completion.) """ index: int | None if self.complete_state: if self.complete_state.complete_index == 0: index = None if disable_wrap_around: return elif self.complete_state.complete_index is None: index = len(self.complete_state.completions) - 1 else: index = max(0, self.complete_state.complete_index - count) self.go_to_completion(index) def cancel_completion(self) -> None: """ Cancel completion, go back to the original text. """ if self.complete_state: self.go_to_completion(None) self.complete_state = None def _set_completions(self, completions: list[Completion]) -> CompletionState: """ Start completions. (Generate list of completions and initialize.) By default, no completion will be selected. """ self.complete_state = CompletionState( original_document=self.document, completions=completions ) # Trigger event. This should eventually invalidate the layout. self.on_completions_changed.fire() return self.complete_state def start_history_lines_completion(self) -> None: """ Start a completion based on all the other lines in the document and the history. """ found_completions: set[str] = set() completions = [] # For every line of the whole history, find matches with the current line. current_line = self.document.current_line_before_cursor.lstrip() for i, string in enumerate(self._working_lines): for j, l in enumerate(string.split("\n")): l = l.strip() if l and l.startswith(current_line): # When a new line has been found. if l not in found_completions: found_completions.add(l) # Create completion. if i == self.working_index: display_meta = "Current, line %s" % (j + 1) else: display_meta = f"History {i + 1}, line {j + 1}" completions.append( Completion( text=l, start_position=-len(current_line), display_meta=display_meta, ) ) self._set_completions(completions=completions[::-1]) self.go_to_completion(0) def go_to_completion(self, index: int | None) -> None: """ Select a completion from the list of current completions. """ assert self.complete_state # Set new completion state = self.complete_state state.go_to_index(index) # Set text/cursor position new_text, new_cursor_position = state.new_text_and_position() self.document = Document(new_text, new_cursor_position) # (changing text/cursor position will unset complete_state.) self.complete_state = state def apply_completion(self, completion: Completion) -> None: """ Insert a given completion. """ # If there was already a completion active, cancel that one. if self.complete_state: self.go_to_completion(None) self.complete_state = None # Insert text from the given completion. self.delete_before_cursor(-completion.start_position) self.insert_text(completion.text) def _set_history_search(self) -> None: """ Set `history_search_text`. (The text before the cursor will be used for filtering the history.) """ if self.enable_history_search(): if self.history_search_text is None: self.history_search_text = self.document.text_before_cursor else: self.history_search_text = None def _history_matches(self, i: int) -> bool: """ True when the current entry matches the history search. (when we don't have history search, it's also True.) """ return self.history_search_text is None or self._working_lines[i].startswith( self.history_search_text ) def history_forward(self, count: int = 1) -> None: """ Move forwards through the history. :param count: Amount of items to move forward. """ self._set_history_search() # Go forward in history. found_something = False for i in range(self.working_index + 1, len(self._working_lines)): if self._history_matches(i): self.working_index = i count -= 1 found_something = True if count == 0: break # If we found an entry, move cursor to the end of the first line. if found_something: self.cursor_position = 0 self.cursor_position += self.document.get_end_of_line_position() def history_backward(self, count: int = 1) -> None: """ Move backwards through history. """ self._set_history_search() # Go back in history. found_something = False for i in range(self.working_index - 1, -1, -1): if self._history_matches(i): self.working_index = i count -= 1 found_something = True if count == 0: break # If we move to another entry, move cursor to the end of the line. if found_something: self.cursor_position = len(self.text) def yank_nth_arg(self, n: int | None = None, _yank_last_arg: bool = False) -> None: """ Pick nth word from previous history entry (depending on current `yank_nth_arg_state`) and insert it at current position. Rotate through history if called repeatedly. If no `n` has been given, take the first argument. (The second word.) :param n: (None or int), The index of the word from the previous line to take. """ assert n is None or isinstance(n, int) history_strings = self.history.get_strings() if not len(history_strings): return # Make sure we have a `YankNthArgState`. if self.yank_nth_arg_state is None: state = YankNthArgState(n=-1 if _yank_last_arg else 1) else: state = self.yank_nth_arg_state if n is not None: state.n = n # Get new history position. new_pos = state.history_position - 1 if -new_pos > len(history_strings): new_pos = -1 # Take argument from line. line = history_strings[new_pos] words = [w.strip() for w in _QUOTED_WORDS_RE.split(line)] words = [w for w in words if w] try: word = words[state.n] except IndexError: word = "" # Insert new argument. if state.previous_inserted_word: self.delete_before_cursor(len(state.previous_inserted_word)) self.insert_text(word) # Save state again for next completion. (Note that the 'insert' # operation from above clears `self.yank_nth_arg_state`.) state.previous_inserted_word = word state.history_position = new_pos self.yank_nth_arg_state = state def yank_last_arg(self, n: int | None = None) -> None: """ Like `yank_nth_arg`, but if no argument has been given, yank the last word by default. """ self.yank_nth_arg(n=n, _yank_last_arg=True) def start_selection( self, selection_type: SelectionType = SelectionType.CHARACTERS ) -> None: """ Take the current cursor position as the start of this selection. """ self.selection_state = SelectionState(self.cursor_position, selection_type) def copy_selection(self, _cut: bool = False) -> ClipboardData: """ Copy selected text and return :class:`.ClipboardData` instance. Notice that this doesn't store the copied data on the clipboard yet. You can store it like this: .. code:: python data = buffer.copy_selection() get_app().clipboard.set_data(data) """ new_document, clipboard_data = self.document.cut_selection() if _cut: self.document = new_document self.selection_state = None return clipboard_data def cut_selection(self) -> ClipboardData: """ Delete selected text and return :class:`.ClipboardData` instance. """ return self.copy_selection(_cut=True) def paste_clipboard_data( self, data: ClipboardData, paste_mode: PasteMode = PasteMode.EMACS, count: int = 1, ) -> None: """ Insert the data from the clipboard. """ assert isinstance(data, ClipboardData) assert paste_mode in (PasteMode.VI_BEFORE, PasteMode.VI_AFTER, PasteMode.EMACS) original_document = self.document self.document = self.document.paste_clipboard_data( data, paste_mode=paste_mode, count=count ) # Remember original document. This assignment should come at the end, # because assigning to 'document' will erase it. self.document_before_paste = original_document def newline(self, copy_margin: bool = True) -> None: """ Insert a line ending at the current position. """ if copy_margin: self.insert_text("\n" + self.document.leading_whitespace_in_current_line) else: self.insert_text("\n") def insert_line_above(self, copy_margin: bool = True) -> None: """ Insert a new line above the current one. """ if copy_margin: insert = self.document.leading_whitespace_in_current_line + "\n" else: insert = "\n" self.cursor_position += self.document.get_start_of_line_position() self.insert_text(insert) self.cursor_position -= 1 def insert_line_below(self, copy_margin: bool = True) -> None: """ Insert a new line below the current one. """ if copy_margin: insert = "\n" + self.document.leading_whitespace_in_current_line else: insert = "\n" self.cursor_position += self.document.get_end_of_line_position() self.insert_text(insert) def insert_text( self, data: str, overwrite: bool = False, move_cursor: bool = True, fire_event: bool = True, ) -> None: """ Insert characters at cursor position. :param fire_event: Fire `on_text_insert` event. This is mainly used to trigger autocompletion while typing. """ # Original text & cursor position. otext = self.text ocpos = self.cursor_position # In insert/text mode. if overwrite: # Don't overwrite the newline itself. Just before the line ending, # it should act like insert mode. overwritten_text = otext[ocpos : ocpos + len(data)] if "\n" in overwritten_text: overwritten_text = overwritten_text[: overwritten_text.find("\n")] text = otext[:ocpos] + data + otext[ocpos + len(overwritten_text) :] else: text = otext[:ocpos] + data + otext[ocpos:] if move_cursor: cpos = self.cursor_position + len(data) else: cpos = self.cursor_position # Set new document. # (Set text and cursor position at the same time. Otherwise, setting # the text will fire a change event before the cursor position has been # set. It works better to have this atomic.) self.document = Document(text, cpos) # Fire 'on_text_insert' event. if fire_event: # XXX: rename to `start_complete`. self.on_text_insert.fire() # Only complete when "complete_while_typing" is enabled. if self.completer and self.complete_while_typing(): get_app().create_background_task(self._async_completer()) # Call auto_suggest. if self.auto_suggest: get_app().create_background_task(self._async_suggester()) def undo(self) -> None: # Pop from the undo-stack until we find a text that if different from # the current text. (The current logic of `save_to_undo_stack` will # cause that the top of the undo stack is usually the same as the # current text, so in that case we have to pop twice.) while self._undo_stack: text, pos = self._undo_stack.pop() if text != self.text: # Push current text to redo stack. self._redo_stack.append((self.text, self.cursor_position)) # Set new text/cursor_position. self.document = Document(text, cursor_position=pos) break def redo(self) -> None: if self._redo_stack: # Copy current state on undo stack. self.save_to_undo_stack(clear_redo_stack=False) # Pop state from redo stack. text, pos = self._redo_stack.pop() self.document = Document(text, cursor_position=pos) def validate(self, set_cursor: bool = False) -> bool: """ Returns `True` if valid. :param set_cursor: Set the cursor position, if an error was found. """ # Don't call the validator again, if it was already called for the # current input. if self.validation_state != ValidationState.UNKNOWN: return self.validation_state == ValidationState.VALID # Call validator. if self.validator: try: self.validator.validate(self.document) except ValidationError as e: # Set cursor position (don't allow invalid values.) if set_cursor: self.cursor_position = min( max(0, e.cursor_position), len(self.text) ) self.validation_state = ValidationState.INVALID self.validation_error = e return False # Handle validation result. self.validation_state = ValidationState.VALID self.validation_error = None return True async def _validate_async(self) -> None: """ Asynchronous version of `validate()`. This one doesn't set the cursor position. We have both variants, because a synchronous version is required. Handling the ENTER key needs to be completely synchronous, otherwise stuff like type-ahead is going to give very weird results. (People could type input while the ENTER key is still processed.) An asynchronous version is required if we have `validate_while_typing` enabled. """ while True: # Don't call the validator again, if it was already called for the # current input. if self.validation_state != ValidationState.UNKNOWN: return # Call validator. error = None document = self.document if self.validator: try: await self.validator.validate_async(self.document) except ValidationError as e: error = e # If the document changed during the validation, try again. if self.document != document: continue # Handle validation result. if error: self.validation_state = ValidationState.INVALID else: self.validation_state = ValidationState.VALID self.validation_error = error get_app().invalidate() # Trigger redraw (display error). def append_to_history(self) -> None: """ Append the current input to the history. """ # Save at the tail of the history. (But don't if the last entry the # history is already the same.) if self.text: history_strings = self.history.get_strings() if not len(history_strings) or history_strings[-1] != self.text: self.history.append_string(self.text) def _search( self, search_state: SearchState, include_current_position: bool = False, count: int = 1, ) -> tuple[int, int] | None: """ Execute search. Return (working_index, cursor_position) tuple when this search is applied. Returns `None` when this text cannot be found. """ assert count > 0 text = search_state.text direction = search_state.direction ignore_case = search_state.ignore_case() def search_once( working_index: int, document: Document ) -> tuple[int, Document] | None: """ Do search one time. Return (working_index, document) or `None` """ if direction == SearchDirection.FORWARD: # Try find at the current input. new_index = document.find( text, include_current_position=include_current_position, ignore_case=ignore_case, ) if new_index is not None: return ( working_index, Document(document.text, document.cursor_position + new_index), ) else: # No match, go forward in the history. (Include len+1 to wrap around.) # (Here we should always include all cursor positions, because # it's a different line.) for i in range(working_index + 1, len(self._working_lines) + 1): i %= len(self._working_lines) document = Document(self._working_lines[i], 0) new_index = document.find( text, include_current_position=True, ignore_case=ignore_case ) if new_index is not None: return (i, Document(document.text, new_index)) else: # Try find at the current input. new_index = document.find_backwards(text, ignore_case=ignore_case) if new_index is not None: return ( working_index, Document(document.text, document.cursor_position + new_index), ) else: # No match, go back in the history. (Include -1 to wrap around.) for i in range(working_index - 1, -2, -1): i %= len(self._working_lines) document = Document( self._working_lines[i], len(self._working_lines[i]) ) new_index = document.find_backwards( text, ignore_case=ignore_case ) if new_index is not None: return ( i, Document(document.text, len(document.text) + new_index), ) return None # Do 'count' search iterations. working_index = self.working_index document = self.document for _ in range(count): result = search_once(working_index, document) if result is None: return None # Nothing found. else: working_index, document = result return (working_index, document.cursor_position) def document_for_search(self, search_state: SearchState) -> Document: """ Return a :class:`~prompt_toolkit.document.Document` instance that has the text/cursor position for this search, if we would apply it. This will be used in the :class:`~prompt_toolkit.layout.BufferControl` to display feedback while searching. """ search_result = self._search(search_state, include_current_position=True) if search_result is None: return self.document else: working_index, cursor_position = search_result # Keep selection, when `working_index` was not changed. if working_index == self.working_index: selection = self.selection_state else: selection = None return Document( self._working_lines[working_index], cursor_position, selection=selection ) def get_search_position( self, search_state: SearchState, include_current_position: bool = True, count: int = 1, ) -> int: """ Get the cursor position for this search. (This operation won't change the `working_index`. It's won't go through the history. Vi text objects can't span multiple items.) """ search_result = self._search( search_state, include_current_position=include_current_position, count=count ) if search_result is None: return self.cursor_position else: working_index, cursor_position = search_result return cursor_position def apply_search( self, search_state: SearchState, include_current_position: bool = True, count: int = 1, ) -> None: """ Apply search. If something is found, set `working_index` and `cursor_position`. """ search_result = self._search( search_state, include_current_position=include_current_position, count=count ) if search_result is not None: working_index, cursor_position = search_result self.working_index = working_index self.cursor_position = cursor_position def exit_selection(self) -> None: self.selection_state = None def _editor_simple_tempfile(self) -> tuple[str, Callable[[], None]]: """ Simple (file) tempfile implementation. Return (tempfile, cleanup_func). """ suffix = to_str(self.tempfile_suffix) descriptor, filename = tempfile.mkstemp(suffix) os.write(descriptor, self.text.encode("utf-8")) os.close(descriptor) def cleanup() -> None: os.unlink(filename) return filename, cleanup def _editor_complex_tempfile(self) -> tuple[str, Callable[[], None]]: # Complex (directory) tempfile implementation. headtail = to_str(self.tempfile) if not headtail: # Revert to simple case. return self._editor_simple_tempfile() headtail = str(headtail) # Try to make according to tempfile logic. head, tail = os.path.split(headtail) if os.path.isabs(head): head = head[1:] dirpath = tempfile.mkdtemp() if head: dirpath = os.path.join(dirpath, head) # Assume there is no issue creating dirs in this temp dir. os.makedirs(dirpath) # Open the filename and write current text. filename = os.path.join(dirpath, tail) with open(filename, "w", encoding="utf-8") as fh: fh.write(self.text) def cleanup() -> None: shutil.rmtree(dirpath) return filename, cleanup def open_in_editor(self, validate_and_handle: bool = False) -> asyncio.Task[None]: """ Open code in editor. This returns a future, and runs in a thread executor. """ if self.read_only(): raise EditReadOnlyBuffer() # Write current text to temporary file if self.tempfile: filename, cleanup_func = self._editor_complex_tempfile() else: filename, cleanup_func = self._editor_simple_tempfile() async def run() -> None: try: # Open in editor # (We need to use `run_in_terminal`, because not all editors go to # the alternate screen buffer, and some could influence the cursor # position.) success = await run_in_terminal( lambda: self._open_file_in_editor(filename), in_executor=True ) # Read content again. if success: with open(filename, "rb") as f: text = f.read().decode("utf-8") # Drop trailing newline. (Editors are supposed to add it at the # end, but we don't need it.) if text.endswith("\n"): text = text[:-1] self.document = Document(text=text, cursor_position=len(text)) # Accept the input. if validate_and_handle: self.validate_and_handle() finally: # Clean up temp dir/file. cleanup_func() return get_app().create_background_task(run()) def _open_file_in_editor(self, filename: str) -> bool: """ Call editor executable. Return True when we received a zero return code. """ # If the 'VISUAL' or 'EDITOR' environment variable has been set, use that. # Otherwise, fall back to the first available editor that we can find. visual = os.environ.get("VISUAL") editor = os.environ.get("EDITOR") editors = [ visual, editor, # Order of preference. "/usr/bin/editor", "/usr/bin/nano", "/usr/bin/pico", "/usr/bin/vi", "/usr/bin/emacs", ] for e in editors: if e: try: # Use 'shlex.split()', because $VISUAL can contain spaces # and quotes. returncode = subprocess.call(shlex.split(e) + [filename]) return returncode == 0 except OSError: # Executable does not exist, try the next one. pass return False def start_completion( self, select_first: bool = False, select_last: bool = False, insert_common_part: bool = False, complete_event: CompleteEvent | None = None, ) -> None: """ Start asynchronous autocompletion of this buffer. (This will do nothing if a previous completion was still in progress.) """ # Only one of these options can be selected. assert select_first + select_last + insert_common_part <= 1 get_app().create_background_task( self._async_completer( select_first=select_first, select_last=select_last, insert_common_part=insert_common_part, complete_event=complete_event or CompleteEvent(completion_requested=True), ) ) def _create_completer_coroutine(self) -> Callable[..., Coroutine[Any, Any, None]]: """ Create function for asynchronous autocompletion. (This consumes the asynchronous completer generator, which possibly runs the completion algorithm in another thread.) """ def completion_does_nothing(document: Document, completion: Completion) -> bool: """ Return `True` if applying this completion doesn't have any effect. (When it doesn't insert any new text. """ text_before_cursor = document.text_before_cursor replaced_text = text_before_cursor[ len(text_before_cursor) + completion.start_position : ] return replaced_text == completion.text @_only_one_at_a_time async def async_completer( select_first: bool = False, select_last: bool = False, insert_common_part: bool = False, complete_event: CompleteEvent | None = None, ) -> None: document = self.document complete_event = complete_event or CompleteEvent(text_inserted=True) # Don't complete when we already have completions. if self.complete_state or not self.completer: return # Create an empty CompletionState. complete_state = CompletionState(original_document=self.document) self.complete_state = complete_state def proceed() -> bool: """Keep retrieving completions. Input text has not yet changed while generating completions.""" return self.complete_state == complete_state refresh_needed = asyncio.Event() async def refresh_while_loading() -> None: """Background loop to refresh the UI at most 3 times a second while the completion are loading. Calling `on_completions_changed.fire()` for every completion that we receive is too expensive when there are many completions. (We could tune `Application.max_render_postpone_time` and `Application.min_redraw_interval`, but having this here is a better approach.) """ while True: self.on_completions_changed.fire() refresh_needed.clear() await asyncio.sleep(0.3) await refresh_needed.wait() refresh_task = asyncio.ensure_future(refresh_while_loading()) try: # Load. async with aclosing( self.completer.get_completions_async(document, complete_event) ) as async_generator: async for completion in async_generator: complete_state.completions.append(completion) refresh_needed.set() # If the input text changes, abort. if not proceed(): break # Always stop at 10k completions. if ( len(complete_state.completions) >= self.max_number_of_completions ): break finally: refresh_task.cancel() # Refresh one final time after we got everything. self.on_completions_changed.fire() completions = complete_state.completions # When there is only one completion, which has nothing to add, ignore it. if len(completions) == 1 and completion_does_nothing( document, completions[0] ): del completions[:] # Set completions if the text was not yet changed. if proceed(): # When no completions were found, or when the user selected # already a completion by using the arrow keys, don't do anything. if ( not self.complete_state or self.complete_state.complete_index is not None ): return # When there are no completions, reset completion state anyway. if not completions: self.complete_state = None # Render the ui if the completion menu was shown # it is needed especially if there is one completion and it was deleted. self.on_completions_changed.fire() return # Select first/last or insert common part, depending on the key # binding. (For this we have to wait until all completions are # loaded.) if select_first: self.go_to_completion(0) elif select_last: self.go_to_completion(len(completions) - 1) elif insert_common_part: common_part = get_common_complete_suffix(document, completions) if common_part: # Insert the common part, update completions. self.insert_text(common_part) if len(completions) > 1: # (Don't call `async_completer` again, but # recalculate completions. See: # https://github.com/ipython/ipython/issues/9658) completions[:] = [ c.new_completion_from_position(len(common_part)) for c in completions ] self._set_completions(completions=completions) else: self.complete_state = None else: # When we were asked to insert the "common" # prefix, but there was no common suffix but # still exactly one match, then select the # first. (It could be that we have a completion # which does * expansion, like '*.py', with # exactly one match.) if len(completions) == 1: self.go_to_completion(0) else: # If the last operation was an insert, (not a delete), restart # the completion coroutine. if self.document.text_before_cursor == document.text_before_cursor: return # Nothing changed. if self.document.text_before_cursor.startswith( document.text_before_cursor ): raise _Retry return async_completer def _create_auto_suggest_coroutine(self) -> Callable[[], Coroutine[Any, Any, None]]: """ Create function for asynchronous auto suggestion. (This can be in another thread.) """ @_only_one_at_a_time async def async_suggestor() -> None: document = self.document # Don't suggest when we already have a suggestion. if self.suggestion or not self.auto_suggest: return suggestion = await self.auto_suggest.get_suggestion_async(self, document) # Set suggestion only if the text was not yet changed. if self.document == document: # Set suggestion and redraw interface. self.suggestion = suggestion self.on_suggestion_set.fire() else: # Otherwise, restart thread. raise _Retry return async_suggestor def _create_auto_validate_coroutine( self, ) -> Callable[[], Coroutine[Any, Any, None]]: """ Create a function for asynchronous validation while typing. (This can be in another thread.) """ @_only_one_at_a_time async def async_validator() -> None: await self._validate_async() return async_validator def validate_and_handle(self) -> None: """ Validate buffer and handle the accept action. """ valid = self.validate(set_cursor=True) # When the validation succeeded, accept the input. if valid: if self.accept_handler: keep_text = self.accept_handler(self) else: keep_text = False self.append_to_history() if not keep_text: self.reset() _T = TypeVar("_T", bound=Callable[..., Coroutine[Any, Any, None]]) def _only_one_at_a_time(coroutine: _T) -> _T: """ Decorator that only starts the coroutine only if the previous call has finished. (Used to make sure that we have only one autocompleter, auto suggestor and validator running at a time.) When the coroutine raises `_Retry`, it is restarted. """ running = False @wraps(coroutine) async def new_coroutine(*a: Any, **kw: Any) -> Any: nonlocal running # Don't start a new function, if the previous is still in progress. if running: return running = True try: while True: try: await coroutine(*a, **kw) except _Retry: continue else: return None finally: running = False return cast(_T, new_coroutine) class _Retry(Exception): "Retry in `_only_one_at_a_time`." def indent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None: """ Indent text of a :class:`.Buffer` object. """ current_row = buffer.document.cursor_position_row current_col = buffer.document.cursor_position_col line_range = range(from_row, to_row) # Apply transformation. indent_content = " " * count new_text = buffer.transform_lines(line_range, lambda l: indent_content + l) buffer.document = Document( new_text, Document(new_text).translate_row_col_to_index(current_row, 0) ) # Place cursor in the same position in text after indenting buffer.cursor_position += current_col + len(indent_content) def unindent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None: """ Unindent text of a :class:`.Buffer` object. """ current_row = buffer.document.cursor_position_row current_col = buffer.document.cursor_position_col line_range = range(from_row, to_row) indent_content = " " * count def transform(text: str) -> str: remove = indent_content if text.startswith(remove): return text[len(remove) :] else: return text.lstrip() # Apply transformation. new_text = buffer.transform_lines(line_range, transform) buffer.document = Document( new_text, Document(new_text).translate_row_col_to_index(current_row, 0) ) # Place cursor in the same position in text after dedent buffer.cursor_position += current_col - len(indent_content) def reshape_text(buffer: Buffer, from_row: int, to_row: int) -> None: """ Reformat text, taking the width into account. `to_row` is included. (Vi 'gq' operator.) """ lines = buffer.text.splitlines(True) lines_before = lines[:from_row] lines_after = lines[to_row + 1 :] lines_to_reformat = lines[from_row : to_row + 1] if lines_to_reformat: # Take indentation from the first line. match = re.search(r"^\s*", lines_to_reformat[0]) length = match.end() if match else 0 # `match` can't be None, actually. indent = lines_to_reformat[0][:length].replace("\n", "") # Now, take all the 'words' from the lines to be reshaped. words = "".join(lines_to_reformat).split() # And reshape. width = (buffer.text_width or 80) - len(indent) reshaped_text = [indent] current_width = 0 for w in words: if current_width: if len(w) + current_width + 1 > width: reshaped_text.append("\n") reshaped_text.append(indent) current_width = 0 else: reshaped_text.append(" ") current_width += 1 reshaped_text.append(w) current_width += len(w) if reshaped_text[-1] != "\n": reshaped_text.append("\n") # Apply result. buffer.document = Document( text="".join(lines_before + reshaped_text + lines_after), cursor_position=len("".join(lines_before + reshaped_text)), ) ================================================ FILE: src/prompt_toolkit/cache.py ================================================ from __future__ import annotations from collections import deque from collections.abc import Callable, Hashable from functools import wraps from typing import Any, Generic, TypeVar, cast __all__ = [ "SimpleCache", "FastDictCache", "memoized", ] _T = TypeVar("_T", bound=Hashable) _U = TypeVar("_U") class SimpleCache(Generic[_T, _U]): """ Very simple cache that discards the oldest item when the cache size is exceeded. :param maxsize: Maximum size of the cache. (Don't make it too big.) """ def __init__(self, maxsize: int = 8) -> None: assert maxsize > 0 self._data: dict[_T, _U] = {} self._keys: deque[_T] = deque() self.maxsize: int = maxsize def get(self, key: _T, getter_func: Callable[[], _U]) -> _U: """ Get object from the cache. If not found, call `getter_func` to resolve it, and put that on the top of the cache instead. """ # Look in cache first. try: return self._data[key] except KeyError: # Not found? Get it. value = getter_func() self._data[key] = value self._keys.append(key) # Remove the oldest key when the size is exceeded. if len(self._data) > self.maxsize: key_to_remove = self._keys.popleft() if key_to_remove in self._data: del self._data[key_to_remove] return value def clear(self) -> None: "Clear cache." self._data = {} self._keys = deque() _K = TypeVar("_K", bound=tuple[Hashable, ...]) _V = TypeVar("_V") class FastDictCache(dict[_K, _V]): """ Fast, lightweight cache which keeps at most `size` items. It will discard the oldest items in the cache first. The cache is a dictionary, which doesn't keep track of access counts. It is perfect to cache little immutable objects which are not expensive to create, but where a dictionary lookup is still much faster than an object instantiation. :param get_value: Callable that's called in case of a missing key. """ # NOTE: This cache is used to cache `prompt_toolkit.layout.screen.Char` and # `prompt_toolkit.Document`. Make sure to keep this really lightweight. # Accessing the cache should stay faster than instantiating new # objects. # (Dictionary lookups are really fast.) # SimpleCache is still required for cases where the cache key is not # the same as the arguments given to the function that creates the # value.) def __init__(self, get_value: Callable[..., _V], size: int = 1000000) -> None: assert size > 0 self._keys: deque[_K] = deque() self.get_value = get_value self.size = size def __missing__(self, key: _K) -> _V: # Remove the oldest key when the size is exceeded. if len(self) > self.size: key_to_remove = self._keys.popleft() if key_to_remove in self: del self[key_to_remove] result = self.get_value(*key) self[key] = result self._keys.append(key) return result _F = TypeVar("_F", bound=Callable[..., object]) def memoized(maxsize: int = 1024) -> Callable[[_F], _F]: """ Memoization decorator for immutable classes and pure functions. """ def decorator(obj: _F) -> _F: cache: SimpleCache[Hashable, Any] = SimpleCache(maxsize=maxsize) @wraps(obj) def new_callable(*a: Any, **kw: Any) -> Any: def create_new() -> Any: return obj(*a, **kw) key = (a, tuple(sorted(kw.items()))) return cache.get(key, create_new) return cast(_F, new_callable) return decorator ================================================ FILE: src/prompt_toolkit/clipboard/__init__.py ================================================ from __future__ import annotations from .base import Clipboard, ClipboardData, DummyClipboard, DynamicClipboard from .in_memory import InMemoryClipboard # We are not importing `PyperclipClipboard` here, because it would require the # `pyperclip` module to be present. # from .pyperclip import PyperclipClipboard __all__ = [ "Clipboard", "ClipboardData", "DummyClipboard", "DynamicClipboard", "InMemoryClipboard", ] ================================================ FILE: src/prompt_toolkit/clipboard/base.py ================================================ """ Clipboard for command line interface. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable from prompt_toolkit.selection import SelectionType __all__ = [ "Clipboard", "ClipboardData", "DummyClipboard", "DynamicClipboard", ] class ClipboardData: """ Text on the clipboard. :param text: string :param type: :class:`~prompt_toolkit.selection.SelectionType` """ def __init__( self, text: str = "", type: SelectionType = SelectionType.CHARACTERS ) -> None: self.text = text self.type = type class Clipboard(metaclass=ABCMeta): """ Abstract baseclass for clipboards. (An implementation can be in memory, it can share the X11 or Windows keyboard, or can be persistent.) """ @abstractmethod def set_data(self, data: ClipboardData) -> None: """ Set data to the clipboard. :param data: :class:`~.ClipboardData` instance. """ def set_text(self, text: str) -> None: # Not abstract. """ Shortcut for setting plain text on clipboard. """ self.set_data(ClipboardData(text)) def rotate(self) -> None: """ For Emacs mode, rotate the kill ring. """ @abstractmethod def get_data(self) -> ClipboardData: """ Return clipboard data. """ class DummyClipboard(Clipboard): """ Clipboard implementation that doesn't remember anything. """ def set_data(self, data: ClipboardData) -> None: pass def set_text(self, text: str) -> None: pass def rotate(self) -> None: pass def get_data(self) -> ClipboardData: return ClipboardData() class DynamicClipboard(Clipboard): """ Clipboard class that can dynamically returns any Clipboard. :param get_clipboard: Callable that returns a :class:`.Clipboard` instance. """ def __init__(self, get_clipboard: Callable[[], Clipboard | None]) -> None: self.get_clipboard = get_clipboard def _clipboard(self) -> Clipboard: return self.get_clipboard() or DummyClipboard() def set_data(self, data: ClipboardData) -> None: self._clipboard().set_data(data) def set_text(self, text: str) -> None: self._clipboard().set_text(text) def rotate(self) -> None: self._clipboard().rotate() def get_data(self) -> ClipboardData: return self._clipboard().get_data() ================================================ FILE: src/prompt_toolkit/clipboard/in_memory.py ================================================ from __future__ import annotations from collections import deque from .base import Clipboard, ClipboardData __all__ = [ "InMemoryClipboard", ] class InMemoryClipboard(Clipboard): """ Default clipboard implementation. Just keep the data in memory. This implements a kill-ring, for Emacs mode. """ def __init__(self, data: ClipboardData | None = None, max_size: int = 60) -> None: assert max_size >= 1 self.max_size = max_size self._ring: deque[ClipboardData] = deque() if data is not None: self.set_data(data) def set_data(self, data: ClipboardData) -> None: self._ring.appendleft(data) while len(self._ring) > self.max_size: self._ring.pop() def get_data(self) -> ClipboardData: if self._ring: return self._ring[0] else: return ClipboardData() def rotate(self) -> None: if self._ring: # Add the very first item at the end. self._ring.append(self._ring.popleft()) ================================================ FILE: src/prompt_toolkit/clipboard/pyperclip.py ================================================ from __future__ import annotations import pyperclip from prompt_toolkit.selection import SelectionType from .base import Clipboard, ClipboardData __all__ = [ "PyperclipClipboard", ] class PyperclipClipboard(Clipboard): """ Clipboard that synchronizes with the Windows/Mac/Linux system clipboard, using the pyperclip module. """ def __init__(self) -> None: self._data: ClipboardData | None = None def set_data(self, data: ClipboardData) -> None: self._data = data pyperclip.copy(data.text) def get_data(self) -> ClipboardData: text = pyperclip.paste() # When the clipboard data is equal to what we copied last time, reuse # the `ClipboardData` instance. That way we're sure to keep the same # `SelectionType`. if self._data and self._data.text == text: return self._data # Pyperclip returned something else. Create a new `ClipboardData` # instance. else: return ClipboardData( text=text, type=SelectionType.LINES if "\n" in text else SelectionType.CHARACTERS, ) ================================================ FILE: src/prompt_toolkit/completion/__init__.py ================================================ from __future__ import annotations from .base import ( CompleteEvent, Completer, Completion, ConditionalCompleter, DummyCompleter, DynamicCompleter, ThreadedCompleter, get_common_complete_suffix, merge_completers, ) from .deduplicate import DeduplicateCompleter from .filesystem import ExecutableCompleter, PathCompleter from .fuzzy_completer import FuzzyCompleter, FuzzyWordCompleter from .nested import NestedCompleter from .word_completer import WordCompleter __all__ = [ # Base. "Completion", "Completer", "ThreadedCompleter", "DummyCompleter", "DynamicCompleter", "CompleteEvent", "ConditionalCompleter", "merge_completers", "get_common_complete_suffix", # Filesystem. "PathCompleter", "ExecutableCompleter", # Fuzzy "FuzzyCompleter", "FuzzyWordCompleter", # Nested. "NestedCompleter", # Word completer. "WordCompleter", # Deduplicate "DeduplicateCompleter", ] ================================================ FILE: src/prompt_toolkit/completion/base.py ================================================ """ """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import AsyncGenerator, Callable, Iterable, Sequence from prompt_toolkit.document import Document from prompt_toolkit.eventloop import aclosing, generator_to_async_generator from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples __all__ = [ "Completion", "Completer", "ThreadedCompleter", "DummyCompleter", "DynamicCompleter", "CompleteEvent", "ConditionalCompleter", "merge_completers", "get_common_complete_suffix", ] class Completion: """ :param text: The new string that will be inserted into the document. :param start_position: Position relative to the cursor_position where the new text will start. The text will be inserted between the start_position and the original cursor position. :param display: (optional string or formatted text) If the completion has to be displayed differently in the completion menu. :param display_meta: (Optional string or formatted text) Meta information about the completion, e.g. the path or source where it's coming from. This can also be a callable that returns a string. :param style: Style string. :param selected_style: Style string, used for a selected completion. This can override the `style` parameter. """ def __init__( self, text: str, start_position: int = 0, display: AnyFormattedText | None = None, display_meta: AnyFormattedText | None = None, style: str = "", selected_style: str = "", ) -> None: from prompt_toolkit.formatted_text import to_formatted_text self.text = text self.start_position = start_position self._display_meta = display_meta if display is None: display = text self.display = to_formatted_text(display) self.style = style self.selected_style = selected_style assert self.start_position <= 0 def __repr__(self) -> str: if isinstance(self.display, str) and self.display == self.text: return f"{self.__class__.__name__}(text={self.text!r}, start_position={self.start_position!r})" else: return f"{self.__class__.__name__}(text={self.text!r}, start_position={self.start_position!r}, display={self.display!r})" def __eq__(self, other: object) -> bool: if not isinstance(other, Completion): return False return ( self.text == other.text and self.start_position == other.start_position and self.display == other.display and self._display_meta == other._display_meta ) def __hash__(self) -> int: return hash((self.text, self.start_position, self.display, self._display_meta)) @property def display_text(self) -> str: "The 'display' field as plain text." from prompt_toolkit.formatted_text import fragment_list_to_text return fragment_list_to_text(self.display) @property def display_meta(self) -> StyleAndTextTuples: "Return meta-text. (This is lazy when using a callable)." from prompt_toolkit.formatted_text import to_formatted_text return to_formatted_text(self._display_meta or "") @property def display_meta_text(self) -> str: "The 'meta' field as plain text." from prompt_toolkit.formatted_text import fragment_list_to_text return fragment_list_to_text(self.display_meta) def new_completion_from_position(self, position: int) -> Completion: """ (Only for internal use!) Get a new completion by splitting this one. Used by `Application` when it needs to have a list of new completions after inserting the common prefix. """ assert position - self.start_position >= 0 return Completion( text=self.text[position - self.start_position :], display=self.display, display_meta=self._display_meta, ) class CompleteEvent: """ Event that called the completer. :param text_inserted: When True, it means that completions are requested because of a text insert. (`Buffer.complete_while_typing`.) :param completion_requested: When True, it means that the user explicitly pressed the `Tab` key in order to view the completions. These two flags can be used for instance to implement a completer that shows some completions when ``Tab`` has been pressed, but not automatically when the user presses a space. (Because of `complete_while_typing`.) """ def __init__( self, text_inserted: bool = False, completion_requested: bool = False ) -> None: assert not (text_inserted and completion_requested) #: Automatic completion while typing. self.text_inserted = text_inserted #: Used explicitly requested completion by pressing 'tab'. self.completion_requested = completion_requested def __repr__(self) -> str: return f"{self.__class__.__name__}(text_inserted={self.text_inserted!r}, completion_requested={self.completion_requested!r})" class Completer(metaclass=ABCMeta): """ Base class for completer implementations. """ @abstractmethod def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: """ This should be a generator that yields :class:`.Completion` instances. If the generation of completions is something expensive (that takes a lot of time), consider wrapping this `Completer` class in a `ThreadedCompleter`. In that case, the completer algorithm runs in a background thread and completions will be displayed as soon as they arrive. :param document: :class:`~prompt_toolkit.document.Document` instance. :param complete_event: :class:`.CompleteEvent` instance. """ while False: yield async def get_completions_async( self, document: Document, complete_event: CompleteEvent ) -> AsyncGenerator[Completion, None]: """ Asynchronous generator for completions. (Probably, you won't have to override this.) Asynchronous generator of :class:`.Completion` objects. """ for item in self.get_completions(document, complete_event): yield item class ThreadedCompleter(Completer): """ Wrapper that runs the `get_completions` generator in a thread. (Use this to prevent the user interface from becoming unresponsive if the generation of completions takes too much time.) The completions will be displayed as soon as they are produced. The user can already select a completion, even if not all completions are displayed. """ def __init__(self, completer: Completer) -> None: self.completer = completer def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: return self.completer.get_completions(document, complete_event) async def get_completions_async( self, document: Document, complete_event: CompleteEvent ) -> AsyncGenerator[Completion, None]: """ Asynchronous generator of completions. """ # NOTE: Right now, we are consuming the `get_completions` generator in # a synchronous background thread, then passing the results one # at a time over a queue, and consuming this queue in the main # thread (that's what `generator_to_async_generator` does). That # means that if the completer is *very* slow, we'll be showing # completions in the UI once they are computed. # It's very tempting to replace this implementation with the # commented code below for several reasons: # - `generator_to_async_generator` is not perfect and hard to get # right. It's a lot of complexity for little gain. The # implementation needs a huge buffer for it to be efficient # when there are many completions (like 50k+). # - Normally, a completer is supposed to be fast, users can have # "complete while typing" enabled, and want to see the # completions within a second. Handling one completion at a # time, and rendering once we get it here doesn't make any # sense if this is quick anyway. # - Completers like `FuzzyCompleter` prepare all completions # anyway so that they can be sorted by accuracy before they are # yielded. At the point that we start yielding completions # here, we already have all completions. # - The `Buffer` class has complex logic to invalidate the UI # while it is consuming the completions. We don't want to # invalidate the UI for every completion (if there are many), # but we want to do it often enough so that completions are # being displayed while they are produced. # We keep the current behavior mainly for backward-compatibility. # Similarly, it would be better for this function to not return # an async generator, but simply be a coroutine that returns a # list of `Completion` objects, containing all completions at # once. # Note that this argument doesn't mean we shouldn't use # `ThreadedCompleter`. It still makes sense to produce # completions in a background thread, because we don't want to # freeze the UI while the user is typing. But sending the # completions one at a time to the UI maybe isn't worth it. # def get_all_in_thread() -> List[Completion]: # return list(self.get_completions(document, complete_event)) # completions = await get_running_loop().run_in_executor(None, get_all_in_thread) # for completion in completions: # yield completion async with aclosing( generator_to_async_generator( lambda: self.completer.get_completions(document, complete_event) ) ) as async_generator: async for completion in async_generator: yield completion def __repr__(self) -> str: return f"ThreadedCompleter({self.completer!r})" class DummyCompleter(Completer): """ A completer that doesn't return any completion. """ def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: return [] def __repr__(self) -> str: return "DummyCompleter()" class DynamicCompleter(Completer): """ Completer class that can dynamically returns any Completer. :param get_completer: Callable that returns a :class:`.Completer` instance. """ def __init__(self, get_completer: Callable[[], Completer | None]) -> None: self.get_completer = get_completer def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: completer = self.get_completer() or DummyCompleter() return completer.get_completions(document, complete_event) async def get_completions_async( self, document: Document, complete_event: CompleteEvent ) -> AsyncGenerator[Completion, None]: completer = self.get_completer() or DummyCompleter() async for completion in completer.get_completions_async( document, complete_event ): yield completion def __repr__(self) -> str: return f"DynamicCompleter({self.get_completer!r} -> {self.get_completer()!r})" class ConditionalCompleter(Completer): """ Wrapper around any other completer that will enable/disable the completions depending on whether the received condition is satisfied. :param completer: :class:`.Completer` instance. :param filter: :class:`.Filter` instance. """ def __init__(self, completer: Completer, filter: FilterOrBool) -> None: self.completer = completer self.filter = to_filter(filter) def __repr__(self) -> str: return f"ConditionalCompleter({self.completer!r}, filter={self.filter!r})" def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: # Get all completions in a blocking way. if self.filter(): yield from self.completer.get_completions(document, complete_event) async def get_completions_async( self, document: Document, complete_event: CompleteEvent ) -> AsyncGenerator[Completion, None]: # Get all completions in a non-blocking way. if self.filter(): async with aclosing( self.completer.get_completions_async(document, complete_event) ) as async_generator: async for item in async_generator: yield item class _MergedCompleter(Completer): """ Combine several completers into one. """ def __init__(self, completers: Sequence[Completer]) -> None: self.completers = completers def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: # Get all completions from the other completers in a blocking way. for completer in self.completers: yield from completer.get_completions(document, complete_event) async def get_completions_async( self, document: Document, complete_event: CompleteEvent ) -> AsyncGenerator[Completion, None]: # Get all completions from the other completers in a non-blocking way. for completer in self.completers: async with aclosing( completer.get_completions_async(document, complete_event) ) as async_generator: async for item in async_generator: yield item def merge_completers( completers: Sequence[Completer], deduplicate: bool = False ) -> Completer: """ Combine several completers into one. :param deduplicate: If `True`, wrap the result in a `DeduplicateCompleter` so that completions that would result in the same text will be deduplicated. """ if deduplicate: from .deduplicate import DeduplicateCompleter return DeduplicateCompleter(_MergedCompleter(completers)) return _MergedCompleter(completers) def get_common_complete_suffix( document: Document, completions: Sequence[Completion] ) -> str: """ Return the common prefix for all completions. """ # Take only completions that don't change the text before the cursor. def doesnt_change_before_cursor(completion: Completion) -> bool: end = completion.text[: -completion.start_position] return document.text_before_cursor.endswith(end) completions2 = [c for c in completions if doesnt_change_before_cursor(c)] # When there is at least one completion that changes the text before the # cursor, don't return any common part. if len(completions2) != len(completions): return "" # Return the common prefix. def get_suffix(completion: Completion) -> str: return completion.text[-completion.start_position :] return _commonprefix([get_suffix(c) for c in completions2]) def _commonprefix(strings: Iterable[str]) -> str: # Similar to os.path.commonprefix if not strings: return "" else: s1 = min(strings) s2 = max(strings) for i, c in enumerate(s1): if c != s2[i]: return s1[:i] return s1 ================================================ FILE: src/prompt_toolkit/completion/deduplicate.py ================================================ from __future__ import annotations from collections.abc import Iterable from prompt_toolkit.document import Document from .base import CompleteEvent, Completer, Completion __all__ = ["DeduplicateCompleter"] class DeduplicateCompleter(Completer): """ Wrapper around a completer that removes duplicates. Only the first unique completions are kept. Completions are considered to be a duplicate if they result in the same document text when they would be applied. """ def __init__(self, completer: Completer) -> None: self.completer = completer def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: # Keep track of the document strings we'd get after applying any completion. found_so_far: set[str] = set() for completion in self.completer.get_completions(document, complete_event): text_if_applied = ( document.text[: document.cursor_position + completion.start_position] + completion.text + document.text[document.cursor_position :] ) if text_if_applied == document.text: # Don't include completions that don't have any effect at all. continue if text_if_applied in found_so_far: continue found_so_far.add(text_if_applied) yield completion ================================================ FILE: src/prompt_toolkit/completion/filesystem.py ================================================ from __future__ import annotations import os from collections.abc import Callable, Iterable from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.document import Document __all__ = [ "PathCompleter", "ExecutableCompleter", ] class PathCompleter(Completer): """ Complete for Path variables. :param get_paths: Callable which returns a list of directories to look into when the user enters a relative path. :param file_filter: Callable which takes a filename and returns whether this file should show up in the completion. ``None`` when no filtering has to be done. :param min_input_len: Don't do autocompletion when the input string is shorter. """ def __init__( self, only_directories: bool = False, get_paths: Callable[[], list[str]] | None = None, file_filter: Callable[[str], bool] | None = None, min_input_len: int = 0, expanduser: bool = False, ) -> None: self.only_directories = only_directories self.get_paths = get_paths or (lambda: ["."]) self.file_filter = file_filter or (lambda _: True) self.min_input_len = min_input_len self.expanduser = expanduser def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: text = document.text_before_cursor # Complete only when we have at least the minimal input length, # otherwise, we can too many results and autocompletion will become too # heavy. if len(text) < self.min_input_len: return try: # Do tilde expansion. if self.expanduser: text = os.path.expanduser(text) # Directories where to look. dirname = os.path.dirname(text) if dirname: directories = [ os.path.dirname(os.path.join(p, text)) for p in self.get_paths() ] else: directories = self.get_paths() # Start of current file. prefix = os.path.basename(text) # Get all filenames. filenames = [] for directory in directories: # Look for matches in this directory. if os.path.isdir(directory): for filename in os.listdir(directory): if filename.startswith(prefix): filenames.append((directory, filename)) # Sort filenames = sorted(filenames, key=lambda k: k[1]) # Yield them. for directory, filename in filenames: completion = filename[len(prefix) :] full_name = os.path.join(directory, filename) if os.path.isdir(full_name): # For directories, add a slash to the filename. # (We don't add them to the `completion`. Users can type it # to trigger the autocompletion themselves.) filename += "/" elif self.only_directories: continue if not self.file_filter(full_name): continue yield Completion( text=completion, start_position=0, display=filename, ) except OSError: pass class ExecutableCompleter(PathCompleter): """ Complete only executable files in the current path. """ def __init__(self) -> None: super().__init__( only_directories=False, min_input_len=1, get_paths=lambda: os.environ.get("PATH", "").split(os.pathsep), file_filter=lambda name: os.access(name, os.X_OK), expanduser=True, ) ================================================ FILE: src/prompt_toolkit/completion/fuzzy_completer.py ================================================ from __future__ import annotations import re from collections.abc import Callable, Iterable, Sequence from typing import NamedTuple from prompt_toolkit.document import Document from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples from .base import CompleteEvent, Completer, Completion from .word_completer import WordCompleter __all__ = [ "FuzzyCompleter", "FuzzyWordCompleter", ] class FuzzyCompleter(Completer): """ Fuzzy completion. This wraps any other completer and turns it into a fuzzy completer. If the list of words is: ["leopard" , "gorilla", "dinosaur", "cat", "bee"] Then trying to complete "oar" would yield "leopard" and "dinosaur", but not the others, because they match the regular expression 'o.*a.*r'. Similar, in another application "djm" could expand to "django_migrations". The results are sorted by relevance, which is defined as the start position and the length of the match. Notice that this is not really a tool to work around spelling mistakes, like what would be possible with difflib. The purpose is rather to have a quicker or more intuitive way to filter the given completions, especially when many completions have a common prefix. Fuzzy algorithm is based on this post: https://blog.amjith.com/fuzzyfinder-in-10-lines-of-python :param completer: A :class:`~.Completer` instance. :param WORD: When True, use WORD characters. :param pattern: Regex pattern which selects the characters before the cursor that are considered for the fuzzy matching. :param enable_fuzzy: (bool or `Filter`) Enabled the fuzzy behavior. For easily turning fuzzyness on or off according to a certain condition. """ def __init__( self, completer: Completer, WORD: bool = False, pattern: str | None = None, enable_fuzzy: FilterOrBool = True, ) -> None: assert pattern is None or pattern.startswith("^") self.completer = completer self.pattern = pattern self.WORD = WORD self.pattern = pattern self.enable_fuzzy = to_filter(enable_fuzzy) def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: if self.enable_fuzzy(): return self._get_fuzzy_completions(document, complete_event) else: return self.completer.get_completions(document, complete_event) def _get_pattern(self) -> str: if self.pattern: return self.pattern if self.WORD: return r"[^\s]+" return "^[a-zA-Z0-9_]*" def _get_fuzzy_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: word_before_cursor = document.get_word_before_cursor( pattern=re.compile(self._get_pattern()) ) # Get completions document2 = Document( text=document.text[: document.cursor_position - len(word_before_cursor)], cursor_position=document.cursor_position - len(word_before_cursor), ) inner_completions = list( self.completer.get_completions(document2, complete_event) ) fuzzy_matches: list[_FuzzyMatch] = [] if word_before_cursor == "": # If word before the cursor is an empty string, consider all # completions, without filtering everything with an empty regex # pattern. fuzzy_matches = [_FuzzyMatch(0, 0, compl) for compl in inner_completions] else: pat = ".*?".join(map(re.escape, word_before_cursor)) pat = f"(?=({pat}))" # lookahead regex to manage overlapping matches regex = re.compile(pat, re.IGNORECASE) for compl in inner_completions: matches = list(regex.finditer(compl.text)) if matches: # Prefer the match, closest to the left, then shortest. best = min(matches, key=lambda m: (m.start(), len(m.group(1)))) fuzzy_matches.append( _FuzzyMatch(len(best.group(1)), best.start(), compl) ) def sort_key(fuzzy_match: _FuzzyMatch) -> tuple[int, int]: "Sort by start position, then by the length of the match." return fuzzy_match.start_pos, fuzzy_match.match_length fuzzy_matches = sorted(fuzzy_matches, key=sort_key) for match in fuzzy_matches: # Include these completions, but set the correct `display` # attribute and `start_position`. yield Completion( text=match.completion.text, start_position=match.completion.start_position - len(word_before_cursor), # We access to private `_display_meta` attribute, because that one is lazy. display_meta=match.completion._display_meta, display=self._get_display(match, word_before_cursor), style=match.completion.style, ) def _get_display( self, fuzzy_match: _FuzzyMatch, word_before_cursor: str ) -> AnyFormattedText: """ Generate formatted text for the display label. """ def get_display() -> AnyFormattedText: m = fuzzy_match word = m.completion.text if m.match_length == 0: # No highlighting when we have zero length matches (no input text). # In this case, use the original display text (which can include # additional styling or characters). return m.completion.display result: StyleAndTextTuples = [] # Text before match. result.append(("class:fuzzymatch.outside", word[: m.start_pos])) # The match itself. characters = list(word_before_cursor) for c in word[m.start_pos : m.start_pos + m.match_length]: classname = "class:fuzzymatch.inside" if characters and c.lower() == characters[0].lower(): classname += ".character" del characters[0] result.append((classname, c)) # Text after match. result.append( ("class:fuzzymatch.outside", word[m.start_pos + m.match_length :]) ) return result return get_display() class FuzzyWordCompleter(Completer): """ Fuzzy completion on a list of words. (This is basically a `WordCompleter` wrapped in a `FuzzyCompleter`.) :param words: List of words or callable that returns a list of words. :param meta_dict: Optional dict mapping words to their meta-information. :param WORD: When True, use WORD characters. """ def __init__( self, words: Sequence[str] | Callable[[], Sequence[str]], meta_dict: dict[str, str] | None = None, WORD: bool = False, ) -> None: self.words = words self.meta_dict = meta_dict or {} self.WORD = WORD self.word_completer = WordCompleter( words=self.words, WORD=self.WORD, meta_dict=self.meta_dict ) self.fuzzy_completer = FuzzyCompleter(self.word_completer, WORD=self.WORD) def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: return self.fuzzy_completer.get_completions(document, complete_event) class _FuzzyMatch(NamedTuple): match_length: int start_pos: int completion: Completion ================================================ FILE: src/prompt_toolkit/completion/nested.py ================================================ """ Nestedcompleter for completion of hierarchical data structures. """ from __future__ import annotations from collections.abc import Iterable, Mapping from typing import Any from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.completion.word_completer import WordCompleter from prompt_toolkit.document import Document __all__ = ["NestedCompleter"] # NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] NestedDict = Mapping[str, Any | set[str] | None | Completer] class NestedCompleter(Completer): """ Completer which wraps around several other completers, and calls any the one that corresponds with the first word of the input. By combining multiple `NestedCompleter` instances, we can achieve multiple hierarchical levels of autocompletion. This is useful when `WordCompleter` is not sufficient. If you need multiple levels, check out the `from_nested_dict` classmethod. """ def __init__( self, options: dict[str, Completer | None], ignore_case: bool = True ) -> None: self.options = options self.ignore_case = ignore_case def __repr__(self) -> str: return f"NestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})" @classmethod def from_nested_dict(cls, data: NestedDict) -> NestedCompleter: """ Create a `NestedCompleter`, starting from a nested dictionary data structure, like this: .. code:: data = { 'show': { 'version': None, 'interfaces': None, 'clock': None, 'ip': {'interface': {'brief'}} }, 'exit': None 'enable': None } The value should be `None` if there is no further completion at some point. If all values in the dictionary are None, it is also possible to use a set instead. Values in this data structure can be a completers as well. """ options: dict[str, Completer | None] = {} for key, value in data.items(): if isinstance(value, Completer): options[key] = value elif isinstance(value, dict): options[key] = cls.from_nested_dict(value) elif isinstance(value, set): options[key] = cls.from_nested_dict(dict.fromkeys(value)) else: assert value is None options[key] = None return cls(options) def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: # Split document. text = document.text_before_cursor.lstrip() stripped_len = len(document.text_before_cursor) - len(text) # If there is a space, check for the first term, and use a # subcompleter. if " " in text: first_term = text.split()[0] completer = self.options.get(first_term) # If we have a sub completer, use this for the completions. if completer is not None: remaining_text = text[len(first_term) :].lstrip() move_cursor = len(text) - len(remaining_text) + stripped_len new_document = Document( remaining_text, cursor_position=document.cursor_position - move_cursor, ) yield from completer.get_completions(new_document, complete_event) # No space in the input: behave exactly like `WordCompleter`. else: completer = WordCompleter( list(self.options.keys()), ignore_case=self.ignore_case ) yield from completer.get_completions(document, complete_event) ================================================ FILE: src/prompt_toolkit/completion/word_completer.py ================================================ from __future__ import annotations from collections.abc import Callable, Iterable, Mapping, Sequence from re import Pattern from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import AnyFormattedText __all__ = [ "WordCompleter", ] class WordCompleter(Completer): """ Simple autocompletion on a list of words. :param words: List of words or callable that returns a list of words. :param ignore_case: If True, case-insensitive completion. :param meta_dict: Optional dict mapping words to their meta-text. (This should map strings to strings or formatted text.) :param WORD: When True, use WORD characters. :param sentence: When True, don't complete by comparing the word before the cursor, but by comparing all the text before the cursor. In this case, the list of words is just a list of strings, where each string can contain spaces. (Can not be used together with the WORD option.) :param match_middle: When True, match not only the start, but also in the middle of the word. :param pattern: Optional compiled regex for finding the word before the cursor to complete. When given, use this regex pattern instead of default one (see document._FIND_WORD_RE) """ def __init__( self, words: Sequence[str] | Callable[[], Sequence[str]], ignore_case: bool = False, display_dict: Mapping[str, AnyFormattedText] | None = None, meta_dict: Mapping[str, AnyFormattedText] | None = None, WORD: bool = False, sentence: bool = False, match_middle: bool = False, pattern: Pattern[str] | None = None, ) -> None: assert not (WORD and sentence) self.words = words self.ignore_case = ignore_case self.display_dict = display_dict or {} self.meta_dict = meta_dict or {} self.WORD = WORD self.sentence = sentence self.match_middle = match_middle self.pattern = pattern def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: # Get list of words. words = self.words if callable(words): words = words() # Get word/text before cursor. if self.sentence: word_before_cursor = document.text_before_cursor else: word_before_cursor = document.get_word_before_cursor( WORD=self.WORD, pattern=self.pattern ) if self.ignore_case: word_before_cursor = word_before_cursor.lower() def word_matches(word: str) -> bool: """True when the word before the cursor matches.""" if self.ignore_case: word = word.lower() if self.match_middle: return word_before_cursor in word else: return word.startswith(word_before_cursor) for a in words: if word_matches(a): display = self.display_dict.get(a, a) display_meta = self.meta_dict.get(a, "") yield Completion( text=a, start_position=-len(word_before_cursor), display=display, display_meta=display_meta, ) ================================================ FILE: src/prompt_toolkit/contrib/__init__.py ================================================ ================================================ FILE: src/prompt_toolkit/contrib/completers/__init__.py ================================================ from __future__ import annotations from .system import SystemCompleter __all__ = ["SystemCompleter"] ================================================ FILE: src/prompt_toolkit/contrib/completers/system.py ================================================ from __future__ import annotations from prompt_toolkit.completion.filesystem import ExecutableCompleter, PathCompleter from prompt_toolkit.contrib.regular_languages.compiler import compile from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter __all__ = [ "SystemCompleter", ] class SystemCompleter(GrammarCompleter): """ Completer for system commands. """ def __init__(self) -> None: # Compile grammar. g = compile( r""" # First we have an executable. (?P<executable>[^\s]+) # Ignore literals in between. ( \s+ ("[^"]*" | '[^']*' | [^'"]+ ) )* \s+ # Filename as parameters. ( (?P<filename>[^\s]+) | "(?P<double_quoted_filename>[^\s]+)" | '(?P<single_quoted_filename>[^\s]+)' ) """, escape_funcs={ "double_quoted_filename": (lambda string: string.replace('"', '\\"')), "single_quoted_filename": (lambda string: string.replace("'", "\\'")), }, unescape_funcs={ "double_quoted_filename": ( lambda string: string.replace('\\"', '"') ), # XXX: not entirely correct. "single_quoted_filename": (lambda string: string.replace("\\'", "'")), }, ) # Create GrammarCompleter super().__init__( g, { "executable": ExecutableCompleter(), "filename": PathCompleter(only_directories=False, expanduser=True), "double_quoted_filename": PathCompleter( only_directories=False, expanduser=True ), "single_quoted_filename": PathCompleter( only_directories=False, expanduser=True ), }, ) ================================================ FILE: src/prompt_toolkit/contrib/regular_languages/__init__.py ================================================ r""" Tool for expressing the grammar of an input as a regular language. ================================================================== The grammar for the input of many simple command line interfaces can be expressed by a regular language. Examples are PDB (the Python debugger); a simple (bash-like) shell with "pwd", "cd", "cat" and "ls" commands; arguments that you can pass to an executable; etc. It is possible to use regular expressions for validation and parsing of such a grammar. (More about regular languages: http://en.wikipedia.org/wiki/Regular_language) Example ------- Let's take the pwd/cd/cat/ls example. We want to have a shell that accepts these three commands. "cd" is followed by a quoted directory name and "cat" is followed by a quoted file name. (We allow quotes inside the filename when they're escaped with a backslash.) We could define the grammar using the following regular expression:: grammar = \s* ( pwd | ls | (cd \s+ " ([^"]|\.)+ ") | (cat \s+ " ([^"]|\.)+ ") ) \s* What can we do with this grammar? --------------------------------- - Syntax highlighting: We could use this for instance to give file names different color. - Parse the result: .. We can extract the file names and commands by using a regular expression with named groups. - Input validation: .. Don't accept anything that does not match this grammar. When combined with a parser, we can also recursively do filename validation (and accept only existing files.) - Autocompletion: .... Each part of the grammar can have its own autocompleter. "cat" has to be completed using file names, while "cd" has to be completed using directory names. How does it work? ----------------- As a user of this library, you have to define the grammar of the input as a regular expression. The parts of this grammar where autocompletion, validation or any other processing is required need to be marked using a regex named group. Like ``(?P<varname>...)`` for instance. When the input is processed for validation (for instance), the regex will execute, the named group is captured, and the validator associated with this named group will test the captured string. There is one tricky bit: Often we operate on incomplete input (this is by definition the case for autocompletion) and we have to decide for the cursor position in which possible state the grammar it could be and in which way variables could be matched up to that point. To solve this problem, the compiler takes the original regular expression and translates it into a set of other regular expressions which each match certain prefixes of the original regular expression. We generate one prefix regular expression for every named variable (with this variable being the end of that expression). TODO: some examples of: - How to create a highlighter from this grammar. - How to create a validator from this grammar. - How to create an autocompleter from this grammar. - How to create a parser from this grammar. """ from __future__ import annotations from .compiler import compile __all__ = ["compile"] ================================================ FILE: src/prompt_toolkit/contrib/regular_languages/compiler.py ================================================ r""" Compiler for a regular grammar. Example usage:: # Create and compile grammar. p = compile('add \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)') # Match input string. m = p.match('add 23 432') # Get variables. m.variables().get('var1') # Returns "23" m.variables().get('var2') # Returns "432" Partial matches are possible:: # Create and compile grammar. p = compile(''' # Operators with two arguments. ((?P<operator1>[^\s]+) \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)) | # Operators with only one arguments. ((?P<operator2>[^\s]+) \s+ (?P<var1>[^\s]+)) ''') # Match partial input string. m = p.match_prefix('add 23') # Get variables. (Notice that both operator1 and operator2 contain the # value "add".) This is because our input is incomplete, and we don't know # yet in which rule of the regex we we'll end up. It could also be that # `operator1` and `operator2` have a different autocompleter and we want to # call all possible autocompleters that would result in valid input.) m.variables().get('var1') # Returns "23" m.variables().get('operator1') # Returns "add" m.variables().get('operator2') # Returns "add" """ from __future__ import annotations import re from collections.abc import Callable, Iterable, Iterator from re import Match as RegexMatch from re import Pattern from typing import TypeVar, overload from .regex_parser import ( AnyNode, Lookahead, Node, NodeSequence, Regex, Repeat, Variable, parse_regex, tokenize_regex, ) __all__ = ["compile", "Match", "Variables"] # Name of the named group in the regex, matching trailing input. # (Trailing input is when the input contains characters after the end of the # expression has been matched.) _INVALID_TRAILING_INPUT = "invalid_trailing" EscapeFuncDict = dict[str, Callable[[str], str]] class _CompiledGrammar: """ Compiles a grammar. This will take the parse tree of a regular expression and compile the grammar. :param root_node: :class~`.regex_parser.Node` instance. :param escape_funcs: `dict` mapping variable names to escape callables. :param unescape_funcs: `dict` mapping variable names to unescape callables. """ def __init__( self, root_node: Node, escape_funcs: EscapeFuncDict | None = None, unescape_funcs: EscapeFuncDict | None = None, ) -> None: self.root_node = root_node self.escape_funcs = escape_funcs or {} self.unescape_funcs = unescape_funcs or {} #: Dictionary that will map the regex names to Node instances. self._group_names_to_nodes: dict[ str, str ] = {} # Maps regex group names to varnames. counter = [0] def create_group_func(node: Variable) -> str: name = f"n{counter[0]}" self._group_names_to_nodes[name] = node.varname counter[0] += 1 return name # Compile regex strings. self._re_pattern = f"^{self._transform(root_node, create_group_func)}$" self._re_prefix_patterns = list( self._transform_prefix(root_node, create_group_func) ) # Compile the regex itself. flags = re.DOTALL # Note that we don't need re.MULTILINE! (^ and $ # still represent the start and end of input text.) self._re = re.compile(self._re_pattern, flags) self._re_prefix = [re.compile(t, flags) for t in self._re_prefix_patterns] # We compile one more set of regexes, similar to `_re_prefix`, but accept any trailing # input. This will ensure that we can still highlight the input correctly, even when the # input contains some additional characters at the end that don't match the grammar.) self._re_prefix_with_trailing_input = [ re.compile( r"(?:{})(?P<{}>.*?)$".format(t.rstrip("$"), _INVALID_TRAILING_INPUT), flags, ) for t in self._re_prefix_patterns ] def escape(self, varname: str, value: str) -> str: """ Escape `value` to fit in the place of this variable into the grammar. """ f = self.escape_funcs.get(varname) return f(value) if f else value def unescape(self, varname: str, value: str) -> str: """ Unescape `value`. """ f = self.unescape_funcs.get(varname) return f(value) if f else value @classmethod def _transform( cls, root_node: Node, create_group_func: Callable[[Variable], str] ) -> str: """ Turn a :class:`Node` object into a regular expression. :param root_node: The :class:`Node` instance for which we generate the grammar. :param create_group_func: A callable which takes a `Node` and returns the next free name for this node. """ def transform(node: Node) -> str: # Turn `AnyNode` into an OR. if isinstance(node, AnyNode): return "(?:{})".format("|".join(transform(c) for c in node.children)) # Concatenate a `NodeSequence` elif isinstance(node, NodeSequence): return "".join(transform(c) for c in node.children) # For Regex and Lookahead nodes, just insert them literally. elif isinstance(node, Regex): return node.regex elif isinstance(node, Lookahead): before = "(?!" if node.negative else "(=" return before + transform(node.childnode) + ")" # A `Variable` wraps the children into a named group. elif isinstance(node, Variable): return f"(?P<{create_group_func(node)}>{transform(node.childnode)})" # `Repeat`. elif isinstance(node, Repeat): if node.max_repeat is None: if node.min_repeat == 0: repeat_sign = "*" elif node.min_repeat == 1: repeat_sign = "+" else: repeat_sign = "{%i,%s}" % ( node.min_repeat, ("" if node.max_repeat is None else str(node.max_repeat)), ) return "(?:{}){}{}".format( transform(node.childnode), repeat_sign, ("" if node.greedy else "?"), ) else: raise TypeError(f"Got {node!r}") return transform(root_node) @classmethod def _transform_prefix( cls, root_node: Node, create_group_func: Callable[[Variable], str] ) -> Iterable[str]: """ Yield all the regular expressions matching a prefix of the grammar defined by the `Node` instance. For each `Variable`, one regex pattern will be generated, with this named group at the end. This is required because a regex engine will terminate once a match is found. For autocompletion however, we need the matches for all possible paths, so that we can provide completions for each `Variable`. - So, in the case of an `Any` (`A|B|C)', we generate a pattern for each clause. This is one for `A`, one for `B` and one for `C`. Unless some groups don't contain a `Variable`, then these can be merged together. - In the case of a `NodeSequence` (`ABC`), we generate a pattern for each prefix that ends with a variable, and one pattern for the whole sequence. So, that's one for `A`, one for `AB` and one for `ABC`. :param root_node: The :class:`Node` instance for which we generate the grammar. :param create_group_func: A callable which takes a `Node` and returns the next free name for this node. """ def contains_variable(node: Node) -> bool: if isinstance(node, Regex): return False elif isinstance(node, Variable): return True elif isinstance(node, (Lookahead, Repeat)): return contains_variable(node.childnode) elif isinstance(node, (NodeSequence, AnyNode)): return any(contains_variable(child) for child in node.children) return False def transform(node: Node) -> Iterable[str]: # Generate separate pattern for all terms that contain variables # within this OR. Terms that don't contain a variable can be merged # together in one pattern. if isinstance(node, AnyNode): # If we have a definition like: # (?P<name> .*) | (?P<city> .*) # Then we want to be able to generate completions for both the # name as well as the city. We do this by yielding two # different regular expressions, because the engine won't # follow multiple paths, if multiple are possible. children_with_variable = [] children_without_variable = [] for c in node.children: if contains_variable(c): children_with_variable.append(c) else: children_without_variable.append(c) for c in children_with_variable: yield from transform(c) # Merge options without variable together. if children_without_variable: yield "|".join( r for c in children_without_variable for r in transform(c) ) # For a sequence, generate a pattern for each prefix that ends with # a variable + one pattern of the complete sequence. # (This is because, for autocompletion, we match the text before # the cursor, and completions are given for the variable that we # match right before the cursor.) elif isinstance(node, NodeSequence): # For all components in the sequence, compute prefix patterns, # as well as full patterns. complete = [cls._transform(c, create_group_func) for c in node.children] prefixes = [list(transform(c)) for c in node.children] variable_nodes = [contains_variable(c) for c in node.children] # If any child is contains a variable, we should yield a # pattern up to that point, so that we are sure this will be # matched. for i in range(len(node.children)): if variable_nodes[i]: for c_str in prefixes[i]: yield "".join(complete[:i]) + c_str # If there are non-variable nodes, merge all the prefixes into # one pattern. If the input is: "[part1] [part2] [part3]", then # this gets compiled into: # (complete1 + (complete2 + (complete3 | partial3) | partial2) | partial1 ) # For nodes that contain a variable, we skip the "|partial" # part here, because thees are matched with the previous # patterns. if not all(variable_nodes): result = [] # Start with complete patterns. for i in range(len(node.children)): result.append("(?:") result.append(complete[i]) # Add prefix patterns. for i in range(len(node.children) - 1, -1, -1): if variable_nodes[i]: # No need to yield a prefix for this one, we did # the variable prefixes earlier. result.append(")") else: result.append("|(?:") # If this yields multiple, we should yield all combinations. assert len(prefixes[i]) == 1 result.append(prefixes[i][0]) result.append("))") yield "".join(result) elif isinstance(node, Regex): yield f"(?:{node.regex})?" elif isinstance(node, Lookahead): if node.negative: yield f"(?!{cls._transform(node.childnode, create_group_func)})" else: # Not sure what the correct semantics are in this case. # (Probably it's not worth implementing this.) raise Exception("Positive lookahead not yet supported.") elif isinstance(node, Variable): # (Note that we should not append a '?' here. the 'transform' # method will already recursively do that.) for c_str in transform(node.childnode): yield f"(?P<{create_group_func(node)}>{c_str})" elif isinstance(node, Repeat): # If we have a repetition of 8 times. That would mean that the # current input could have for instance 7 times a complete # match, followed by a partial match. prefix = cls._transform(node.childnode, create_group_func) if node.max_repeat == 1: yield from transform(node.childnode) else: for c_str in transform(node.childnode): if node.max_repeat: repeat_sign = "{,%i}" % (node.max_repeat - 1) else: repeat_sign = "*" yield "(?:{}){}{}{}".format( prefix, repeat_sign, ("" if node.greedy else "?"), c_str, ) else: raise TypeError(f"Got {node!r}") for r in transform(root_node): yield f"^(?:{r})$" def match(self, string: str) -> Match | None: """ Match the string with the grammar. Returns a :class:`Match` instance or `None` when the input doesn't match the grammar. :param string: The input string. """ m = self._re.match(string) if m: return Match( string, [(self._re, m)], self._group_names_to_nodes, self.unescape_funcs ) return None def match_prefix(self, string: str) -> Match | None: """ Do a partial match of the string with the grammar. The returned :class:`Match` instance can contain multiple representations of the match. This will never return `None`. If it doesn't match at all, the "trailing input" part will capture all of the input. :param string: The input string. """ # First try to match using `_re_prefix`. If nothing is found, use the patterns that # also accept trailing characters. for patterns in [self._re_prefix, self._re_prefix_with_trailing_input]: matches = [(r, r.match(string)) for r in patterns] matches2 = [(r, m) for r, m in matches if m] if matches2 != []: return Match( string, matches2, self._group_names_to_nodes, self.unescape_funcs ) return None class Match: """ :param string: The input string. :param re_matches: List of (compiled_re_pattern, re_match) tuples. :param group_names_to_nodes: Dictionary mapping all the re group names to the matching Node instances. """ def __init__( self, string: str, re_matches: list[tuple[Pattern[str], RegexMatch[str]]], group_names_to_nodes: dict[str, str], unescape_funcs: dict[str, Callable[[str], str]], ): self.string = string self._re_matches = re_matches self._group_names_to_nodes = group_names_to_nodes self._unescape_funcs = unescape_funcs def _nodes_to_regs(self) -> list[tuple[str, tuple[int, int]]]: """ Return a list of (varname, reg) tuples. """ def get_tuples() -> Iterable[tuple[str, tuple[int, int]]]: for r, re_match in self._re_matches: for group_name, group_index in r.groupindex.items(): if group_name != _INVALID_TRAILING_INPUT: regs = re_match.regs reg = regs[group_index] node = self._group_names_to_nodes[group_name] yield (node, reg) return list(get_tuples()) def _nodes_to_values(self) -> list[tuple[str, str, tuple[int, int]]]: """ Returns list of (Node, string_value) tuples. """ def is_none(sl: tuple[int, int]) -> bool: return sl[0] == -1 and sl[1] == -1 def get(sl: tuple[int, int]) -> str: return self.string[sl[0] : sl[1]] return [ (varname, get(slice), slice) for varname, slice in self._nodes_to_regs() if not is_none(slice) ] def _unescape(self, varname: str, value: str) -> str: unwrapper = self._unescape_funcs.get(varname) return unwrapper(value) if unwrapper else value def variables(self) -> Variables: """ Returns :class:`Variables` instance. """ return Variables( [(k, self._unescape(k, v), sl) for k, v, sl in self._nodes_to_values()] ) def trailing_input(self) -> MatchVariable | None: """ Get the `MatchVariable` instance, representing trailing input, if there is any. "Trailing input" is input at the end that does not match the grammar anymore, but when this is removed from the end of the input, the input would be a valid string. """ slices: list[tuple[int, int]] = [] # Find all regex group for the name _INVALID_TRAILING_INPUT. for r, re_match in self._re_matches: for group_name, group_index in r.groupindex.items(): if group_name == _INVALID_TRAILING_INPUT: slices.append(re_match.regs[group_index]) # Take the smallest part. (Smaller trailing text means that a larger input has # been matched, so that is better.) if slices: slice = (max(i[0] for i in slices), max(i[1] for i in slices)) value = self.string[slice[0] : slice[1]] return MatchVariable("<trailing_input>", value, slice) return None def end_nodes(self) -> Iterable[MatchVariable]: """ Yields `MatchVariable` instances for all the nodes having their end position at the end of the input string. """ for varname, reg in self._nodes_to_regs(): # If this part goes until the end of the input string. if reg[1] == len(self.string): value = self._unescape(varname, self.string[reg[0] : reg[1]]) yield MatchVariable(varname, value, (reg[0], reg[1])) _T = TypeVar("_T") class Variables: def __init__(self, tuples: list[tuple[str, str, tuple[int, int]]]) -> None: #: List of (varname, value, slice) tuples. self._tuples = tuples def __repr__(self) -> str: return "{}({})".format( self.__class__.__name__, ", ".join(f"{k}={v!r}" for k, v, _ in self._tuples), ) @overload def get(self, key: str) -> str | None: ... @overload def get(self, key: str, default: str | _T) -> str | _T: ... def get(self, key: str, default: str | _T | None = None) -> str | _T | None: items = self.getall(key) return items[0] if items else default def getall(self, key: str) -> list[str]: return [v for k, v, _ in self._tuples if k == key] def __getitem__(self, key: str) -> str | None: return self.get(key) def __iter__(self) -> Iterator[MatchVariable]: """ Yield `MatchVariable` instances. """ for varname, value, slice in self._tuples: yield MatchVariable(varname, value, slice) class MatchVariable: """ Represents a match of a variable in the grammar. :param varname: (string) Name of the variable. :param value: (string) Value of this variable. :param slice: (start, stop) tuple, indicating the position of this variable in the input string. """ def __init__(self, varname: str, value: str, slice: tuple[int, int]) -> None: self.varname = varname self.value = value self.slice = slice self.start = self.slice[0] self.stop = self.slice[1] def __repr__(self) -> str: return f"{self.__class__.__name__}({self.varname!r}, {self.value!r})" def compile( expression: str, escape_funcs: EscapeFuncDict | None = None, unescape_funcs: EscapeFuncDict | None = None, ) -> _CompiledGrammar: """ Compile grammar (given as regex string), returning a `CompiledGrammar` instance. """ return _compile_from_parse_tree( parse_regex(tokenize_regex(expression)), escape_funcs=escape_funcs, unescape_funcs=unescape_funcs, ) def _compile_from_parse_tree( root_node: Node, escape_funcs: EscapeFuncDict | None = None, unescape_funcs: EscapeFuncDict | None = None, ) -> _CompiledGrammar: """ Compile grammar (given as parse tree), returning a `CompiledGrammar` instance. """ return _CompiledGrammar( root_node, escape_funcs=escape_funcs, unescape_funcs=unescape_funcs ) ================================================ FILE: src/prompt_toolkit/contrib/regular_languages/completion.py ================================================ """ Completer for a regular grammar. """ from __future__ import annotations from collections.abc import Iterable from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.document import Document from .compiler import Match, _CompiledGrammar __all__ = [ "GrammarCompleter", ] class GrammarCompleter(Completer): """ Completer which can be used for autocompletion according to variables in the grammar. Each variable can have a different autocompleter. :param compiled_grammar: `GrammarCompleter` instance. :param completers: `dict` mapping variable names of the grammar to the `Completer` instances to be used for each variable. """ def __init__( self, compiled_grammar: _CompiledGrammar, completers: dict[str, Completer] ) -> None: self.compiled_grammar = compiled_grammar self.completers = completers def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: m = self.compiled_grammar.match_prefix(document.text_before_cursor) if m: yield from self._remove_duplicates( self._get_completions_for_match(m, complete_event) ) def _get_completions_for_match( self, match: Match, complete_event: CompleteEvent ) -> Iterable[Completion]: """ Yield all the possible completions for this input string. (The completer assumes that the cursor position was at the end of the input string.) """ for match_variable in match.end_nodes(): varname = match_variable.varname start = match_variable.start completer = self.completers.get(varname) if completer: text = match_variable.value # Unwrap text. unwrapped_text = self.compiled_grammar.unescape(varname, text) # Create a document, for the completions API (text/cursor_position) document = Document(unwrapped_text, len(unwrapped_text)) # Call completer for completion in completer.get_completions(document, complete_event): new_text = ( unwrapped_text[: len(text) + completion.start_position] + completion.text ) # Wrap again. yield Completion( text=self.compiled_grammar.escape(varname, new_text), start_position=start - len(match.string), display=completion.display, display_meta=completion.display_meta, ) def _remove_duplicates(self, items: Iterable[Completion]) -> Iterable[Completion]: """ Remove duplicates, while keeping the order. (Sometimes we have duplicates, because the there several matches of the same grammar, each yielding similar completions.) """ def hash_completion(completion: Completion) -> tuple[str, int]: return completion.text, completion.start_position yielded_so_far: set[tuple[str, int]] = set() for completion in items: hash_value = hash_completion(completion) if hash_value not in yielded_so_far: yielded_so_far.add(hash_value) yield completion ================================================ FILE: src/prompt_toolkit/contrib/regular_languages/lexer.py ================================================ """ `GrammarLexer` is compatible with other lexers and can be used to highlight the input using a regular grammar with annotations. """ from __future__ import annotations from collections.abc import Callable from prompt_toolkit.document import Document from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.formatted_text.utils import split_lines from prompt_toolkit.lexers import Lexer from .compiler import _CompiledGrammar __all__ = [ "GrammarLexer", ] class GrammarLexer(Lexer): """ Lexer which can be used for highlighting of fragments according to variables in the grammar. (It does not actual lexing of the string, but it exposes an API, compatible with the Pygments lexer class.) :param compiled_grammar: Grammar as returned by the `compile()` function. :param lexers: Dictionary mapping variable names of the regular grammar to the lexers that should be used for this part. (This can call other lexers recursively.) If you wish a part of the grammar to just get one fragment, use a `prompt_toolkit.lexers.SimpleLexer`. """ def __init__( self, compiled_grammar: _CompiledGrammar, default_style: str = "", lexers: dict[str, Lexer] | None = None, ) -> None: self.compiled_grammar = compiled_grammar self.default_style = default_style self.lexers = lexers or {} def _get_text_fragments(self, text: str) -> StyleAndTextTuples: m = self.compiled_grammar.match_prefix(text) if m: characters: StyleAndTextTuples = [(self.default_style, c) for c in text] for v in m.variables(): # If we have a `Lexer` instance for this part of the input. # Tokenize recursively and apply tokens. lexer = self.lexers.get(v.varname) if lexer: document = Document(text[v.start : v.stop]) lexer_tokens_for_line = lexer.lex_document(document) text_fragments: StyleAndTextTuples = [] for i in range(len(document.lines)): text_fragments.extend(lexer_tokens_for_line(i)) text_fragments.append(("", "\n")) if text_fragments: text_fragments.pop() i = v.start for t, s, *_ in text_fragments: for c in s: if characters[i][0] == self.default_style: characters[i] = (t, characters[i][1]) i += 1 # Highlight trailing input. trailing_input = m.trailing_input() if trailing_input: for i in range(trailing_input.start, trailing_input.stop): characters[i] = ("class:trailing-input", characters[i][1]) return characters else: return [("", text)] def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: lines = list(split_lines(self._get_text_fragments(document.text))) def get_line(lineno: int) -> StyleAndTextTuples: try: return lines[lineno] except IndexError: return [] return get_line ================================================ FILE: src/prompt_toolkit/contrib/regular_languages/regex_parser.py ================================================ """ Parser for parsing a regular expression. Take a string representing a regular expression and return the root node of its parse tree. usage:: root_node = parse_regex('(hello|world)') Remarks: - The regex parser processes multiline, it ignores all whitespace and supports multiple named groups with the same name and #-style comments. Limitations: - Lookahead is not supported. """ from __future__ import annotations import re __all__ = [ "Repeat", "Variable", "Regex", "Lookahead", "tokenize_regex", "parse_regex", ] class Node: """ Base class for all the grammar nodes. (You don't initialize this one.) """ def __add__(self, other_node: Node) -> NodeSequence: return NodeSequence([self, other_node]) def __or__(self, other_node: Node) -> AnyNode: return AnyNode([self, other_node]) class AnyNode(Node): """ Union operation (OR operation) between several grammars. You don't initialize this yourself, but it's a result of a "Grammar1 | Grammar2" operation. """ def __init__(self, children: list[Node]) -> None: self.children = children def __or__(self, other_node: Node) -> AnyNode: return AnyNode(self.children + [other_node]) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.children!r})" class NodeSequence(Node): """ Concatenation operation of several grammars. You don't initialize this yourself, but it's a result of a "Grammar1 + Grammar2" operation. """ def __init__(self, children: list[Node]) -> None: self.children = children def __add__(self, other_node: Node) -> NodeSequence: return NodeSequence(self.children + [other_node]) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.children!r})" class Regex(Node): """ Regular expression. """ def __init__(self, regex: str) -> None: re.compile(regex) # Validate self.regex = regex def __repr__(self) -> str: return f"{self.__class__.__name__}(/{self.regex}/)" class Lookahead(Node): """ Lookahead expression. """ def __init__(self, childnode: Node, negative: bool = False) -> None: self.childnode = childnode self.negative = negative def __repr__(self) -> str: return f"{self.__class__.__name__}({self.childnode!r})" class Variable(Node): """ Mark a variable in the regular grammar. This will be translated into a named group. Each variable can have his own completer, validator, etc.. :param childnode: The grammar which is wrapped inside this variable. :param varname: String. """ def __init__(self, childnode: Node, varname: str = "") -> None: self.childnode = childnode self.varname = varname def __repr__(self) -> str: return f"{self.__class__.__name__}(childnode={self.childnode!r}, varname={self.varname!r})" class Repeat(Node): def __init__( self, childnode: Node, min_repeat: int = 0, max_repeat: int | None = None, greedy: bool = True, ) -> None: self.childnode = childnode self.min_repeat = min_repeat self.max_repeat = max_repeat self.greedy = greedy def __repr__(self) -> str: return f"{self.__class__.__name__}(childnode={self.childnode!r})" def tokenize_regex(input: str) -> list[str]: """ Takes a string, representing a regular expression as input, and tokenizes it. :param input: string, representing a regular expression. :returns: List of tokens. """ # Regular expression for tokenizing other regular expressions. p = re.compile( r"""^( \(\?P\<[a-zA-Z0-9_-]+\> | # Start of named group. \(\?#[^)]*\) | # Comment \(\?= | # Start of lookahead assertion \(\?! | # Start of negative lookahead assertion \(\?<= | # If preceded by. \(\?< | # If not preceded by. \(?: | # Start of group. (non capturing.) \( | # Start of group. \(?[iLmsux] | # Flags. \(?P=[a-zA-Z]+\) | # Back reference to named group \) | # End of group. \{[^{}]*\} | # Repetition \*\? | \+\? | \?\?\ | # Non greedy repetition. \* | \+ | \? | # Repetition \#.*\n | # Comment \\. | # Character group. \[ ( [^\]\\] | \\.)* \] | [^(){}] | . )""", re.VERBOSE, ) tokens = [] while input: m = p.match(input) if m: token, input = input[: m.end()], input[m.end() :] if not token.isspace(): tokens.append(token) else: raise Exception("Could not tokenize input regex.") return tokens def parse_regex(regex_tokens: list[str]) -> Node: """ Takes a list of tokens from the tokenizer, and returns a parse tree. """ # We add a closing brace because that represents the final pop of the stack. tokens: list[str] = [")"] + regex_tokens[::-1] def wrap(lst: list[Node]) -> Node: """Turn list into sequence when it contains several items.""" if len(lst) == 1: return lst[0] else: return NodeSequence(lst) def _parse() -> Node: or_list: list[list[Node]] = [] result: list[Node] = [] def wrapped_result() -> Node: if or_list == []: return wrap(result) else: or_list.append(result) return AnyNode([wrap(i) for i in or_list]) while tokens: t = tokens.pop() if t.startswith("(?P<"): variable = Variable(_parse(), varname=t[4:-1]) result.append(variable) elif t in ("*", "*?"): greedy = t == "*" result[-1] = Repeat(result[-1], greedy=greedy) elif t in ("+", "+?"): greedy = t == "+" result[-1] = Repeat(result[-1], min_repeat=1, greedy=greedy) elif t in ("?", "??"): if result == []: raise Exception("Nothing to repeat." + repr(tokens)) else: greedy = t == "?" result[-1] = Repeat( result[-1], min_repeat=0, max_repeat=1, greedy=greedy ) elif t == "|": or_list.append(result) result = [] elif t in ("(", "(?:"): result.append(_parse()) elif t == "(?!": result.append(Lookahead(_parse(), negative=True)) elif t == "(?=": result.append(Lookahead(_parse(), negative=False)) elif t == ")": return wrapped_result() elif t.startswith("#"): pass elif t.startswith("{"): # TODO: implement! raise Exception(f"{t}-style repetition not yet supported") elif t.startswith("(?"): raise Exception(f"{t!r} not supported") elif t.isspace(): pass else: result.append(Regex(t)) raise Exception("Expecting ')' token") result = _parse() if len(tokens) != 0: raise Exception("Unmatched parentheses.") else: return result ================================================ FILE: src/prompt_toolkit/contrib/regular_languages/validation.py ================================================ """ Validator for a regular language. """ from __future__ import annotations from prompt_toolkit.document import Document from prompt_toolkit.validation import ValidationError, Validator from .compiler import _CompiledGrammar __all__ = [ "GrammarValidator", ] class GrammarValidator(Validator): """ Validator which can be used for validation according to variables in the grammar. Each variable can have its own validator. :param compiled_grammar: `GrammarCompleter` instance. :param validators: `dict` mapping variable names of the grammar to the `Validator` instances to be used for each variable. """ def __init__( self, compiled_grammar: _CompiledGrammar, validators: dict[str, Validator] ) -> None: self.compiled_grammar = compiled_grammar self.validators = validators def validate(self, document: Document) -> None: # Parse input document. # We use `match`, not `match_prefix`, because for validation, we want # the actual, unambiguous interpretation of the input. m = self.compiled_grammar.match(document.text) if m: for v in m.variables(): validator = self.validators.get(v.varname) if validator: # Unescape text. unwrapped_text = self.compiled_grammar.unescape(v.varname, v.value) # Create a document, for the completions API (text/cursor_position) inner_document = Document(unwrapped_text, len(unwrapped_text)) try: validator.validate(inner_document) except ValidationError as e: raise ValidationError( cursor_position=v.start + e.cursor_position, message=e.message, ) from e else: raise ValidationError( cursor_position=len(document.text), message="Invalid command" ) ================================================ FILE: src/prompt_toolkit/contrib/ssh/__init__.py ================================================ from __future__ import annotations from .server import PromptToolkitSSHServer, PromptToolkitSSHSession __all__ = [ "PromptToolkitSSHSession", "PromptToolkitSSHServer", ] ================================================ FILE: src/prompt_toolkit/contrib/ssh/server.py ================================================ """ Utility for running a prompt_toolkit application in an asyncssh server. """ from __future__ import annotations import asyncio import traceback from asyncio import get_running_loop from collections.abc import Callable, Coroutine from typing import Any, TextIO, cast import asyncssh from prompt_toolkit.application.current import AppSession, create_app_session from prompt_toolkit.data_structures import Size from prompt_toolkit.input import PipeInput, create_pipe_input from prompt_toolkit.output.vt100 import Vt100_Output __all__ = ["PromptToolkitSSHSession", "PromptToolkitSSHServer"] class PromptToolkitSSHSession(asyncssh.SSHServerSession): # type: ignore def __init__( self, interact: Callable[[PromptToolkitSSHSession], Coroutine[Any, Any, None]], *, enable_cpr: bool, ) -> None: self.interact = interact self.enable_cpr = enable_cpr self.interact_task: asyncio.Task[None] | None = None self._chan: Any | None = None self.app_session: AppSession | None = None # PipInput object, for sending input in the CLI. # (This is something that we can use in the prompt_toolkit event loop, # but still write date in manually.) self._input: PipeInput | None = None self._output: Vt100_Output | None = None # Output object. Don't render to the real stdout, but write everything # in the SSH channel. class Stdout: def write(s, data: str) -> None: try: if self._chan is not None: self._chan.write(data.replace("\n", "\r\n")) except BrokenPipeError: pass # Channel not open for sending. def isatty(s) -> bool: return True def flush(s) -> None: pass @property def encoding(s) -> str: assert self._chan is not None return str(self._chan._orig_chan.get_encoding()[0]) self.stdout = cast(TextIO, Stdout()) def _get_size(self) -> Size: """ Callable that returns the current `Size`, required by Vt100_Output. """ if self._chan is None: return Size(rows=20, columns=79) else: width, height, pixwidth, pixheight = self._chan.get_terminal_size() return Size(rows=height, columns=width) def connection_made(self, chan: Any) -> None: self._chan = chan def shell_requested(self) -> bool: return True def session_started(self) -> None: self.interact_task = get_running_loop().create_task(self._interact()) async def _interact(self) -> None: if self._chan is None: # Should not happen. raise Exception("`_interact` called before `connection_made`.") if hasattr(self._chan, "set_line_mode") and self._chan._editor is not None: # Disable the line editing provided by asyncssh. Prompt_toolkit # provides the line editing. self._chan.set_line_mode(False) term = self._chan.get_terminal_type() self._output = Vt100_Output( self.stdout, self._get_size, term=term, enable_cpr=self.enable_cpr ) with create_pipe_input() as self._input: with create_app_session(input=self._input, output=self._output) as session: self.app_session = session try: await self.interact(self) except BaseException: traceback.print_exc() finally: # Close the connection. self._chan.close() self._input.close() def terminal_size_changed( self, width: int, height: int, pixwidth: object, pixheight: object ) -> None: # Send resize event to the current application. if self.app_session and self.app_session.app: self.app_session.app._on_resize() def data_received(self, data: str, datatype: object) -> None: if self._input is None: # Should not happen. return self._input.send_text(data) class PromptToolkitSSHServer(asyncssh.SSHServer): """ Run a prompt_toolkit application over an asyncssh server. This takes one argument, an `interact` function, which is called for each connection. This should be an asynchronous function that runs the prompt_toolkit applications. This function runs in an `AppSession`, which means that we can have multiple UI interactions concurrently. Example usage: .. code:: python async def interact(ssh_session: PromptToolkitSSHSession) -> None: await yes_no_dialog("my title", "my text").run_async() prompt_session = PromptSession() text = await prompt_session.prompt_async("Type something: ") print_formatted_text('You said: ', text) server = PromptToolkitSSHServer(interact=interact) loop = get_running_loop() loop.run_until_complete( asyncssh.create_server( lambda: MySSHServer(interact), "", port, server_host_keys=["/etc/ssh/..."], ) ) loop.run_forever() :param enable_cpr: When `True`, the default, try to detect whether the SSH client runs in a terminal that responds to "cursor position requests". That way, we can properly determine how much space there is available for the UI (especially for drop down menus) to render. """ def __init__( self, interact: Callable[[PromptToolkitSSHSession], Coroutine[Any, Any, None]], *, enable_cpr: bool = True, ) -> None: self.interact = interact self.enable_cpr = enable_cpr def begin_auth(self, username: str) -> bool: # No authentication. return False def session_requested(self) -> PromptToolkitSSHSession: return PromptToolkitSSHSession(self.interact, enable_cpr=self.enable_cpr) ================================================ FILE: src/prompt_toolkit/contrib/telnet/__init__.py ================================================ from __future__ import annotations from .server import TelnetServer __all__ = [ "TelnetServer", ] ================================================ FILE: src/prompt_toolkit/contrib/telnet/log.py ================================================ """ Python logger for the telnet server. """ from __future__ import annotations import logging logger = logging.getLogger(__package__) __all__ = [ "logger", ] ================================================ FILE: src/prompt_toolkit/contrib/telnet/protocol.py ================================================ """ Parser for the Telnet protocol. (Not a complete implementation of the telnet specification, but sufficient for a command line interface.) Inspired by `Twisted.conch.telnet`. """ from __future__ import annotations import struct from collections.abc import Callable, Generator from .log import logger __all__ = [ "TelnetProtocolParser", ] def int2byte(number: int) -> bytes: return bytes((number,)) # Telnet constants. NOP = int2byte(0) SGA = int2byte(3) IAC = int2byte(255) DO = int2byte(253) DONT = int2byte(254) LINEMODE = int2byte(34) SB = int2byte(250) WILL = int2byte(251) WONT = int2byte(252) MODE = int2byte(1) SE = int2byte(240) ECHO = int2byte(1) NAWS = int2byte(31) LINEMODE = int2byte(34) SUPPRESS_GO_AHEAD = int2byte(3) TTYPE = int2byte(24) SEND = int2byte(1) IS = int2byte(0) DM = int2byte(242) BRK = int2byte(243) IP = int2byte(244) AO = int2byte(245) AYT = int2byte(246) EC = int2byte(247) EL = int2byte(248) GA = int2byte(249) class TelnetProtocolParser: """ Parser for the Telnet protocol. Usage:: def data_received(data): print(data) def size_received(rows, columns): print(rows, columns) p = TelnetProtocolParser(data_received, size_received) p.feed(binary_data) """ def __init__( self, data_received_callback: Callable[[bytes], None], size_received_callback: Callable[[int, int], None], ttype_received_callback: Callable[[str], None], ) -> None: self.data_received_callback = data_received_callback self.size_received_callback = size_received_callback self.ttype_received_callback = ttype_received_callback self._parser = self._parse_coroutine() self._parser.send(None) # type: ignore def received_data(self, data: bytes) -> None: self.data_received_callback(data) def do_received(self, data: bytes) -> None: """Received telnet DO command.""" logger.info("DO %r", data) def dont_received(self, data: bytes) -> None: """Received telnet DONT command.""" logger.info("DONT %r", data) def will_received(self, data: bytes) -> None: """Received telnet WILL command.""" logger.info("WILL %r", data) def wont_received(self, data: bytes) -> None: """Received telnet WONT command.""" logger.info("WONT %r", data) def command_received(self, command: bytes, data: bytes) -> None: if command == DO: self.do_received(data) elif command == DONT: self.dont_received(data) elif command == WILL: self.will_received(data) elif command == WONT: self.wont_received(data) else: logger.info("command received %r %r", command, data) def naws(self, data: bytes) -> None: """ Received NAWS. (Window dimensions.) """ if len(data) == 4: # NOTE: the first parameter of struct.unpack should be # a 'str' object. Both on Py2/py3. This crashes on OSX # otherwise. columns, rows = struct.unpack("!HH", data) self.size_received_callback(rows, columns) else: logger.warning("Wrong number of NAWS bytes") def ttype(self, data: bytes) -> None: """ Received terminal type. """ subcmd, data = data[0:1], data[1:] if subcmd == IS: ttype = data.decode("ascii") self.ttype_received_callback(ttype) else: logger.warning("Received a non-IS terminal type Subnegotiation") def negotiate(self, data: bytes) -> None: """ Got negotiate data. """ command, payload = data[0:1], data[1:] if command == NAWS: self.naws(payload) elif command == TTYPE: self.ttype(payload) else: logger.info("Negotiate (%r got bytes)", len(data)) def _parse_coroutine(self) -> Generator[None, bytes, None]: """ Parser state machine. Every 'yield' expression returns the next byte. """ while True: d = yield if d == int2byte(0): pass # NOP # Go to state escaped. elif d == IAC: d2 = yield if d2 == IAC: self.received_data(d2) # Handle simple commands. elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA): self.command_received(d2, b"") # Handle IAC-[DO/DONT/WILL/WONT] commands. elif d2 in (DO, DONT, WILL, WONT): d3 = yield self.command_received(d2, d3) # Subnegotiation elif d2 == SB: # Consume everything until next IAC-SE data = [] while True: d3 = yield if d3 == IAC: d4 = yield if d4 == SE: break else: data.append(d4) else: data.append(d3) self.negotiate(b"".join(data)) else: self.received_data(d) def feed(self, data: bytes) -> None: """ Feed data to the parser. """ for b in data: self._parser.send(int2byte(b)) ================================================ FILE: src/prompt_toolkit/contrib/telnet/server.py ================================================ """ Telnet server. """ from __future__ import annotations import asyncio import contextvars import socket from asyncio import get_running_loop from collections.abc import Callable, Coroutine from typing import Any, TextIO, cast from prompt_toolkit.application.current import create_app_session, get_app from prompt_toolkit.application.run_in_terminal import run_in_terminal from prompt_toolkit.data_structures import Size from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text from prompt_toolkit.input import PipeInput, create_pipe_input from prompt_toolkit.output.vt100 import Vt100_Output from prompt_toolkit.renderer import print_formatted_text as print_formatted_text from prompt_toolkit.styles import BaseStyle, DummyStyle from .log import logger from .protocol import ( DO, ECHO, IAC, LINEMODE, MODE, NAWS, SB, SE, SEND, SUPPRESS_GO_AHEAD, TTYPE, WILL, TelnetProtocolParser, ) __all__ = [ "TelnetServer", ] def int2byte(number: int) -> bytes: return bytes((number,)) def _initialize_telnet(connection: socket.socket) -> None: logger.info("Initializing telnet connection") # Iac Do Linemode connection.send(IAC + DO + LINEMODE) # Suppress Go Ahead. (This seems important for Putty to do correct echoing.) # This will allow bi-directional operation. connection.send(IAC + WILL + SUPPRESS_GO_AHEAD) # Iac sb connection.send(IAC + SB + LINEMODE + MODE + int2byte(0) + IAC + SE) # IAC Will Echo connection.send(IAC + WILL + ECHO) # Negotiate window size connection.send(IAC + DO + NAWS) # Negotiate terminal type # Assume the client will accept the negotiation with `IAC + WILL + TTYPE` connection.send(IAC + DO + TTYPE) # We can then select the first terminal type supported by the client, # which is generally the best type the client supports # The client should reply with a `IAC + SB + TTYPE + IS + ttype + IAC + SE` connection.send(IAC + SB + TTYPE + SEND + IAC + SE) class _ConnectionStdout: """ Wrapper around socket which provides `write` and `flush` methods for the Vt100_Output output. """ def __init__(self, connection: socket.socket, encoding: str) -> None: self._encoding = encoding self._connection = connection self._errors = "strict" self._buffer: list[bytes] = [] self._closed = False def write(self, data: str) -> None: data = data.replace("\n", "\r\n") self._buffer.append(data.encode(self._encoding, errors=self._errors)) self.flush() def isatty(self) -> bool: return True def flush(self) -> None: try: if not self._closed: self._connection.send(b"".join(self._buffer)) except OSError as e: logger.warning(f"Couldn't send data over socket: {e}") self._buffer = [] def close(self) -> None: self._closed = True @property def encoding(self) -> str: return self._encoding @property def errors(self) -> str: return self._errors class TelnetConnection: """ Class that represents one Telnet connection. """ def __init__( self, conn: socket.socket, addr: tuple[str, int], interact: Callable[[TelnetConnection], Coroutine[Any, Any, None]], server: TelnetServer, encoding: str, style: BaseStyle | None, vt100_input: PipeInput, enable_cpr: bool = True, ) -> None: self.conn = conn self.addr = addr self.interact = interact self.server = server self.encoding = encoding self.style = style self._closed = False self._ready = asyncio.Event() self.vt100_input = vt100_input self.enable_cpr = enable_cpr self.vt100_output: Vt100_Output | None = None # Create "Output" object. self.size = Size(rows=40, columns=79) # Initialize. _initialize_telnet(conn) # Create output. def get_size() -> Size: return self.size self.stdout = cast(TextIO, _ConnectionStdout(conn, encoding=encoding)) def data_received(data: bytes) -> None: """TelnetProtocolParser 'data_received' callback""" self.vt100_input.send_bytes(data) def size_received(rows: int, columns: int) -> None: """TelnetProtocolParser 'size_received' callback""" self.size = Size(rows=rows, columns=columns) if self.vt100_output is not None and self.context: self.context.run(lambda: get_app()._on_resize()) def ttype_received(ttype: str) -> None: """TelnetProtocolParser 'ttype_received' callback""" self.vt100_output = Vt100_Output( self.stdout, get_size, term=ttype, enable_cpr=enable_cpr ) self._ready.set() self.parser = TelnetProtocolParser(data_received, size_received, ttype_received) self.context: contextvars.Context | None = None async def run_application(self) -> None: """ Run application. """ def handle_incoming_data() -> None: data = self.conn.recv(1024) if data: self.feed(data) else: # Connection closed by client. logger.info("Connection closed by client. {!r} {!r}".format(*self.addr)) self.close() # Add reader. loop = get_running_loop() loop.add_reader(self.conn, handle_incoming_data) try: # Wait for v100_output to be properly instantiated await self._ready.wait() with create_app_session(input=self.vt100_input, output=self.vt100_output): self.context = contextvars.copy_context() await self.interact(self) finally: self.close() def feed(self, data: bytes) -> None: """ Handler for incoming data. (Called by TelnetServer.) """ self.parser.feed(data) def close(self) -> None: """ Closed by client. """ if not self._closed: self._closed = True self.vt100_input.close() get_running_loop().remove_reader(self.conn) self.conn.close() self.stdout.close() def send(self, formatted_text: AnyFormattedText) -> None: """ Send text to the client. """ if self.vt100_output is None: return formatted_text = to_formatted_text(formatted_text) print_formatted_text( self.vt100_output, formatted_text, self.style or DummyStyle() ) def send_above_prompt(self, formatted_text: AnyFormattedText) -> None: """ Send text to the client. This is asynchronous, returns a `Future`. """ formatted_text = to_formatted_text(formatted_text) return self._run_in_terminal(lambda: self.send(formatted_text)) def _run_in_terminal(self, func: Callable[[], None]) -> None: # Make sure that when an application was active for this connection, # that we print the text above the application. if self.context: self.context.run(run_in_terminal, func) else: raise RuntimeError("Called _run_in_terminal outside `run_application`.") def erase_screen(self) -> None: """ Erase the screen and move the cursor to the top. """ if self.vt100_output is None: return self.vt100_output.erase_screen() self.vt100_output.cursor_goto(0, 0) self.vt100_output.flush() async def _dummy_interact(connection: TelnetConnection) -> None: pass class TelnetServer: """ Telnet server implementation. Example:: async def interact(connection): connection.send("Welcome") session = PromptSession() result = await session.prompt_async(message="Say something: ") connection.send(f"You said: {result}\n") async def main(): server = TelnetServer(interact=interact, port=2323) await server.run() """ def __init__( self, host: str = "127.0.0.1", port: int = 23, interact: Callable[ [TelnetConnection], Coroutine[Any, Any, None] ] = _dummy_interact, encoding: str = "utf-8", style: BaseStyle | None = None, enable_cpr: bool = True, ) -> None: self.host = host self.port = port self.interact = interact self.encoding = encoding self.style = style self.enable_cpr = enable_cpr self._run_task: asyncio.Task[None] | None = None self._application_tasks: list[asyncio.Task[None]] = [] self.connections: set[TelnetConnection] = set() @classmethod def _create_socket(cls, host: str, port: int) -> socket.socket: # Create and bind socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) s.listen(4) return s async def run(self, ready_cb: Callable[[], None] | None = None) -> None: """ Run the telnet server, until this gets cancelled. :param ready_cb: Callback that will be called at the point that we're actually listening. """ socket = self._create_socket(self.host, self.port) logger.info( "Listening for telnet connections on %s port %r", self.host, self.port ) get_running_loop().add_reader(socket, lambda: self._accept(socket)) if ready_cb: ready_cb() try: # Run forever, until cancelled. await asyncio.Future() finally: get_running_loop().remove_reader(socket) socket.close() # Wait for all applications to finish. for t in self._application_tasks: t.cancel() # (This is similar to # `Application.cancel_and_wait_for_background_tasks`. We wait for the # background tasks to complete, but don't propagate exceptions, because # we can't use `ExceptionGroup` yet.) if len(self._application_tasks) > 0: await asyncio.wait( self._application_tasks, timeout=None, return_when=asyncio.ALL_COMPLETED, ) def start(self) -> None: """ Deprecated: Use `.run()` instead. Start the telnet server (stop by calling and awaiting `stop()`). """ if self._run_task is not None: # Already running. return self._run_task = get_running_loop().create_task(self.run()) async def stop(self) -> None: """ Deprecated: Use `.run()` instead. Stop a telnet server that was started using `.start()` and wait for the cancellation to complete. """ if self._run_task is not None: self._run_task.cancel() try: await self._run_task except asyncio.CancelledError: pass def _accept(self, listen_socket: socket.socket) -> None: """ Accept new incoming connection. """ conn, addr = listen_socket.accept() logger.info("New connection %r %r", *addr) # Run application for this connection. async def run() -> None: try: with create_pipe_input() as vt100_input: connection = TelnetConnection( conn, addr, self.interact, self, encoding=self.encoding, style=self.style, vt100_input=vt100_input, enable_cpr=self.enable_cpr, ) self.connections.add(connection) logger.info("Starting interaction %r %r", *addr) try: await connection.run_application() finally: self.connections.remove(connection) logger.info("Stopping interaction %r %r", *addr) except EOFError: # Happens either when the connection is closed by the client # (e.g., when the user types 'control-]', then 'quit' in the # telnet client) or when the user types control-d in a prompt # and this is not handled by the interact function. logger.info("Unhandled EOFError in telnet application.") except KeyboardInterrupt: # Unhandled control-c propagated by a prompt. logger.info("Unhandled KeyboardInterrupt in telnet application.") except BaseException as e: print(f"Got {type(e).__name__}", e) import traceback traceback.print_exc() finally: self._application_tasks.remove(task) task = get_running_loop().create_task(run()) self._application_tasks.append(task) ================================================ FILE: src/prompt_toolkit/cursor_shapes.py ================================================ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Callable from enum import Enum from typing import TYPE_CHECKING, Any from prompt_toolkit.enums import EditingMode from prompt_toolkit.key_binding.vi_state import InputMode if TYPE_CHECKING: from .application import Application __all__ = [ "CursorShape", "CursorShapeConfig", "SimpleCursorShapeConfig", "ModalCursorShapeConfig", "DynamicCursorShapeConfig", "to_cursor_shape_config", ] class CursorShape(Enum): # Default value that should tell the output implementation to never send # cursor shape escape sequences. This is the default right now, because # before this `CursorShape` functionality was introduced into # prompt_toolkit itself, people had workarounds to send cursor shapes # escapes into the terminal, by monkey patching some of prompt_toolkit's # internals. We don't want the default prompt_toolkit implementation to # interfere with that. E.g., IPython patches the `ViState.input_mode` # property. See: https://github.com/ipython/ipython/pull/13501/files _NEVER_CHANGE = "_NEVER_CHANGE" BLOCK = "BLOCK" BEAM = "BEAM" UNDERLINE = "UNDERLINE" BLINKING_BLOCK = "BLINKING_BLOCK" BLINKING_BEAM = "BLINKING_BEAM" BLINKING_UNDERLINE = "BLINKING_UNDERLINE" class CursorShapeConfig(ABC): @abstractmethod def get_cursor_shape(self, application: Application[Any]) -> CursorShape: """ Return the cursor shape to be used in the current state. """ AnyCursorShapeConfig = CursorShape | CursorShapeConfig | None class SimpleCursorShapeConfig(CursorShapeConfig): """ Always show the given cursor shape. """ def __init__(self, cursor_shape: CursorShape = CursorShape._NEVER_CHANGE) -> None: self.cursor_shape = cursor_shape def get_cursor_shape(self, application: Application[Any]) -> CursorShape: return self.cursor_shape class ModalCursorShapeConfig(CursorShapeConfig): """ Show cursor shape according to the current input mode. """ def get_cursor_shape(self, application: Application[Any]) -> CursorShape: if application.editing_mode == EditingMode.VI: if application.vi_state.input_mode in { InputMode.NAVIGATION, }: return CursorShape.BLOCK if application.vi_state.input_mode in { InputMode.INSERT, InputMode.INSERT_MULTIPLE, }: return CursorShape.BEAM if application.vi_state.input_mode in { InputMode.REPLACE, InputMode.REPLACE_SINGLE, }: return CursorShape.UNDERLINE elif application.editing_mode == EditingMode.EMACS: # like vi's INSERT return CursorShape.BEAM # Default return CursorShape.BLOCK class DynamicCursorShapeConfig(CursorShapeConfig): def __init__( self, get_cursor_shape_config: Callable[[], AnyCursorShapeConfig] ) -> None: self.get_cursor_shape_config = get_cursor_shape_config def get_cursor_shape(self, application: Application[Any]) -> CursorShape: return to_cursor_shape_config(self.get_cursor_shape_config()).get_cursor_shape( application ) def to_cursor_shape_config(value: AnyCursorShapeConfig) -> CursorShapeConfig: """ Take a `CursorShape` instance or `CursorShapeConfig` and turn it into a `CursorShapeConfig`. """ if value is None: return SimpleCursorShapeConfig() if isinstance(value, CursorShape): return SimpleCursorShapeConfig(value) return value ================================================ FILE: src/prompt_toolkit/data_structures.py ================================================ from __future__ import annotations from typing import NamedTuple __all__ = [ "Point", "Size", ] class Point(NamedTuple): x: int y: int class Size(NamedTuple): rows: int columns: int ================================================ FILE: src/prompt_toolkit/document.py ================================================ """ The `Document` that implements all the text operations/querying. """ from __future__ import annotations import bisect import re import string import weakref from collections.abc import Callable, Iterable from re import Pattern from typing import NoReturn, cast from .clipboard import ClipboardData from .filters import vi_mode from .selection import PasteMode, SelectionState, SelectionType __all__ = [ "Document", ] # Regex for finding "words" in documents. (We consider a group of alnum # characters a word, but also a group of special characters a word, as long as # it doesn't contain a space.) # (This is a 'word' in Vi.) _FIND_WORD_RE = re.compile(r"([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") _FIND_CURRENT_WORD_RE = re.compile(r"^([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") _FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile( r"^(([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)\s*)" ) # Regex for finding "WORDS" in documents. # (This is a 'WORD in Vi.) _FIND_BIG_WORD_RE = re.compile(r"([^\s]+)") _FIND_CURRENT_BIG_WORD_RE = re.compile(r"^([^\s]+)") _FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(r"^([^\s]+\s*)") # Share the Document._cache between all Document instances. # (Document instances are considered immutable. That means that if another # `Document` is constructed with the same text, it should have the same # `_DocumentCache`.) _text_to_document_cache: dict[str, _DocumentCache] = cast( dict[str, "_DocumentCache"], weakref.WeakValueDictionary(), # Maps document.text to DocumentCache instance. ) class _ImmutableLineList(list[str]): """ Some protection for our 'lines' list, which is assumed to be immutable in the cache. (Useful for detecting obvious bugs.) """ def _error(self, *a: object, **kw: object) -> NoReturn: raise NotImplementedError("Attempt to modify an immutable list.") __setitem__ = _error append = _error clear = _error extend = _error insert = _error pop = _error remove = _error reverse = _error sort = _error class _DocumentCache: def __init__(self) -> None: #: List of lines for the Document text. self.lines: _ImmutableLineList | None = None #: List of index positions, pointing to the start of all the lines. self.line_indexes: list[int] | None = None class Document: """ This is a immutable class around the text and cursor position, and contains methods for querying this data, e.g. to give the text before the cursor. This class is usually instantiated by a :class:`~prompt_toolkit.buffer.Buffer` object, and accessed as the `document` property of that class. :param text: string :param cursor_position: int :param selection: :class:`.SelectionState` """ __slots__ = ("_text", "_cursor_position", "_selection", "_cache") def __init__( self, text: str = "", cursor_position: int | None = None, selection: SelectionState | None = None, ) -> None: # Check cursor position. It can also be right after the end. (Where we # insert text.) assert cursor_position is None or cursor_position <= len(text), AssertionError( f"cursor_position={cursor_position!r}, len_text={len(text)!r}" ) # By default, if no cursor position was given, make sure to put the # cursor position is at the end of the document. This is what makes # sense in most places. if cursor_position is None: cursor_position = len(text) # Keep these attributes private. A `Document` really has to be # considered to be immutable, because otherwise the caching will break # things. Because of that, we wrap these into read-only properties. self._text = text self._cursor_position = cursor_position self._selection = selection # Cache for lines/indexes. (Shared with other Document instances that # contain the same text. try: self._cache = _text_to_document_cache[self.text] except KeyError: self._cache = _DocumentCache() _text_to_document_cache[self.text] = self._cache # XX: For some reason, above, we can't use 'WeakValueDictionary.setdefault'. # This fails in Pypy3. `self._cache` becomes None, because that's what # 'setdefault' returns. # self._cache = _text_to_document_cache.setdefault(self.text, _DocumentCache()) # assert self._cache def __repr__(self) -> str: return f"{self.__class__.__name__}({self.text!r}, {self.cursor_position!r})" def __eq__(self, other: object) -> bool: if not isinstance(other, Document): return False return ( self.text == other.text and self.cursor_position == other.cursor_position and self.selection == other.selection ) @property def text(self) -> str: "The document text." return self._text @property def cursor_position(self) -> int: "The document cursor position." return self._cursor_position @property def selection(self) -> SelectionState | None: ":class:`.SelectionState` object." return self._selection @property def current_char(self) -> str: """Return character under cursor or an empty string.""" return self._get_char_relative_to_cursor(0) or "" @property def char_before_cursor(self) -> str: """Return character before the cursor or an empty string.""" return self._get_char_relative_to_cursor(-1) or "" @property def text_before_cursor(self) -> str: return self.text[: self.cursor_position :] @property def text_after_cursor(self) -> str: return self.text[self.cursor_position :] @property def current_line_before_cursor(self) -> str: """Text from the start of the line until the cursor.""" _, _, text = self.text_before_cursor.rpartition("\n") return text @property def current_line_after_cursor(self) -> str: """Text from the cursor until the end of the line.""" text, _, _ = self.text_after_cursor.partition("\n") return text @property def lines(self) -> list[str]: """ Array of all the lines. """ # Cache, because this one is reused very often. if self._cache.lines is None: self._cache.lines = _ImmutableLineList(self.text.split("\n")) return self._cache.lines @property def _line_start_indexes(self) -> list[int]: """ Array pointing to the start indexes of all the lines. """ # Cache, because this is often reused. (If it is used, it's often used # many times. And this has to be fast for editing big documents!) if self._cache.line_indexes is None: # Create list of line lengths. line_lengths = map(len, self.lines) # Calculate cumulative sums. indexes = [0] append = indexes.append pos = 0 for line_length in line_lengths: pos += line_length + 1 append(pos) # Remove the last item. (This is not a new line.) if len(indexes) > 1: indexes.pop() self._cache.line_indexes = indexes return self._cache.line_indexes @property def lines_from_current(self) -> list[str]: """ Array of the lines starting from the current line, until the last line. """ return self.lines[self.cursor_position_row :] @property def line_count(self) -> int: r"""Return the number of lines in this document. If the document ends with a trailing \n, that counts as the beginning of a new line.""" return len(self.lines) @property def current_line(self) -> str: """Return the text on the line where the cursor is. (when the input consists of just one line, it equals `text`.""" return self.current_line_before_cursor + self.current_line_after_cursor @property def leading_whitespace_in_current_line(self) -> str: """The leading whitespace in the left margin of the current line.""" current_line = self.current_line length = len(current_line) - len(current_line.lstrip()) return current_line[:length] def _get_char_relative_to_cursor(self, offset: int = 0) -> str: """ Return character relative to cursor position, or empty string """ try: return self.text[self.cursor_position + offset] except IndexError: return "" @property def on_first_line(self) -> bool: """ True when we are at the first line. """ return self.cursor_position_row == 0 @property def on_last_line(self) -> bool: """ True when we are at the last line. """ return self.cursor_position_row == self.line_count - 1 @property def cursor_position_row(self) -> int: """ Current row. (0-based.) """ row, _ = self._find_line_start_index(self.cursor_position) return row @property def cursor_position_col(self) -> int: """ Current column. (0-based.) """ # (Don't use self.text_before_cursor to calculate this. Creating # substrings and doing rsplit is too expensive for getting the cursor # position.) _, line_start_index = self._find_line_start_index(self.cursor_position) return self.cursor_position - line_start_index def _find_line_start_index(self, index: int) -> tuple[int, int]: """ For the index of a character at a certain line, calculate the index of the first character on that line. Return (row, index) tuple. """ indexes = self._line_start_indexes pos = bisect.bisect_right(indexes, index) - 1 return pos, indexes[pos] def translate_index_to_position(self, index: int) -> tuple[int, int]: """ Given an index for the text, return the corresponding (row, col) tuple. (0-based. Returns (0, 0) for index=0.) """ # Find start of this line. row, row_index = self._find_line_start_index(index) col = index - row_index return row, col def translate_row_col_to_index(self, row: int, col: int) -> int: """ Given a (row, col) tuple, return the corresponding index. (Row and col params are 0-based.) Negative row/col values are turned into zero. """ try: result = self._line_start_indexes[row] line = self.lines[row] except IndexError: if row < 0: result = self._line_start_indexes[0] line = self.lines[0] else: result = self._line_start_indexes[-1] line = self.lines[-1] result += max(0, min(col, len(line))) # Keep in range. (len(self.text) is included, because the cursor can be # right after the end of the text as well.) result = max(0, min(result, len(self.text))) return result @property def is_cursor_at_the_end(self) -> bool: """True when the cursor is at the end of the text.""" return self.cursor_position == len(self.text) @property def is_cursor_at_the_end_of_line(self) -> bool: """True when the cursor is at the end of this line.""" return self.current_char in ("\n", "") def has_match_at_current_position(self, sub: str) -> bool: """ `True` when this substring is found at the cursor position. """ return self.text.find(sub, self.cursor_position) == self.cursor_position def find( self, sub: str, in_current_line: bool = False, include_current_position: bool = False, ignore_case: bool = False, count: int = 1, ) -> int | None: """ Find `text` after the cursor, return position relative to the cursor position. Return `None` if nothing was found. :param count: Find the n-th occurrence. """ assert isinstance(ignore_case, bool) if in_current_line: text = self.current_line_after_cursor else: text = self.text_after_cursor if not include_current_position: if len(text) == 0: return None # (Otherwise, we always get a match for the empty string.) else: text = text[1:] flags = re.IGNORECASE if ignore_case else 0 iterator = re.finditer(re.escape(sub), text, flags) try: for i, match in enumerate(iterator): if i + 1 == count: if include_current_position: return match.start(0) else: return match.start(0) + 1 except StopIteration: pass return None def find_all(self, sub: str, ignore_case: bool = False) -> list[int]: """ Find all occurrences of the substring. Return a list of absolute positions in the document. """ flags = re.IGNORECASE if ignore_case else 0 return [a.start() for a in re.finditer(re.escape(sub), self.text, flags)] def find_backwards( self, sub: str, in_current_line: bool = False, ignore_case: bool = False, count: int = 1, ) -> int | None: """ Find `text` before the cursor, return position relative to the cursor position. Return `None` if nothing was found. :param count: Find the n-th occurrence. """ if in_current_line: before_cursor = self.current_line_before_cursor[::-1] else: before_cursor = self.text_before_cursor[::-1] flags = re.IGNORECASE if ignore_case else 0 iterator = re.finditer(re.escape(sub[::-1]), before_cursor, flags) try: for i, match in enumerate(iterator): if i + 1 == count: return -match.start(0) - len(sub) except StopIteration: pass return None def get_word_before_cursor( self, WORD: bool = False, pattern: Pattern[str] | None = None ) -> str: """ Give the word before the cursor. If we have whitespace before the cursor this returns an empty string. :param pattern: (None or compiled regex). When given, use this regex pattern. """ if self._is_word_before_cursor_complete(WORD=WORD, pattern=pattern): # Space before the cursor or no text before cursor. return "" text_before_cursor = self.text_before_cursor start = self.find_start_of_previous_word(WORD=WORD, pattern=pattern) or 0 return text_before_cursor[len(text_before_cursor) + start :] def _is_word_before_cursor_complete( self, WORD: bool = False, pattern: Pattern[str] | None = None ) -> bool: if self.text_before_cursor == "" or self.text_before_cursor[-1:].isspace(): return True if pattern: return self.find_start_of_previous_word(WORD=WORD, pattern=pattern) is None return False def find_start_of_previous_word( self, count: int = 1, WORD: bool = False, pattern: Pattern[str] | None = None ) -> int | None: """ Return an index relative to the cursor position pointing to the start of the previous word. Return `None` if nothing was found. :param pattern: (None or compiled regex). When given, use this regex pattern. """ assert not (WORD and pattern) # Reverse the text before the cursor, in order to do an efficient # backwards search. text_before_cursor = self.text_before_cursor[::-1] if pattern: regex = pattern elif WORD: regex = _FIND_BIG_WORD_RE else: regex = _FIND_WORD_RE iterator = regex.finditer(text_before_cursor) try: for i, match in enumerate(iterator): if i + 1 == count: return -match.end(0) except StopIteration: pass return None def find_boundaries_of_current_word( self, WORD: bool = False, include_leading_whitespace: bool = False, include_trailing_whitespace: bool = False, ) -> tuple[int, int]: """ Return the relative boundaries (startpos, endpos) of the current word under the cursor. (This is at the current line, because line boundaries obviously don't belong to any word.) If not on a word, this returns (0,0) """ text_before_cursor = self.current_line_before_cursor[::-1] text_after_cursor = self.current_line_after_cursor def get_regex(include_whitespace: bool) -> Pattern[str]: return { (False, False): _FIND_CURRENT_WORD_RE, (False, True): _FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE, (True, False): _FIND_CURRENT_BIG_WORD_RE, (True, True): _FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE, }[(WORD, include_whitespace)] match_before = get_regex(include_leading_whitespace).search(text_before_cursor) match_after = get_regex(include_trailing_whitespace).search(text_after_cursor) # When there is a match before and after, and we're not looking for # WORDs, make sure that both the part before and after the cursor are # either in the [a-zA-Z_] alphabet or not. Otherwise, drop the part # before the cursor. if not WORD and match_before and match_after: c1 = self.text[self.cursor_position - 1] c2 = self.text[self.cursor_position] alphabet = string.ascii_letters + "0123456789_" if (c1 in alphabet) != (c2 in alphabet): match_before = None return ( -match_before.end(1) if match_before else 0, match_after.end(1) if match_after else 0, ) def get_word_under_cursor(self, WORD: bool = False) -> str: """ Return the word, currently below the cursor. This returns an empty string when the cursor is on a whitespace region. """ start, end = self.find_boundaries_of_current_word(WORD=WORD) return self.text[self.cursor_position + start : self.cursor_position + end] def find_next_word_beginning( self, count: int = 1, WORD: bool = False ) -> int | None: """ Return an index relative to the cursor position pointing to the start of the next word. Return `None` if nothing was found. """ if count < 0: return self.find_previous_word_beginning(count=-count, WORD=WORD) regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE iterator = regex.finditer(self.text_after_cursor) try: for i, match in enumerate(iterator): # Take first match, unless it's the word on which we're right now. if i == 0 and match.start(1) == 0: count += 1 if i + 1 == count: return match.start(1) except StopIteration: pass return None def find_next_word_ending( self, include_current_position: bool = False, count: int = 1, WORD: bool = False ) -> int | None: """ Return an index relative to the cursor position pointing to the end of the next word. Return `None` if nothing was found. """ if count < 0: return self.find_previous_word_ending(count=-count, WORD=WORD) if include_current_position: text = self.text_after_cursor else: text = self.text_after_cursor[1:] regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE iterable = regex.finditer(text) try: for i, match in enumerate(iterable): if i + 1 == count: value = match.end(1) if include_current_position: return value else: return value + 1 except StopIteration: pass return None def find_previous_word_beginning( self, count: int = 1, WORD: bool = False ) -> int | None: """ Return an index relative to the cursor position pointing to the start of the previous word. Return `None` if nothing was found. """ if count < 0: return self.find_next_word_beginning(count=-count, WORD=WORD) regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE iterator = regex.finditer(self.text_before_cursor[::-1]) try: for i, match in enumerate(iterator): if i + 1 == count: return -match.end(1) except StopIteration: pass return None def find_previous_word_ending( self, count: int = 1, WORD: bool = False ) -> int | None: """ Return an index relative to the cursor position pointing to the end of the previous word. Return `None` if nothing was found. """ if count < 0: return self.find_next_word_ending(count=-count, WORD=WORD) text_before_cursor = self.text_after_cursor[:1] + self.text_before_cursor[::-1] regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE iterator = regex.finditer(text_before_cursor) try: for i, match in enumerate(iterator): # Take first match, unless it's the word on which we're right now. if i == 0 and match.start(1) == 0: count += 1 if i + 1 == count: return -match.start(1) + 1 except StopIteration: pass return None def find_next_matching_line( self, match_func: Callable[[str], bool], count: int = 1 ) -> int | None: """ Look downwards for empty lines. Return the line index, relative to the current line. """ result = None for index, line in enumerate(self.lines[self.cursor_position_row + 1 :]): if match_func(line): result = 1 + index count -= 1 if count == 0: break return result def find_previous_matching_line( self, match_func: Callable[[str], bool], count: int = 1 ) -> int | None: """ Look upwards for empty lines. Return the line index, relative to the current line. """ result = None for index, line in enumerate(self.lines[: self.cursor_position_row][::-1]): if match_func(line): result = -1 - index count -= 1 if count == 0: break return result def get_cursor_left_position(self, count: int = 1) -> int: """ Relative position for cursor left. """ if count < 0: return self.get_cursor_right_position(-count) return -min(self.cursor_position_col, count) def get_cursor_right_position(self, count: int = 1) -> int: """ Relative position for cursor_right. """ if count < 0: return self.get_cursor_left_position(-count) return min(count, len(self.current_line_after_cursor)) def get_cursor_up_position( self, count: int = 1, preferred_column: int | None = None ) -> int: """ Return the relative cursor position (character index) where we would be if the user pressed the arrow-up button. :param preferred_column: When given, go to this column instead of staying at the current column. """ assert count >= 1 column = ( self.cursor_position_col if preferred_column is None else preferred_column ) return ( self.translate_row_col_to_index( max(0, self.cursor_position_row - count), column ) - self.cursor_position ) def get_cursor_down_position( self, count: int = 1, preferred_column: int | None = None ) -> int: """ Return the relative cursor position (character index) where we would be if the user pressed the arrow-down button. :param preferred_column: When given, go to this column instead of staying at the current column. """ assert count >= 1 column = ( self.cursor_position_col if preferred_column is None else preferred_column ) return ( self.translate_row_col_to_index(self.cursor_position_row + count, column) - self.cursor_position ) def find_enclosing_bracket_right( self, left_ch: str, right_ch: str, end_pos: int | None = None ) -> int | None: """ Find the right bracket enclosing current position. Return the relative position to the cursor position. When `end_pos` is given, don't look past the position. """ if self.current_char == right_ch: return 0 if end_pos is None: end_pos = len(self.text) else: end_pos = min(len(self.text), end_pos) stack = 1 # Look forward. for i in range(self.cursor_position + 1, end_pos): c = self.text[i] if c == left_ch: stack += 1 elif c == right_ch: stack -= 1 if stack == 0: return i - self.cursor_position return None def find_enclosing_bracket_left( self, left_ch: str, right_ch: str, start_pos: int | None = None ) -> int | None: """ Find the left bracket enclosing current position. Return the relative position to the cursor position. When `start_pos` is given, don't look past the position. """ if self.current_char == left_ch: return 0 if start_pos is None: start_pos = 0 else: start_pos = max(0, start_pos) stack = 1 # Look backward. for i in range(self.cursor_position - 1, start_pos - 1, -1): c = self.text[i] if c == right_ch: stack += 1 elif c == left_ch: stack -= 1 if stack == 0: return i - self.cursor_position return None def find_matching_bracket_position( self, start_pos: int | None = None, end_pos: int | None = None ) -> int: """ Return relative cursor position of matching [, (, { or < bracket. When `start_pos` or `end_pos` are given. Don't look past the positions. """ # Look for a match. for pair in "()", "[]", "{}", "<>": A = pair[0] B = pair[1] if self.current_char == A: return self.find_enclosing_bracket_right(A, B, end_pos=end_pos) or 0 elif self.current_char == B: return self.find_enclosing_bracket_left(A, B, start_pos=start_pos) or 0 return 0 def get_start_of_document_position(self) -> int: """Relative position for the start of the document.""" return -self.cursor_position def get_end_of_document_position(self) -> int: """Relative position for the end of the document.""" return len(self.text) - self.cursor_position def get_start_of_line_position(self, after_whitespace: bool = False) -> int: """Relative position for the start of this line.""" if after_whitespace: current_line = self.current_line return ( len(current_line) - len(current_line.lstrip()) - self.cursor_position_col ) else: return -len(self.current_line_before_cursor) def get_end_of_line_position(self) -> int: """Relative position for the end of this line.""" return len(self.current_line_after_cursor) def last_non_blank_of_current_line_position(self) -> int: """ Relative position for the last non blank character of this line. """ return len(self.current_line.rstrip()) - self.cursor_position_col - 1 def get_column_cursor_position(self, column: int) -> int: """ Return the relative cursor position for this column at the current line. (It will stay between the boundaries of the line in case of a larger number.) """ line_length = len(self.current_line) current_column = self.cursor_position_col column = max(0, min(line_length, column)) return column - current_column def selection_range( self, ) -> tuple[ int, int ]: # XXX: shouldn't this return `None` if there is no selection??? """ Return (from, to) tuple of the selection. start and end position are included. This doesn't take the selection type into account. Use `selection_ranges` instead. """ if self.selection: from_, to = sorted( [self.cursor_position, self.selection.original_cursor_position] ) else: from_, to = self.cursor_position, self.cursor_position return from_, to def selection_ranges(self) -> Iterable[tuple[int, int]]: """ Return a list of `(from, to)` tuples for the selection or none if nothing was selected. The upper boundary is not included. This will yield several (from, to) tuples in case of a BLOCK selection. This will return zero ranges, like (8,8) for empty lines in a block selection. """ if self.selection: from_, to = sorted( [self.cursor_position, self.selection.original_cursor_position] ) if self.selection.type == SelectionType.BLOCK: from_line, from_column = self.translate_index_to_position(from_) to_line, to_column = self.translate_index_to_position(to) from_column, to_column = sorted([from_column, to_column]) lines = self.lines if vi_mode(): to_column += 1 for l in range(from_line, to_line + 1): line_length = len(lines[l]) if from_column <= line_length: yield ( self.translate_row_col_to_index(l, from_column), self.translate_row_col_to_index( l, min(line_length, to_column) ), ) else: # In case of a LINES selection, go to the start/end of the lines. if self.selection.type == SelectionType.LINES: from_ = max(0, self.text.rfind("\n", 0, from_) + 1) if self.text.find("\n", to) >= 0: to = self.text.find("\n", to) else: to = len(self.text) - 1 # In Vi mode, the upper boundary is always included. For Emacs, # that's not the case. if vi_mode(): to += 1 yield from_, to def selection_range_at_line(self, row: int) -> tuple[int, int] | None: """ If the selection spans a portion of the given line, return a (from, to) tuple. The returned upper boundary is not included in the selection, so `(0, 0)` is an empty selection. `(0, 1)`, is a one character selection. Returns None if the selection doesn't cover this line at all. """ if self.selection: line = self.lines[row] row_start = self.translate_row_col_to_index(row, 0) row_end = self.translate_row_col_to_index(row, len(line)) from_, to = sorted( [self.cursor_position, self.selection.original_cursor_position] ) # Take the intersection of the current line and the selection. intersection_start = max(row_start, from_) intersection_end = min(row_end, to) if intersection_start <= intersection_end: if self.selection.type == SelectionType.LINES: intersection_start = row_start intersection_end = row_end elif self.selection.type == SelectionType.BLOCK: _, col1 = self.translate_index_to_position(from_) _, col2 = self.translate_index_to_position(to) col1, col2 = sorted([col1, col2]) if col1 > len(line): return None # Block selection doesn't cross this line. intersection_start = self.translate_row_col_to_index(row, col1) intersection_end = self.translate_row_col_to_index(row, col2) _, from_column = self.translate_index_to_position(intersection_start) _, to_column = self.translate_index_to_position(intersection_end) # In Vi mode, the upper boundary is always included. For Emacs # mode, that's not the case. if vi_mode(): to_column += 1 return from_column, to_column return None def cut_selection(self) -> tuple[Document, ClipboardData]: """ Return a (:class:`.Document`, :class:`.ClipboardData`) tuple, where the document represents the new document when the selection is cut, and the clipboard data, represents whatever has to be put on the clipboard. """ if self.selection: cut_parts = [] remaining_parts = [] new_cursor_position = self.cursor_position last_to = 0 for from_, to in self.selection_ranges(): if last_to == 0: new_cursor_position = from_ remaining_parts.append(self.text[last_to:from_]) cut_parts.append(self.text[from_:to]) last_to = to remaining_parts.append(self.text[last_to:]) cut_text = "\n".join(cut_parts) remaining_text = "".join(remaining_parts) # In case of a LINES selection, don't include the trailing newline. if self.selection.type == SelectionType.LINES and cut_text.endswith("\n"): cut_text = cut_text[:-1] return ( Document(text=remaining_text, cursor_position=new_cursor_position), ClipboardData(cut_text, self.selection.type), ) else: return self, ClipboardData("") def paste_clipboard_data( self, data: ClipboardData, paste_mode: PasteMode = PasteMode.EMACS, count: int = 1, ) -> Document: """ Return a new :class:`.Document` instance which contains the result if we would paste this data at the current cursor position. :param paste_mode: Where to paste. (Before/after/emacs.) :param count: When >1, Paste multiple times. """ before = paste_mode == PasteMode.VI_BEFORE after = paste_mode == PasteMode.VI_AFTER if data.type == SelectionType.CHARACTERS: if after: new_text = ( self.text[: self.cursor_position + 1] + data.text * count + self.text[self.cursor_position + 1 :] ) else: new_text = ( self.text_before_cursor + data.text * count + self.text_after_cursor ) new_cursor_position = self.cursor_position + len(data.text) * count if before: new_cursor_position -= 1 elif data.type == SelectionType.LINES: l = self.cursor_position_row if before: lines = self.lines[:l] + [data.text] * count + self.lines[l:] new_text = "\n".join(lines) new_cursor_position = len("".join(self.lines[:l])) + l else: lines = self.lines[: l + 1] + [data.text] * count + self.lines[l + 1 :] new_cursor_position = len("".join(self.lines[: l + 1])) + l + 1 new_text = "\n".join(lines) elif data.type == SelectionType.BLOCK: lines = self.lines[:] start_line = self.cursor_position_row start_column = self.cursor_position_col + (0 if before else 1) for i, line in enumerate(data.text.split("\n")): index = i + start_line if index >= len(lines): lines.append("") lines[index] = lines[index].ljust(start_column) lines[index] = ( lines[index][:start_column] + line * count + lines[index][start_column:] ) new_text = "\n".join(lines) new_cursor_position = self.cursor_position + (0 if before else 1) return Document(text=new_text, cursor_position=new_cursor_position) def empty_line_count_at_the_end(self) -> int: """ Return number of empty lines at the end of the document. """ count = 0 for line in self.lines[::-1]: if not line or line.isspace(): count += 1 else: break return count def start_of_paragraph(self, count: int = 1, before: bool = False) -> int: """ Return the start of the current paragraph. (Relative cursor position.) """ def match_func(text: str) -> bool: return not text or text.isspace() line_index = self.find_previous_matching_line( match_func=match_func, count=count ) if line_index: add = 0 if before else 1 return min(0, self.get_cursor_up_position(count=-line_index) + add) else: return -self.cursor_position def end_of_paragraph(self, count: int = 1, after: bool = False) -> int: """ Return the end of the current paragraph. (Relative cursor position.) """ def match_func(text: str) -> bool: return not text or text.isspace() line_index = self.find_next_matching_line(match_func=match_func, count=count) if line_index: add = 0 if after else 1 return max(0, self.get_cursor_down_position(count=line_index) - add) else: return len(self.text_after_cursor) # Modifiers. def insert_after(self, text: str) -> Document: """ Create a new document, with this text inserted after the buffer. It keeps selection ranges and cursor position in sync. """ return Document( text=self.text + text, cursor_position=self.cursor_position, selection=self.selection, ) def insert_before(self, text: str) -> Document: """ Create a new document, with this text inserted before the buffer. It keeps selection ranges and cursor position in sync. """ selection_state = self.selection if selection_state: selection_state = SelectionState( original_cursor_position=selection_state.original_cursor_position + len(text), type=selection_state.type, ) return Document( text=text + self.text, cursor_position=self.cursor_position + len(text), selection=selection_state, ) ================================================ FILE: src/prompt_toolkit/enums.py ================================================ from __future__ import annotations from enum import Enum class EditingMode(Enum): # The set of key bindings that is active. VI = "VI" EMACS = "EMACS" #: Name of the search buffer. SEARCH_BUFFER = "SEARCH_BUFFER" #: Name of the default buffer. DEFAULT_BUFFER = "DEFAULT_BUFFER" #: Name of the system buffer. SYSTEM_BUFFER = "SYSTEM_BUFFER" ================================================ FILE: src/prompt_toolkit/eventloop/__init__.py ================================================ from __future__ import annotations from .async_generator import aclosing, generator_to_async_generator from .inputhook import ( InputHook, InputHookContext, InputHookSelector, new_eventloop_with_inputhook, set_eventloop_with_inputhook, ) from .utils import ( call_soon_threadsafe, get_traceback_from_context, run_in_executor_with_context, ) __all__ = [ # Async generator "generator_to_async_generator", "aclosing", # Utils. "run_in_executor_with_context", "call_soon_threadsafe", "get_traceback_from_context", # Inputhooks. "InputHook", "new_eventloop_with_inputhook", "set_eventloop_with_inputhook", "InputHookSelector", "InputHookContext", ] ================================================ FILE: src/prompt_toolkit/eventloop/async_generator.py ================================================ """ Implementation for async generators. """ from __future__ import annotations from asyncio import get_running_loop from collections.abc import AsyncGenerator, Callable, Iterable from contextlib import asynccontextmanager from queue import Empty, Full, Queue from typing import Any, TypeVar from .utils import run_in_executor_with_context __all__ = [ "aclosing", "generator_to_async_generator", ] _T_Generator = TypeVar("_T_Generator", bound=AsyncGenerator[Any, None]) @asynccontextmanager async def aclosing( thing: _T_Generator, ) -> AsyncGenerator[_T_Generator, None]: "Similar to `contextlib.aclosing`, in Python 3.10." try: yield thing finally: await thing.aclose() # By default, choose a buffer size that's a good balance between having enough # throughput, but not consuming too much memory. We use this to consume a sync # generator of completions as an async generator. If the queue size is very # small (like 1), consuming the completions goes really slow (when there are a # lot of items). If the queue size would be unlimited or too big, this can # cause overconsumption of memory, and cause CPU time spent producing items # that are no longer needed (if the consumption of the async generator stops at # some point). We need a fixed size in order to get some back pressure from the # async consumer to the sync producer. We choose 1000 by default here. If we # have around 50k completions, measurements show that 1000 is still # significantly faster than a buffer of 100. DEFAULT_BUFFER_SIZE: int = 1000 _T = TypeVar("_T") class _Done: pass async def generator_to_async_generator( get_iterable: Callable[[], Iterable[_T]], buffer_size: int = DEFAULT_BUFFER_SIZE, ) -> AsyncGenerator[_T, None]: """ Turn a generator or iterable into an async generator. This works by running the generator in a background thread. :param get_iterable: Function that returns a generator or iterable when called. :param buffer_size: Size of the queue between the async consumer and the synchronous generator that produces items. """ quitting = False # NOTE: We are limiting the queue size in order to have back-pressure. q: Queue[_T | _Done] = Queue(maxsize=buffer_size) loop = get_running_loop() def runner() -> None: """ Consume the generator in background thread. When items are received, they'll be pushed to the queue. """ try: for item in get_iterable(): # When this async generator was cancelled (closed), stop this # thread. if quitting: return while True: try: q.put(item, timeout=1) except Full: if quitting: return continue else: break finally: while True: try: q.put(_Done(), timeout=1) except Full: if quitting: return continue else: break # Start background thread. runner_f = run_in_executor_with_context(runner) try: while True: try: item = q.get_nowait() except Empty: item = await loop.run_in_executor(None, q.get) if isinstance(item, _Done): break else: yield item finally: # When this async generator is closed (GeneratorExit exception, stop # the background thread as well. - we don't need that anymore.) quitting = True # Wait for the background thread to finish. (should happen right after # the last item is yielded). await runner_f ================================================ FILE: src/prompt_toolkit/eventloop/inputhook.py ================================================ """ Similar to `PyOS_InputHook` of the Python API, we can plug in an input hook in the asyncio event loop. The way this works is by using a custom 'selector' that runs the other event loop until the real selector is ready. It's the responsibility of this event hook to return when there is input ready. There are two ways to detect when input is ready: The inputhook itself is a callable that receives an `InputHookContext`. This callable should run the other event loop, and return when the main loop has stuff to do. There are two ways to detect when to return: - Call the `input_is_ready` method periodically. Quit when this returns `True`. - Add the `fileno` as a watch to the external eventloop. Quit when file descriptor becomes readable. (But don't read from it.) Note that this is not the same as checking for `sys.stdin.fileno()`. The eventloop of prompt-toolkit allows thread-based executors, for example for asynchronous autocompletion. When the completion for instance is ready, we also want prompt-toolkit to gain control again in order to display that. """ from __future__ import annotations import asyncio import os import select import selectors import sys import threading from asyncio import AbstractEventLoop, get_running_loop from collections.abc import Callable, Mapping from selectors import BaseSelector, SelectorKey from typing import TYPE_CHECKING, Any __all__ = [ "new_eventloop_with_inputhook", "set_eventloop_with_inputhook", "InputHookSelector", "InputHookContext", "InputHook", ] if TYPE_CHECKING: from typing import TypeAlias from _typeshed import FileDescriptorLike _EventMask = int class InputHookContext: """ Given as a parameter to the inputhook. """ def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None: self._fileno = fileno self.input_is_ready = input_is_ready def fileno(self) -> int: return self._fileno InputHook: TypeAlias = Callable[[InputHookContext], None] def new_eventloop_with_inputhook( inputhook: Callable[[InputHookContext], None], ) -> AbstractEventLoop: """ Create a new event loop with the given inputhook. """ selector = InputHookSelector(selectors.DefaultSelector(), inputhook) loop = asyncio.SelectorEventLoop(selector) return loop def set_eventloop_with_inputhook( inputhook: Callable[[InputHookContext], None], ) -> AbstractEventLoop: """ Create a new event loop with the given inputhook, and activate it. """ # Deprecated! loop = new_eventloop_with_inputhook(inputhook) asyncio.set_event_loop(loop) return loop class InputHookSelector(BaseSelector): """ Usage: selector = selectors.SelectSelector() loop = asyncio.SelectorEventLoop(InputHookSelector(selector, inputhook)) asyncio.set_event_loop(loop) """ def __init__( self, selector: BaseSelector, inputhook: Callable[[InputHookContext], None] ) -> None: self.selector = selector self.inputhook = inputhook self._r, self._w = os.pipe() def register( self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None ) -> SelectorKey: return self.selector.register(fileobj, events, data=data) def unregister(self, fileobj: FileDescriptorLike) -> SelectorKey: return self.selector.unregister(fileobj) def modify( self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None ) -> SelectorKey: return self.selector.modify(fileobj, events, data=None) def select( self, timeout: float | None = None ) -> list[tuple[SelectorKey, _EventMask]]: # If there are tasks in the current event loop, # don't run the input hook. if len(getattr(get_running_loop(), "_ready", [])) > 0: return self.selector.select(timeout=timeout) ready = False result = None # Run selector in other thread. def run_selector() -> None: nonlocal ready, result result = self.selector.select(timeout=timeout) os.write(self._w, b"x") ready = True th = threading.Thread(target=run_selector) th.start() def input_is_ready() -> bool: return ready # Call inputhook. # The inputhook function is supposed to return when our selector # becomes ready. The inputhook can do that by registering the fd in its # own loop, or by checking the `input_is_ready` function regularly. self.inputhook(InputHookContext(self._r, input_is_ready)) # Flush the read end of the pipe. try: # Before calling 'os.read', call select.select. This is required # when the gevent monkey patch has been applied. 'os.read' is never # monkey patched and won't be cooperative, so that would block all # other select() calls otherwise. # See: http://www.gevent.org/gevent.os.html # Note: On Windows, this is apparently not an issue. # However, if we would ever want to add a select call, it # should use `windll.kernel32.WaitForMultipleObjects`, # because `select.select` can't wait for a pipe on Windows. if sys.platform != "win32": select.select([self._r], [], [], None) os.read(self._r, 1024) except OSError: # This happens when the window resizes and a SIGWINCH was received. # We get 'Error: [Errno 4] Interrupted system call' # Just ignore. pass # Wait for the real selector to be done. th.join() assert result is not None return result def close(self) -> None: """ Clean up resources. """ if self._r: os.close(self._r) os.close(self._w) self._r = self._w = -1 self.selector.close() def get_map(self) -> Mapping[FileDescriptorLike, SelectorKey]: return self.selector.get_map() ================================================ FILE: src/prompt_toolkit/eventloop/utils.py ================================================ from __future__ import annotations import asyncio import contextvars import sys import time from asyncio import get_running_loop from collections.abc import Awaitable, Callable from types import TracebackType from typing import Any, TypeVar, cast __all__ = [ "run_in_executor_with_context", "call_soon_threadsafe", "get_traceback_from_context", ] _T = TypeVar("_T") def run_in_executor_with_context( func: Callable[..., _T], *args: Any, loop: asyncio.AbstractEventLoop | None = None, ) -> Awaitable[_T]: """ Run a function in an executor, but make sure it uses the same contextvars. This is required so that the function will see the right application. See also: https://bugs.python.org/issue34014 """ loop = loop or get_running_loop() ctx: contextvars.Context = contextvars.copy_context() return loop.run_in_executor(None, ctx.run, func, *args) def call_soon_threadsafe( func: Callable[[], None], max_postpone_time: float | None = None, loop: asyncio.AbstractEventLoop | None = None, ) -> None: """ Wrapper around asyncio's `call_soon_threadsafe`. This takes a `max_postpone_time` which can be used to tune the urgency of the method. Asyncio runs tasks in first-in-first-out. However, this is not what we want for the render function of the prompt_toolkit UI. Rendering is expensive, but since the UI is invalidated very often, in some situations we render the UI too often, so much that the rendering CPU usage slows down the rest of the processing of the application. (Pymux is an example where we have to balance the CPU time spend on rendering the UI, and parsing process output.) However, we want to set a deadline value, for when the rendering should happen. (The UI should stay responsive). """ loop2 = loop or get_running_loop() # If no `max_postpone_time` has been given, schedule right now. if max_postpone_time is None: loop2.call_soon_threadsafe(func) return max_postpone_until = time.time() + max_postpone_time def schedule() -> None: # When there are no other tasks scheduled in the event loop. Run it # now. # Notice: uvloop doesn't have this _ready attribute. In that case, # always call immediately. if not getattr(loop2, "_ready", []): func() return # If the timeout expired, run this now. if time.time() > max_postpone_until: func() return # Schedule again for later. loop2.call_soon_threadsafe(schedule) loop2.call_soon_threadsafe(schedule) def get_traceback_from_context(context: dict[str, Any]) -> TracebackType | None: """ Get the traceback object from the context. """ exception = context.get("exception") if exception: if hasattr(exception, "__traceback__"): return cast(TracebackType, exception.__traceback__) else: # call_exception_handler() is usually called indirectly # from an except block. If it's not the case, the traceback # is undefined... return sys.exc_info()[2] return None ================================================ FILE: src/prompt_toolkit/eventloop/win32.py ================================================ from __future__ import annotations import sys assert sys.platform == "win32" from ctypes import pointer from ..utils import SPHINX_AUTODOC_RUNNING # Do not import win32-specific stuff when generating documentation. # Otherwise RTD would be unable to generate docs for this module. if not SPHINX_AUTODOC_RUNNING: from ctypes import windll from ctypes.wintypes import BOOL, DWORD, HANDLE from prompt_toolkit.win32_types import SECURITY_ATTRIBUTES __all__ = ["wait_for_handles", "create_win32_event"] WAIT_TIMEOUT = 0x00000102 INFINITE = -1 def wait_for_handles(handles: list[HANDLE], timeout: int = INFINITE) -> HANDLE | None: """ Waits for multiple handles. (Similar to 'select') Returns the handle which is ready. Returns `None` on timeout. http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx Note that handles should be a list of `HANDLE` objects, not integers. See this comment in the patch by @quark-zju for the reason why: ''' Make sure HANDLE on Windows has a correct size Previously, the type of various HANDLEs are native Python integer types. The ctypes library will treat them as 4-byte integer when used in function arguments. On 64-bit Windows, HANDLE is 8-byte and usually a small integer. Depending on whether the extra 4 bytes are zero-ed out or not, things can happen to work, or break. ''' This function returns either `None` or one of the given `HANDLE` objects. (The return value can be tested with the `is` operator.) """ arrtype = HANDLE * len(handles) handle_array = arrtype(*handles) ret: int = windll.kernel32.WaitForMultipleObjects( len(handle_array), handle_array, BOOL(False), DWORD(timeout) ) if ret == WAIT_TIMEOUT: return None else: return handles[ret] def create_win32_event() -> HANDLE: """ Creates a Win32 unnamed Event . http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx """ return HANDLE( windll.kernel32.CreateEventA( pointer(SECURITY_ATTRIBUTES()), BOOL(True), # Manual reset event. BOOL(False), # Initial state. None, # Unnamed event object. ) ) ================================================ FILE: src/prompt_toolkit/filters/__init__.py ================================================ """ Filters decide whether something is active or not (they decide about a boolean state). This is used to enable/disable features, like key bindings, parts of the layout and other stuff. For instance, we could have a `HasSearch` filter attached to some part of the layout, in order to show that part of the user interface only while the user is searching. Filters are made to avoid having to attach callbacks to all event in order to propagate state. However, they are lazy, they don't automatically propagate the state of what they are observing. Only when a filter is called (it's actually a callable), it will calculate its value. So, its not really reactive programming, but it's made to fit for this framework. Filters can be chained using ``&`` and ``|`` operations, and inverted using the ``~`` operator, for instance:: filter = has_focus('default') & ~ has_selection """ from __future__ import annotations from .app import * from .base import Always, Condition, Filter, FilterOrBool, Never from .cli import * from .utils import is_true, to_filter __all__ = [ # app "has_arg", "has_completions", "completion_is_selected", "has_focus", "buffer_has_focus", "has_selection", "has_validation_error", "is_done", "is_read_only", "is_multiline", "renderer_height_is_known", "in_editing_mode", "in_paste_mode", "vi_mode", "vi_navigation_mode", "vi_insert_mode", "vi_insert_multiple_mode", "vi_replace_mode", "vi_selection_mode", "vi_waiting_for_text_object_mode", "vi_digraph_mode", "vi_recording_macro", "emacs_mode", "emacs_insert_mode", "emacs_selection_mode", "shift_selection_mode", "is_searching", "control_is_searchable", "vi_search_direction_reversed", # base. "Filter", "Never", "Always", "Condition", "FilterOrBool", # utils. "is_true", "to_filter", ] from .cli import __all__ as cli_all __all__.extend(cli_all) ================================================ FILE: src/prompt_toolkit/filters/app.py ================================================ """ Filters that accept a `Application` as argument. """ from __future__ import annotations from typing import TYPE_CHECKING from prompt_toolkit.application.current import get_app from prompt_toolkit.cache import memoized from prompt_toolkit.enums import EditingMode from .base import Condition if TYPE_CHECKING: from prompt_toolkit.layout.layout import FocusableElement __all__ = [ "has_arg", "has_completions", "completion_is_selected", "has_focus", "buffer_has_focus", "has_selection", "has_suggestion", "has_validation_error", "is_done", "is_read_only", "is_multiline", "renderer_height_is_known", "in_editing_mode", "in_paste_mode", "vi_mode", "vi_navigation_mode", "vi_insert_mode", "vi_insert_multiple_mode", "vi_replace_mode", "vi_selection_mode", "vi_waiting_for_text_object_mode", "vi_digraph_mode", "vi_recording_macro", "emacs_mode", "emacs_insert_mode", "emacs_selection_mode", "shift_selection_mode", "is_searching", "control_is_searchable", "vi_search_direction_reversed", ] # NOTE: `has_focus` below should *not* be `memoized`. It can reference any user # control. For instance, if we would continuously create new # `PromptSession` instances, then previous instances won't be released, # because this memoize (which caches results in the global scope) will # still refer to each instance. def has_focus(value: FocusableElement) -> Condition: """ Enable when this buffer has the focus. """ from prompt_toolkit.buffer import Buffer from prompt_toolkit.layout import walk from prompt_toolkit.layout.containers import Window, to_container from prompt_toolkit.layout.controls import UIControl if isinstance(value, str): def test() -> bool: return get_app().current_buffer.name == value elif isinstance(value, Buffer): def test() -> bool: return get_app().current_buffer == value elif isinstance(value, UIControl): def test() -> bool: return get_app().layout.current_control == value else: value = to_container(value) if isinstance(value, Window): def test() -> bool: return get_app().layout.current_window == value else: def test() -> bool: # Consider focused when any window inside this container is # focused. current_window = get_app().layout.current_window for c in walk(value): if isinstance(c, Window) and c == current_window: return True return False @Condition def has_focus_filter() -> bool: return test() return has_focus_filter @Condition def buffer_has_focus() -> bool: """ Enabled when the currently focused control is a `BufferControl`. """ return get_app().layout.buffer_has_focus @Condition def has_selection() -> bool: """ Enable when the current buffer has a selection. """ return bool(get_app().current_buffer.selection_state) @Condition def has_suggestion() -> bool: """ Enable when the current buffer has a suggestion. """ buffer = get_app().current_buffer return buffer.suggestion is not None and buffer.suggestion.text != "" @Condition def has_completions() -> bool: """ Enable when the current buffer has completions. """ state = get_app().current_buffer.complete_state return state is not None and len(state.completions) > 0 @Condition def completion_is_selected() -> bool: """ True when the user selected a completion. """ complete_state = get_app().current_buffer.complete_state return complete_state is not None and complete_state.current_completion is not None @Condition def is_read_only() -> bool: """ True when the current buffer is read only. """ return get_app().current_buffer.read_only() @Condition def is_multiline() -> bool: """ True when the current buffer has been marked as multiline. """ return get_app().current_buffer.multiline() @Condition def has_validation_error() -> bool: "Current buffer has validation error." return get_app().current_buffer.validation_error is not None @Condition def has_arg() -> bool: "Enable when the input processor has an 'arg'." return get_app().key_processor.arg is not None @Condition def is_done() -> bool: """ True when the CLI is returning, aborting or exiting. """ return get_app().is_done @Condition def renderer_height_is_known() -> bool: """ Only True when the renderer knows it's real height. (On VT100 terminals, we have to wait for a CPR response, before we can be sure of the available height between the cursor position and the bottom of the terminal. And usually it's nicer to wait with drawing bottom toolbars until we receive the height, in order to avoid flickering -- first drawing somewhere in the middle, and then again at the bottom.) """ return get_app().renderer.height_is_known @memoized() def in_editing_mode(editing_mode: EditingMode) -> Condition: """ Check whether a given editing mode is active. (Vi or Emacs.) """ @Condition def in_editing_mode_filter() -> bool: return get_app().editing_mode == editing_mode return in_editing_mode_filter @Condition def in_paste_mode() -> bool: return get_app().paste_mode() @Condition def vi_mode() -> bool: return get_app().editing_mode == EditingMode.VI @Condition def vi_navigation_mode() -> bool: """ Active when the set for Vi navigation key bindings are active. """ from prompt_toolkit.key_binding.vi_state import InputMode app = get_app() if ( app.editing_mode != EditingMode.VI or app.vi_state.operator_func or app.vi_state.waiting_for_digraph or app.current_buffer.selection_state ): return False return ( app.vi_state.input_mode == InputMode.NAVIGATION or app.vi_state.temporary_navigation_mode or app.current_buffer.read_only() ) @Condition def vi_insert_mode() -> bool: from prompt_toolkit.key_binding.vi_state import InputMode app = get_app() if ( app.editing_mode != EditingMode.VI or app.vi_state.operator_func or app.vi_state.waiting_for_digraph or app.current_buffer.selection_state or app.vi_state.temporary_navigation_mode or app.current_buffer.read_only() ): return False return app.vi_state.input_mode == InputMode.INSERT @Condition def vi_insert_multiple_mode() -> bool: from prompt_toolkit.key_binding.vi_state import InputMode app = get_app() if ( app.editing_mode != EditingMode.VI or app.vi_state.operator_func or app.vi_state.waiting_for_digraph or app.current_buffer.selection_state or app.vi_state.temporary_navigation_mode or app.current_buffer.read_only() ): return False return app.vi_state.input_mode == InputMode.INSERT_MULTIPLE @Condition def vi_replace_mode() -> bool: from prompt_toolkit.key_binding.vi_state import InputMode app = get_app() if ( app.editing_mode != EditingMode.VI or app.vi_state.operator_func or app.vi_state.waiting_for_digraph or app.current_buffer.selection_state or app.vi_state.temporary_navigation_mode or app.current_buffer.read_only() ): return False return app.vi_state.input_mode == InputMode.REPLACE @Condition def vi_replace_single_mode() -> bool: from prompt_toolkit.key_binding.vi_state import InputMode app = get_app() if ( app.editing_mode != EditingMode.VI or app.vi_state.operator_func or app.vi_state.waiting_for_digraph or app.current_buffer.selection_state or app.vi_state.temporary_navigation_mode or app.current_buffer.read_only() ): return False return app.vi_state.input_mode == InputMode.REPLACE_SINGLE @Condition def vi_selection_mode() -> bool: app = get_app() if app.editing_mode != EditingMode.VI: return False return bool(app.current_buffer.selection_state) @Condition def vi_waiting_for_text_object_mode() -> bool: app = get_app() if app.editing_mode != EditingMode.VI: return False return app.vi_state.operator_func is not None @Condition def vi_digraph_mode() -> bool: app = get_app() if app.editing_mode != EditingMode.VI: return False return app.vi_state.waiting_for_digraph @Condition def vi_recording_macro() -> bool: "When recording a Vi macro." app = get_app() if app.editing_mode != EditingMode.VI: return False return app.vi_state.recording_register is not None @Condition def emacs_mode() -> bool: "When the Emacs bindings are active." return get_app().editing_mode == EditingMode.EMACS @Condition def emacs_insert_mode() -> bool: app = get_app() if ( app.editing_mode != EditingMode.EMACS or app.current_buffer.selection_state or app.current_buffer.read_only() ): return False return True @Condition def emacs_selection_mode() -> bool: app = get_app() return bool( app.editing_mode == EditingMode.EMACS and app.current_buffer.selection_state ) @Condition def shift_selection_mode() -> bool: app = get_app() return bool( app.current_buffer.selection_state and app.current_buffer.selection_state.shift_mode ) @Condition def is_searching() -> bool: "When we are searching." app = get_app() return app.layout.is_searching @Condition def control_is_searchable() -> bool: "When the current UIControl is searchable." from prompt_toolkit.layout.controls import BufferControl control = get_app().layout.current_control return ( isinstance(control, BufferControl) and control.search_buffer_control is not None ) @Condition def vi_search_direction_reversed() -> bool: "When the '/' and '?' key bindings for Vi-style searching have been reversed." return get_app().reverse_vi_search_direction() ================================================ FILE: src/prompt_toolkit/filters/base.py ================================================ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable, Iterable __all__ = ["Filter", "Never", "Always", "Condition", "FilterOrBool"] class Filter(metaclass=ABCMeta): """ Base class for any filter to activate/deactivate a feature, depending on a condition. The return value of ``__call__`` will tell if the feature should be active. """ def __init__(self) -> None: self._and_cache: dict[Filter, Filter] = {} self._or_cache: dict[Filter, Filter] = {} self._invert_result: Filter | None = None @abstractmethod def __call__(self) -> bool: """ The actual call to evaluate the filter. """ return True def __and__(self, other: Filter) -> Filter: """ Chaining of filters using the & operator. """ assert isinstance(other, Filter), f"Expecting filter, got {other!r}" if isinstance(other, Always): return self if isinstance(other, Never): return other if other in self._and_cache: return self._and_cache[other] result = _AndList.create([self, other]) self._and_cache[other] = result return result def __or__(self, other: Filter) -> Filter: """ Chaining of filters using the | operator. """ assert isinstance(other, Filter), f"Expecting filter, got {other!r}" if isinstance(other, Always): return other if isinstance(other, Never): return self if other in self._or_cache: return self._or_cache[other] result = _OrList.create([self, other]) self._or_cache[other] = result return result def __invert__(self) -> Filter: """ Inverting of filters using the ~ operator. """ if self._invert_result is None: self._invert_result = _Invert(self) return self._invert_result def __bool__(self) -> None: """ By purpose, we don't allow bool(...) operations directly on a filter, because the meaning is ambiguous. Executing a filter has to be done always by calling it. Providing defaults for `None` values should be done through an `is None` check instead of for instance ``filter1 or Always()``. """ raise ValueError( "The truth value of a Filter is ambiguous. Instead, call it as a function." ) def _remove_duplicates(filters: list[Filter]) -> list[Filter]: result = [] for f in filters: if f not in result: result.append(f) return result class _AndList(Filter): """ Result of &-operation between several filters. """ def __init__(self, filters: list[Filter]) -> None: super().__init__() self.filters = filters @classmethod def create(cls, filters: Iterable[Filter]) -> Filter: """ Create a new filter by applying an `&` operator between them. If there's only one unique filter in the given iterable, it will return that one filter instead of an `_AndList`. """ filters_2: list[Filter] = [] for f in filters: if isinstance(f, _AndList): # Turn nested _AndLists into one. filters_2.extend(f.filters) else: filters_2.append(f) # Remove duplicates. This could speed up execution, and doesn't make a # difference for the evaluation. filters = _remove_duplicates(filters_2) # If only one filter is left, return that without wrapping into an # `_AndList`. if len(filters) == 1: return filters[0] return cls(filters) def __call__(self) -> bool: return all(f() for f in self.filters) def __repr__(self) -> str: return "&".join(repr(f) for f in self.filters) class _OrList(Filter): """ Result of |-operation between several filters. """ def __init__(self, filters: list[Filter]) -> None: super().__init__() self.filters = filters @classmethod def create(cls, filters: Iterable[Filter]) -> Filter: """ Create a new filter by applying an `|` operator between them. If there's only one unique filter in the given iterable, it will return that one filter instead of an `_OrList`. """ filters_2: list[Filter] = [] for f in filters: if isinstance(f, _OrList): # Turn nested _AndLists into one. filters_2.extend(f.filters) else: filters_2.append(f) # Remove duplicates. This could speed up execution, and doesn't make a # difference for the evaluation. filters = _remove_duplicates(filters_2) # If only one filter is left, return that without wrapping into an # `_AndList`. if len(filters) == 1: return filters[0] return cls(filters) def __call__(self) -> bool: return any(f() for f in self.filters) def __repr__(self) -> str: return "|".join(repr(f) for f in self.filters) class _Invert(Filter): """ Negation of another filter. """ def __init__(self, filter: Filter) -> None: super().__init__() self.filter = filter def __call__(self) -> bool: return not self.filter() def __repr__(self) -> str: return f"~{self.filter!r}" class Always(Filter): """ Always enable feature. """ def __call__(self) -> bool: return True def __or__(self, other: Filter) -> Filter: return self def __and__(self, other: Filter) -> Filter: return other def __invert__(self) -> Never: return Never() class Never(Filter): """ Never enable feature. """ def __call__(self) -> bool: return False def __and__(self, other: Filter) -> Filter: return self def __or__(self, other: Filter) -> Filter: return other def __invert__(self) -> Always: return Always() class Condition(Filter): """ Turn any callable into a Filter. The callable is supposed to not take any arguments. This can be used as a decorator:: @Condition def feature_is_active(): # `feature_is_active` becomes a Filter. return True :param func: Callable which takes no inputs and returns a boolean. """ def __init__(self, func: Callable[[], bool]) -> None: super().__init__() self.func = func def __call__(self) -> bool: return self.func() def __repr__(self) -> str: return f"Condition({self.func!r})" # Often used as type annotation. FilterOrBool = Filter | bool ================================================ FILE: src/prompt_toolkit/filters/cli.py ================================================ """ For backwards-compatibility. keep this file. (Many people are going to have key bindings that rely on this file.) """ from __future__ import annotations from .app import * __all__ = [ # Old names. "HasArg", "HasCompletions", "HasFocus", "HasSelection", "HasValidationError", "IsDone", "IsReadOnly", "IsMultiline", "RendererHeightIsKnown", "InEditingMode", "InPasteMode", "ViMode", "ViNavigationMode", "ViInsertMode", "ViInsertMultipleMode", "ViReplaceMode", "ViSelectionMode", "ViWaitingForTextObjectMode", "ViDigraphMode", "EmacsMode", "EmacsInsertMode", "EmacsSelectionMode", "IsSearching", "HasSearch", "ControlIsSearchable", ] # Keep the original classnames for backwards compatibility. HasValidationError = lambda: has_validation_error HasArg = lambda: has_arg IsDone = lambda: is_done RendererHeightIsKnown = lambda: renderer_height_is_known ViNavigationMode = lambda: vi_navigation_mode InPasteMode = lambda: in_paste_mode EmacsMode = lambda: emacs_mode EmacsInsertMode = lambda: emacs_insert_mode ViMode = lambda: vi_mode IsSearching = lambda: is_searching HasSearch = lambda: is_searching ControlIsSearchable = lambda: control_is_searchable EmacsSelectionMode = lambda: emacs_selection_mode ViDigraphMode = lambda: vi_digraph_mode ViWaitingForTextObjectMode = lambda: vi_waiting_for_text_object_mode ViSelectionMode = lambda: vi_selection_mode ViReplaceMode = lambda: vi_replace_mode ViInsertMultipleMode = lambda: vi_insert_multiple_mode ViInsertMode = lambda: vi_insert_mode HasSelection = lambda: has_selection HasCompletions = lambda: has_completions IsReadOnly = lambda: is_read_only IsMultiline = lambda: is_multiline HasFocus = has_focus # No lambda here! (Has_focus is callable that returns a callable.) InEditingMode = in_editing_mode ================================================ FILE: src/prompt_toolkit/filters/utils.py ================================================ from __future__ import annotations from .base import Always, Filter, FilterOrBool, Never __all__ = [ "to_filter", "is_true", ] _always = Always() _never = Never() _bool_to_filter: dict[bool, Filter] = { True: _always, False: _never, } def to_filter(bool_or_filter: FilterOrBool) -> Filter: """ Accept both booleans and Filters as input and turn it into a Filter. """ if isinstance(bool_or_filter, bool): return _bool_to_filter[bool_or_filter] if isinstance(bool_or_filter, Filter): return bool_or_filter raise TypeError(f"Expecting a bool or a Filter instance. Got {bool_or_filter!r}") def is_true(value: FilterOrBool) -> bool: """ Test whether `value` is True. In case of a Filter, call it. :param value: Boolean or `Filter` instance. """ return to_filter(value)() ================================================ FILE: src/prompt_toolkit/formatted_text/__init__.py ================================================ """ Many places in prompt_toolkit can take either plain text, or formatted text. For instance the :func:`~prompt_toolkit.shortcuts.prompt` function takes either plain text or formatted text for the prompt. The :class:`~prompt_toolkit.layout.FormattedTextControl` can also take either plain text or formatted text. In any case, there is an input that can either be just plain text (a string), an :class:`.HTML` object, an :class:`.ANSI` object or a sequence of `(style_string, text)` tuples. The :func:`.to_formatted_text` conversion function takes any of these and turns all of them into such a tuple sequence. """ from __future__ import annotations from .ansi import ANSI from .base import ( AnyFormattedText, FormattedText, OneStyleAndTextTuple, StyleAndTextTuples, Template, is_formatted_text, merge_formatted_text, to_formatted_text, ) from .html import HTML from .pygments import PygmentsTokens from .utils import ( fragment_list_len, fragment_list_to_text, fragment_list_width, split_lines, to_plain_text, ) __all__ = [ # Base. "AnyFormattedText", "OneStyleAndTextTuple", "to_formatted_text", "is_formatted_text", "Template", "merge_formatted_text", "FormattedText", "StyleAndTextTuples", # HTML. "HTML", # ANSI. "ANSI", # Pygments. "PygmentsTokens", # Utils. "fragment_list_len", "fragment_list_width", "fragment_list_to_text", "split_lines", "to_plain_text", ] ================================================ FILE: src/prompt_toolkit/formatted_text/ansi.py ================================================ from __future__ import annotations from collections.abc import Generator from string import Formatter from prompt_toolkit.output.vt100 import BG_ANSI_COLORS, FG_ANSI_COLORS from prompt_toolkit.output.vt100 import _256_colors as _256_colors_table from .base import StyleAndTextTuples __all__ = [ "ANSI", "ansi_escape", ] class ANSI: """ ANSI formatted text. Take something ANSI escaped text, for use as a formatted string. E.g. :: ANSI('\\x1b[31mhello \\x1b[32mworld') Characters between ``\\001`` and ``\\002`` are supposed to have a zero width when printed, but these are literally sent to the terminal output. This can be used for instance, for inserting Final Term prompt commands. They will be translated into a prompt_toolkit '[ZeroWidthEscape]' fragment. """ def __init__(self, value: str) -> None: self.value = value self._formatted_text: StyleAndTextTuples = [] # Default style attributes. self._color: str | None = None self._bgcolor: str | None = None self._bold = False self._dim = False self._underline = False self._strike = False self._italic = False self._blink = False self._reverse = False self._hidden = False # Process received text. parser = self._parse_corot() parser.send(None) # type: ignore for c in value: parser.send(c) def _parse_corot(self) -> Generator[None, str, None]: """ Coroutine that parses the ANSI escape sequences. """ style = "" formatted_text = self._formatted_text while True: # NOTE: CSI is a special token within a stream of characters that # introduces an ANSI control sequence used to set the # style attributes of the following characters. csi = False c = yield # Everything between \001 and \002 should become a ZeroWidthEscape. if c == "\001": escaped_text = "" while c != "\002": c = yield if c == "\002": formatted_text.append(("[ZeroWidthEscape]", escaped_text)) c = yield break else: escaped_text += c # Check for CSI if c == "\x1b": # Start of color escape sequence. square_bracket = yield if square_bracket == "[": csi = True else: continue elif c == "\x9b": csi = True if csi: # Got a CSI sequence. Color codes are following. current = "" params = [] while True: char = yield # Construct number if char.isdigit(): current += char # Eval number else: # Limit and save number value params.append(min(int(current or 0), 9999)) # Get delimiter token if present if char == ";": current = "" # Check and evaluate color codes elif char == "m": # Set attributes and token. self._select_graphic_rendition(params) style = self._create_style_string() break # Check and evaluate cursor forward elif char == "C": for i in range(params[0]): # add <SPACE> using current style formatted_text.append((style, " ")) break else: # Ignore unsupported sequence. break else: # Add current character. # NOTE: At this point, we could merge the current character # into the previous tuple if the style did not change, # however, it's not worth the effort given that it will # be "Exploded" once again when it's rendered to the # output. formatted_text.append((style, c)) def _select_graphic_rendition(self, attrs: list[int]) -> None: """ Taken a list of graphics attributes and apply changes. """ if not attrs: attrs = [0] else: attrs = list(attrs[::-1]) while attrs: attr = attrs.pop() if attr in _fg_colors: self._color = _fg_colors[attr] elif attr in _bg_colors: self._bgcolor = _bg_colors[attr] elif attr == 1: self._bold = True elif attr == 2: self._dim = True elif attr == 3: self._italic = True elif attr == 4: self._underline = True elif attr == 5: self._blink = True # Slow blink elif attr == 6: self._blink = True # Fast blink elif attr == 7: self._reverse = True elif attr == 8: self._hidden = True elif attr == 9: self._strike = True elif attr == 22: self._bold = False # Normal intensity self._dim = False elif attr == 23: self._italic = False elif attr == 24: self._underline = False elif attr == 25: self._blink = False elif attr == 27: self._reverse = False elif attr == 28: self._hidden = False elif attr == 29: self._strike = False elif not attr: # Reset all style attributes self._color = None self._bgcolor = None self._bold = False self._dim = False self._underline = False self._strike = False self._italic = False self._blink = False self._reverse = False self._hidden = False elif attr in (38, 48) and len(attrs) > 1: n = attrs.pop() # 256 colors. if n == 5 and len(attrs) >= 1: if attr == 38: m = attrs.pop() self._color = _256_colors.get(m) elif attr == 48: m = attrs.pop() self._bgcolor = _256_colors.get(m) # True colors. if n == 2 and len(attrs) >= 3: try: color_str = ( f"#{attrs.pop():02x}{attrs.pop():02x}{attrs.pop():02x}" ) except IndexError: pass else: if attr == 38: self._color = color_str elif attr == 48: self._bgcolor = color_str def _create_style_string(self) -> str: """ Turn current style flags into a string for usage in a formatted text. """ result = [] if self._color: result.append(self._color) if self._bgcolor: result.append("bg:" + self._bgcolor) if self._bold: result.append("bold") if self._dim: result.append("dim") if self._underline: result.append("underline") if self._strike: result.append("strike") if self._italic: result.append("italic") if self._blink: result.append("blink") if self._reverse: result.append("reverse") if self._hidden: result.append("hidden") return " ".join(result) def __repr__(self) -> str: return f"ANSI({self.value!r})" def __pt_formatted_text__(self) -> StyleAndTextTuples: return self._formatted_text def format(self, *args: str, **kwargs: str) -> ANSI: """ Like `str.format`, but make sure that the arguments are properly escaped. (No ANSI escapes can be injected.) """ return ANSI(FORMATTER.vformat(self.value, args, kwargs)) def __mod__(self, value: object) -> ANSI: """ ANSI('<b>%s</b>') % value """ if not isinstance(value, tuple): value = (value,) value = tuple(ansi_escape(i) for i in value) return ANSI(self.value % value) # Mapping of the ANSI color codes to their names. _fg_colors = {v: k for k, v in FG_ANSI_COLORS.items()} _bg_colors = {v: k for k, v in BG_ANSI_COLORS.items()} # Mapping of the escape codes for 256colors to their 'ffffff' value. _256_colors = {} for i, (r, g, b) in enumerate(_256_colors_table.colors): _256_colors[i] = f"#{r:02x}{g:02x}{b:02x}" def ansi_escape(text: object) -> str: """ Replace characters with a special meaning. """ return str(text).replace("\x1b", "?").replace("\b", "?") class ANSIFormatter(Formatter): def format_field(self, value: object, format_spec: str) -> str: return ansi_escape(format(value, format_spec)) FORMATTER = ANSIFormatter() ================================================ FILE: src/prompt_toolkit/formatted_text/base.py ================================================ from __future__ import annotations from collections.abc import Callable, Iterable from typing import TYPE_CHECKING, Union, cast from prompt_toolkit.mouse_events import MouseEvent if TYPE_CHECKING: from typing_extensions import Protocol from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone __all__ = [ "OneStyleAndTextTuple", "StyleAndTextTuples", "MagicFormattedText", "AnyFormattedText", "to_formatted_text", "is_formatted_text", "Template", "merge_formatted_text", "FormattedText", ] OneStyleAndTextTuple = ( tuple[str, str] | tuple[str, str, Callable[[MouseEvent], "NotImplementedOrNone"]] ) # List of (style, text) tuples. StyleAndTextTuples = list[OneStyleAndTextTuple] if TYPE_CHECKING: from typing import TypeGuard class MagicFormattedText(Protocol): """ Any object that implements ``__pt_formatted_text__`` represents formatted text. """ def __pt_formatted_text__(self) -> StyleAndTextTuples: ... AnyFormattedText = Union[ str, "MagicFormattedText", StyleAndTextTuples, Callable[[], "AnyFormattedText"], None, ] def to_formatted_text( value: AnyFormattedText, style: str = "", auto_convert: bool = False ) -> FormattedText: """ Convert the given value (which can be formatted text) into a list of text fragments. (Which is the canonical form of formatted text.) The outcome is always a `FormattedText` instance, which is a list of (style, text) tuples. It can take a plain text string, an `HTML` or `ANSI` object, anything that implements `__pt_formatted_text__` or a callable that takes no arguments and returns one of those. :param style: An additional style string which is applied to all text fragments. :param auto_convert: If `True`, also accept other types, and convert them to a string first. """ result: FormattedText | StyleAndTextTuples if value is None: result = [] elif isinstance(value, str): result = [("", value)] elif isinstance(value, list): result = value # StyleAndTextTuples elif hasattr(value, "__pt_formatted_text__"): result = cast("MagicFormattedText", value).__pt_formatted_text__() elif callable(value): return to_formatted_text(value(), style=style) elif auto_convert: result = [("", f"{value}")] else: raise ValueError( f"No formatted text. Expecting a unicode object, HTML, ANSI or a FormattedText instance. Got {value!r}" ) # Apply extra style. if style: result = cast( StyleAndTextTuples, [(style + " " + item_style, *rest) for item_style, *rest in result], ) # Make sure the result is wrapped in a `FormattedText`. Among other # reasons, this is important for `print_formatted_text` to work correctly # and distinguish between lists and formatted text. if isinstance(result, FormattedText): return result else: return FormattedText(result) def is_formatted_text(value: object) -> TypeGuard[AnyFormattedText]: """ Check whether the input is valid formatted text (for use in assert statements). In case of a callable, it doesn't check the return type. """ if callable(value): return True if isinstance(value, (str, list)): return True if hasattr(value, "__pt_formatted_text__"): return True return False class FormattedText(StyleAndTextTuples): """ A list of ``(style, text)`` tuples. (In some situations, this can also be ``(style, text, mouse_handler)`` tuples.) """ def __pt_formatted_text__(self) -> StyleAndTextTuples: return self def __repr__(self) -> str: return f"FormattedText({super().__repr__()})" class Template: """ Template for string interpolation with formatted text. Example:: Template(' ... {} ... ').format(HTML(...)) :param text: Plain text. """ def __init__(self, text: str) -> None: assert "{0}" not in text self.text = text def format(self, *values: AnyFormattedText) -> AnyFormattedText: def get_result() -> AnyFormattedText: # Split the template in parts. parts = self.text.split("{}") assert len(parts) - 1 == len(values) result = FormattedText() for part, val in zip(parts, values): result.append(("", part)) result.extend(to_formatted_text(val)) result.append(("", parts[-1])) return result return get_result def merge_formatted_text(items: Iterable[AnyFormattedText]) -> AnyFormattedText: """ Merge (Concatenate) several pieces of formatted text together. """ def _merge_formatted_text() -> AnyFormattedText: result = FormattedText() for i in items: result.extend(to_formatted_text(i)) return result return _merge_formatted_text ================================================ FILE: src/prompt_toolkit/formatted_text/html.py ================================================ from __future__ import annotations import xml.dom.minidom as minidom from string import Formatter from typing import Any from .base import FormattedText, StyleAndTextTuples __all__ = ["HTML"] class HTML: """ HTML formatted text. Take something HTML-like, for use as a formatted string. :: # Turn something into red. HTML('<style fg="ansired" bg="#00ff44">...</style>') # Italic, bold, underline and strike. HTML('<i>...</i>') HTML('<b>...</b>') HTML('<u>...</u>') HTML('<s>...</s>') All HTML elements become available as a "class" in the style sheet. E.g. ``<username>...</username>`` can be styled, by setting a style for ``username``. """ def __init__(self, value: str) -> None: self.value = value document = minidom.parseString(f"<html-root>{value}</html-root>") result: StyleAndTextTuples = [] name_stack: list[str] = [] fg_stack: list[str] = [] bg_stack: list[str] = [] def get_current_style() -> str: "Build style string for current node." parts = [] if name_stack: parts.append("class:" + ",".join(name_stack)) if fg_stack: parts.append("fg:" + fg_stack[-1]) if bg_stack: parts.append("bg:" + bg_stack[-1]) return " ".join(parts) def process_node(node: Any) -> None: "Process node recursively." for child in node.childNodes: if child.nodeType == child.TEXT_NODE: result.append((get_current_style(), child.data)) else: add_to_name_stack = child.nodeName not in ( "#document", "html-root", "style", ) fg = bg = "" for k, v in child.attributes.items(): if k == "fg": fg = v if k == "bg": bg = v if k == "color": fg = v # Alias for 'fg'. # Check for spaces in attributes. This would result in # invalid style strings otherwise. if " " in fg: raise ValueError('"fg" attribute contains a space.') if " " in bg: raise ValueError('"bg" attribute contains a space.') if add_to_name_stack: name_stack.append(child.nodeName) if fg: fg_stack.append(fg) if bg: bg_stack.append(bg) process_node(child) if add_to_name_stack: name_stack.pop() if fg: fg_stack.pop() if bg: bg_stack.pop() process_node(document) self.formatted_text = FormattedText(result) def __repr__(self) -> str: return f"HTML({self.value!r})" def __pt_formatted_text__(self) -> StyleAndTextTuples: return self.formatted_text def format(self, *args: object, **kwargs: object) -> HTML: """ Like `str.format`, but make sure that the arguments are properly escaped. """ return HTML(FORMATTER.vformat(self.value, args, kwargs)) def __mod__(self, value: object) -> HTML: """ HTML('<b>%s</b>') % value """ if not isinstance(value, tuple): value = (value,) value = tuple(html_escape(i) for i in value) return HTML(self.value % value) class HTMLFormatter(Formatter): def format_field(self, value: object, format_spec: str) -> str: return html_escape(format(value, format_spec)) def html_escape(text: object) -> str: # The string interpolation functions also take integers and other types. # Convert to string first. if not isinstance(text, str): text = f"{text}" return ( text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """) ) FORMATTER = HTMLFormatter() ================================================ FILE: src/prompt_toolkit/formatted_text/pygments.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from prompt_toolkit.styles.pygments import pygments_token_to_classname from .base import StyleAndTextTuples if TYPE_CHECKING: from pygments.token import Token __all__ = [ "PygmentsTokens", ] class PygmentsTokens: """ Turn a pygments token list into a list of prompt_toolkit text fragments (``(style_str, text)`` tuples). """ def __init__(self, token_list: list[tuple[Token, str]]) -> None: self.token_list = token_list def __pt_formatted_text__(self) -> StyleAndTextTuples: result: StyleAndTextTuples = [] for token, text in self.token_list: result.append(("class:" + pygments_token_to_classname(token), text)) return result ================================================ FILE: src/prompt_toolkit/formatted_text/utils.py ================================================ """ Utilities for manipulating formatted text. When ``to_formatted_text`` has been called, we get a list of ``(style, text)`` tuples. This file contains functions for manipulating such a list. """ from __future__ import annotations from collections.abc import Iterable from typing import cast from prompt_toolkit.utils import get_cwidth from .base import ( AnyFormattedText, OneStyleAndTextTuple, StyleAndTextTuples, to_formatted_text, ) __all__ = [ "to_plain_text", "fragment_list_len", "fragment_list_width", "fragment_list_to_text", "split_lines", ] def to_plain_text(value: AnyFormattedText) -> str: """ Turn any kind of formatted text back into plain text. """ return fragment_list_to_text(to_formatted_text(value)) def fragment_list_len(fragments: StyleAndTextTuples) -> int: """ Return the amount of characters in this text fragment list. :param fragments: List of ``(style_str, text)`` or ``(style_str, text, mouse_handler)`` tuples. """ ZeroWidthEscape = "[ZeroWidthEscape]" return sum(len(item[1]) for item in fragments if ZeroWidthEscape not in item[0]) def fragment_list_width(fragments: StyleAndTextTuples) -> int: """ Return the character width of this text fragment list. (Take double width characters into account.) :param fragments: List of ``(style_str, text)`` or ``(style_str, text, mouse_handler)`` tuples. """ ZeroWidthEscape = "[ZeroWidthEscape]" return sum( get_cwidth(c) for item in fragments for c in item[1] if ZeroWidthEscape not in item[0] ) def fragment_list_to_text(fragments: StyleAndTextTuples) -> str: """ Concatenate all the text parts again. :param fragments: List of ``(style_str, text)`` or ``(style_str, text, mouse_handler)`` tuples. """ ZeroWidthEscape = "[ZeroWidthEscape]" return "".join(item[1] for item in fragments if ZeroWidthEscape not in item[0]) def split_lines( fragments: Iterable[OneStyleAndTextTuple], ) -> Iterable[StyleAndTextTuples]: """ Take a single list of (style_str, text) tuples and yield one such list for each line. Just like str.split, this will yield at least one item. :param fragments: Iterable of ``(style_str, text)`` or ``(style_str, text, mouse_handler)`` tuples. """ line: StyleAndTextTuples = [] for style, string, *mouse_handler in fragments: parts = string.split("\n") for part in parts[:-1]: line.append(cast(OneStyleAndTextTuple, (style, part, *mouse_handler))) yield line line = [] line.append(cast(OneStyleAndTextTuple, (style, parts[-1], *mouse_handler))) # Always yield the last line, even when this is an empty line. This ensures # that when `fragments` ends with a newline character, an additional empty # line is yielded. (Otherwise, there's no way to differentiate between the # cases where `fragments` does and doesn't end with a newline.) yield line ================================================ FILE: src/prompt_toolkit/history.py ================================================ """ Implementations for the history of a `Buffer`. NOTE: There is no `DynamicHistory`: This doesn't work well, because the `Buffer` needs to be able to attach an event handler to the event when a history entry is loaded. This loading can be done asynchronously and making the history swappable would probably break this. """ from __future__ import annotations import datetime import os import threading from abc import ABCMeta, abstractmethod from asyncio import get_running_loop from collections.abc import AsyncGenerator, Iterable, Sequence from typing import Union __all__ = [ "History", "ThreadedHistory", "DummyHistory", "FileHistory", "InMemoryHistory", ] class History(metaclass=ABCMeta): """ Base ``History`` class. This also includes abstract methods for loading/storing history. """ def __init__(self) -> None: # In memory storage for strings. self._loaded = False # History that's loaded already, in reverse order. Latest, most recent # item first. self._loaded_strings: list[str] = [] # # Methods expected by `Buffer`. # async def load(self) -> AsyncGenerator[str, None]: """ Load the history and yield all the entries in reverse order (latest, most recent history entry first). This method can be called multiple times from the `Buffer` to repopulate the history when prompting for a new input. So we are responsible here for both caching, and making sure that strings that were were appended to the history will be incorporated next time this method is called. """ if not self._loaded: self._loaded_strings = list(self.load_history_strings()) self._loaded = True for item in self._loaded_strings: yield item def get_strings(self) -> list[str]: """ Get the strings from the history that are loaded so far. (In order. Oldest item first.) """ return self._loaded_strings[::-1] def append_string(self, string: str) -> None: "Add string to the history." self._loaded_strings.insert(0, string) self.store_string(string) # # Implementation for specific backends. # @abstractmethod def load_history_strings(self) -> Iterable[str]: """ This should be a generator that yields `str` instances. It should yield the most recent items first, because they are the most important. (The history can already be used, even when it's only partially loaded.) """ while False: yield @abstractmethod def store_string(self, string: str) -> None: """ Store the string in persistent storage. """ class ThreadedHistory(History): """ Wrapper around `History` implementations that run the `load()` generator in a thread. Use this to increase the start-up time of prompt_toolkit applications. History entries are available as soon as they are loaded. We don't have to wait for everything to be loaded. """ def __init__(self, history: History) -> None: super().__init__() self.history = history self._load_thread: threading.Thread | None = None # Lock for accessing/manipulating `_loaded_strings` and `_loaded` # together in a consistent state. self._lock = threading.Lock() # Events created by each `load()` call. Used to wait for new history # entries from the loader thread. self._string_load_events: list[threading.Event] = [] async def load(self) -> AsyncGenerator[str, None]: """ Like `History.load(), but call `self.load_history_strings()` in a background thread. """ # Start the load thread, if this is called for the first time. if not self._load_thread: self._load_thread = threading.Thread( target=self._in_load_thread, daemon=True, ) self._load_thread.start() # Consume the `_loaded_strings` list, using asyncio. loop = get_running_loop() # Create threading Event so that we can wait for new items. event = threading.Event() event.set() self._string_load_events.append(event) items_yielded = 0 try: while True: # Wait for new items to be available. # (Use a timeout, because the executor thread is not a daemon # thread. The "slow-history.py" example would otherwise hang if # Control-C is pressed before the history is fully loaded, # because there's still this non-daemon executor thread waiting # for this event.) got_timeout = await loop.run_in_executor( None, lambda: event.wait(timeout=0.5) ) if not got_timeout: continue # Read new items (in lock). def in_executor() -> tuple[list[str], bool]: with self._lock: new_items = self._loaded_strings[items_yielded:] done = self._loaded event.clear() return new_items, done new_items, done = await loop.run_in_executor(None, in_executor) items_yielded += len(new_items) for item in new_items: yield item if done: break finally: self._string_load_events.remove(event) def _in_load_thread(self) -> None: try: # Start with an empty list. In case `append_string()` was called # before `load()` happened. Then `.store_string()` will have # written these entries back to disk and we will reload it. self._loaded_strings = [] for item in self.history.load_history_strings(): with self._lock: self._loaded_strings.append(item) for event in self._string_load_events: event.set() finally: with self._lock: self._loaded = True for event in self._string_load_events: event.set() def append_string(self, string: str) -> None: with self._lock: self._loaded_strings.insert(0, string) self.store_string(string) # All of the following are proxied to `self.history`. def load_history_strings(self) -> Iterable[str]: return self.history.load_history_strings() def store_string(self, string: str) -> None: self.history.store_string(string) def __repr__(self) -> str: return f"ThreadedHistory({self.history!r})" class InMemoryHistory(History): """ :class:`.History` class that keeps a list of all strings in memory. In order to prepopulate the history, it's possible to call either `append_string` for all items or pass a list of strings to `__init__` here. """ def __init__(self, history_strings: Sequence[str] | None = None) -> None: super().__init__() # Emulating disk storage. if history_strings is None: self._storage = [] else: self._storage = list(history_strings) def load_history_strings(self) -> Iterable[str]: yield from self._storage[::-1] def store_string(self, string: str) -> None: self._storage.append(string) class DummyHistory(History): """ :class:`.History` object that doesn't remember anything. """ def load_history_strings(self) -> Iterable[str]: return [] def store_string(self, string: str) -> None: pass def append_string(self, string: str) -> None: # Don't remember this. pass _StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] class FileHistory(History): """ :class:`.History` class that stores all strings in a file. """ def __init__(self, filename: _StrOrBytesPath) -> None: self.filename = filename super().__init__() def load_history_strings(self) -> Iterable[str]: strings: list[str] = [] lines: list[str] = [] def add() -> None: if lines: # Join and drop trailing newline. string = "".join(lines)[:-1] strings.append(string) if os.path.exists(self.filename): with open(self.filename, "rb") as f: for line_bytes in f: line = line_bytes.decode("utf-8", errors="replace") if line.startswith("+"): lines.append(line[1:]) else: add() lines = [] add() # Reverse the order, because newest items have to go first. return reversed(strings) def store_string(self, string: str) -> None: # Save to file. with open(self.filename, "ab") as f: def write(t: str) -> None: f.write(t.encode("utf-8")) write(f"\n# {datetime.datetime.now()}\n") for line in string.split("\n"): write(f"+{line}\n") ================================================ FILE: src/prompt_toolkit/input/__init__.py ================================================ from __future__ import annotations from .base import DummyInput, Input, PipeInput from .defaults import create_input, create_pipe_input __all__ = [ # Base. "Input", "PipeInput", "DummyInput", # Defaults. "create_input", "create_pipe_input", ] ================================================ FILE: src/prompt_toolkit/input/ansi_escape_sequences.py ================================================ """ Mappings from VT100 (ANSI) escape sequences to the corresponding prompt_toolkit keys. We are not using the terminfo/termcap databases to detect the ANSI escape sequences for the input. Instead, we recognize 99% of the most common sequences. This works well, because in practice, every modern terminal is mostly Xterm compatible. Some useful docs: - Mintty: https://github.com/mintty/mintty/blob/master/wiki/Keycodes.md """ from __future__ import annotations from ..keys import Keys __all__ = [ "ANSI_SEQUENCES", "REVERSE_ANSI_SEQUENCES", ] # Mapping of vt100 escape codes to Keys. ANSI_SEQUENCES: dict[str, Keys | tuple[Keys, ...]] = { # Control keys. "\x00": Keys.ControlAt, # Control-At (Also for Ctrl-Space) "\x01": Keys.ControlA, # Control-A (home) "\x02": Keys.ControlB, # Control-B (emacs cursor left) "\x03": Keys.ControlC, # Control-C (interrupt) "\x04": Keys.ControlD, # Control-D (exit) "\x05": Keys.ControlE, # Control-E (end) "\x06": Keys.ControlF, # Control-F (cursor forward) "\x07": Keys.ControlG, # Control-G "\x08": Keys.ControlH, # Control-H (8) (Identical to '\b') "\x09": Keys.ControlI, # Control-I (9) (Identical to '\t') "\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n') "\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab) "\x0c": Keys.ControlL, # Control-L (clear; form feed) "\x0d": Keys.ControlM, # Control-M (13) (Identical to '\r') "\x0e": Keys.ControlN, # Control-N (14) (history forward) "\x0f": Keys.ControlO, # Control-O (15) "\x10": Keys.ControlP, # Control-P (16) (history back) "\x11": Keys.ControlQ, # Control-Q "\x12": Keys.ControlR, # Control-R (18) (reverse search) "\x13": Keys.ControlS, # Control-S (19) (forward search) "\x14": Keys.ControlT, # Control-T "\x15": Keys.ControlU, # Control-U "\x16": Keys.ControlV, # Control-V "\x17": Keys.ControlW, # Control-W "\x18": Keys.ControlX, # Control-X "\x19": Keys.ControlY, # Control-Y (25) "\x1a": Keys.ControlZ, # Control-Z "\x1b": Keys.Escape, # Also Control-[ "\x9b": Keys.ShiftEscape, "\x1c": Keys.ControlBackslash, # Both Control-\ (also Ctrl-| ) "\x1d": Keys.ControlSquareClose, # Control-] "\x1e": Keys.ControlCircumflex, # Control-^ "\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.) # ASCII Delete (0x7f) # Vt220 (and Linux terminal) send this when pressing backspace. We map this # to ControlH, because that will make it easier to create key bindings that # work everywhere, with the trade-off that it's no longer possible to # handle backspace and control-h individually for the few terminals that # support it. (Most terminals send ControlH when backspace is pressed.) # See: http://www.ibb.net/~anne/keyboard.html "\x7f": Keys.ControlH, # -- # Various "\x1b[1~": Keys.Home, # tmux "\x1b[2~": Keys.Insert, "\x1b[3~": Keys.Delete, "\x1b[4~": Keys.End, # tmux "\x1b[5~": Keys.PageUp, "\x1b[6~": Keys.PageDown, "\x1b[7~": Keys.Home, # xrvt "\x1b[8~": Keys.End, # xrvt "\x1b[Z": Keys.BackTab, # shift + tab "\x1b\x09": Keys.BackTab, # Linux console "\x1b[~": Keys.BackTab, # Windows console # -- # Function keys. "\x1bOP": Keys.F1, "\x1bOQ": Keys.F2, "\x1bOR": Keys.F3, "\x1bOS": Keys.F4, "\x1b[[A": Keys.F1, # Linux console. "\x1b[[B": Keys.F2, # Linux console. "\x1b[[C": Keys.F3, # Linux console. "\x1b[[D": Keys.F4, # Linux console. "\x1b[[E": Keys.F5, # Linux console. "\x1b[11~": Keys.F1, # rxvt-unicode "\x1b[12~": Keys.F2, # rxvt-unicode "\x1b[13~": Keys.F3, # rxvt-unicode "\x1b[14~": Keys.F4, # rxvt-unicode "\x1b[15~": Keys.F5, "\x1b[17~": Keys.F6, "\x1b[18~": Keys.F7, "\x1b[19~": Keys.F8, "\x1b[20~": Keys.F9, "\x1b[21~": Keys.F10, "\x1b[23~": Keys.F11, "\x1b[24~": Keys.F12, "\x1b[25~": Keys.F13, "\x1b[26~": Keys.F14, "\x1b[28~": Keys.F15, "\x1b[29~": Keys.F16, "\x1b[31~": Keys.F17, "\x1b[32~": Keys.F18, "\x1b[33~": Keys.F19, "\x1b[34~": Keys.F20, # Xterm "\x1b[1;2P": Keys.F13, "\x1b[1;2Q": Keys.F14, # '\x1b[1;2R': Keys.F15, # Conflicts with CPR response. "\x1b[1;2S": Keys.F16, "\x1b[15;2~": Keys.F17, "\x1b[17;2~": Keys.F18, "\x1b[18;2~": Keys.F19, "\x1b[19;2~": Keys.F20, "\x1b[20;2~": Keys.F21, "\x1b[21;2~": Keys.F22, "\x1b[23;2~": Keys.F23, "\x1b[24;2~": Keys.F24, # -- # CSI 27 disambiguated modified "other" keys (xterm) # Ref: https://invisible-island.net/xterm/modified-keys.html # These are currently unsupported, so just re-map some common ones to the # unmodified versions "\x1b[27;2;13~": Keys.ControlM, # Shift + Enter "\x1b[27;5;13~": Keys.ControlM, # Ctrl + Enter "\x1b[27;6;13~": Keys.ControlM, # Ctrl + Shift + Enter # -- # Control + function keys. "\x1b[1;5P": Keys.ControlF1, "\x1b[1;5Q": Keys.ControlF2, # "\x1b[1;5R": Keys.ControlF3, # Conflicts with CPR response. "\x1b[1;5S": Keys.ControlF4, "\x1b[15;5~": Keys.ControlF5, "\x1b[17;5~": Keys.ControlF6, "\x1b[18;5~": Keys.ControlF7, "\x1b[19;5~": Keys.ControlF8, "\x1b[20;5~": Keys.ControlF9, "\x1b[21;5~": Keys.ControlF10, "\x1b[23;5~": Keys.ControlF11, "\x1b[24;5~": Keys.ControlF12, "\x1b[1;6P": Keys.ControlF13, "\x1b[1;6Q": Keys.ControlF14, # "\x1b[1;6R": Keys.ControlF15, # Conflicts with CPR response. "\x1b[1;6S": Keys.ControlF16, "\x1b[15;6~": Keys.ControlF17, "\x1b[17;6~": Keys.ControlF18, "\x1b[18;6~": Keys.ControlF19, "\x1b[19;6~": Keys.ControlF20, "\x1b[20;6~": Keys.ControlF21, "\x1b[21;6~": Keys.ControlF22, "\x1b[23;6~": Keys.ControlF23, "\x1b[24;6~": Keys.ControlF24, # -- # Tmux (Win32 subsystem) sends the following scroll events. "\x1b[62~": Keys.ScrollUp, "\x1b[63~": Keys.ScrollDown, "\x1b[200~": Keys.BracketedPaste, # Start of bracketed paste. # -- # Sequences generated by numpad 5. Not sure what it means. (It doesn't # appear in 'infocmp'. Just ignore. "\x1b[E": Keys.Ignore, # Xterm. "\x1b[G": Keys.Ignore, # Linux console. # -- # Meta/control/escape + pageup/pagedown/insert/delete. "\x1b[3;2~": Keys.ShiftDelete, # xterm, gnome-terminal. "\x1b[5;2~": Keys.ShiftPageUp, "\x1b[6;2~": Keys.ShiftPageDown, "\x1b[2;3~": (Keys.Escape, Keys.Insert), "\x1b[3;3~": (Keys.Escape, Keys.Delete), "\x1b[5;3~": (Keys.Escape, Keys.PageUp), "\x1b[6;3~": (Keys.Escape, Keys.PageDown), "\x1b[2;4~": (Keys.Escape, Keys.ShiftInsert), "\x1b[3;4~": (Keys.Escape, Keys.ShiftDelete), "\x1b[5;4~": (Keys.Escape, Keys.ShiftPageUp), "\x1b[6;4~": (Keys.Escape, Keys.ShiftPageDown), "\x1b[3;5~": Keys.ControlDelete, # xterm, gnome-terminal. "\x1b[5;5~": Keys.ControlPageUp, "\x1b[6;5~": Keys.ControlPageDown, "\x1b[3;6~": Keys.ControlShiftDelete, "\x1b[5;6~": Keys.ControlShiftPageUp, "\x1b[6;6~": Keys.ControlShiftPageDown, "\x1b[2;7~": (Keys.Escape, Keys.ControlInsert), "\x1b[5;7~": (Keys.Escape, Keys.ControlPageDown), "\x1b[6;7~": (Keys.Escape, Keys.ControlPageDown), "\x1b[2;8~": (Keys.Escape, Keys.ControlShiftInsert), "\x1b[5;8~": (Keys.Escape, Keys.ControlShiftPageDown), "\x1b[6;8~": (Keys.Escape, Keys.ControlShiftPageDown), # -- # Arrows. # (Normal cursor mode). "\x1b[A": Keys.Up, "\x1b[B": Keys.Down, "\x1b[C": Keys.Right, "\x1b[D": Keys.Left, "\x1b[H": Keys.Home, "\x1b[F": Keys.End, # Tmux sends following keystrokes when control+arrow is pressed, but for # Emacs ansi-term sends the same sequences for normal arrow keys. Consider # it a normal arrow press, because that's more important. # (Application cursor mode). "\x1bOA": Keys.Up, "\x1bOB": Keys.Down, "\x1bOC": Keys.Right, "\x1bOD": Keys.Left, "\x1bOF": Keys.End, "\x1bOH": Keys.Home, # Shift + arrows. "\x1b[1;2A": Keys.ShiftUp, "\x1b[1;2B": Keys.ShiftDown, "\x1b[1;2C": Keys.ShiftRight, "\x1b[1;2D": Keys.ShiftLeft, "\x1b[1;2F": Keys.ShiftEnd, "\x1b[1;2H": Keys.ShiftHome, # Meta + arrow keys. Several terminals handle this differently. # The following sequences are for xterm and gnome-terminal. # (Iterm sends ESC followed by the normal arrow_up/down/left/right # sequences, and the OSX Terminal sends ESCb and ESCf for "alt # arrow_left" and "alt arrow_right." We don't handle these # explicitly, in here, because would could not distinguish between # pressing ESC (to go to Vi navigation mode), followed by just the # 'b' or 'f' key. These combinations are handled in # the input processor.) "\x1b[1;3A": (Keys.Escape, Keys.Up), "\x1b[1;3B": (Keys.Escape, Keys.Down), "\x1b[1;3C": (Keys.Escape, Keys.Right), "\x1b[1;3D": (Keys.Escape, Keys.Left), "\x1b[1;3F": (Keys.Escape, Keys.End), "\x1b[1;3H": (Keys.Escape, Keys.Home), # Alt+shift+number. "\x1b[1;4A": (Keys.Escape, Keys.ShiftDown), "\x1b[1;4B": (Keys.Escape, Keys.ShiftUp), "\x1b[1;4C": (Keys.Escape, Keys.ShiftRight), "\x1b[1;4D": (Keys.Escape, Keys.ShiftLeft), "\x1b[1;4F": (Keys.Escape, Keys.ShiftEnd), "\x1b[1;4H": (Keys.Escape, Keys.ShiftHome), # Control + arrows. "\x1b[1;5A": Keys.ControlUp, # Cursor Mode "\x1b[1;5B": Keys.ControlDown, # Cursor Mode "\x1b[1;5C": Keys.ControlRight, # Cursor Mode "\x1b[1;5D": Keys.ControlLeft, # Cursor Mode "\x1b[1;5F": Keys.ControlEnd, "\x1b[1;5H": Keys.ControlHome, # Tmux sends following keystrokes when control+arrow is pressed, but for # Emacs ansi-term sends the same sequences for normal arrow keys. Consider # it a normal arrow press, because that's more important. "\x1b[5A": Keys.ControlUp, "\x1b[5B": Keys.ControlDown, "\x1b[5C": Keys.ControlRight, "\x1b[5D": Keys.ControlLeft, "\x1bOc": Keys.ControlRight, # rxvt "\x1bOd": Keys.ControlLeft, # rxvt # Control + shift + arrows. "\x1b[1;6A": Keys.ControlShiftDown, "\x1b[1;6B": Keys.ControlShiftUp, "\x1b[1;6C": Keys.ControlShiftRight, "\x1b[1;6D": Keys.ControlShiftLeft, "\x1b[1;6F": Keys.ControlShiftEnd, "\x1b[1;6H": Keys.ControlShiftHome, # Control + Meta + arrows. "\x1b[1;7A": (Keys.Escape, Keys.ControlDown), "\x1b[1;7B": (Keys.Escape, Keys.ControlUp), "\x1b[1;7C": (Keys.Escape, Keys.ControlRight), "\x1b[1;7D": (Keys.Escape, Keys.ControlLeft), "\x1b[1;7F": (Keys.Escape, Keys.ControlEnd), "\x1b[1;7H": (Keys.Escape, Keys.ControlHome), # Meta + Shift + arrows. "\x1b[1;8A": (Keys.Escape, Keys.ControlShiftDown), "\x1b[1;8B": (Keys.Escape, Keys.ControlShiftUp), "\x1b[1;8C": (Keys.Escape, Keys.ControlShiftRight), "\x1b[1;8D": (Keys.Escape, Keys.ControlShiftLeft), "\x1b[1;8F": (Keys.Escape, Keys.ControlShiftEnd), "\x1b[1;8H": (Keys.Escape, Keys.ControlShiftHome), # Meta + arrow on (some?) Macs when using iTerm defaults (see issue #483). "\x1b[1;9A": (Keys.Escape, Keys.Up), "\x1b[1;9B": (Keys.Escape, Keys.Down), "\x1b[1;9C": (Keys.Escape, Keys.Right), "\x1b[1;9D": (Keys.Escape, Keys.Left), # -- # Control/shift/meta + number in mintty. # (c-2 will actually send c-@ and c-6 will send c-^.) "\x1b[1;5p": Keys.Control0, "\x1b[1;5q": Keys.Control1, "\x1b[1;5r": Keys.Control2, "\x1b[1;5s": Keys.Control3, "\x1b[1;5t": Keys.Control4, "\x1b[1;5u": Keys.Control5, "\x1b[1;5v": Keys.Control6, "\x1b[1;5w": Keys.Control7, "\x1b[1;5x": Keys.Control8, "\x1b[1;5y": Keys.Control9, "\x1b[1;6p": Keys.ControlShift0, "\x1b[1;6q": Keys.ControlShift1, "\x1b[1;6r": Keys.ControlShift2, "\x1b[1;6s": Keys.ControlShift3, "\x1b[1;6t": Keys.ControlShift4, "\x1b[1;6u": Keys.ControlShift5, "\x1b[1;6v": Keys.ControlShift6, "\x1b[1;6w": Keys.ControlShift7, "\x1b[1;6x": Keys.ControlShift8, "\x1b[1;6y": Keys.ControlShift9, "\x1b[1;7p": (Keys.Escape, Keys.Control0), "\x1b[1;7q": (Keys.Escape, Keys.Control1), "\x1b[1;7r": (Keys.Escape, Keys.Control2), "\x1b[1;7s": (Keys.Escape, Keys.Control3), "\x1b[1;7t": (Keys.Escape, Keys.Control4), "\x1b[1;7u": (Keys.Escape, Keys.Control5), "\x1b[1;7v": (Keys.Escape, Keys.Control6), "\x1b[1;7w": (Keys.Escape, Keys.Control7), "\x1b[1;7x": (Keys.Escape, Keys.Control8), "\x1b[1;7y": (Keys.Escape, Keys.Control9), "\x1b[1;8p": (Keys.Escape, Keys.ControlShift0), "\x1b[1;8q": (Keys.Escape, Keys.ControlShift1), "\x1b[1;8r": (Keys.Escape, Keys.ControlShift2), "\x1b[1;8s": (Keys.Escape, Keys.ControlShift3), "\x1b[1;8t": (Keys.Escape, Keys.ControlShift4), "\x1b[1;8u": (Keys.Escape, Keys.ControlShift5), "\x1b[1;8v": (Keys.Escape, Keys.ControlShift6), "\x1b[1;8w": (Keys.Escape, Keys.ControlShift7), "\x1b[1;8x": (Keys.Escape, Keys.ControlShift8), "\x1b[1;8y": (Keys.Escape, Keys.ControlShift9), } def _get_reverse_ansi_sequences() -> dict[Keys, str]: """ Create a dictionary that maps prompt_toolkit keys back to the VT100 escape sequences. """ result: dict[Keys, str] = {} for sequence, key in ANSI_SEQUENCES.items(): if not isinstance(key, tuple): if key not in result: result[key] = sequence return result REVERSE_ANSI_SEQUENCES = _get_reverse_ansi_sequences() ================================================ FILE: src/prompt_toolkit/input/base.py ================================================ """ Abstraction of CLI Input. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable, Generator from contextlib import AbstractContextManager, contextmanager from prompt_toolkit.key_binding import KeyPress __all__ = [ "Input", "PipeInput", "DummyInput", ] class Input(metaclass=ABCMeta): """ Abstraction for any input. An instance of this class can be given to the constructor of a :class:`~prompt_toolkit.application.Application` and will also be passed to the :class:`~prompt_toolkit.eventloop.base.EventLoop`. """ @abstractmethod def fileno(self) -> int: """ Fileno for putting this in an event loop. """ @abstractmethod def typeahead_hash(self) -> str: """ Identifier for storing type ahead key presses. """ @abstractmethod def read_keys(self) -> list[KeyPress]: """ Return a list of Key objects which are read/parsed from the input. """ def flush_keys(self) -> list[KeyPress]: """ Flush the underlying parser. and return the pending keys. (Used for vt100 input.) """ return [] def flush(self) -> None: "The event loop can call this when the input has to be flushed." pass @property @abstractmethod def closed(self) -> bool: "Should be true when the input stream is closed." return False @abstractmethod def raw_mode(self) -> AbstractContextManager[None]: """ Context manager that turns the input into raw mode. """ @abstractmethod def cooked_mode(self) -> AbstractContextManager[None]: """ Context manager that turns the input into cooked mode. """ @abstractmethod def attach( self, input_ready_callback: Callable[[], None] ) -> AbstractContextManager[None]: """ Return a context manager that makes this input active in the current event loop. """ @abstractmethod def detach(self) -> AbstractContextManager[None]: """ Return a context manager that makes sure that this input is not active in the current event loop. """ def close(self) -> None: "Close input." pass class PipeInput(Input): """ Abstraction for pipe input. """ @abstractmethod def send_bytes(self, data: bytes) -> None: """Feed byte string into the pipe""" @abstractmethod def send_text(self, data: str) -> None: """Feed a text string into the pipe""" class DummyInput(Input): """ Input for use in a `DummyApplication` If used in an actual application, it will make the application render itself once and exit immediately, due to an `EOFError`. """ def fileno(self) -> int: raise NotImplementedError def typeahead_hash(self) -> str: return f"dummy-{id(self)}" def read_keys(self) -> list[KeyPress]: return [] @property def closed(self) -> bool: # This needs to be true, so that the dummy input will trigger an # `EOFError` immediately in the application. return True def raw_mode(self) -> AbstractContextManager[None]: return _dummy_context_manager() def cooked_mode(self) -> AbstractContextManager[None]: return _dummy_context_manager() def attach( self, input_ready_callback: Callable[[], None] ) -> AbstractContextManager[None]: # Call the callback immediately once after attaching. # This tells the callback to call `read_keys` and check the # `input.closed` flag, after which it won't receive any keys, but knows # that `EOFError` should be raised. This unblocks `read_from_input` in # `application.py`. input_ready_callback() return _dummy_context_manager() def detach(self) -> AbstractContextManager[None]: return _dummy_context_manager() @contextmanager def _dummy_context_manager() -> Generator[None, None, None]: yield ================================================ FILE: src/prompt_toolkit/input/defaults.py ================================================ from __future__ import annotations import io import sys from contextlib import AbstractContextManager from typing import TextIO from .base import DummyInput, Input, PipeInput __all__ = [ "create_input", "create_pipe_input", ] def create_input(stdin: TextIO | None = None, always_prefer_tty: bool = False) -> Input: """ Create the appropriate `Input` object for the current os/environment. :param always_prefer_tty: When set, if `sys.stdin` is connected to a Unix `pipe`, check whether `sys.stdout` or `sys.stderr` are connected to a pseudo terminal. If so, open the tty for reading instead of reading for `sys.stdin`. (We can open `stdout` or `stderr` for reading, this is how a `$PAGER` works.) """ if sys.platform == "win32": from .win32 import Win32Input # If `stdin` was assigned `None` (which happens with pythonw.exe), use # a `DummyInput`. This triggers `EOFError` in the application code. if stdin is None and sys.stdin is None: return DummyInput() return Win32Input(stdin or sys.stdin) else: from .vt100 import Vt100Input # If no input TextIO is given, use stdin/stdout. if stdin is None: stdin = sys.stdin if always_prefer_tty: for obj in [sys.stdin, sys.stdout, sys.stderr]: if obj.isatty(): stdin = obj break # If we can't access the file descriptor for the selected stdin, return # a `DummyInput` instead. This can happen for instance in unit tests, # when `sys.stdin` is patched by something that's not an actual file. # (Instantiating `Vt100Input` would fail in this case.) try: stdin.fileno() except io.UnsupportedOperation: return DummyInput() return Vt100Input(stdin) def create_pipe_input() -> AbstractContextManager[PipeInput]: """ Create an input pipe. This is mostly useful for unit testing. Usage:: with create_pipe_input() as input: input.send_text('inputdata') Breaking change: In prompt_toolkit 3.0.28 and earlier, this was returning the `PipeInput` directly, rather than through a context manager. """ if sys.platform == "win32": from .win32_pipe import Win32PipeInput return Win32PipeInput.create() else: from .posix_pipe import PosixPipeInput return PosixPipeInput.create() ================================================ FILE: src/prompt_toolkit/input/posix_pipe.py ================================================ from __future__ import annotations import sys assert sys.platform != "win32" import os from collections.abc import Iterator from contextlib import AbstractContextManager, contextmanager from typing import TextIO, cast from ..utils import DummyContext from .base import PipeInput from .vt100 import Vt100Input __all__ = [ "PosixPipeInput", ] class _Pipe: "Wrapper around os.pipe, that ensures we don't double close any end." def __init__(self) -> None: self.read_fd, self.write_fd = os.pipe() self._read_closed = False self._write_closed = False def close_read(self) -> None: "Close read-end if not yet closed." if self._read_closed: return os.close(self.read_fd) self._read_closed = True def close_write(self) -> None: "Close write-end if not yet closed." if self._write_closed: return os.close(self.write_fd) self._write_closed = True def close(self) -> None: "Close both read and write ends." self.close_read() self.close_write() class PosixPipeInput(Vt100Input, PipeInput): """ Input that is send through a pipe. This is useful if we want to send the input programmatically into the application. Mostly useful for unit testing. Usage:: with PosixPipeInput.create() as input: input.send_text('inputdata') """ _id = 0 def __init__(self, _pipe: _Pipe, _text: str = "") -> None: # Private constructor. Users should use the public `.create()` method. self.pipe = _pipe class Stdin: encoding = "utf-8" def isatty(stdin) -> bool: return True def fileno(stdin) -> int: return self.pipe.read_fd super().__init__(cast(TextIO, Stdin())) self.send_text(_text) # Identifier for every PipeInput for the hash. self.__class__._id += 1 self._id = self.__class__._id @classmethod @contextmanager def create(cls, text: str = "") -> Iterator[PosixPipeInput]: pipe = _Pipe() try: yield PosixPipeInput(_pipe=pipe, _text=text) finally: pipe.close() def send_bytes(self, data: bytes) -> None: os.write(self.pipe.write_fd, data) def send_text(self, data: str) -> None: "Send text to the input." os.write(self.pipe.write_fd, data.encode("utf-8")) def raw_mode(self) -> AbstractContextManager[None]: return DummyContext() def cooked_mode(self) -> AbstractContextManager[None]: return DummyContext() def close(self) -> None: "Close pipe fds." # Only close the write-end of the pipe. This will unblock the reader # callback (in vt100.py > _attached_input), which eventually will raise # `EOFError`. If we'd also close the read-end, then the event loop # won't wake up the corresponding callback because of this. self.pipe.close_write() def typeahead_hash(self) -> str: """ This needs to be unique for every `PipeInput`. """ return f"pipe-input-{self._id}" ================================================ FILE: src/prompt_toolkit/input/posix_utils.py ================================================ from __future__ import annotations import os import select from codecs import getincrementaldecoder __all__ = [ "PosixStdinReader", ] class PosixStdinReader: """ Wrapper around stdin which reads (nonblocking) the next available 1024 bytes and decodes it. Note that you can't be sure that the input file is closed if the ``read`` function returns an empty string. When ``errors=ignore`` is passed, ``read`` can return an empty string if all malformed input was replaced by an empty string. (We can't block here and wait for more input.) So, because of that, check the ``closed`` attribute, to be sure that the file has been closed. :param stdin_fd: File descriptor from which we read. :param errors: Can be 'ignore', 'strict' or 'replace'. On Python3, this can be 'surrogateescape', which is the default. 'surrogateescape' is preferred, because this allows us to transfer unrecognized bytes to the key bindings. Some terminals, like lxterminal and Guake, use the 'Mxx' notation to send mouse events, where each 'x' can be any possible byte. """ # By default, we want to 'ignore' errors here. The input stream can be full # of junk. One occurrence of this that I had was when using iTerm2 on OS X, # with "Option as Meta" checked (You should choose "Option as +Esc".) def __init__( self, stdin_fd: int, errors: str = "surrogateescape", encoding: str = "utf-8" ) -> None: self.stdin_fd = stdin_fd self.errors = errors # Create incremental decoder for decoding stdin. # We can not just do `os.read(stdin.fileno(), 1024).decode('utf-8')`, because # it could be that we are in the middle of a utf-8 byte sequence. self._stdin_decoder_cls = getincrementaldecoder(encoding) self._stdin_decoder = self._stdin_decoder_cls(errors=errors) #: True when there is nothing anymore to read. self.closed = False def read(self, count: int = 1024) -> str: # By default we choose a rather small chunk size, because reading # big amounts of input at once, causes the event loop to process # all these key bindings also at once without going back to the # loop. This will make the application feel unresponsive. """ Read the input and return it as a string. Return the text. Note that this can return an empty string, even when the input stream was not yet closed. This means that something went wrong during the decoding. """ if self.closed: return "" # Check whether there is some input to read. `os.read` would block # otherwise. # (Actually, the event loop is responsible to make sure that this # function is only called when there is something to read, but for some # reason this happens in certain situations.) try: if not select.select([self.stdin_fd], [], [], 0)[0]: return "" except OSError: # Happens for instance when the file descriptor was closed. # (We had this in ptterm, where the FD became ready, a callback was # scheduled, but in the meantime another callback closed it already.) self.closed = True # Note: the following works better than wrapping `self.stdin` like # `codecs.getreader('utf-8')(stdin)` and doing `read(1)`. # Somehow that causes some latency when the escape # character is pressed. (Especially on combination with the `select`.) try: data = os.read(self.stdin_fd, count) # Nothing more to read, stream is closed. if data == b"": self.closed = True return "" except OSError: # In case of SIGWINCH data = b"" return self._stdin_decoder.decode(data) ================================================ FILE: src/prompt_toolkit/input/typeahead.py ================================================ r""" Store input key strokes if we did read more than was required. The input classes `Vt100Input` and `Win32Input` read the input text in chunks of a few kilobytes. This means that if we read input from stdin, it could be that we read a couple of lines (with newlines in between) at once. This creates a problem: potentially, we read too much from stdin. Sometimes people paste several lines at once because they paste input in a REPL and expect each input() call to process one line. Or they rely on type ahead because the application can't keep up with the processing. However, we need to read input in bigger chunks. We need this mostly to support pasting of larger chunks of text. We don't want everything to become unresponsive because we: - read one character; - parse one character; - call the key binding, which does a string operation with one character; - and render the user interface. Doing text operations on single characters is very inefficient in Python, so we prefer to work on bigger chunks of text. This is why we have to read the input in bigger chunks. Further, line buffering is also not an option, because it doesn't work well in the architecture. We use lower level Posix APIs, that work better with the event loop and so on. In fact, there is also nothing that defines that only \n can accept the input, you could create a key binding for any key to accept the input. To support type ahead, this module will store all the key strokes that were read too early, so that they can be feed into to the next `prompt()` call or to the next prompt_toolkit `Application`. """ from __future__ import annotations from collections import defaultdict from ..key_binding import KeyPress from .base import Input __all__ = [ "store_typeahead", "get_typeahead", "clear_typeahead", ] _buffer: dict[str, list[KeyPress]] = defaultdict(list) def store_typeahead(input_obj: Input, key_presses: list[KeyPress]) -> None: """ Insert typeahead key presses for the given input. """ global _buffer key = input_obj.typeahead_hash() _buffer[key].extend(key_presses) def get_typeahead(input_obj: Input) -> list[KeyPress]: """ Retrieve typeahead and reset the buffer for this input. """ global _buffer key = input_obj.typeahead_hash() result = _buffer[key] _buffer[key] = [] return result def clear_typeahead(input_obj: Input) -> None: """ Clear typeahead buffer. """ global _buffer key = input_obj.typeahead_hash() _buffer[key] = [] ================================================ FILE: src/prompt_toolkit/input/vt100.py ================================================ from __future__ import annotations import sys assert sys.platform != "win32" import contextlib import io import termios import tty from asyncio import AbstractEventLoop, get_running_loop from collections.abc import Callable, Generator from contextlib import AbstractContextManager from typing import TextIO from ..key_binding import KeyPress from .base import Input from .posix_utils import PosixStdinReader from .vt100_parser import Vt100Parser __all__ = [ "Vt100Input", "raw_mode", "cooked_mode", ] class Vt100Input(Input): """ Vt100 input for Posix systems. (This uses a posix file descriptor that can be registered in the event loop.) """ # For the error messages. Only display "Input is not a terminal" once per # file descriptor. _fds_not_a_terminal: set[int] = set() def __init__(self, stdin: TextIO) -> None: # Test whether the given input object has a file descriptor. # (Idle reports stdin to be a TTY, but fileno() is not implemented.) try: # This should not raise, but can return 0. stdin.fileno() except io.UnsupportedOperation as e: if "idlelib.run" in sys.modules: raise io.UnsupportedOperation( "Stdin is not a terminal. Running from Idle is not supported." ) from e else: raise io.UnsupportedOperation("Stdin is not a terminal.") from e # Even when we have a file descriptor, it doesn't mean it's a TTY. # Normally, this requires a real TTY device, but people instantiate # this class often during unit tests as well. They use for instance # pexpect to pipe data into an application. For convenience, we print # an error message and go on. isatty = stdin.isatty() fd = stdin.fileno() if not isatty and fd not in Vt100Input._fds_not_a_terminal: msg = "Warning: Input is not a terminal (fd=%r).\n" sys.stderr.write(msg % fd) sys.stderr.flush() Vt100Input._fds_not_a_terminal.add(fd) # self.stdin = stdin # Create a backup of the fileno(). We want this to work even if the # underlying file is closed, so that `typeahead_hash()` keeps working. self._fileno = stdin.fileno() self._buffer: list[KeyPress] = [] # Buffer to collect the Key objects. self.stdin_reader = PosixStdinReader(self._fileno, encoding=stdin.encoding) self.vt100_parser = Vt100Parser( lambda key_press: self._buffer.append(key_press) ) def attach( self, input_ready_callback: Callable[[], None] ) -> AbstractContextManager[None]: """ Return a context manager that makes this input active in the current event loop. """ return _attached_input(self, input_ready_callback) def detach(self) -> AbstractContextManager[None]: """ Return a context manager that makes sure that this input is not active in the current event loop. """ return _detached_input(self) def read_keys(self) -> list[KeyPress]: "Read list of KeyPress." # Read text from stdin. data = self.stdin_reader.read() # Pass it through our vt100 parser. self.vt100_parser.feed(data) # Return result. result = self._buffer self._buffer = [] return result def flush_keys(self) -> list[KeyPress]: """ Flush pending keys and return them. (Used for flushing the 'escape' key.) """ # Flush all pending keys. (This is most important to flush the vt100 # 'Escape' key early when nothing else follows.) self.vt100_parser.flush() # Return result. result = self._buffer self._buffer = [] return result @property def closed(self) -> bool: return self.stdin_reader.closed def raw_mode(self) -> AbstractContextManager[None]: return raw_mode(self.stdin.fileno()) def cooked_mode(self) -> AbstractContextManager[None]: return cooked_mode(self.stdin.fileno()) def fileno(self) -> int: return self.stdin.fileno() def typeahead_hash(self) -> str: return f"fd-{self._fileno}" _current_callbacks: dict[ tuple[AbstractEventLoop, int], Callable[[], None] | None ] = {} # (loop, fd) -> current callback @contextlib.contextmanager def _attached_input( input: Vt100Input, callback: Callable[[], None] ) -> Generator[None, None, None]: """ Context manager that makes this input active in the current event loop. :param input: :class:`~prompt_toolkit.input.Input` object. :param callback: Called when the input is ready to read. """ loop = get_running_loop() fd = input.fileno() previous = _current_callbacks.get((loop, fd)) def callback_wrapper() -> None: """Wrapper around the callback that already removes the reader when the input is closed. Otherwise, we keep continuously calling this callback, until we leave the context manager (which can happen a bit later). This fixes issues when piping /dev/null into a prompt_toolkit application.""" if input.closed: loop.remove_reader(fd) callback() try: loop.add_reader(fd, callback_wrapper) except PermissionError: # For `EPollSelector`, adding /dev/null to the event loop will raise # `PermissionError` (that doesn't happen for `SelectSelector` # apparently). Whenever we get a `PermissionError`, we can raise # `EOFError`, because there's not more to be read anyway. `EOFError` is # an exception that people expect in # `prompt_toolkit.application.Application.run()`. # To reproduce, do: `ptpython 0< /dev/null 1< /dev/null` raise EOFError _current_callbacks[loop, fd] = callback try: yield finally: loop.remove_reader(fd) if previous: loop.add_reader(fd, previous) _current_callbacks[loop, fd] = previous else: del _current_callbacks[loop, fd] @contextlib.contextmanager def _detached_input(input: Vt100Input) -> Generator[None, None, None]: loop = get_running_loop() fd = input.fileno() previous = _current_callbacks.get((loop, fd)) if previous: loop.remove_reader(fd) _current_callbacks[loop, fd] = None try: yield finally: if previous: loop.add_reader(fd, previous) _current_callbacks[loop, fd] = previous class raw_mode: """ :: with raw_mode(stdin): ''' the pseudo-terminal stdin is now used in raw mode ''' We ignore errors when executing `tcgetattr` fails. """ # There are several reasons for ignoring errors: # 1. To avoid the "Inappropriate ioctl for device" crash if somebody would # execute this code (In a Python REPL, for instance): # # import os; f = open(os.devnull); os.dup2(f.fileno(), 0) # # The result is that the eventloop will stop correctly, because it has # to logic to quit when stdin is closed. However, we should not fail at # this point. See: # https://github.com/jonathanslenders/python-prompt-toolkit/pull/393 # https://github.com/jonathanslenders/python-prompt-toolkit/issues/392 # 2. Related, when stdin is an SSH pipe, and no full terminal was allocated. # See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/165 def __init__(self, fileno: int) -> None: self.fileno = fileno self.attrs_before: list[int | list[bytes | int]] | None try: self.attrs_before = termios.tcgetattr(fileno) except termios.error: # Ignore attribute errors. self.attrs_before = None def __enter__(self) -> None: # NOTE: On os X systems, using pty.setraw() fails. Therefor we are using this: try: newattr = termios.tcgetattr(self.fileno) except termios.error: pass else: newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG]) newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG]) # VMIN defines the number of characters read at a time in # non-canonical mode. It seems to default to 1 on Linux, but on # Solaris and derived operating systems it defaults to 4. (This is # because the VMIN slot is the same as the VEOF slot, which # defaults to ASCII EOT = Ctrl-D = 4.) newattr[tty.CC][termios.VMIN] = 1 termios.tcsetattr(self.fileno, termios.TCSANOW, newattr) @classmethod def _patch_lflag(cls, attrs: int) -> int: return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) @classmethod def _patch_iflag(cls, attrs: int) -> int: return attrs & ~( # Disable XON/XOFF flow control on output and input. # (Don't capture Ctrl-S and Ctrl-Q.) # Like executing: "stty -ixon." termios.IXON | termios.IXOFF | # Don't translate carriage return into newline on input. termios.ICRNL | termios.INLCR | termios.IGNCR ) def __exit__(self, *a: object) -> None: if self.attrs_before is not None: try: termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before) except termios.error: pass # # Put the terminal in application mode. # self._stdout.write('\x1b[?1h') class cooked_mode(raw_mode): """ The opposite of ``raw_mode``, used when we need cooked mode inside a `raw_mode` block. Used in `Application.run_in_terminal`.:: with cooked_mode(stdin): ''' the pseudo-terminal stdin is now used in cooked mode. ''' """ @classmethod def _patch_lflag(cls, attrs: int) -> int: return attrs | (termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) @classmethod def _patch_iflag(cls, attrs: int) -> int: # Turn the ICRNL flag back on. (Without this, calling `input()` in # run_in_terminal doesn't work and displays ^M instead. Ptpython # evaluates commands using `run_in_terminal`, so it's important that # they translate ^M back into ^J.) return attrs | termios.ICRNL ================================================ FILE: src/prompt_toolkit/input/vt100_parser.py ================================================ """ Parser for VT100 input stream. """ from __future__ import annotations import re from collections.abc import Callable, Generator from ..key_binding.key_processor import KeyPress from ..keys import Keys from .ansi_escape_sequences import ANSI_SEQUENCES __all__ = [ "Vt100Parser", ] # Regex matching any CPR response # (Note that we use '\Z' instead of '$', because '$' could include a trailing # newline.) _cpr_response_re = re.compile("^" + re.escape("\x1b[") + r"\d+;\d+R\Z") # Mouse events: # Typical: "Esc[MaB*" Urxvt: "Esc[96;14;13M" and for Xterm SGR: "Esc[<64;85;12M" _mouse_event_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]+[mM]|M...)\Z") # Regex matching any valid prefix of a CPR response. # (Note that it doesn't contain the last character, the 'R'. The prefix has to # be shorter.) _cpr_response_prefix_re = re.compile("^" + re.escape("\x1b[") + r"[\d;]*\Z") _mouse_event_prefix_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]*|M.{0,2})\Z") class _Flush: """Helper object to indicate flush operation to the parser.""" pass class _IsPrefixOfLongerMatchCache(dict[str, bool]): """ Dictionary that maps input sequences to a boolean indicating whether there is any key that start with this characters. """ def __missing__(self, prefix: str) -> bool: # (hard coded) If this could be a prefix of a CPR response, return # True. if _cpr_response_prefix_re.match(prefix) or _mouse_event_prefix_re.match( prefix ): result = True else: # If this could be a prefix of anything else, also return True. result = any( v for k, v in ANSI_SEQUENCES.items() if k.startswith(prefix) and k != prefix ) self[prefix] = result return result _IS_PREFIX_OF_LONGER_MATCH_CACHE = _IsPrefixOfLongerMatchCache() class Vt100Parser: """ Parser for VT100 input stream. Data can be fed through the `feed` method and the given callback will be called with KeyPress objects. :: def callback(key): pass i = Vt100Parser(callback) i.feed('data\x01...') :attr feed_key_callback: Function that will be called when a key is parsed. """ # Lookup table of ANSI escape sequences for a VT100 terminal # Hint: in order to know what sequences your terminal writes to stdin, run # "od -c" and start typing. def __init__(self, feed_key_callback: Callable[[KeyPress], None]) -> None: self.feed_key_callback = feed_key_callback self.reset() def reset(self, request: bool = False) -> None: self._in_bracketed_paste = False self._start_parser() def _start_parser(self) -> None: """ Start the parser coroutine. """ self._input_parser = self._input_parser_generator() self._input_parser.send(None) # type: ignore def _get_match(self, prefix: str) -> None | Keys | tuple[Keys, ...]: """ Return the key (or keys) that maps to this prefix. """ # (hard coded) If we match a CPR response, return Keys.CPRResponse. # (This one doesn't fit in the ANSI_SEQUENCES, because it contains # integer variables.) if _cpr_response_re.match(prefix): return Keys.CPRResponse elif _mouse_event_re.match(prefix): return Keys.Vt100MouseEvent # Otherwise, use the mappings. try: return ANSI_SEQUENCES[prefix] except KeyError: return None def _input_parser_generator(self) -> Generator[None, str | _Flush, None]: """ Coroutine (state machine) for the input parser. """ prefix = "" retry = False flush = False while True: flush = False if retry: retry = False else: # Get next character. c = yield if isinstance(c, _Flush): flush = True else: prefix += c # If we have some data, check for matches. if prefix: is_prefix_of_longer_match = _IS_PREFIX_OF_LONGER_MATCH_CACHE[prefix] match = self._get_match(prefix) # Exact matches found, call handlers.. if (flush or not is_prefix_of_longer_match) and match: self._call_handler(match, prefix) prefix = "" # No exact match found. elif (flush or not is_prefix_of_longer_match) and not match: found = False retry = True # Loop over the input, try the longest match first and # shift. for i in range(len(prefix), 0, -1): match = self._get_match(prefix[:i]) if match: self._call_handler(match, prefix[:i]) prefix = prefix[i:] found = True if not found: self._call_handler(prefix[0], prefix[0]) prefix = prefix[1:] def _call_handler( self, key: str | Keys | tuple[Keys, ...], insert_text: str ) -> None: """ Callback to handler. """ if isinstance(key, tuple): # Received ANSI sequence that corresponds with multiple keys # (probably alt+something). Handle keys individually, but only pass # data payload to first KeyPress (so that we won't insert it # multiple times). for i, k in enumerate(key): self._call_handler(k, insert_text if i == 0 else "") else: if key == Keys.BracketedPaste: self._in_bracketed_paste = True self._paste_buffer = "" else: self.feed_key_callback(KeyPress(key, insert_text)) def feed(self, data: str) -> None: """ Feed the input stream. :param data: Input string (unicode). """ # Handle bracketed paste. (We bypass the parser that matches all other # key presses and keep reading input until we see the end mark.) # This is much faster then parsing character by character. if self._in_bracketed_paste: self._paste_buffer += data end_mark = "\x1b[201~" if end_mark in self._paste_buffer: end_index = self._paste_buffer.index(end_mark) # Feed content to key bindings. paste_content = self._paste_buffer[:end_index] self.feed_key_callback(KeyPress(Keys.BracketedPaste, paste_content)) # Quit bracketed paste mode and handle remaining input. self._in_bracketed_paste = False remaining = self._paste_buffer[end_index + len(end_mark) :] self._paste_buffer = "" self.feed(remaining) # Handle normal input character by character. else: for i, c in enumerate(data): if self._in_bracketed_paste: # Quit loop and process from this position when the parser # entered bracketed paste. self.feed(data[i:]) break else: self._input_parser.send(c) def flush(self) -> None: """ Flush the buffer of the input stream. This will allow us to handle the escape key (or maybe meta) sooner. The input received by the escape key is actually the same as the first characters of e.g. Arrow-Up, so without knowing what follows the escape sequence, we don't know whether escape has been pressed, or whether it's something else. This flush function should be called after a timeout, and processes everything that's still in the buffer as-is, so without assuming any characters will follow. """ self._input_parser.send(_Flush()) def feed_and_flush(self, data: str) -> None: """ Wrapper around ``feed`` and ``flush``. """ self.feed(data) self.flush() ================================================ FILE: src/prompt_toolkit/input/win32.py ================================================ from __future__ import annotations import os import sys from abc import abstractmethod from asyncio import get_running_loop from contextlib import AbstractContextManager, contextmanager from ..utils import SPHINX_AUTODOC_RUNNING assert sys.platform == "win32" # Do not import win32-specific stuff when generating documentation. # Otherwise RTD would be unable to generate docs for this module. if not SPHINX_AUTODOC_RUNNING: import msvcrt from ctypes import windll from collections.abc import Callable, Iterable, Iterator from ctypes import Array, byref, pointer from ctypes.wintypes import DWORD, HANDLE from typing import TextIO from prompt_toolkit.eventloop import run_in_executor_with_context from prompt_toolkit.eventloop.win32 import create_win32_event, wait_for_handles from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys from prompt_toolkit.mouse_events import MouseButton, MouseEventType from prompt_toolkit.win32_types import ( INPUT_RECORD, KEY_EVENT_RECORD, MOUSE_EVENT_RECORD, STD_INPUT_HANDLE, EventTypes, ) from .ansi_escape_sequences import REVERSE_ANSI_SEQUENCES from .base import Input from .vt100_parser import Vt100Parser __all__ = [ "Win32Input", "ConsoleInputReader", "raw_mode", "cooked_mode", "attach_win32_input", "detach_win32_input", ] # Win32 Constants for MOUSE_EVENT_RECORD. # See: https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str FROM_LEFT_1ST_BUTTON_PRESSED = 0x1 RIGHTMOST_BUTTON_PRESSED = 0x2 MOUSE_MOVED = 0x0001 MOUSE_WHEELED = 0x0004 # See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 class _Win32InputBase(Input): """ Base class for `Win32Input` and `Win32PipeInput`. """ def __init__(self) -> None: self.win32_handles = _Win32Handles() @property @abstractmethod def handle(self) -> HANDLE: pass class Win32Input(_Win32InputBase): """ `Input` class that reads from the Windows console. """ def __init__(self, stdin: TextIO | None = None) -> None: super().__init__() self._use_virtual_terminal_input = _is_win_vt100_input_enabled() self.console_input_reader: Vt100ConsoleInputReader | ConsoleInputReader if self._use_virtual_terminal_input: self.console_input_reader = Vt100ConsoleInputReader() else: self.console_input_reader = ConsoleInputReader() def attach( self, input_ready_callback: Callable[[], None] ) -> AbstractContextManager[None]: """ Return a context manager that makes this input active in the current event loop. """ return attach_win32_input(self, input_ready_callback) def detach(self) -> AbstractContextManager[None]: """ Return a context manager that makes sure that this input is not active in the current event loop. """ return detach_win32_input(self) def read_keys(self) -> list[KeyPress]: return list(self.console_input_reader.read()) def flush_keys(self) -> list[KeyPress]: return self.console_input_reader.flush_keys() @property def closed(self) -> bool: return False def raw_mode(self) -> AbstractContextManager[None]: return raw_mode( use_win10_virtual_terminal_input=self._use_virtual_terminal_input ) def cooked_mode(self) -> AbstractContextManager[None]: return cooked_mode() def fileno(self) -> int: # The windows console doesn't depend on the file handle, so # this is not used for the event loop (which uses the # handle instead). But it's used in `Application.run_system_command` # which opens a subprocess with a given stdin/stdout. return sys.stdin.fileno() def typeahead_hash(self) -> str: return "win32-input" def close(self) -> None: self.console_input_reader.close() @property def handle(self) -> HANDLE: return self.console_input_reader.handle class ConsoleInputReader: """ :param recognize_paste: When True, try to discover paste actions and turn the event into a BracketedPaste. """ # Keys with character data. mappings = { b"\x1b": Keys.Escape, b"\x00": Keys.ControlSpace, # Control-Space (Also for Ctrl-@) b"\x01": Keys.ControlA, # Control-A (home) b"\x02": Keys.ControlB, # Control-B (emacs cursor left) b"\x03": Keys.ControlC, # Control-C (interrupt) b"\x04": Keys.ControlD, # Control-D (exit) b"\x05": Keys.ControlE, # Control-E (end) b"\x06": Keys.ControlF, # Control-F (cursor forward) b"\x07": Keys.ControlG, # Control-G b"\x08": Keys.ControlH, # Control-H (8) (Identical to '\b') b"\x09": Keys.ControlI, # Control-I (9) (Identical to '\t') b"\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n') b"\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab) b"\x0c": Keys.ControlL, # Control-L (clear; form feed) b"\x0d": Keys.ControlM, # Control-M (enter) b"\x0e": Keys.ControlN, # Control-N (14) (history forward) b"\x0f": Keys.ControlO, # Control-O (15) b"\x10": Keys.ControlP, # Control-P (16) (history back) b"\x11": Keys.ControlQ, # Control-Q b"\x12": Keys.ControlR, # Control-R (18) (reverse search) b"\x13": Keys.ControlS, # Control-S (19) (forward search) b"\x14": Keys.ControlT, # Control-T b"\x15": Keys.ControlU, # Control-U b"\x16": Keys.ControlV, # Control-V b"\x17": Keys.ControlW, # Control-W b"\x18": Keys.ControlX, # Control-X b"\x19": Keys.ControlY, # Control-Y (25) b"\x1a": Keys.ControlZ, # Control-Z b"\x1c": Keys.ControlBackslash, # Both Control-\ and Ctrl-| b"\x1d": Keys.ControlSquareClose, # Control-] b"\x1e": Keys.ControlCircumflex, # Control-^ b"\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.) b"\x7f": Keys.Backspace, # (127) Backspace (ASCII Delete.) } # Keys that don't carry character data. keycodes = { # Home/End 33: Keys.PageUp, 34: Keys.PageDown, 35: Keys.End, 36: Keys.Home, # Arrows 37: Keys.Left, 38: Keys.Up, 39: Keys.Right, 40: Keys.Down, 45: Keys.Insert, 46: Keys.Delete, # F-keys. 112: Keys.F1, 113: Keys.F2, 114: Keys.F3, 115: Keys.F4, 116: Keys.F5, 117: Keys.F6, 118: Keys.F7, 119: Keys.F8, 120: Keys.F9, 121: Keys.F10, 122: Keys.F11, 123: Keys.F12, } LEFT_ALT_PRESSED = 0x0002 RIGHT_ALT_PRESSED = 0x0001 SHIFT_PRESSED = 0x0010 LEFT_CTRL_PRESSED = 0x0008 RIGHT_CTRL_PRESSED = 0x0004 def __init__(self, recognize_paste: bool = True) -> None: self._fdcon = None self.recognize_paste = recognize_paste # When stdin is a tty, use that handle, otherwise, create a handle from # CONIN$. self.handle: HANDLE if sys.stdin.isatty(): self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) else: self._fdcon = os.open("CONIN$", os.O_RDWR | os.O_BINARY) self.handle = HANDLE(msvcrt.get_osfhandle(self._fdcon)) def close(self) -> None: "Close fdcon." if self._fdcon is not None: os.close(self._fdcon) def read(self) -> Iterable[KeyPress]: """ Return a list of `KeyPress` instances. It won't return anything when there was nothing to read. (This function doesn't block.) http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx """ max_count = 2048 # Max events to read at the same time. read = DWORD(0) arrtype = INPUT_RECORD * max_count input_records = arrtype() # Check whether there is some input to read. `ReadConsoleInputW` would # block otherwise. # (Actually, the event loop is responsible to make sure that this # function is only called when there is something to read, but for some # reason this happened in the asyncio_win32 loop, and it's better to be # safe anyway.) if not wait_for_handles([self.handle], timeout=0): return # Get next batch of input event. windll.kernel32.ReadConsoleInputW( self.handle, pointer(input_records), max_count, pointer(read) ) # First, get all the keys from the input buffer, in order to determine # whether we should consider this a paste event or not. all_keys = list(self._get_keys(read, input_records)) # Fill in 'data' for key presses. all_keys = [self._insert_key_data(key) for key in all_keys] # Correct non-bmp characters that are passed as separate surrogate codes all_keys = list(self._merge_paired_surrogates(all_keys)) if self.recognize_paste and self._is_paste(all_keys): gen = iter(all_keys) k: KeyPress | None for k in gen: # Pasting: if the current key consists of text or \n, turn it # into a BracketedPaste. data = [] while k and ( not isinstance(k.key, Keys) or k.key in {Keys.ControlJ, Keys.ControlM} ): data.append(k.data) try: k = next(gen) except StopIteration: k = None if data: yield KeyPress(Keys.BracketedPaste, "".join(data)) if k is not None: yield k else: yield from all_keys def flush_keys(self) -> list[KeyPress]: # Method only needed for structural compatibility with `Vt100ConsoleInputReader`. return [] def _insert_key_data(self, key_press: KeyPress) -> KeyPress: """ Insert KeyPress data, for vt100 compatibility. """ if key_press.data: return key_press if isinstance(key_press.key, Keys): data = REVERSE_ANSI_SEQUENCES.get(key_press.key, "") else: data = "" return KeyPress(key_press.key, data) def _get_keys( self, read: DWORD, input_records: Array[INPUT_RECORD] ) -> Iterator[KeyPress]: """ Generator that yields `KeyPress` objects from the input records. """ for i in range(read.value): ir = input_records[i] # Get the right EventType from the EVENT_RECORD. # (For some reason the Windows console application 'cmder' # [http://gooseberrycreative.com/cmder/] can return '0' for # ir.EventType. -- Just ignore that.) if ir.EventType in EventTypes: ev = getattr(ir.Event, EventTypes[ir.EventType]) # Process if this is a key event. (We also have mouse, menu and # focus events.) if isinstance(ev, KEY_EVENT_RECORD) and ev.KeyDown: yield from self._event_to_key_presses(ev) elif isinstance(ev, MOUSE_EVENT_RECORD): yield from self._handle_mouse(ev) @staticmethod def _merge_paired_surrogates(key_presses: list[KeyPress]) -> Iterator[KeyPress]: """ Combines consecutive KeyPresses with high and low surrogates into single characters """ buffered_high_surrogate = None for key in key_presses: is_text = not isinstance(key.key, Keys) is_high_surrogate = is_text and "\ud800" <= key.key <= "\udbff" is_low_surrogate = is_text and "\udc00" <= key.key <= "\udfff" if buffered_high_surrogate: if is_low_surrogate: # convert high surrogate + low surrogate to single character fullchar = ( (buffered_high_surrogate.key + key.key) .encode("utf-16-le", "surrogatepass") .decode("utf-16-le") ) key = KeyPress(fullchar, fullchar) else: yield buffered_high_surrogate buffered_high_surrogate = None if is_high_surrogate: buffered_high_surrogate = key else: yield key if buffered_high_surrogate: yield buffered_high_surrogate @staticmethod def _is_paste(keys: list[KeyPress]) -> bool: """ Return `True` when we should consider this list of keys as a paste event. Pasted text on windows will be turned into a `Keys.BracketedPaste` event. (It's not 100% correct, but it is probably the best possible way to detect pasting of text and handle that correctly.) """ # Consider paste when it contains at least one newline and at least one # other character. text_count = 0 newline_count = 0 for k in keys: if not isinstance(k.key, Keys): text_count += 1 if k.key == Keys.ControlM: newline_count += 1 return newline_count >= 1 and text_count >= 1 def _event_to_key_presses(self, ev: KEY_EVENT_RECORD) -> list[KeyPress]: """ For this `KEY_EVENT_RECORD`, return a list of `KeyPress` instances. """ assert isinstance(ev, KEY_EVENT_RECORD) and ev.KeyDown result: KeyPress | None = None control_key_state = ev.ControlKeyState u_char = ev.uChar.UnicodeChar # Use surrogatepass because u_char may be an unmatched surrogate ascii_char = u_char.encode("utf-8", "surrogatepass") # NOTE: We don't use `ev.uChar.AsciiChar`. That appears to be the # unicode code point truncated to 1 byte. See also: # https://github.com/ipython/ipython/issues/10004 # https://github.com/jonathanslenders/python-prompt-toolkit/issues/389 if u_char == "\x00": if ev.VirtualKeyCode in self.keycodes: result = KeyPress(self.keycodes[ev.VirtualKeyCode], "") else: if ascii_char in self.mappings: if self.mappings[ascii_char] == Keys.ControlJ: u_char = ( "\n" # Windows sends \n, turn into \r for unix compatibility. ) result = KeyPress(self.mappings[ascii_char], u_char) else: result = KeyPress(u_char, u_char) # First we handle Shift-Control-Arrow/Home/End (need to do this first) if ( ( control_key_state & self.LEFT_CTRL_PRESSED or control_key_state & self.RIGHT_CTRL_PRESSED ) and control_key_state & self.SHIFT_PRESSED and result ): mapping: dict[str, str] = { Keys.Left: Keys.ControlShiftLeft, Keys.Right: Keys.ControlShiftRight, Keys.Up: Keys.ControlShiftUp, Keys.Down: Keys.ControlShiftDown, Keys.Home: Keys.ControlShiftHome, Keys.End: Keys.ControlShiftEnd, Keys.Insert: Keys.ControlShiftInsert, Keys.PageUp: Keys.ControlShiftPageUp, Keys.PageDown: Keys.ControlShiftPageDown, } result.key = mapping.get(result.key, result.key) # Correctly handle Control-Arrow/Home/End and Control-Insert/Delete keys. if ( control_key_state & self.LEFT_CTRL_PRESSED or control_key_state & self.RIGHT_CTRL_PRESSED ) and result: mapping = { Keys.Left: Keys.ControlLeft, Keys.Right: Keys.ControlRight, Keys.Up: Keys.ControlUp, Keys.Down: Keys.ControlDown, Keys.Home: Keys.ControlHome, Keys.End: Keys.ControlEnd, Keys.Insert: Keys.ControlInsert, Keys.Delete: Keys.ControlDelete, Keys.PageUp: Keys.ControlPageUp, Keys.PageDown: Keys.ControlPageDown, } result.key = mapping.get(result.key, result.key) # Turn 'Tab' into 'BackTab' when shift was pressed. # Also handle other shift-key combination if control_key_state & self.SHIFT_PRESSED and result: mapping = { Keys.Tab: Keys.BackTab, Keys.Left: Keys.ShiftLeft, Keys.Right: Keys.ShiftRight, Keys.Up: Keys.ShiftUp, Keys.Down: Keys.ShiftDown, Keys.Home: Keys.ShiftHome, Keys.End: Keys.ShiftEnd, Keys.Insert: Keys.ShiftInsert, Keys.Delete: Keys.ShiftDelete, Keys.PageUp: Keys.ShiftPageUp, Keys.PageDown: Keys.ShiftPageDown, } result.key = mapping.get(result.key, result.key) # Turn 'Space' into 'ControlSpace' when control was pressed. if ( ( control_key_state & self.LEFT_CTRL_PRESSED or control_key_state & self.RIGHT_CTRL_PRESSED ) and result and result.data == " " ): result = KeyPress(Keys.ControlSpace, " ") # Turn Control-Enter into META-Enter. (On a vt100 terminal, we cannot # detect this combination. But it's really practical on Windows.) if ( ( control_key_state & self.LEFT_CTRL_PRESSED or control_key_state & self.RIGHT_CTRL_PRESSED ) and result and result.key == Keys.ControlJ ): return [KeyPress(Keys.Escape, ""), result] # Return result. If alt was pressed, prefix the result with an # 'Escape' key, just like unix VT100 terminals do. # NOTE: Only replace the left alt with escape. The right alt key often # acts as altgr and is used in many non US keyboard layouts for # typing some special characters, like a backslash. We don't want # all backslashes to be prefixed with escape. (Esc-\ has a # meaning in E-macs, for instance.) if result: meta_pressed = control_key_state & self.LEFT_ALT_PRESSED if meta_pressed: return [KeyPress(Keys.Escape, ""), result] else: return [result] else: return [] def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> list[KeyPress]: """ Handle mouse events. Return a list of KeyPress instances. """ event_flags = ev.EventFlags button_state = ev.ButtonState event_type: MouseEventType | None = None button: MouseButton = MouseButton.NONE # Scroll events. if event_flags & MOUSE_WHEELED: if button_state > 0: event_type = MouseEventType.SCROLL_UP else: event_type = MouseEventType.SCROLL_DOWN else: # Handle button state for non-scroll events. if button_state == FROM_LEFT_1ST_BUTTON_PRESSED: button = MouseButton.LEFT elif button_state == RIGHTMOST_BUTTON_PRESSED: button = MouseButton.RIGHT # Move events. if event_flags & MOUSE_MOVED: event_type = MouseEventType.MOUSE_MOVE # No key pressed anymore: mouse up. if event_type is None: if button_state > 0: # Some button pressed. event_type = MouseEventType.MOUSE_DOWN else: # No button pressed. event_type = MouseEventType.MOUSE_UP data = ";".join( [ button.value, event_type.value, str(ev.MousePosition.X), str(ev.MousePosition.Y), ] ) return [KeyPress(Keys.WindowsMouseEvent, data)] class Vt100ConsoleInputReader: """ Similar to `ConsoleInputReader`, but for usage when `ENABLE_VIRTUAL_TERMINAL_INPUT` is enabled. This assumes that Windows sends us the right vt100 escape sequences and we parse those with our vt100 parser. (Using this instead of `ConsoleInputReader` results in the "data" attribute from the `KeyPress` instances to be more correct in edge cases, because this responds to for instance the terminal being in application cursor keys mode.) """ def __init__(self) -> None: self._fdcon = None self._buffer: list[KeyPress] = [] # Buffer to collect the Key objects. self._vt100_parser = Vt100Parser( lambda key_press: self._buffer.append(key_press) ) # When stdin is a tty, use that handle, otherwise, create a handle from # CONIN$. self.handle: HANDLE if sys.stdin.isatty(): self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) else: self._fdcon = os.open("CONIN$", os.O_RDWR | os.O_BINARY) self.handle = HANDLE(msvcrt.get_osfhandle(self._fdcon)) def close(self) -> None: "Close fdcon." if self._fdcon is not None: os.close(self._fdcon) def read(self) -> Iterable[KeyPress]: """ Return a list of `KeyPress` instances. It won't return anything when there was nothing to read. (This function doesn't block.) http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx """ max_count = 2048 # Max events to read at the same time. read = DWORD(0) arrtype = INPUT_RECORD * max_count input_records = arrtype() # Check whether there is some input to read. `ReadConsoleInputW` would # block otherwise. # (Actually, the event loop is responsible to make sure that this # function is only called when there is something to read, but for some # reason this happened in the asyncio_win32 loop, and it's better to be # safe anyway.) if not wait_for_handles([self.handle], timeout=0): return [] # Get next batch of input event. windll.kernel32.ReadConsoleInputW( self.handle, pointer(input_records), max_count, pointer(read) ) # First, get all the keys from the input buffer, in order to determine # whether we should consider this a paste event or not. for key_data in self._get_keys(read, input_records): self._vt100_parser.feed(key_data) # Return result. result = self._buffer self._buffer = [] return result def flush_keys(self) -> list[KeyPress]: """ Flush pending keys and return them. (Used for flushing the 'escape' key.) """ # Flush all pending keys. (This is most important to flush the vt100 # 'Escape' key early when nothing else follows.) self._vt100_parser.flush() # Return result. result = self._buffer self._buffer = [] return result def _get_keys( self, read: DWORD, input_records: Array[INPUT_RECORD] ) -> Iterator[str]: """ Generator that yields `KeyPress` objects from the input records. """ for i in range(read.value): ir = input_records[i] # Get the right EventType from the EVENT_RECORD. # (For some reason the Windows console application 'cmder' # [http://gooseberrycreative.com/cmder/] can return '0' for # ir.EventType. -- Just ignore that.) if ir.EventType in EventTypes: ev = getattr(ir.Event, EventTypes[ir.EventType]) # Process if this is a key event. (We also have mouse, menu and # focus events.) if isinstance(ev, KEY_EVENT_RECORD) and ev.KeyDown: u_char = ev.uChar.UnicodeChar if u_char != "\x00": yield u_char class _Win32Handles: """ Utility to keep track of which handles are connectod to which callbacks. `add_win32_handle` starts a tiny event loop in another thread which waits for the Win32 handle to become ready. When this happens, the callback will be called in the current asyncio event loop using `call_soon_threadsafe`. `remove_win32_handle` will stop this tiny event loop. NOTE: We use this technique, so that we don't have to use the `ProactorEventLoop` on Windows and we can wait for things like stdin in a `SelectorEventLoop`. This is important, because our inputhook mechanism (used by IPython), only works with the `SelectorEventLoop`. """ def __init__(self) -> None: self._handle_callbacks: dict[int, Callable[[], None]] = {} # Windows Events that are triggered when we have to stop watching this # handle. self._remove_events: dict[int, HANDLE] = {} def add_win32_handle(self, handle: HANDLE, callback: Callable[[], None]) -> None: """ Add a Win32 handle to the event loop. """ handle_value = handle.value if handle_value is None: raise ValueError("Invalid handle.") # Make sure to remove a previous registered handler first. self.remove_win32_handle(handle) loop = get_running_loop() self._handle_callbacks[handle_value] = callback # Create remove event. remove_event = create_win32_event() self._remove_events[handle_value] = remove_event # Add reader. def ready() -> None: # Tell the callback that input's ready. try: callback() finally: run_in_executor_with_context(wait, loop=loop) # Wait for the input to become ready. # (Use an executor for this, the Windows asyncio event loop doesn't # allow us to wait for handles like stdin.) def wait() -> None: # Wait until either the handle becomes ready, or the remove event # has been set. result = wait_for_handles([remove_event, handle]) if result is remove_event: windll.kernel32.CloseHandle(remove_event) return else: loop.call_soon_threadsafe(ready) run_in_executor_with_context(wait, loop=loop) def remove_win32_handle(self, handle: HANDLE) -> Callable[[], None] | None: """ Remove a Win32 handle from the event loop. Return either the registered handler or `None`. """ if handle.value is None: return None # Ignore. # Trigger remove events, so that the reader knows to stop. try: event = self._remove_events.pop(handle.value) except KeyError: pass else: windll.kernel32.SetEvent(event) try: return self._handle_callbacks.pop(handle.value) except KeyError: return None @contextmanager def attach_win32_input( input: _Win32InputBase, callback: Callable[[], None] ) -> Iterator[None]: """ Context manager that makes this input active in the current event loop. :param input: :class:`~prompt_toolkit.input.Input` object. :param input_ready_callback: Called when the input is ready to read. """ win32_handles = input.win32_handles handle = input.handle if handle.value is None: raise ValueError("Invalid handle.") # Add reader. previous_callback = win32_handles.remove_win32_handle(handle) win32_handles.add_win32_handle(handle, callback) try: yield finally: win32_handles.remove_win32_handle(handle) if previous_callback: win32_handles.add_win32_handle(handle, previous_callback) @contextmanager def detach_win32_input(input: _Win32InputBase) -> Iterator[None]: win32_handles = input.win32_handles handle = input.handle if handle.value is None: raise ValueError("Invalid handle.") previous_callback = win32_handles.remove_win32_handle(handle) try: yield finally: if previous_callback: win32_handles.add_win32_handle(handle, previous_callback) class raw_mode: """ :: with raw_mode(stdin): ''' the windows terminal is now in 'raw' mode. ''' The ``fileno`` attribute is ignored. This is to be compatible with the `raw_input` method of `.vt100_input`. """ def __init__( self, fileno: int | None = None, use_win10_virtual_terminal_input: bool = False ) -> None: self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) self.use_win10_virtual_terminal_input = use_win10_virtual_terminal_input def __enter__(self) -> None: # Remember original mode. original_mode = DWORD() windll.kernel32.GetConsoleMode(self.handle, pointer(original_mode)) self.original_mode = original_mode self._patch() def _patch(self) -> None: # Set raw ENABLE_ECHO_INPUT = 0x0004 ENABLE_LINE_INPUT = 0x0002 ENABLE_PROCESSED_INPUT = 0x0001 new_mode = self.original_mode.value & ~( ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT ) if self.use_win10_virtual_terminal_input: new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT windll.kernel32.SetConsoleMode(self.handle, new_mode) def __exit__(self, *a: object) -> None: # Restore original mode windll.kernel32.SetConsoleMode(self.handle, self.original_mode) class cooked_mode(raw_mode): """ :: with cooked_mode(stdin): ''' The pseudo-terminal stdin is now used in cooked mode. ''' """ def _patch(self) -> None: # Set cooked. ENABLE_ECHO_INPUT = 0x0004 ENABLE_LINE_INPUT = 0x0002 ENABLE_PROCESSED_INPUT = 0x0001 windll.kernel32.SetConsoleMode( self.handle, self.original_mode.value | (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT), ) def _is_win_vt100_input_enabled() -> bool: """ Returns True when we're running Windows and VT100 escape sequences are supported. """ hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) # Get original console mode. original_mode = DWORD(0) windll.kernel32.GetConsoleMode(hconsole, byref(original_mode)) try: # Try to enable VT100 sequences. result: int = windll.kernel32.SetConsoleMode( hconsole, DWORD(ENABLE_VIRTUAL_TERMINAL_INPUT) ) return result == 1 finally: windll.kernel32.SetConsoleMode(hconsole, original_mode) ================================================ FILE: src/prompt_toolkit/input/win32_pipe.py ================================================ from __future__ import annotations import sys assert sys.platform == "win32" from collections.abc import Callable, Iterator from contextlib import AbstractContextManager, contextmanager from ctypes import windll from ctypes.wintypes import HANDLE from prompt_toolkit.eventloop.win32 import create_win32_event from ..key_binding import KeyPress from ..utils import DummyContext from .base import PipeInput from .vt100_parser import Vt100Parser from .win32 import _Win32InputBase, attach_win32_input, detach_win32_input __all__ = ["Win32PipeInput"] class Win32PipeInput(_Win32InputBase, PipeInput): """ This is an input pipe that works on Windows. Text or bytes can be feed into the pipe, and key strokes can be read from the pipe. This is useful if we want to send the input programmatically into the application. Mostly useful for unit testing. Notice that even though it's Windows, we use vt100 escape sequences over the pipe. Usage:: input = Win32PipeInput() input.send_text('inputdata') """ _id = 0 def __init__(self, _event: HANDLE) -> None: super().__init__() # Event (handle) for registering this input in the event loop. # This event is set when there is data available to read from the pipe. # Note: We use this approach instead of using a regular pipe, like # returned from `os.pipe()`, because making such a regular pipe # non-blocking is tricky and this works really well. self._event = create_win32_event() self._closed = False # Parser for incoming keys. self._buffer: list[KeyPress] = [] # Buffer to collect the Key objects. self.vt100_parser = Vt100Parser(lambda key: self._buffer.append(key)) # Identifier for every PipeInput for the hash. self.__class__._id += 1 self._id = self.__class__._id @classmethod @contextmanager def create(cls) -> Iterator[Win32PipeInput]: event = create_win32_event() try: yield Win32PipeInput(_event=event) finally: windll.kernel32.CloseHandle(event) @property def closed(self) -> bool: return self._closed def fileno(self) -> int: """ The windows pipe doesn't depend on the file handle. """ raise NotImplementedError @property def handle(self) -> HANDLE: "The handle used for registering this pipe in the event loop." return self._event def attach( self, input_ready_callback: Callable[[], None] ) -> AbstractContextManager[None]: """ Return a context manager that makes this input active in the current event loop. """ return attach_win32_input(self, input_ready_callback) def detach(self) -> AbstractContextManager[None]: """ Return a context manager that makes sure that this input is not active in the current event loop. """ return detach_win32_input(self) def read_keys(self) -> list[KeyPress]: "Read list of KeyPress." # Return result. result = self._buffer self._buffer = [] # Reset event. if not self._closed: # (If closed, the event should not reset.) windll.kernel32.ResetEvent(self._event) return result def flush_keys(self) -> list[KeyPress]: """ Flush pending keys and return them. (Used for flushing the 'escape' key.) """ # Flush all pending keys. (This is most important to flush the vt100 # 'Escape' key early when nothing else follows.) self.vt100_parser.flush() # Return result. result = self._buffer self._buffer = [] return result def send_bytes(self, data: bytes) -> None: "Send bytes to the input." self.send_text(data.decode("utf-8", "ignore")) def send_text(self, text: str) -> None: "Send text to the input." if self._closed: raise ValueError("Attempt to write into a closed pipe.") # Pass it through our vt100 parser. self.vt100_parser.feed(text) # Set event. windll.kernel32.SetEvent(self._event) def raw_mode(self) -> AbstractContextManager[None]: return DummyContext() def cooked_mode(self) -> AbstractContextManager[None]: return DummyContext() def close(self) -> None: "Close write-end of the pipe." self._closed = True windll.kernel32.SetEvent(self._event) def typeahead_hash(self) -> str: """ This needs to be unique for every `PipeInput`. """ return f"pipe-input-{self._id}" ================================================ FILE: src/prompt_toolkit/key_binding/__init__.py ================================================ from __future__ import annotations from .key_bindings import ( ConditionalKeyBindings, DynamicKeyBindings, KeyBindings, KeyBindingsBase, merge_key_bindings, ) from .key_processor import KeyPress, KeyPressEvent __all__ = [ # key_bindings. "ConditionalKeyBindings", "DynamicKeyBindings", "KeyBindings", "KeyBindingsBase", "merge_key_bindings", # key_processor "KeyPress", "KeyPressEvent", ] ================================================ FILE: src/prompt_toolkit/key_binding/bindings/__init__.py ================================================ ================================================ FILE: src/prompt_toolkit/key_binding/bindings/auto_suggest.py ================================================ """ Key bindings for auto suggestion (for fish-style auto suggestion). """ from __future__ import annotations import re from prompt_toolkit.application.current import get_app from prompt_toolkit.filters import Condition, emacs_mode from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent __all__ = [ "load_auto_suggest_bindings", ] E = KeyPressEvent def load_auto_suggest_bindings() -> KeyBindings: """ Key bindings for accepting auto suggestion text. (This has to come after the Vi bindings, because they also have an implementation for the "right arrow", but we really want the suggestion binding when a suggestion is available.) """ key_bindings = KeyBindings() handle = key_bindings.add @Condition def suggestion_available() -> bool: app = get_app() return ( app.current_buffer.suggestion is not None and len(app.current_buffer.suggestion.text) > 0 and app.current_buffer.document.is_cursor_at_the_end ) @handle("c-f", filter=suggestion_available) @handle("c-e", filter=suggestion_available) @handle("right", filter=suggestion_available) def _accept(event: E) -> None: """ Accept suggestion. """ b = event.current_buffer suggestion = b.suggestion if suggestion: b.insert_text(suggestion.text) @handle("escape", "f", filter=suggestion_available & emacs_mode) def _fill(event: E) -> None: """ Fill partial suggestion. """ b = event.current_buffer suggestion = b.suggestion if suggestion: t = re.split(r"([^\s/]+(?:\s+|/))", suggestion.text) b.insert_text(next(x for x in t if x)) return key_bindings ================================================ FILE: src/prompt_toolkit/key_binding/bindings/basic.py ================================================ # pylint: disable=function-redefined from __future__ import annotations from prompt_toolkit.application.current import get_app from prompt_toolkit.filters import ( Condition, emacs_insert_mode, has_selection, in_paste_mode, is_multiline, vi_insert_mode, ) from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent from prompt_toolkit.keys import Keys from ..key_bindings import KeyBindings from .named_commands import get_by_name __all__ = [ "load_basic_bindings", ] E = KeyPressEvent def if_no_repeat(event: E) -> bool: """Callable that returns True when the previous event was delivered to another handler.""" return not event.is_repeat @Condition def has_text_before_cursor() -> bool: return bool(get_app().current_buffer.text) @Condition def in_quoted_insert() -> bool: return get_app().quoted_insert def load_basic_bindings() -> KeyBindings: key_bindings = KeyBindings() insert_mode = vi_insert_mode | emacs_insert_mode handle = key_bindings.add @handle("c-a") @handle("c-b") @handle("c-c") @handle("c-d") @handle("c-e") @handle("c-f") @handle("c-g") @handle("c-h") @handle("c-i") @handle("c-j") @handle("c-k") @handle("c-l") @handle("c-m") @handle("c-n") @handle("c-o") @handle("c-p") @handle("c-q") @handle("c-r") @handle("c-s") @handle("c-t") @handle("c-u") @handle("c-v") @handle("c-w") @handle("c-x") @handle("c-y") @handle("c-z") @handle("f1") @handle("f2") @handle("f3") @handle("f4") @handle("f5") @handle("f6") @handle("f7") @handle("f8") @handle("f9") @handle("f10") @handle("f11") @handle("f12") @handle("f13") @handle("f14") @handle("f15") @handle("f16") @handle("f17") @handle("f18") @handle("f19") @handle("f20") @handle("f21") @handle("f22") @handle("f23") @handle("f24") @handle("c-@") # Also c-space. @handle("c-\\") @handle("c-]") @handle("c-^") @handle("c-_") @handle("backspace") @handle("up") @handle("down") @handle("right") @handle("left") @handle("s-up") @handle("s-down") @handle("s-right") @handle("s-left") @handle("home") @handle("end") @handle("s-home") @handle("s-end") @handle("delete") @handle("s-delete") @handle("c-delete") @handle("pageup") @handle("pagedown") @handle("s-tab") @handle("tab") @handle("c-s-left") @handle("c-s-right") @handle("c-s-home") @handle("c-s-end") @handle("c-left") @handle("c-right") @handle("c-up") @handle("c-down") @handle("c-home") @handle("c-end") @handle("insert") @handle("s-insert") @handle("c-insert") @handle("<sigint>") @handle(Keys.Ignore) def _ignore(event: E) -> None: """ First, for any of these keys, Don't do anything by default. Also don't catch them in the 'Any' handler which will insert them as data. If people want to insert these characters as a literal, they can always do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi mode.) """ pass # Readline-style bindings. handle("home")(get_by_name("beginning-of-line")) handle("end")(get_by_name("end-of-line")) handle("left")(get_by_name("backward-char")) handle("right")(get_by_name("forward-char")) handle("c-up")(get_by_name("previous-history")) handle("c-down")(get_by_name("next-history")) handle("c-l")(get_by_name("clear-screen")) handle("c-k", filter=insert_mode)(get_by_name("kill-line")) handle("c-u", filter=insert_mode)(get_by_name("unix-line-discard")) handle("backspace", filter=insert_mode, save_before=if_no_repeat)( get_by_name("backward-delete-char") ) handle("delete", filter=insert_mode, save_before=if_no_repeat)( get_by_name("delete-char") ) handle("c-delete", filter=insert_mode, save_before=if_no_repeat)( get_by_name("delete-char") ) handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)( get_by_name("self-insert") ) handle("c-t", filter=insert_mode)(get_by_name("transpose-chars")) handle("c-i", filter=insert_mode)(get_by_name("menu-complete")) handle("s-tab", filter=insert_mode)(get_by_name("menu-complete-backward")) # Control-W should delete, using whitespace as separator, while M-Del # should delete using [^a-zA-Z0-9] as a boundary. handle("c-w", filter=insert_mode)(get_by_name("unix-word-rubout")) handle("pageup", filter=~has_selection)(get_by_name("previous-history")) handle("pagedown", filter=~has_selection)(get_by_name("next-history")) # CTRL keys. handle("c-d", filter=has_text_before_cursor & insert_mode)( get_by_name("delete-char") ) @handle("enter", filter=insert_mode & is_multiline) def _newline(event: E) -> None: """ Newline (in case of multiline input. """ event.current_buffer.newline(copy_margin=not in_paste_mode()) @handle("c-j") def _newline2(event: E) -> None: r""" By default, handle \n as if it were a \r (enter). (It appears that some terminals send \n instead of \r when pressing enter. - at least the Linux subsystem for Windows.) """ event.key_processor.feed(KeyPress(Keys.ControlM, "\r"), first=True) # Delete the word before the cursor. @handle("up") def _go_up(event: E) -> None: event.current_buffer.auto_up(count=event.arg) @handle("down") def _go_down(event: E) -> None: event.current_buffer.auto_down(count=event.arg) @handle("delete", filter=has_selection) def _cut(event: E) -> None: data = event.current_buffer.cut_selection() event.app.clipboard.set_data(data) # Global bindings. @handle("c-z") def _insert_ctrl_z(event: E) -> None: """ By default, control-Z should literally insert Ctrl-Z. (Ansi Ctrl-Z, code 26 in MSDOS means End-Of-File. In a Python REPL for instance, it's possible to type Control-Z followed by enter to quit.) When the system bindings are loaded and suspend-to-background is supported, that will override this binding. """ event.current_buffer.insert_text(event.data) @handle(Keys.BracketedPaste) def _paste(event: E) -> None: """ Pasting from clipboard. """ data = event.data # Be sure to use \n as line ending. # Some terminals (Like iTerm2) seem to paste \r\n line endings in a # bracketed paste. See: https://github.com/ipython/ipython/issues/9737 data = data.replace("\r\n", "\n") data = data.replace("\r", "\n") event.current_buffer.insert_text(data) @handle(Keys.Any, filter=in_quoted_insert, eager=True) def _insert_text(event: E) -> None: """ Handle quoted insert. """ event.current_buffer.insert_text(event.data, overwrite=False) event.app.quoted_insert = False return key_bindings ================================================ FILE: src/prompt_toolkit/key_binding/bindings/completion.py ================================================ """ Key binding handlers for displaying completions. """ from __future__ import annotations import asyncio import math from typing import TYPE_CHECKING from prompt_toolkit.application.run_in_terminal import in_terminal from prompt_toolkit.completion import ( CompleteEvent, Completion, get_common_complete_suffix, ) from prompt_toolkit.formatted_text import StyleAndTextTuples from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from prompt_toolkit.utils import get_cwidth if TYPE_CHECKING: from prompt_toolkit.application import Application from prompt_toolkit.shortcuts import PromptSession __all__ = [ "generate_completions", "display_completions_like_readline", ] E = KeyPressEvent def generate_completions(event: E) -> None: r""" Tab-completion: where the first tab completes the common suffix and the second tab lists all the completions. """ b = event.current_buffer # When already navigating through completions, select the next one. if b.complete_state: b.complete_next() else: b.start_completion(insert_common_part=True) def display_completions_like_readline(event: E) -> None: """ Key binding handler for readline-style tab completion. This is meant to be as similar as possible to the way how readline displays completions. Generate the completions immediately (blocking) and display them above the prompt in columns. Usage:: # Call this handler when 'Tab' has been pressed. key_bindings.add(Keys.ControlI)(display_completions_like_readline) """ # Request completions. b = event.current_buffer if b.completer is None: return complete_event = CompleteEvent(completion_requested=True) completions = list(b.completer.get_completions(b.document, complete_event)) # Calculate the common suffix. common_suffix = get_common_complete_suffix(b.document, completions) # One completion: insert it. if len(completions) == 1: b.delete_before_cursor(-completions[0].start_position) b.insert_text(completions[0].text) # Multiple completions with common part. elif common_suffix: b.insert_text(common_suffix) # Otherwise: display all completions. elif completions: _display_completions_like_readline(event.app, completions) def _display_completions_like_readline( app: Application[object], completions: list[Completion] ) -> asyncio.Task[None]: """ Display the list of completions in columns above the prompt. This will ask for a confirmation if there are too many completions to fit on a single page and provide a paginator to walk through them. """ from prompt_toolkit.formatted_text import to_formatted_text from prompt_toolkit.shortcuts.prompt import create_confirm_session # Get terminal dimensions. term_size = app.output.get_size() term_width = term_size.columns term_height = term_size.rows # Calculate amount of required columns/rows for displaying the # completions. (Keep in mind that completions are displayed # alphabetically column-wise.) max_compl_width = min( term_width, max(get_cwidth(c.display_text) for c in completions) + 1 ) column_count = max(1, term_width // max_compl_width) completions_per_page = column_count * (term_height - 1) page_count = int(math.ceil(len(completions) / float(completions_per_page))) # Note: math.ceil can return float on Python2. def display(page: int) -> None: # Display completions. page_completions = completions[ page * completions_per_page : (page + 1) * completions_per_page ] page_row_count = int(math.ceil(len(page_completions) / float(column_count))) page_columns = [ page_completions[i * page_row_count : (i + 1) * page_row_count] for i in range(column_count) ] result: StyleAndTextTuples = [] for r in range(page_row_count): for c in range(column_count): try: completion = page_columns[c][r] style = "class:readline-like-completions.completion " + ( completion.style or "" ) result.extend(to_formatted_text(completion.display, style=style)) # Add padding. padding = max_compl_width - get_cwidth(completion.display_text) result.append((completion.style, " " * padding)) except IndexError: pass result.append(("", "\n")) app.print_text(to_formatted_text(result, "class:readline-like-completions")) # User interaction through an application generator function. async def run_compl() -> None: "Coroutine." async with in_terminal(render_cli_done=True): if len(completions) > completions_per_page: # Ask confirmation if it doesn't fit on the screen. confirm = await create_confirm_session( f"Display all {len(completions)} possibilities?", ).prompt_async() if confirm: # Display pages. for page in range(page_count): display(page) if page != page_count - 1: # Display --MORE-- and go to the next page. show_more = await _create_more_session( "--MORE--" ).prompt_async() if not show_more: return else: app.output.flush() else: # Display all completions. display(0) return app.create_background_task(run_compl()) def _create_more_session(message: str = "--MORE--") -> PromptSession[bool]: """ Create a `PromptSession` object for displaying the "--MORE--". """ from prompt_toolkit.shortcuts import PromptSession bindings = KeyBindings() @bindings.add(" ") @bindings.add("y") @bindings.add("Y") @bindings.add(Keys.ControlJ) @bindings.add(Keys.ControlM) @bindings.add(Keys.ControlI) # Tab. def _yes(event: E) -> None: event.app.exit(result=True) @bindings.add("n") @bindings.add("N") @bindings.add("q") @bindings.add("Q") @bindings.add(Keys.ControlC) def _no(event: E) -> None: event.app.exit(result=False) @bindings.add(Keys.Any) def _ignore(event: E) -> None: "Disable inserting of text." return PromptSession(message, key_bindings=bindings, erase_when_done=True) ================================================ FILE: src/prompt_toolkit/key_binding/bindings/cpr.py ================================================ from __future__ import annotations from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from ..key_bindings import KeyBindings __all__ = [ "load_cpr_bindings", ] E = KeyPressEvent def load_cpr_bindings() -> KeyBindings: key_bindings = KeyBindings() @key_bindings.add(Keys.CPRResponse, save_before=lambda e: False) def _(event: E) -> None: """ Handle incoming Cursor-Position-Request response. """ # The incoming data looks like u'\x1b[35;1R' # Parse row/col information. row, col = map(int, event.data[2:-1].split(";")) # Report absolute cursor position to the renderer. event.app.renderer.report_absolute_cursor_row(row) return key_bindings ================================================ FILE: src/prompt_toolkit/key_binding/bindings/emacs.py ================================================ # pylint: disable=function-redefined from __future__ import annotations from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer, indent, unindent from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.filters import ( Condition, emacs_insert_mode, emacs_mode, has_arg, has_selection, in_paste_mode, is_multiline, is_read_only, shift_selection_mode, vi_search_direction_reversed, ) from prompt_toolkit.key_binding.key_bindings import Binding from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from prompt_toolkit.selection import SelectionType from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase from .named_commands import get_by_name __all__ = [ "load_emacs_bindings", "load_emacs_search_bindings", "load_emacs_shift_selection_bindings", ] E = KeyPressEvent @Condition def is_returnable() -> bool: return get_app().current_buffer.is_returnable @Condition def is_arg() -> bool: return get_app().key_processor.arg == "-" def load_emacs_bindings() -> KeyBindingsBase: """ Some e-macs extensions. """ # Overview of Readline emacs commands: # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf key_bindings = KeyBindings() handle = key_bindings.add insert_mode = emacs_insert_mode @handle("escape") def _esc(event: E) -> None: """ By default, ignore escape key. (If we don't put this here, and Esc is followed by a key which sequence is not handled, we'll insert an Escape character in the input stream. Something we don't want and happens to easily in emacs mode. Further, people can always use ControlQ to do a quoted insert.) """ pass handle("c-a")(get_by_name("beginning-of-line")) handle("c-b")(get_by_name("backward-char")) handle("c-delete", filter=insert_mode)(get_by_name("kill-word")) handle("c-e")(get_by_name("end-of-line")) handle("c-f")(get_by_name("forward-char")) handle("c-left")(get_by_name("backward-word")) handle("c-right")(get_by_name("forward-word")) handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank")) handle("c-y", filter=insert_mode)(get_by_name("yank")) handle("escape", "b")(get_by_name("backward-word")) handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word")) handle("escape", "d", filter=insert_mode)(get_by_name("kill-word")) handle("escape", "f")(get_by_name("forward-word")) handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word")) handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word")) handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop")) handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word")) handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space")) handle("c-home")(get_by_name("beginning-of-buffer")) handle("c-end")(get_by_name("end-of-buffer")) handle("c-_", save_before=(lambda e: False), filter=insert_mode)( get_by_name("undo") ) handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)( get_by_name("undo") ) handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history")) handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history")) handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg")) handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg")) handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg")) handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment")) handle("c-o")(get_by_name("operate-and-get-next")) # ControlQ does a quoted insert. Not that for vt100 terminals, you have to # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and # Ctrl-S are captured by the terminal. handle("c-q", filter=~has_selection)(get_by_name("quoted-insert")) handle("c-x", "(")(get_by_name("start-kbd-macro")) handle("c-x", ")")(get_by_name("end-kbd-macro")) handle("c-x", "e")(get_by_name("call-last-kbd-macro")) @handle("c-n") def _next(event: E) -> None: "Next line." event.current_buffer.auto_down() @handle("c-p") def _prev(event: E) -> None: "Previous line." event.current_buffer.auto_up(count=event.arg) def handle_digit(c: str) -> None: """ Handle input of arguments. The first number needs to be preceded by escape. """ @handle(c, filter=has_arg) @handle("escape", c) def _(event: E) -> None: event.append_to_arg_count(c) for c in "0123456789": handle_digit(c) @handle("escape", "-", filter=~has_arg) def _meta_dash(event: E) -> None: """""" if event._arg is None: event.append_to_arg_count("-") @handle("-", filter=is_arg) def _dash(event: E) -> None: """ When '-' is typed again, after exactly '-' has been given as an argument, ignore this. """ event.app.key_processor.arg = "-" # Meta + Enter: always accept input. handle("escape", "enter", filter=insert_mode & is_returnable)( get_by_name("accept-line") ) # Enter: accept input in single line mode. handle("enter", filter=insert_mode & is_returnable & ~is_multiline)( get_by_name("accept-line") ) def character_search(buff: Buffer, char: str, count: int) -> None: if count < 0: match = buff.document.find_backwards( char, in_current_line=True, count=-count ) else: match = buff.document.find(char, in_current_line=True, count=count) if match is not None: buff.cursor_position += match @handle("c-]", Keys.Any) def _goto_char(event: E) -> None: "When Ctl-] + a character is pressed. go to that character." # Also named 'character-search' character_search(event.current_buffer, event.data, event.arg) @handle("escape", "c-]", Keys.Any) def _goto_char_backwards(event: E) -> None: "Like Ctl-], but backwards." # Also named 'character-search-backward' character_search(event.current_buffer, event.data, -event.arg) @handle("escape", "a") def _prev_sentence(event: E) -> None: "Previous sentence." # TODO: @handle("escape", "e") def _end_of_sentence(event: E) -> None: "Move to end of sentence." # TODO: @handle("escape", "t", filter=insert_mode) def _swap_characters(event: E) -> None: """ Swap the last two words before the cursor. """ # TODO @handle("escape", "*", filter=insert_mode) def _insert_all_completions(event: E) -> None: """ `meta-*`: Insert all possible completions of the preceding text. """ buff = event.current_buffer # List all completions. complete_event = CompleteEvent(text_inserted=False, completion_requested=True) completions = list( buff.completer.get_completions(buff.document, complete_event) ) # Insert them. text_to_insert = " ".join(c.text for c in completions) buff.insert_text(text_to_insert) @handle("c-x", "c-x") def _toggle_start_end(event: E) -> None: """ Move cursor back and forth between the start and end of the current line. """ buffer = event.current_buffer if buffer.document.is_cursor_at_the_end_of_line: buffer.cursor_position += buffer.document.get_start_of_line_position( after_whitespace=False ) else: buffer.cursor_position += buffer.document.get_end_of_line_position() @handle("c-@") # Control-space or Control-@ def _start_selection(event: E) -> None: """ Start of the selection (if the current buffer is not empty). """ # Take the current cursor position as the start of this selection. buff = event.current_buffer if buff.text: buff.start_selection(selection_type=SelectionType.CHARACTERS) @handle("c-g", filter=~has_selection) def _cancel(event: E) -> None: """ Control + G: Cancel completion menu and validation state. """ event.current_buffer.complete_state = None event.current_buffer.validation_error = None @handle("c-g", filter=has_selection) def _cancel_selection(event: E) -> None: """ Cancel selection. """ event.current_buffer.exit_selection() @handle("c-w", filter=has_selection) @handle("c-x", "r", "k", filter=has_selection) def _cut(event: E) -> None: """ Cut selected text. """ data = event.current_buffer.cut_selection() event.app.clipboard.set_data(data) @handle("escape", "w", filter=has_selection) def _copy(event: E) -> None: """ Copy selected text. """ data = event.current_buffer.copy_selection() event.app.clipboard.set_data(data) @handle("escape", "left") def _start_of_word(event: E) -> None: """ Cursor to start of previous word. """ buffer = event.current_buffer buffer.cursor_position += ( buffer.document.find_previous_word_beginning(count=event.arg) or 0 ) @handle("escape", "right") def _start_next_word(event: E) -> None: """ Cursor to start of next word. """ buffer = event.current_buffer buffer.cursor_position += ( buffer.document.find_next_word_beginning(count=event.arg) or buffer.document.get_end_of_document_position() ) @handle("escape", "/", filter=insert_mode) def _complete(event: E) -> None: """ M-/: Complete. """ b = event.current_buffer if b.complete_state: b.complete_next() else: b.start_completion(select_first=True) @handle("c-c", ">", filter=has_selection) def _indent(event: E) -> None: """ Indent selected text. """ buffer = event.current_buffer buffer.cursor_position += buffer.document.get_start_of_line_position( after_whitespace=True ) from_, to = buffer.document.selection_range() from_, _ = buffer.document.translate_index_to_position(from_) to, _ = buffer.document.translate_index_to_position(to) indent(buffer, from_, to + 1, count=event.arg) @handle("c-c", "<", filter=has_selection) def _unindent(event: E) -> None: """ Unindent selected text. """ buffer = event.current_buffer from_, to = buffer.document.selection_range() from_, _ = buffer.document.translate_index_to_position(from_) to, _ = buffer.document.translate_index_to_position(to) unindent(buffer, from_, to + 1, count=event.arg) return ConditionalKeyBindings(key_bindings, emacs_mode) def load_emacs_search_bindings() -> KeyBindingsBase: key_bindings = KeyBindings() handle = key_bindings.add from . import search # NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we # want Alt+Enter to accept input directly in incremental search mode. # Instead, we have double escape. handle("c-r")(search.start_reverse_incremental_search) handle("c-s")(search.start_forward_incremental_search) handle("c-c")(search.abort_search) handle("c-g")(search.abort_search) handle("c-r")(search.reverse_incremental_search) handle("c-s")(search.forward_incremental_search) handle("up")(search.reverse_incremental_search) handle("down")(search.forward_incremental_search) handle("enter")(search.accept_search) # Handling of escape. handle("escape", eager=True)(search.accept_search) # Like Readline, it's more natural to accept the search when escape has # been pressed, however instead the following two bindings could be used # instead. # #handle('escape', 'escape', eager=True)(search.abort_search) # #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input) # If Read-only: also include the following key bindings: # '/' and '?' key bindings for searching, just like Vi mode. handle("?", filter=is_read_only & ~vi_search_direction_reversed)( search.start_reverse_incremental_search ) handle("/", filter=is_read_only & ~vi_search_direction_reversed)( search.start_forward_incremental_search ) handle("?", filter=is_read_only & vi_search_direction_reversed)( search.start_forward_incremental_search ) handle("/", filter=is_read_only & vi_search_direction_reversed)( search.start_reverse_incremental_search ) @handle("n", filter=is_read_only) def _jump_next(event: E) -> None: "Jump to next match." event.current_buffer.apply_search( event.app.current_search_state, include_current_position=False, count=event.arg, ) @handle("N", filter=is_read_only) def _jump_prev(event: E) -> None: "Jump to previous match." event.current_buffer.apply_search( ~event.app.current_search_state, include_current_position=False, count=event.arg, ) return ConditionalKeyBindings(key_bindings, emacs_mode) def load_emacs_shift_selection_bindings() -> KeyBindingsBase: """ Bindings to select text with shift + cursor movements """ key_bindings = KeyBindings() handle = key_bindings.add def unshift_move(event: E) -> None: """ Used for the shift selection mode. When called with a shift + movement key press event, moves the cursor as if shift is not pressed. """ key = event.key_sequence[0].key if key == Keys.ShiftUp: event.current_buffer.auto_up(count=event.arg) return if key == Keys.ShiftDown: event.current_buffer.auto_down(count=event.arg) return # the other keys are handled through their readline command key_to_command: dict[Keys | str, str] = { Keys.ShiftLeft: "backward-char", Keys.ShiftRight: "forward-char", Keys.ShiftHome: "beginning-of-line", Keys.ShiftEnd: "end-of-line", Keys.ControlShiftLeft: "backward-word", Keys.ControlShiftRight: "forward-word", Keys.ControlShiftHome: "beginning-of-buffer", Keys.ControlShiftEnd: "end-of-buffer", } try: # Both the dict lookup and `get_by_name` can raise KeyError. binding = get_by_name(key_to_command[key]) except KeyError: pass else: # (`else` is not really needed here.) if isinstance(binding, Binding): # (It should always be a binding here) binding.call(event) @handle("s-left", filter=~has_selection) @handle("s-right", filter=~has_selection) @handle("s-up", filter=~has_selection) @handle("s-down", filter=~has_selection) @handle("s-home", filter=~has_selection) @handle("s-end", filter=~has_selection) @handle("c-s-left", filter=~has_selection) @handle("c-s-right", filter=~has_selection) @handle("c-s-home", filter=~has_selection) @handle("c-s-end", filter=~has_selection) def _start_selection(event: E) -> None: """ Start selection with shift + movement. """ # Take the current cursor position as the start of this selection. buff = event.current_buffer if buff.text: buff.start_selection(selection_type=SelectionType.CHARACTERS) if buff.selection_state is not None: # (`selection_state` should never be `None`, it is created by # `start_selection`.) buff.selection_state.enter_shift_mode() # Then move the cursor original_position = buff.cursor_position unshift_move(event) if buff.cursor_position == original_position: # Cursor didn't actually move - so cancel selection # to avoid having an empty selection buff.exit_selection() @handle("s-left", filter=shift_selection_mode) @handle("s-right", filter=shift_selection_mode) @handle("s-up", filter=shift_selection_mode) @handle("s-down", filter=shift_selection_mode) @handle("s-home", filter=shift_selection_mode) @handle("s-end", filter=shift_selection_mode) @handle("c-s-left", filter=shift_selection_mode) @handle("c-s-right", filter=shift_selection_mode) @handle("c-s-home", filter=shift_selection_mode) @handle("c-s-end", filter=shift_selection_mode) def _extend_selection(event: E) -> None: """ Extend the selection """ # Just move the cursor, like shift was not pressed unshift_move(event) buff = event.current_buffer if buff.selection_state is not None: if buff.cursor_position == buff.selection_state.original_cursor_position: # selection is now empty, so cancel selection buff.exit_selection() @handle(Keys.Any, filter=shift_selection_mode) def _replace_selection(event: E) -> None: """ Replace selection by what is typed """ event.current_buffer.cut_selection() get_by_name("self-insert").call(event) @handle("enter", filter=shift_selection_mode & is_multiline) def _newline(event: E) -> None: """ A newline replaces the selection """ event.current_buffer.cut_selection() event.current_buffer.newline(copy_margin=not in_paste_mode()) @handle("backspace", filter=shift_selection_mode) def _delete(event: E) -> None: """ Delete selection. """ event.current_buffer.cut_selection() @handle("c-y", filter=shift_selection_mode) def _yank(event: E) -> None: """ In shift selection mode, yanking (pasting) replace the selection. """ buff = event.current_buffer if buff.selection_state: buff.cut_selection() get_by_name("yank").call(event) # moving the cursor in shift selection mode cancels the selection @handle("left", filter=shift_selection_mode) @handle("right", filter=shift_selection_mode) @handle("up", filter=shift_selection_mode) @handle("down", filter=shift_selection_mode) @handle("home", filter=shift_selection_mode) @handle("end", filter=shift_selection_mode) @handle("c-left", filter=shift_selection_mode) @handle("c-right", filter=shift_selection_mode) @handle("c-home", filter=shift_selection_mode) @handle("c-end", filter=shift_selection_mode) def _cancel(event: E) -> None: """ Cancel selection. """ event.current_buffer.exit_selection() # we then process the cursor movement key_press = event.key_sequence[0] event.key_processor.feed(key_press, first=True) return ConditionalKeyBindings(key_bindings, emacs_mode) ================================================ FILE: src/prompt_toolkit/key_binding/bindings/focus.py ================================================ from __future__ import annotations from prompt_toolkit.key_binding.key_processor import KeyPressEvent __all__ = [ "focus_next", "focus_previous", ] E = KeyPressEvent def focus_next(event: E) -> None: """ Focus the next visible Window. (Often bound to the `Tab` key.) """ event.app.layout.focus_next() def focus_previous(event: E) -> None: """ Focus the previous visible Window. (Often bound to the `BackTab` key.) """ event.app.layout.focus_previous() ================================================ FILE: src/prompt_toolkit/key_binding/bindings/mouse.py ================================================ from __future__ import annotations import sys from typing import TYPE_CHECKING from prompt_toolkit.data_structures import Point from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent from prompt_toolkit.keys import Keys from prompt_toolkit.mouse_events import ( MouseButton, MouseEvent, MouseEventType, MouseModifier, ) from ..key_bindings import KeyBindings if TYPE_CHECKING: from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone __all__ = [ "load_mouse_bindings", ] E = KeyPressEvent # fmt: off SCROLL_UP = MouseEventType.SCROLL_UP SCROLL_DOWN = MouseEventType.SCROLL_DOWN MOUSE_DOWN = MouseEventType.MOUSE_DOWN MOUSE_MOVE = MouseEventType.MOUSE_MOVE MOUSE_UP = MouseEventType.MOUSE_UP NO_MODIFIER : frozenset[MouseModifier] = frozenset() SHIFT : frozenset[MouseModifier] = frozenset({MouseModifier.SHIFT}) ALT : frozenset[MouseModifier] = frozenset({MouseModifier.ALT}) SHIFT_ALT : frozenset[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT}) CONTROL : frozenset[MouseModifier] = frozenset({MouseModifier.CONTROL}) SHIFT_CONTROL : frozenset[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL}) ALT_CONTROL : frozenset[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL}) SHIFT_ALT_CONTROL: frozenset[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL}) UNKNOWN_MODIFIER : frozenset[MouseModifier] = frozenset() LEFT = MouseButton.LEFT MIDDLE = MouseButton.MIDDLE RIGHT = MouseButton.RIGHT NO_BUTTON = MouseButton.NONE UNKNOWN_BUTTON = MouseButton.UNKNOWN xterm_sgr_mouse_events = { ( 0, "m") : (LEFT, MOUSE_UP, NO_MODIFIER), # left_up 0+ + + =0 ( 4, "m") : (LEFT, MOUSE_UP, SHIFT), # left_up Shift 0+4+ + =4 ( 8, "m") : (LEFT, MOUSE_UP, ALT), # left_up Alt 0+ +8+ =8 (12, "m") : (LEFT, MOUSE_UP, SHIFT_ALT), # left_up Shift Alt 0+4+8+ =12 (16, "m") : (LEFT, MOUSE_UP, CONTROL), # left_up Control 0+ + +16=16 (20, "m") : (LEFT, MOUSE_UP, SHIFT_CONTROL), # left_up Shift Control 0+4+ +16=20 (24, "m") : (LEFT, MOUSE_UP, ALT_CONTROL), # left_up Alt Control 0+ +8+16=24 (28, "m") : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL), # left_up Shift Alt Control 0+4+8+16=28 ( 1, "m") : (MIDDLE, MOUSE_UP, NO_MODIFIER), # middle_up 1+ + + =1 ( 5, "m") : (MIDDLE, MOUSE_UP, SHIFT), # middle_up Shift 1+4+ + =5 ( 9, "m") : (MIDDLE, MOUSE_UP, ALT), # middle_up Alt 1+ +8+ =9 (13, "m") : (MIDDLE, MOUSE_UP, SHIFT_ALT), # middle_up Shift Alt 1+4+8+ =13 (17, "m") : (MIDDLE, MOUSE_UP, CONTROL), # middle_up Control 1+ + +16=17 (21, "m") : (MIDDLE, MOUSE_UP, SHIFT_CONTROL), # middle_up Shift Control 1+4+ +16=21 (25, "m") : (MIDDLE, MOUSE_UP, ALT_CONTROL), # middle_up Alt Control 1+ +8+16=25 (29, "m") : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL), # middle_up Shift Alt Control 1+4+8+16=29 ( 2, "m") : (RIGHT, MOUSE_UP, NO_MODIFIER), # right_up 2+ + + =2 ( 6, "m") : (RIGHT, MOUSE_UP, SHIFT), # right_up Shift 2+4+ + =6 (10, "m") : (RIGHT, MOUSE_UP, ALT), # right_up Alt 2+ +8+ =10 (14, "m") : (RIGHT, MOUSE_UP, SHIFT_ALT), # right_up Shift Alt 2+4+8+ =14 (18, "m") : (RIGHT, MOUSE_UP, CONTROL), # right_up Control 2+ + +16=18 (22, "m") : (RIGHT, MOUSE_UP, SHIFT_CONTROL), # right_up Shift Control 2+4+ +16=22 (26, "m") : (RIGHT, MOUSE_UP, ALT_CONTROL), # right_up Alt Control 2+ +8+16=26 (30, "m") : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL), # right_up Shift Alt Control 2+4+8+16=30 ( 0, "M") : (LEFT, MOUSE_DOWN, NO_MODIFIER), # left_down 0+ + + =0 ( 4, "M") : (LEFT, MOUSE_DOWN, SHIFT), # left_down Shift 0+4+ + =4 ( 8, "M") : (LEFT, MOUSE_DOWN, ALT), # left_down Alt 0+ +8+ =8 (12, "M") : (LEFT, MOUSE_DOWN, SHIFT_ALT), # left_down Shift Alt 0+4+8+ =12 (16, "M") : (LEFT, MOUSE_DOWN, CONTROL), # left_down Control 0+ + +16=16 (20, "M") : (LEFT, MOUSE_DOWN, SHIFT_CONTROL), # left_down Shift Control 0+4+ +16=20 (24, "M") : (LEFT, MOUSE_DOWN, ALT_CONTROL), # left_down Alt Control 0+ +8+16=24 (28, "M") : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # left_down Shift Alt Control 0+4+8+16=28 ( 1, "M") : (MIDDLE, MOUSE_DOWN, NO_MODIFIER), # middle_down 1+ + + =1 ( 5, "M") : (MIDDLE, MOUSE_DOWN, SHIFT), # middle_down Shift 1+4+ + =5 ( 9, "M") : (MIDDLE, MOUSE_DOWN, ALT), # middle_down Alt 1+ +8+ =9 (13, "M") : (MIDDLE, MOUSE_DOWN, SHIFT_ALT), # middle_down Shift Alt 1+4+8+ =13 (17, "M") : (MIDDLE, MOUSE_DOWN, CONTROL), # middle_down Control 1+ + +16=17 (21, "M") : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL), # middle_down Shift Control 1+4+ +16=21 (25, "M") : (MIDDLE, MOUSE_DOWN, ALT_CONTROL), # middle_down Alt Control 1+ +8+16=25 (29, "M") : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL), # middle_down Shift Alt Control 1+4+8+16=29 ( 2, "M") : (RIGHT, MOUSE_DOWN, NO_MODIFIER), # right_down 2+ + + =2 ( 6, "M") : (RIGHT, MOUSE_DOWN, SHIFT), # right_down Shift 2+4+ + =6 (10, "M") : (RIGHT, MOUSE_DOWN, ALT), # right_down Alt 2+ +8+ =10 (14, "M") : (RIGHT, MOUSE_DOWN, SHIFT_ALT), # right_down Shift Alt 2+4+8+ =14 (18, "M") : (RIGHT, MOUSE_DOWN, CONTROL), # right_down Control 2+ + +16=18 (22, "M") : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL), # right_down Shift Control 2+4+ +16=22 (26, "M") : (RIGHT, MOUSE_DOWN, ALT_CONTROL), # right_down Alt Control 2+ +8+16=26 (30, "M") : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # right_down Shift Alt Control 2+4+8+16=30 (32, "M") : (LEFT, MOUSE_MOVE, NO_MODIFIER), # left_drag 32+ + + =32 (36, "M") : (LEFT, MOUSE_MOVE, SHIFT), # left_drag Shift 32+4+ + =36 (40, "M") : (LEFT, MOUSE_MOVE, ALT), # left_drag Alt 32+ +8+ =40 (44, "M") : (LEFT, MOUSE_MOVE, SHIFT_ALT), # left_drag Shift Alt 32+4+8+ =44 (48, "M") : (LEFT, MOUSE_MOVE, CONTROL), # left_drag Control 32+ + +16=48 (52, "M") : (LEFT, MOUSE_MOVE, SHIFT_CONTROL), # left_drag Shift Control 32+4+ +16=52 (56, "M") : (LEFT, MOUSE_MOVE, ALT_CONTROL), # left_drag Alt Control 32+ +8+16=56 (60, "M") : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # left_drag Shift Alt Control 32+4+8+16=60 (33, "M") : (MIDDLE, MOUSE_MOVE, NO_MODIFIER), # middle_drag 33+ + + =33 (37, "M") : (MIDDLE, MOUSE_MOVE, SHIFT), # middle_drag Shift 33+4+ + =37 (41, "M") : (MIDDLE, MOUSE_MOVE, ALT), # middle_drag Alt 33+ +8+ =41 (45, "M") : (MIDDLE, MOUSE_MOVE, SHIFT_ALT), # middle_drag Shift Alt 33+4+8+ =45 (49, "M") : (MIDDLE, MOUSE_MOVE, CONTROL), # middle_drag Control 33+ + +16=49 (53, "M") : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL), # middle_drag Shift Control 33+4+ +16=53 (57, "M") : (MIDDLE, MOUSE_MOVE, ALT_CONTROL), # middle_drag Alt Control 33+ +8+16=57 (61, "M") : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL), # middle_drag Shift Alt Control 33+4+8+16=61 (34, "M") : (RIGHT, MOUSE_MOVE, NO_MODIFIER), # right_drag 34+ + + =34 (38, "M") : (RIGHT, MOUSE_MOVE, SHIFT), # right_drag Shift 34+4+ + =38 (42, "M") : (RIGHT, MOUSE_MOVE, ALT), # right_drag Alt 34+ +8+ =42 (46, "M") : (RIGHT, MOUSE_MOVE, SHIFT_ALT), # right_drag Shift Alt 34+4+8+ =46 (50, "M") : (RIGHT, MOUSE_MOVE, CONTROL), # right_drag Control 34+ + +16=50 (54, "M") : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL), # right_drag Shift Control 34+4+ +16=54 (58, "M") : (RIGHT, MOUSE_MOVE, ALT_CONTROL), # right_drag Alt Control 34+ +8+16=58 (62, "M") : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # right_drag Shift Alt Control 34+4+8+16=62 (35, "M") : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER), # none_drag 35+ + + =35 (39, "M") : (NO_BUTTON, MOUSE_MOVE, SHIFT), # none_drag Shift 35+4+ + =39 (43, "M") : (NO_BUTTON, MOUSE_MOVE, ALT), # none_drag Alt 35+ +8+ =43 (47, "M") : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT), # none_drag Shift Alt 35+4+8+ =47 (51, "M") : (NO_BUTTON, MOUSE_MOVE, CONTROL), # none_drag Control 35+ + +16=51 (55, "M") : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL), # none_drag Shift Control 35+4+ +16=55 (59, "M") : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL), # none_drag Alt Control 35+ +8+16=59 (63, "M") : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL), # none_drag Shift Alt Control 35+4+8+16=63 (64, "M") : (NO_BUTTON, SCROLL_UP, NO_MODIFIER), # scroll_up 64+ + + =64 (68, "M") : (NO_BUTTON, SCROLL_UP, SHIFT), # scroll_up Shift 64+4+ + =68 (72, "M") : (NO_BUTTON, SCROLL_UP, ALT), # scroll_up Alt 64+ +8+ =72 (76, "M") : (NO_BUTTON, SCROLL_UP, SHIFT_ALT), # scroll_up Shift Alt 64+4+8+ =76 (80, "M") : (NO_BUTTON, SCROLL_UP, CONTROL), # scroll_up Control 64+ + +16=80 (84, "M") : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL), # scroll_up Shift Control 64+4+ +16=84 (88, "M") : (NO_BUTTON, SCROLL_UP, ALT_CONTROL), # scroll_up Alt Control 64+ +8+16=88 (92, "M") : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL), # scroll_up Shift Alt Control 64+4+8+16=92 (65, "M") : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER), # scroll_down 64+ + + =65 (69, "M") : (NO_BUTTON, SCROLL_DOWN, SHIFT), # scroll_down Shift 64+4+ + =69 (73, "M") : (NO_BUTTON, SCROLL_DOWN, ALT), # scroll_down Alt 64+ +8+ =73 (77, "M") : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT), # scroll_down Shift Alt 64+4+8+ =77 (81, "M") : (NO_BUTTON, SCROLL_DOWN, CONTROL), # scroll_down Control 64+ + +16=81 (85, "M") : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL), # scroll_down Shift Control 64+4+ +16=85 (89, "M") : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL), # scroll_down Alt Control 64+ +8+16=89 (93, "M") : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL), # scroll_down Shift Alt Control 64+4+8+16=93 } typical_mouse_events = { 32: (LEFT , MOUSE_DOWN , UNKNOWN_MODIFIER), 33: (MIDDLE , MOUSE_DOWN , UNKNOWN_MODIFIER), 34: (RIGHT , MOUSE_DOWN , UNKNOWN_MODIFIER), 35: (UNKNOWN_BUTTON , MOUSE_UP , UNKNOWN_MODIFIER), 64: (LEFT , MOUSE_MOVE , UNKNOWN_MODIFIER), 65: (MIDDLE , MOUSE_MOVE , UNKNOWN_MODIFIER), 66: (RIGHT , MOUSE_MOVE , UNKNOWN_MODIFIER), 67: (NO_BUTTON , MOUSE_MOVE , UNKNOWN_MODIFIER), 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), } urxvt_mouse_events={ 32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER), 35: (UNKNOWN_BUTTON, MOUSE_UP , UNKNOWN_MODIFIER), 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), } # fmt:on def load_mouse_bindings() -> KeyBindings: """ Key bindings, required for mouse support. (Mouse events enter through the key binding system.) """ key_bindings = KeyBindings() @key_bindings.add(Keys.Vt100MouseEvent) def _(event: E) -> NotImplementedOrNone: """ Handling of incoming mouse event. """ # TypicaL: "eSC[MaB*" # Urxvt: "Esc[96;14;13M" # Xterm SGR: "Esc[<64;85;12M" # Parse incoming packet. if event.data[2] == "M": # Typical. mouse_event, x, y = map(ord, event.data[3:]) # TODO: Is it possible to add modifiers here? mouse_button, mouse_event_type, mouse_modifiers = typical_mouse_events[ mouse_event ] # Handle situations where `PosixStdinReader` used surrogateescapes. if x >= 0xDC00: x -= 0xDC00 if y >= 0xDC00: y -= 0xDC00 x -= 32 y -= 32 else: # Urxvt and Xterm SGR. # When the '<' is not present, we are not using the Xterm SGR mode, # but Urxvt instead. data = event.data[2:] if data[:1] == "<": sgr = True data = data[1:] else: sgr = False # Extract coordinates. mouse_event, x, y = map(int, data[:-1].split(";")) m = data[-1] # Parse event type. if sgr: try: ( mouse_button, mouse_event_type, mouse_modifiers, ) = xterm_sgr_mouse_events[mouse_event, m] except KeyError: return NotImplemented else: # Some other terminals, like urxvt, Hyper terminal, ... ( mouse_button, mouse_event_type, mouse_modifiers, ) = urxvt_mouse_events.get( mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER) ) x -= 1 y -= 1 # Only handle mouse events when we know the window height. if event.app.renderer.height_is_known and mouse_event_type is not None: # Take region above the layout into account. The reported # coordinates are absolute to the visible part of the terminal. from prompt_toolkit.renderer import HeightIsUnknownError try: y -= event.app.renderer.rows_above_layout except HeightIsUnknownError: return NotImplemented # Call the mouse handler from the renderer. # Note: This can return `NotImplemented` if no mouse handler was # found for this position, or if no repainting needs to # happen. this way, we avoid excessive repaints during mouse # movements. handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] return handler( MouseEvent( position=Point(x=x, y=y), event_type=mouse_event_type, button=mouse_button, modifiers=mouse_modifiers, ) ) return NotImplemented @key_bindings.add(Keys.ScrollUp) def _scroll_up(event: E) -> None: """ Scroll up event without cursor position. """ # We don't receive a cursor position, so we don't know which window to # scroll. Just send an 'up' key press instead. event.key_processor.feed(KeyPress(Keys.Up), first=True) @key_bindings.add(Keys.ScrollDown) def _scroll_down(event: E) -> None: """ Scroll down event without cursor position. """ event.key_processor.feed(KeyPress(Keys.Down), first=True) @key_bindings.add(Keys.WindowsMouseEvent) def _mouse(event: E) -> NotImplementedOrNone: """ Handling of mouse events for Windows. """ # This key binding should only exist for Windows. if sys.platform == "win32": # Parse data. pieces = event.data.split(";") button = MouseButton(pieces[0]) event_type = MouseEventType(pieces[1]) x = int(pieces[2]) y = int(pieces[3]) # Make coordinates absolute to the visible part of the terminal. output = event.app.renderer.output from prompt_toolkit.output.win32 import Win32Output from prompt_toolkit.output.windows10 import Windows10_Output if isinstance(output, (Win32Output, Windows10_Output)): screen_buffer_info = output.get_win32_screen_buffer_info() rows_above_cursor = ( screen_buffer_info.dwCursorPosition.Y - event.app.renderer._cursor_pos.y ) y -= rows_above_cursor # Call the mouse event handler. # (Can return `NotImplemented`.) handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] return handler( MouseEvent( position=Point(x=x, y=y), event_type=event_type, button=button, modifiers=UNKNOWN_MODIFIER, ) ) # No mouse handler found. Return `NotImplemented` so that we don't # invalidate the UI. return NotImplemented return key_bindings ================================================ FILE: src/prompt_toolkit/key_binding/bindings/named_commands.py ================================================ """ Key bindings which are also known by GNU Readline by the given names. See: http://www.delorie.com/gnu/docs/readline/rlman_13.html """ from __future__ import annotations from collections.abc import Callable from typing import TypeVar, cast from prompt_toolkit.document import Document from prompt_toolkit.enums import EditingMode from prompt_toolkit.key_binding.key_bindings import Binding, key_binding from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent from prompt_toolkit.keys import Keys from prompt_toolkit.layout.controls import BufferControl from prompt_toolkit.search import SearchDirection from prompt_toolkit.selection import PasteMode from .completion import display_completions_like_readline, generate_completions __all__ = [ "get_by_name", ] # Typing. _Handler = Callable[[KeyPressEvent], None] _HandlerOrBinding = _Handler | Binding _T = TypeVar("_T", bound=_HandlerOrBinding) E = KeyPressEvent # Registry that maps the Readline command names to their handlers. _readline_commands: dict[str, Binding] = {} def register(name: str) -> Callable[[_T], _T]: """ Store handler in the `_readline_commands` dictionary. """ def decorator(handler: _T) -> _T: "`handler` is a callable or Binding." if isinstance(handler, Binding): _readline_commands[name] = handler else: _readline_commands[name] = key_binding()(cast(_Handler, handler)) return handler return decorator def get_by_name(name: str) -> Binding: """ Return the handler for the (Readline) command with the given name. """ try: return _readline_commands[name] except KeyError as e: raise KeyError(f"Unknown Readline command: {name!r}") from e # # Commands for moving # See: http://www.delorie.com/gnu/docs/readline/rlman_14.html # @register("beginning-of-buffer") def beginning_of_buffer(event: E) -> None: """ Move to the start of the buffer. """ buff = event.current_buffer buff.cursor_position = 0 @register("end-of-buffer") def end_of_buffer(event: E) -> None: """ Move to the end of the buffer. """ buff = event.current_buffer buff.cursor_position = len(buff.text) @register("beginning-of-line") def beginning_of_line(event: E) -> None: """ Move to the start of the current line. """ buff = event.current_buffer buff.cursor_position += buff.document.get_start_of_line_position( after_whitespace=False ) @register("end-of-line") def end_of_line(event: E) -> None: """ Move to the end of the line. """ buff = event.current_buffer buff.cursor_position += buff.document.get_end_of_line_position() @register("forward-char") def forward_char(event: E) -> None: """ Move forward a character. """ buff = event.current_buffer buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg) @register("backward-char") def backward_char(event: E) -> None: "Move back a character." buff = event.current_buffer buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg) @register("forward-word") def forward_word(event: E) -> None: """ Move forward to the end of the next word. Words are composed of letters and digits. """ buff = event.current_buffer pos = buff.document.find_next_word_ending(count=event.arg) if pos: buff.cursor_position += pos @register("backward-word") def backward_word(event: E) -> None: """ Move back to the start of the current or previous word. Words are composed of letters and digits. """ buff = event.current_buffer pos = buff.document.find_previous_word_beginning(count=event.arg) if pos: buff.cursor_position += pos @register("clear-screen") def clear_screen(event: E) -> None: """ Clear the screen and redraw everything at the top of the screen. """ event.app.renderer.clear() @register("redraw-current-line") def redraw_current_line(event: E) -> None: """ Refresh the current line. (Readline defines this command, but prompt-toolkit doesn't have it.) """ pass # # Commands for manipulating the history. # See: http://www.delorie.com/gnu/docs/readline/rlman_15.html # @register("accept-line") def accept_line(event: E) -> None: """ Accept the line regardless of where the cursor is. """ event.current_buffer.validate_and_handle() @register("previous-history") def previous_history(event: E) -> None: """ Move `back` through the history list, fetching the previous command. """ event.current_buffer.history_backward(count=event.arg) @register("next-history") def next_history(event: E) -> None: """ Move `forward` through the history list, fetching the next command. """ event.current_buffer.history_forward(count=event.arg) @register("beginning-of-history") def beginning_of_history(event: E) -> None: """ Move to the first line in the history. """ event.current_buffer.go_to_history(0) @register("end-of-history") def end_of_history(event: E) -> None: """ Move to the end of the input history, i.e., the line currently being entered. """ event.current_buffer.history_forward(count=10**100) buff = event.current_buffer buff.go_to_history(len(buff._working_lines) - 1) @register("reverse-search-history") def reverse_search_history(event: E) -> None: """ Search backward starting at the current line and moving `up` through the history as necessary. This is an incremental search. """ control = event.app.layout.current_control if isinstance(control, BufferControl) and control.search_buffer_control: event.app.current_search_state.direction = SearchDirection.BACKWARD event.app.layout.current_control = control.search_buffer_control # # Commands for changing text # @register("end-of-file") def end_of_file(event: E) -> None: """ Exit. """ event.app.exit() @register("delete-char") def delete_char(event: E) -> None: """ Delete character before the cursor. """ deleted = event.current_buffer.delete(count=event.arg) if not deleted: event.app.output.bell() @register("backward-delete-char") def backward_delete_char(event: E) -> None: """ Delete the character behind the cursor. """ if event.arg < 0: # When a negative argument has been given, this should delete in front # of the cursor. deleted = event.current_buffer.delete(count=-event.arg) else: deleted = event.current_buffer.delete_before_cursor(count=event.arg) if not deleted: event.app.output.bell() @register("self-insert") def self_insert(event: E) -> None: """ Insert yourself. """ event.current_buffer.insert_text(event.data * event.arg) @register("transpose-chars") def transpose_chars(event: E) -> None: """ Emulate Emacs transpose-char behavior: at the beginning of the buffer, do nothing. At the end of a line or buffer, swap the characters before the cursor. Otherwise, move the cursor right, and then swap the characters before the cursor. """ b = event.current_buffer p = b.cursor_position if p == 0: return elif p == len(b.text) or b.text[p] == "\n": b.swap_characters_before_cursor() else: b.cursor_position += b.document.get_cursor_right_position() b.swap_characters_before_cursor() @register("uppercase-word") def uppercase_word(event: E) -> None: """ Uppercase the current (or following) word. """ buff = event.current_buffer for i in range(event.arg): pos = buff.document.find_next_word_ending() words = buff.document.text_after_cursor[:pos] buff.insert_text(words.upper(), overwrite=True) @register("downcase-word") def downcase_word(event: E) -> None: """ Lowercase the current (or following) word. """ buff = event.current_buffer for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!! pos = buff.document.find_next_word_ending() words = buff.document.text_after_cursor[:pos] buff.insert_text(words.lower(), overwrite=True) @register("capitalize-word") def capitalize_word(event: E) -> None: """ Capitalize the current (or following) word. """ buff = event.current_buffer for i in range(event.arg): pos = buff.document.find_next_word_ending() words = buff.document.text_after_cursor[:pos] buff.insert_text(words.title(), overwrite=True) @register("quoted-insert") def quoted_insert(event: E) -> None: """ Add the next character typed to the line verbatim. This is how to insert key sequences like C-q, for example. """ event.app.quoted_insert = True # # Killing and yanking. # @register("kill-line") def kill_line(event: E) -> None: """ Kill the text from the cursor to the end of the line. If we are at the end of the line, this should remove the newline. (That way, it is possible to delete multiple lines by executing this command multiple times.) """ buff = event.current_buffer if event.arg < 0: deleted = buff.delete_before_cursor( count=-buff.document.get_start_of_line_position() ) else: if buff.document.current_char == "\n": deleted = buff.delete(1) else: deleted = buff.delete(count=buff.document.get_end_of_line_position()) event.app.clipboard.set_text(deleted) @register("kill-word") def kill_word(event: E) -> None: """ Kill from point to the end of the current word, or if between words, to the end of the next word. Word boundaries are the same as forward-word. """ buff = event.current_buffer pos = buff.document.find_next_word_ending(count=event.arg) if pos: deleted = buff.delete(count=pos) if event.is_repeat: deleted = event.app.clipboard.get_data().text + deleted event.app.clipboard.set_text(deleted) @register("unix-word-rubout") def unix_word_rubout(event: E, WORD: bool = True) -> None: """ Kill the word behind point, using whitespace as a word boundary. Usually bound to ControlW. """ buff = event.current_buffer pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD) if pos is None: # Nothing found? delete until the start of the document. (The # input starts with whitespace and no words were found before the # cursor.) pos = -buff.cursor_position if pos: deleted = buff.delete_before_cursor(count=-pos) # If the previous key press was also Control-W, concatenate deleted # text. if event.is_repeat: deleted += event.app.clipboard.get_data().text event.app.clipboard.set_text(deleted) else: # Nothing to delete. Bell. event.app.output.bell() @register("backward-kill-word") def backward_kill_word(event: E) -> None: """ Kills the word before point, using "not a letter nor a digit" as a word boundary. Usually bound to M-Del or M-Backspace. """ unix_word_rubout(event, WORD=False) @register("delete-horizontal-space") def delete_horizontal_space(event: E) -> None: """ Delete all spaces and tabs around point. """ buff = event.current_buffer text_before_cursor = buff.document.text_before_cursor text_after_cursor = buff.document.text_after_cursor delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t ")) delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t ")) buff.delete_before_cursor(count=delete_before) buff.delete(count=delete_after) @register("unix-line-discard") def unix_line_discard(event: E) -> None: """ Kill backward from the cursor to the beginning of the current line. """ buff = event.current_buffer if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0: buff.delete_before_cursor(count=1) else: deleted = buff.delete_before_cursor( count=-buff.document.get_start_of_line_position() ) event.app.clipboard.set_text(deleted) @register("yank") def yank(event: E) -> None: """ Paste before cursor. """ event.current_buffer.paste_clipboard_data( event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS ) @register("yank-nth-arg") def yank_nth_arg(event: E) -> None: """ Insert the first argument of the previous command. With an argument, insert the nth word from the previous command (start counting at 0). """ n = event.arg if event.arg_present else None event.current_buffer.yank_nth_arg(n) @register("yank-last-arg") def yank_last_arg(event: E) -> None: """ Like `yank_nth_arg`, but if no argument has been given, yank the last word of each line. """ n = event.arg if event.arg_present else None event.current_buffer.yank_last_arg(n) @register("yank-pop") def yank_pop(event: E) -> None: """ Rotate the kill ring, and yank the new top. Only works following yank or yank-pop. """ buff = event.current_buffer doc_before_paste = buff.document_before_paste clipboard = event.app.clipboard if doc_before_paste is not None: buff.document = doc_before_paste clipboard.rotate() buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS) # # Completion. # @register("complete") def complete(event: E) -> None: """ Attempt to perform completion. """ display_completions_like_readline(event) @register("menu-complete") def menu_complete(event: E) -> None: """ Generate completions, or go to the next completion. (This is the default way of completing input in prompt_toolkit.) """ generate_completions(event) @register("menu-complete-backward") def menu_complete_backward(event: E) -> None: """ Move backward through the list of possible completions. """ event.current_buffer.complete_previous() # # Keyboard macros. # @register("start-kbd-macro") def start_kbd_macro(event: E) -> None: """ Begin saving the characters typed into the current keyboard macro. """ event.app.emacs_state.start_macro() @register("end-kbd-macro") def end_kbd_macro(event: E) -> None: """ Stop saving the characters typed into the current keyboard macro and save the definition. """ event.app.emacs_state.end_macro() @register("call-last-kbd-macro") @key_binding(record_in_macro=False) def call_last_kbd_macro(event: E) -> None: """ Re-execute the last keyboard macro defined, by making the characters in the macro appear as if typed at the keyboard. Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e' key sequence doesn't appear in the recording itself. This function inserts the body of the called macro back into the KeyProcessor, so these keys will be added later on to the macro of their handlers have `record_in_macro=True`. """ # Insert the macro. macro = event.app.emacs_state.macro if macro: event.app.key_processor.feed_multiple(macro, first=True) @register("print-last-kbd-macro") def print_last_kbd_macro(event: E) -> None: """ Print the last keyboard macro. """ # TODO: Make the format suitable for the inputrc file. def print_macro() -> None: macro = event.app.emacs_state.macro if macro: for k in macro: print(k) from prompt_toolkit.application.run_in_terminal import run_in_terminal run_in_terminal(print_macro) # # Miscellaneous Commands. # @register("undo") def undo(event: E) -> None: """ Incremental undo. """ event.current_buffer.undo() @register("insert-comment") def insert_comment(event: E) -> None: """ Without numeric argument, comment all lines. With numeric argument, uncomment all lines. In any case accept the input. """ buff = event.current_buffer # Transform all lines. if event.arg != 1: def change(line: str) -> str: return line[1:] if line.startswith("#") else line else: def change(line: str) -> str: return "#" + line buff.document = Document( text="\n".join(map(change, buff.text.splitlines())), cursor_position=0 ) # Accept input. buff.validate_and_handle() @register("vi-editing-mode") def vi_editing_mode(event: E) -> None: """ Switch to Vi editing mode. """ event.app.editing_mode = EditingMode.VI @register("emacs-editing-mode") def emacs_editing_mode(event: E) -> None: """ Switch to Emacs editing mode. """ event.app.editing_mode = EditingMode.EMACS @register("prefix-meta") def prefix_meta(event: E) -> None: """ Metafy the next character typed. This is for keyboards without a meta key. Sometimes people also want to bind other keys to Meta, e.g. 'jj':: key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta) """ # ('first' should be true, because we want to insert it at the current # position in the queue.) event.app.key_processor.feed(KeyPress(Keys.Escape), first=True) @register("operate-and-get-next") def operate_and_get_next(event: E) -> None: """ Accept the current line for execution and fetch the next line relative to the current line from the history for editing. """ buff = event.current_buffer new_index = buff.working_index + 1 # Accept the current input. (This will also redraw the interface in the # 'done' state.) buff.validate_and_handle() # Set the new index at the start of the next run. def set_working_index() -> None: if new_index < len(buff._working_lines): buff.working_index = new_index event.app.pre_run_callables.append(set_working_index) @register("edit-and-execute-command") def edit_and_execute(event: E) -> None: """ Invoke an editor on the current command line, and accept the result. """ buff = event.current_buffer buff.open_in_editor(validate_and_handle=True) ================================================ FILE: src/prompt_toolkit/key_binding/bindings/open_in_editor.py ================================================ """ Open in editor key bindings. """ from __future__ import annotations from prompt_toolkit.filters import emacs_mode, has_selection, vi_navigation_mode from ..key_bindings import KeyBindings, KeyBindingsBase, merge_key_bindings from .named_commands import get_by_name __all__ = [ "load_open_in_editor_bindings", "load_emacs_open_in_editor_bindings", "load_vi_open_in_editor_bindings", ] def load_open_in_editor_bindings() -> KeyBindingsBase: """ Load both the Vi and emacs key bindings for handling edit-and-execute-command. """ return merge_key_bindings( [ load_emacs_open_in_editor_bindings(), load_vi_open_in_editor_bindings(), ] ) def load_emacs_open_in_editor_bindings() -> KeyBindings: """ Pressing C-X C-E will open the buffer in an external editor. """ key_bindings = KeyBindings() key_bindings.add("c-x", "c-e", filter=emacs_mode & ~has_selection)( get_by_name("edit-and-execute-command") ) return key_bindings def load_vi_open_in_editor_bindings() -> KeyBindings: """ Pressing 'v' in navigation mode will open the buffer in an external editor. """ key_bindings = KeyBindings() key_bindings.add("v", filter=vi_navigation_mode)( get_by_name("edit-and-execute-command") ) return key_bindings ================================================ FILE: src/prompt_toolkit/key_binding/bindings/page_navigation.py ================================================ """ Key bindings for extra page navigation: bindings for up/down scrolling through long pages, like in Emacs or Vi. """ from __future__ import annotations from prompt_toolkit.filters import buffer_has_focus, emacs_mode, vi_mode from prompt_toolkit.key_binding.key_bindings import ( ConditionalKeyBindings, KeyBindings, KeyBindingsBase, merge_key_bindings, ) from .scroll import ( scroll_backward, scroll_forward, scroll_half_page_down, scroll_half_page_up, scroll_one_line_down, scroll_one_line_up, scroll_page_down, scroll_page_up, ) __all__ = [ "load_page_navigation_bindings", "load_emacs_page_navigation_bindings", "load_vi_page_navigation_bindings", ] def load_page_navigation_bindings() -> KeyBindingsBase: """ Load both the Vi and Emacs bindings for page navigation. """ # Only enable when a `Buffer` is focused, otherwise, we would catch keys # when another widget is focused (like for instance `c-d` in a # ptterm.Terminal). return ConditionalKeyBindings( merge_key_bindings( [ load_emacs_page_navigation_bindings(), load_vi_page_navigation_bindings(), ] ), buffer_has_focus, ) def load_emacs_page_navigation_bindings() -> KeyBindingsBase: """ Key bindings, for scrolling up and down through pages. This are separate bindings, because GNU readline doesn't have them. """ key_bindings = KeyBindings() handle = key_bindings.add handle("c-v")(scroll_page_down) handle("pagedown")(scroll_page_down) handle("escape", "v")(scroll_page_up) handle("pageup")(scroll_page_up) return ConditionalKeyBindings(key_bindings, emacs_mode) def load_vi_page_navigation_bindings() -> KeyBindingsBase: """ Key bindings, for scrolling up and down through pages. This are separate bindings, because GNU readline doesn't have them. """ key_bindings = KeyBindings() handle = key_bindings.add handle("c-f")(scroll_forward) handle("c-b")(scroll_backward) handle("c-d")(scroll_half_page_down) handle("c-u")(scroll_half_page_up) handle("c-e")(scroll_one_line_down) handle("c-y")(scroll_one_line_up) handle("pagedown")(scroll_page_down) handle("pageup")(scroll_page_up) return ConditionalKeyBindings(key_bindings, vi_mode) ================================================ FILE: src/prompt_toolkit/key_binding/bindings/scroll.py ================================================ """ Key bindings, for scrolling up and down through pages. This are separate bindings, because GNU readline doesn't have them, but they are very useful for navigating through long multiline buffers, like in Vi, Emacs, etc... """ from __future__ import annotations from prompt_toolkit.key_binding.key_processor import KeyPressEvent __all__ = [ "scroll_forward", "scroll_backward", "scroll_half_page_up", "scroll_half_page_down", "scroll_one_line_up", "scroll_one_line_down", ] E = KeyPressEvent def scroll_forward(event: E, half: bool = False) -> None: """ Scroll window down. """ w = event.app.layout.current_window b = event.app.current_buffer if w and w.render_info: info = w.render_info ui_content = info.ui_content # Height to scroll. scroll_height = info.window_height if half: scroll_height //= 2 # Calculate how many lines is equivalent to that vertical space. y = b.document.cursor_position_row + 1 height = 0 while y < ui_content.line_count: line_height = info.get_height_for_line(y) if height + line_height < scroll_height: height += line_height y += 1 else: break b.cursor_position = b.document.translate_row_col_to_index(y, 0) def scroll_backward(event: E, half: bool = False) -> None: """ Scroll window up. """ w = event.app.layout.current_window b = event.app.current_buffer if w and w.render_info: info = w.render_info # Height to scroll. scroll_height = info.window_height if half: scroll_height //= 2 # Calculate how many lines is equivalent to that vertical space. y = max(0, b.document.cursor_position_row - 1) height = 0 while y > 0: line_height = info.get_height_for_line(y) if height + line_height < scroll_height: height += line_height y -= 1 else: break b.cursor_position = b.document.translate_row_col_to_index(y, 0) def scroll_half_page_down(event: E) -> None: """ Same as ControlF, but only scroll half a page. """ scroll_forward(event, half=True) def scroll_half_page_up(event: E) -> None: """ Same as ControlB, but only scroll half a page. """ scroll_backward(event, half=True) def scroll_one_line_down(event: E) -> None: """ scroll_offset += 1 """ w = event.app.layout.current_window b = event.app.current_buffer if w: # When the cursor is at the top, move to the next line. (Otherwise, only scroll.) if w.render_info: info = w.render_info if w.vertical_scroll < info.content_height - info.window_height: if info.cursor_position.y <= info.configured_scroll_offsets.top: b.cursor_position += b.document.get_cursor_down_position() w.vertical_scroll += 1 def scroll_one_line_up(event: E) -> None: """ scroll_offset -= 1 """ w = event.app.layout.current_window b = event.app.current_buffer if w: # When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.) if w.render_info: info = w.render_info if w.vertical_scroll > 0: first_line_height = info.get_height_for_line(info.first_visible_line()) cursor_up = info.cursor_position.y - ( info.window_height - 1 - first_line_height - info.configured_scroll_offsets.bottom ) # Move cursor up, as many steps as the height of the first line. # TODO: not entirely correct yet, in case of line wrapping and many long lines. for _ in range(max(0, cursor_up)): b.cursor_position += b.document.get_cursor_up_position() # Scroll window w.vertical_scroll -= 1 def scroll_page_down(event: E) -> None: """ Scroll page down. (Prefer the cursor at the top of the page, after scrolling.) """ w = event.app.layout.current_window b = event.app.current_buffer if w and w.render_info: # Scroll down one page. line_index = max(w.render_info.last_visible_line(), w.vertical_scroll + 1) w.vertical_scroll = line_index b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) b.cursor_position += b.document.get_start_of_line_position( after_whitespace=True ) def scroll_page_up(event: E) -> None: """ Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.) """ w = event.app.layout.current_window b = event.app.current_buffer if w and w.render_info: # Put cursor at the first visible line. (But make sure that the cursor # moves at least one line up.) line_index = max( 0, min(w.render_info.first_visible_line(), b.document.cursor_position_row - 1), ) b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) b.cursor_position += b.document.get_start_of_line_position( after_whitespace=True ) # Set the scroll offset. We can safely set it to zero; the Window will # make sure that it scrolls at least until the cursor becomes visible. w.vertical_scroll = 0 ================================================ FILE: src/prompt_toolkit/key_binding/bindings/search.py ================================================ """ Search related key bindings. """ from __future__ import annotations from prompt_toolkit import search from prompt_toolkit.application.current import get_app from prompt_toolkit.filters import Condition, control_is_searchable, is_searching from prompt_toolkit.key_binding.key_processor import KeyPressEvent from ..key_bindings import key_binding __all__ = [ "abort_search", "accept_search", "start_reverse_incremental_search", "start_forward_incremental_search", "reverse_incremental_search", "forward_incremental_search", "accept_search_and_accept_input", ] E = KeyPressEvent @key_binding(filter=is_searching) def abort_search(event: E) -> None: """ Abort an incremental search and restore the original line. (Usually bound to ControlG/ControlC.) """ search.stop_search() @key_binding(filter=is_searching) def accept_search(event: E) -> None: """ When enter pressed in isearch, quit isearch mode. (Multiline isearch would be too complicated.) (Usually bound to Enter.) """ search.accept_search() @key_binding(filter=control_is_searchable) def start_reverse_incremental_search(event: E) -> None: """ Enter reverse incremental search. (Usually ControlR.) """ search.start_search(direction=search.SearchDirection.BACKWARD) @key_binding(filter=control_is_searchable) def start_forward_incremental_search(event: E) -> None: """ Enter forward incremental search. (Usually ControlS.) """ search.start_search(direction=search.SearchDirection.FORWARD) @key_binding(filter=is_searching) def reverse_incremental_search(event: E) -> None: """ Apply reverse incremental search, but keep search buffer focused. """ search.do_incremental_search(search.SearchDirection.BACKWARD, count=event.arg) @key_binding(filter=is_searching) def forward_incremental_search(event: E) -> None: """ Apply forward incremental search, but keep search buffer focused. """ search.do_incremental_search(search.SearchDirection.FORWARD, count=event.arg) @Condition def _previous_buffer_is_returnable() -> bool: """ True if the previously focused buffer has a return handler. """ prev_control = get_app().layout.search_target_buffer_control return bool(prev_control and prev_control.buffer.is_returnable) @key_binding(filter=is_searching & _previous_buffer_is_returnable) def accept_search_and_accept_input(event: E) -> None: """ Accept the search operation first, then accept the input. """ search.accept_search() event.current_buffer.validate_and_handle() ================================================ FILE: src/prompt_toolkit/key_binding/bindings/vi.py ================================================ # pylint: disable=function-redefined from __future__ import annotations import codecs import string from collections.abc import Callable, Iterable from enum import Enum from itertools import accumulate from typing import TypeVar from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer, indent, reshape_text, unindent from prompt_toolkit.clipboard import ClipboardData from prompt_toolkit.document import Document from prompt_toolkit.filters import ( Always, Condition, Filter, has_arg, is_read_only, is_searching, ) from prompt_toolkit.filters.app import ( in_paste_mode, is_multiline, vi_digraph_mode, vi_insert_mode, vi_insert_multiple_mode, vi_mode, vi_navigation_mode, vi_recording_macro, vi_replace_mode, vi_replace_single_mode, vi_search_direction_reversed, vi_selection_mode, vi_waiting_for_text_object_mode, ) from prompt_toolkit.input.vt100_parser import Vt100Parser from prompt_toolkit.key_binding.digraphs import DIGRAPHS from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode from prompt_toolkit.keys import Keys from prompt_toolkit.search import SearchDirection from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase from .named_commands import get_by_name __all__ = [ "load_vi_bindings", "load_vi_search_bindings", ] E = KeyPressEvent ascii_lowercase = string.ascii_lowercase vi_register_names = ascii_lowercase + "0123456789" class TextObjectType(Enum): EXCLUSIVE = "EXCLUSIVE" INCLUSIVE = "INCLUSIVE" LINEWISE = "LINEWISE" BLOCK = "BLOCK" class TextObject: """ Return struct for functions wrapped in ``text_object``. Both `start` and `end` are relative to the current cursor position. """ def __init__( self, start: int, end: int = 0, type: TextObjectType = TextObjectType.EXCLUSIVE ): self.start = start self.end = end self.type = type @property def selection_type(self) -> SelectionType: if self.type == TextObjectType.LINEWISE: return SelectionType.LINES if self.type == TextObjectType.BLOCK: return SelectionType.BLOCK else: return SelectionType.CHARACTERS def sorted(self) -> tuple[int, int]: """ Return a (start, end) tuple where start <= end. """ if self.start < self.end: return self.start, self.end else: return self.end, self.start def operator_range(self, document: Document) -> tuple[int, int]: """ Return a (start, end) tuple with start <= end that indicates the range operators should operate on. `buffer` is used to get start and end of line positions. This should return something that can be used in a slice, so the `end` position is *not* included. """ start, end = self.sorted() doc = document if ( self.type == TextObjectType.EXCLUSIVE and doc.translate_index_to_position(end + doc.cursor_position)[1] == 0 ): # If the motion is exclusive and the end of motion is on the first # column, the end position becomes end of previous line. end -= 1 if self.type == TextObjectType.INCLUSIVE: end += 1 if self.type == TextObjectType.LINEWISE: # Select whole lines row, col = doc.translate_index_to_position(start + doc.cursor_position) start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position row, col = doc.translate_index_to_position(end + doc.cursor_position) end = ( doc.translate_row_col_to_index(row, len(doc.lines[row])) - doc.cursor_position ) return start, end def get_line_numbers(self, buffer: Buffer) -> tuple[int, int]: """ Return a (start_line, end_line) pair. """ # Get absolute cursor positions from the text object. from_, to = self.operator_range(buffer.document) from_ += buffer.cursor_position to += buffer.cursor_position # Take the start of the lines. from_, _ = buffer.document.translate_index_to_position(from_) to, _ = buffer.document.translate_index_to_position(to) return from_, to def cut(self, buffer: Buffer) -> tuple[Document, ClipboardData]: """ Turn text object into `ClipboardData` instance. """ from_, to = self.operator_range(buffer.document) from_ += buffer.cursor_position to += buffer.cursor_position # For Vi mode, the SelectionState does include the upper position, # while `self.operator_range` does not. So, go one to the left, unless # we're in the line mode, then we don't want to risk going to the # previous line, and missing one line in the selection. if self.type != TextObjectType.LINEWISE: to -= 1 document = Document( buffer.text, to, SelectionState(original_cursor_position=from_, type=self.selection_type), ) new_document, clipboard_data = document.cut_selection() return new_document, clipboard_data # Typevar for any text object function: TextObjectFunction = Callable[[E], TextObject] _TOF = TypeVar("_TOF", bound=TextObjectFunction) def create_text_object_decorator( key_bindings: KeyBindings, ) -> Callable[..., Callable[[_TOF], _TOF]]: """ Create a decorator that can be used to register Vi text object implementations. """ def text_object_decorator( *keys: Keys | str, filter: Filter = Always(), no_move_handler: bool = False, no_selection_handler: bool = False, eager: bool = False, ) -> Callable[[_TOF], _TOF]: """ Register a text object function. Usage:: @text_object('w', filter=..., no_move_handler=False) def handler(event): # Return a text object for this key. return TextObject(...) :param no_move_handler: Disable the move handler in navigation mode. (It's still active in selection mode.) """ def decorator(text_object_func: _TOF) -> _TOF: @key_bindings.add( *keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager ) def _apply_operator_to_text_object(event: E) -> None: # Arguments are multiplied. vi_state = event.app.vi_state event._arg = str((vi_state.operator_arg or 1) * (event.arg or 1)) # Call the text object handler. text_obj = text_object_func(event) # Get the operator function. # (Should never be None here, given the # `vi_waiting_for_text_object_mode` filter state.) operator_func = vi_state.operator_func if text_obj is not None and operator_func is not None: # Call the operator function with the text object. operator_func(event, text_obj) # Clear operator. event.app.vi_state.operator_func = None event.app.vi_state.operator_arg = None # Register a move operation. (Doesn't need an operator.) if not no_move_handler: @key_bindings.add( *keys, filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, eager=eager, ) def _move_in_navigation_mode(event: E) -> None: """ Move handler for navigation mode. """ text_object = text_object_func(event) event.current_buffer.cursor_position += text_object.start # Register a move selection operation. if not no_selection_handler: @key_bindings.add( *keys, filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, eager=eager, ) def _move_in_selection_mode(event: E) -> None: """ Move handler for selection mode. """ text_object = text_object_func(event) buff = event.current_buffer selection_state = buff.selection_state if selection_state is None: return # Should not happen, because of the `vi_selection_mode` filter. # When the text object has both a start and end position, like 'i(' or 'iw', # Turn this into a selection, otherwise the cursor. if text_object.end: # Take selection positions from text object. start, end = text_object.operator_range(buff.document) start += buff.cursor_position end += buff.cursor_position selection_state.original_cursor_position = start buff.cursor_position = end # Take selection type from text object. if text_object.type == TextObjectType.LINEWISE: selection_state.type = SelectionType.LINES else: selection_state.type = SelectionType.CHARACTERS else: event.current_buffer.cursor_position += text_object.start # Make it possible to chain @text_object decorators. return text_object_func return decorator return text_object_decorator # Typevar for any operator function: OperatorFunction = Callable[[E, TextObject], None] _OF = TypeVar("_OF", bound=OperatorFunction) def create_operator_decorator( key_bindings: KeyBindings, ) -> Callable[..., Callable[[_OF], _OF]]: """ Create a decorator that can be used for registering Vi operators. """ def operator_decorator( *keys: Keys | str, filter: Filter = Always(), eager: bool = False ) -> Callable[[_OF], _OF]: """ Register a Vi operator. Usage:: @operator('d', filter=...) def handler(event, text_object): # Do something with the text object here. """ def decorator(operator_func: _OF) -> _OF: @key_bindings.add( *keys, filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, eager=eager, ) def _operator_in_navigation(event: E) -> None: """ Handle operator in navigation mode. """ # When this key binding is matched, only set the operator # function in the ViState. We should execute it after a text # object has been received. event.app.vi_state.operator_func = operator_func event.app.vi_state.operator_arg = event.arg @key_bindings.add( *keys, filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, eager=eager, ) def _operator_in_selection(event: E) -> None: """ Handle operator in selection mode. """ buff = event.current_buffer selection_state = buff.selection_state if selection_state is not None: # Create text object from selection. if selection_state.type == SelectionType.LINES: text_obj_type = TextObjectType.LINEWISE elif selection_state.type == SelectionType.BLOCK: text_obj_type = TextObjectType.BLOCK else: text_obj_type = TextObjectType.INCLUSIVE text_object = TextObject( selection_state.original_cursor_position - buff.cursor_position, type=text_obj_type, ) # Execute operator. operator_func(event, text_object) # Quit selection mode. buff.selection_state = None return operator_func return decorator return operator_decorator @Condition def is_returnable() -> bool: return get_app().current_buffer.is_returnable @Condition def in_block_selection() -> bool: buff = get_app().current_buffer return bool( buff.selection_state and buff.selection_state.type == SelectionType.BLOCK ) @Condition def digraph_symbol_1_given() -> bool: return get_app().vi_state.digraph_symbol1 is not None @Condition def search_buffer_is_empty() -> bool: "Returns True when the search buffer is empty." return get_app().current_buffer.text == "" @Condition def tilde_operator() -> bool: return get_app().vi_state.tilde_operator def load_vi_bindings() -> KeyBindingsBase: """ Vi extensions. # Overview of Readline Vi commands: # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf """ # Note: Some key bindings have the "~IsReadOnly()" filter added. This # prevents the handler to be executed when the focus is on a # read-only buffer. # This is however only required for those that change the ViState to # INSERT mode. The `Buffer` class itself throws the # `EditReadOnlyBuffer` exception for any text operations which is # handled correctly. There is no need to add "~IsReadOnly" to all key # bindings that do text manipulation. key_bindings = KeyBindings() handle = key_bindings.add # (Note: Always take the navigation bindings in read-only mode, even when # ViState says different.) TransformFunction = tuple[tuple[str, ...], Filter, Callable[[str], str]] vi_transform_functions: list[TransformFunction] = [ # Rot 13 transformation ( ("g", "?"), Always(), lambda string: codecs.encode(string, "rot_13"), ), # To lowercase (("g", "u"), Always(), lambda string: string.lower()), # To uppercase. (("g", "U"), Always(), lambda string: string.upper()), # Swap case. (("g", "~"), Always(), lambda string: string.swapcase()), ( ("~",), tilde_operator, lambda string: string.swapcase(), ), ] # Insert a character literally (quoted insert). handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert")) @handle("escape") def _back_to_navigation(event: E) -> None: """ Escape goes to vi navigation mode. """ buffer = event.current_buffer vi_state = event.app.vi_state if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): buffer.cursor_position += buffer.document.get_cursor_left_position() vi_state.input_mode = InputMode.NAVIGATION if bool(buffer.selection_state): buffer.exit_selection() @handle("k", filter=vi_selection_mode) def _up_in_selection(event: E) -> None: """ Arrow up in selection mode. """ event.current_buffer.cursor_up(count=event.arg) @handle("j", filter=vi_selection_mode) def _down_in_selection(event: E) -> None: """ Arrow down in selection mode. """ event.current_buffer.cursor_down(count=event.arg) @handle("up", filter=vi_navigation_mode) @handle("c-p", filter=vi_navigation_mode) def _up_in_navigation(event: E) -> None: """ Arrow up and ControlP in navigation mode go up. """ event.current_buffer.auto_up(count=event.arg) @handle("k", filter=vi_navigation_mode) def _go_up(event: E) -> None: """ Go up, but if we enter a new history entry, move to the start of the line. """ event.current_buffer.auto_up( count=event.arg, go_to_start_of_line_if_history_changes=True ) @handle("down", filter=vi_navigation_mode) @handle("c-n", filter=vi_navigation_mode) def _go_down(event: E) -> None: """ Arrow down and Control-N in navigation mode. """ event.current_buffer.auto_down(count=event.arg) @handle("j", filter=vi_navigation_mode) def _go_down2(event: E) -> None: """ Go down, but if we enter a new history entry, go to the start of the line. """ event.current_buffer.auto_down( count=event.arg, go_to_start_of_line_if_history_changes=True ) @handle("backspace", filter=vi_navigation_mode) def _go_left(event: E) -> None: """ In navigation-mode, move cursor. """ event.current_buffer.cursor_position += ( event.current_buffer.document.get_cursor_left_position(count=event.arg) ) @handle("c-n", filter=vi_insert_mode) def _complete_next(event: E) -> None: b = event.current_buffer if b.complete_state: b.complete_next() else: b.start_completion(select_first=True) @handle("c-p", filter=vi_insert_mode) def _complete_prev(event: E) -> None: """ Control-P: To previous completion. """ b = event.current_buffer if b.complete_state: b.complete_previous() else: b.start_completion(select_last=True) @handle("c-g", filter=vi_insert_mode) @handle("c-y", filter=vi_insert_mode) def _accept_completion(event: E) -> None: """ Accept current completion. """ event.current_buffer.complete_state = None @handle("c-e", filter=vi_insert_mode) def _cancel_completion(event: E) -> None: """ Cancel completion. Go back to originally typed text. """ event.current_buffer.cancel_completion() # In navigation mode, pressing enter will always return the input. handle("enter", filter=vi_navigation_mode & is_returnable)( get_by_name("accept-line") ) # In insert mode, also accept input when enter is pressed, and the buffer # has been marked as single line. handle("enter", filter=is_returnable & ~is_multiline)(get_by_name("accept-line")) @handle("enter", filter=~is_returnable & vi_navigation_mode) def _start_of_next_line(event: E) -> None: """ Go to the beginning of next line. """ b = event.current_buffer b.cursor_down(count=event.arg) b.cursor_position += b.document.get_start_of_line_position( after_whitespace=True ) # ** In navigation mode ** # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html @handle("insert", filter=vi_navigation_mode) def _insert_mode(event: E) -> None: """ Pressing the Insert key. """ event.app.vi_state.input_mode = InputMode.INSERT @handle("insert", filter=vi_insert_mode) def _navigation_mode(event: E) -> None: """ Pressing the Insert key. """ event.app.vi_state.input_mode = InputMode.NAVIGATION @handle("a", filter=vi_navigation_mode & ~is_read_only) # ~IsReadOnly, because we want to stay in navigation mode for # read-only buffers. def _a(event: E) -> None: event.current_buffer.cursor_position += ( event.current_buffer.document.get_cursor_right_position() ) event.app.vi_state.input_mode = InputMode.INSERT @handle("A", filter=vi_navigation_mode & ~is_read_only) def _A(event: E) -> None: event.current_buffer.cursor_position += ( event.current_buffer.document.get_end_of_line_position() ) event.app.vi_state.input_mode = InputMode.INSERT @handle("C", filter=vi_navigation_mode & ~is_read_only) def _change_until_end_of_line(event: E) -> None: """ Change to end of line. Same as 'c$' (which is implemented elsewhere.) """ buffer = event.current_buffer deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) event.app.clipboard.set_text(deleted) event.app.vi_state.input_mode = InputMode.INSERT @handle("c", "c", filter=vi_navigation_mode & ~is_read_only) @handle("S", filter=vi_navigation_mode & ~is_read_only) def _change_current_line(event: E) -> None: # TODO: implement 'arg' """ Change current line """ buffer = event.current_buffer # We copy the whole line. data = ClipboardData(buffer.document.current_line, SelectionType.LINES) event.app.clipboard.set_data(data) # But we delete after the whitespace buffer.cursor_position += buffer.document.get_start_of_line_position( after_whitespace=True ) buffer.delete(count=buffer.document.get_end_of_line_position()) event.app.vi_state.input_mode = InputMode.INSERT @handle("D", filter=vi_navigation_mode) def _delete_until_end_of_line(event: E) -> None: """ Delete from cursor position until the end of the line. """ buffer = event.current_buffer deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) event.app.clipboard.set_text(deleted) @handle("d", "d", filter=vi_navigation_mode) def _delete_line(event: E) -> None: """ Delete line. (Or the following 'n' lines.) """ buffer = event.current_buffer # Split string in before/deleted/after text. lines = buffer.document.lines before = "\n".join(lines[: buffer.document.cursor_position_row]) deleted = "\n".join( lines[ buffer.document.cursor_position_row : buffer.document.cursor_position_row + event.arg ] ) after = "\n".join(lines[buffer.document.cursor_position_row + event.arg :]) # Set new text. if before and after: before = before + "\n" # Set text and cursor position. buffer.document = Document( text=before + after, # Cursor At the start of the first 'after' line, after the leading whitespace. cursor_position=len(before) + len(after) - len(after.lstrip(" ")), ) # Set clipboard data event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) @handle("x", filter=vi_selection_mode) def _cut(event: E) -> None: """ Cut selection. ('x' is not an operator.) """ clipboard_data = event.current_buffer.cut_selection() event.app.clipboard.set_data(clipboard_data) @handle("i", filter=vi_navigation_mode & ~is_read_only) def _i(event: E) -> None: event.app.vi_state.input_mode = InputMode.INSERT @handle("I", filter=vi_navigation_mode & ~is_read_only) def _I(event: E) -> None: event.app.vi_state.input_mode = InputMode.INSERT event.current_buffer.cursor_position += ( event.current_buffer.document.get_start_of_line_position( after_whitespace=True ) ) @handle("I", filter=in_block_selection & ~is_read_only) def insert_in_block_selection(event: E, after: bool = False) -> None: """ Insert in block selection mode. """ buff = event.current_buffer # Store all cursor positions. positions = [] if after: def get_pos(from_to: tuple[int, int]) -> int: return from_to[1] else: def get_pos(from_to: tuple[int, int]) -> int: return from_to[0] for i, from_to in enumerate(buff.document.selection_ranges()): positions.append(get_pos(from_to)) if i == 0: buff.cursor_position = get_pos(from_to) buff.multiple_cursor_positions = positions # Go to 'INSERT_MULTIPLE' mode. event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE buff.exit_selection() @handle("A", filter=in_block_selection & ~is_read_only) def _append_after_block(event: E) -> None: insert_in_block_selection(event, after=True) @handle("J", filter=vi_navigation_mode & ~is_read_only) def _join(event: E) -> None: """ Join lines. """ for i in range(event.arg): event.current_buffer.join_next_line() @handle("g", "J", filter=vi_navigation_mode & ~is_read_only) def _join_nospace(event: E) -> None: """ Join lines without space. """ for i in range(event.arg): event.current_buffer.join_next_line(separator="") @handle("J", filter=vi_selection_mode & ~is_read_only) def _join_selection(event: E) -> None: """ Join selected lines. """ event.current_buffer.join_selected_lines() @handle("g", "J", filter=vi_selection_mode & ~is_read_only) def _join_selection_nospace(event: E) -> None: """ Join selected lines without space. """ event.current_buffer.join_selected_lines(separator="") @handle("p", filter=vi_navigation_mode) def _paste(event: E) -> None: """ Paste after """ event.current_buffer.paste_clipboard_data( event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.VI_AFTER, ) @handle("P", filter=vi_navigation_mode) def _paste_before(event: E) -> None: """ Paste before """ event.current_buffer.paste_clipboard_data( event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.VI_BEFORE, ) @handle('"', Keys.Any, "p", filter=vi_navigation_mode) def _paste_register(event: E) -> None: """ Paste from named register. """ c = event.key_sequence[1].data if c in vi_register_names: data = event.app.vi_state.named_registers.get(c) if data: event.current_buffer.paste_clipboard_data( data, count=event.arg, paste_mode=PasteMode.VI_AFTER ) @handle('"', Keys.Any, "P", filter=vi_navigation_mode) def _paste_register_before(event: E) -> None: """ Paste (before) from named register. """ c = event.key_sequence[1].data if c in vi_register_names: data = event.app.vi_state.named_registers.get(c) if data: event.current_buffer.paste_clipboard_data( data, count=event.arg, paste_mode=PasteMode.VI_BEFORE ) @handle("r", filter=vi_navigation_mode) def _replace(event: E) -> None: """ Go to 'replace-single'-mode. """ event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE @handle("R", filter=vi_navigation_mode) def _replace_mode(event: E) -> None: """ Go to 'replace'-mode. """ event.app.vi_state.input_mode = InputMode.REPLACE @handle("s", filter=vi_navigation_mode & ~is_read_only) def _substitute(event: E) -> None: """ Substitute with new text (Delete character(s) and go to insert mode.) """ text = event.current_buffer.delete(count=event.arg) event.app.clipboard.set_text(text) event.app.vi_state.input_mode = InputMode.INSERT @handle("u", filter=vi_navigation_mode, save_before=(lambda e: False)) def _undo(event: E) -> None: for i in range(event.arg): event.current_buffer.undo() @handle("V", filter=vi_navigation_mode) def _visual_line(event: E) -> None: """ Start lines selection. """ event.current_buffer.start_selection(selection_type=SelectionType.LINES) @handle("c-v", filter=vi_navigation_mode) def _visual_block(event: E) -> None: """ Enter block selection mode. """ event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) @handle("V", filter=vi_selection_mode) def _visual_line2(event: E) -> None: """ Exit line selection mode, or go from non line selection mode to line selection mode. """ selection_state = event.current_buffer.selection_state if selection_state is not None: if selection_state.type != SelectionType.LINES: selection_state.type = SelectionType.LINES else: event.current_buffer.exit_selection() @handle("v", filter=vi_navigation_mode) def _visual(event: E) -> None: """ Enter character selection mode. """ event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) @handle("v", filter=vi_selection_mode) def _visual2(event: E) -> None: """ Exit character selection mode, or go from non-character-selection mode to character selection mode. """ selection_state = event.current_buffer.selection_state if selection_state is not None: if selection_state.type != SelectionType.CHARACTERS: selection_state.type = SelectionType.CHARACTERS else: event.current_buffer.exit_selection() @handle("c-v", filter=vi_selection_mode) def _visual_block2(event: E) -> None: """ Exit block selection mode, or go from non block selection mode to block selection mode. """ selection_state = event.current_buffer.selection_state if selection_state is not None: if selection_state.type != SelectionType.BLOCK: selection_state.type = SelectionType.BLOCK else: event.current_buffer.exit_selection() @handle("a", "w", filter=vi_selection_mode) @handle("a", "W", filter=vi_selection_mode) def _visual_auto_word(event: E) -> None: """ Switch from visual linewise mode to visual characterwise mode. """ buffer = event.current_buffer if ( buffer.selection_state and buffer.selection_state.type == SelectionType.LINES ): buffer.selection_state.type = SelectionType.CHARACTERS @handle("x", filter=vi_navigation_mode) def _delete(event: E) -> None: """ Delete character. """ buff = event.current_buffer count = min(event.arg, len(buff.document.current_line_after_cursor)) if count: text = event.current_buffer.delete(count=count) event.app.clipboard.set_text(text) @handle("X", filter=vi_navigation_mode) def _delete_before_cursor(event: E) -> None: buff = event.current_buffer count = min(event.arg, len(buff.document.current_line_before_cursor)) if count: text = event.current_buffer.delete_before_cursor(count=count) event.app.clipboard.set_text(text) @handle("y", "y", filter=vi_navigation_mode) @handle("Y", filter=vi_navigation_mode) def _yank_line(event: E) -> None: """ Yank the whole line. """ text = "\n".join(event.current_buffer.document.lines_from_current[: event.arg]) event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) @handle("+", filter=vi_navigation_mode) def _next_line(event: E) -> None: """ Move to first non whitespace of next line """ buffer = event.current_buffer buffer.cursor_position += buffer.document.get_cursor_down_position( count=event.arg ) buffer.cursor_position += buffer.document.get_start_of_line_position( after_whitespace=True ) @handle("-", filter=vi_navigation_mode) def _prev_line(event: E) -> None: """ Move to first non whitespace of previous line """ buffer = event.current_buffer buffer.cursor_position += buffer.document.get_cursor_up_position( count=event.arg ) buffer.cursor_position += buffer.document.get_start_of_line_position( after_whitespace=True ) @handle(">", ">", filter=vi_navigation_mode) @handle("c-t", filter=vi_insert_mode) def _indent(event: E) -> None: """ Indent lines. """ buffer = event.current_buffer current_row = buffer.document.cursor_position_row indent(buffer, current_row, current_row + event.arg) @handle("<", "<", filter=vi_navigation_mode) @handle("c-d", filter=vi_insert_mode) def _unindent(event: E) -> None: """ Unindent lines. """ current_row = event.current_buffer.document.cursor_position_row unindent(event.current_buffer, current_row, current_row + event.arg) @handle("O", filter=vi_navigation_mode & ~is_read_only) def _open_above(event: E) -> None: """ Open line above and enter insertion mode """ event.current_buffer.insert_line_above(copy_margin=not in_paste_mode()) event.app.vi_state.input_mode = InputMode.INSERT @handle("o", filter=vi_navigation_mode & ~is_read_only) def _open_below(event: E) -> None: """ Open line below and enter insertion mode """ event.current_buffer.insert_line_below(copy_margin=not in_paste_mode()) event.app.vi_state.input_mode = InputMode.INSERT @handle("~", filter=vi_navigation_mode) def _reverse_case(event: E) -> None: """ Reverse case of current character and move cursor forward. """ buffer = event.current_buffer c = buffer.document.current_char if c is not None and c != "\n": buffer.insert_text(c.swapcase(), overwrite=True) @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only) def _lowercase_line(event: E) -> None: """ Lowercase current line. """ buff = event.current_buffer buff.transform_current_line(lambda s: s.lower()) @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only) def _uppercase_line(event: E) -> None: """ Uppercase current line. """ buff = event.current_buffer buff.transform_current_line(lambda s: s.upper()) @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only) def _swapcase_line(event: E) -> None: """ Swap case of the current line. """ buff = event.current_buffer buff.transform_current_line(lambda s: s.swapcase()) @handle("#", filter=vi_navigation_mode) def _prev_occurrence(event: E) -> None: """ Go to previous occurrence of this word. """ b = event.current_buffer search_state = event.app.current_search_state search_state.text = b.document.get_word_under_cursor() search_state.direction = SearchDirection.BACKWARD b.apply_search(search_state, count=event.arg, include_current_position=False) @handle("*", filter=vi_navigation_mode) def _next_occurrence(event: E) -> None: """ Go to next occurrence of this word. """ b = event.current_buffer search_state = event.app.current_search_state search_state.text = b.document.get_word_under_cursor() search_state.direction = SearchDirection.FORWARD b.apply_search(search_state, count=event.arg, include_current_position=False) @handle("(", filter=vi_navigation_mode) def _begin_of_sentence(event: E) -> None: # TODO: go to begin of sentence. # XXX: should become text_object. pass @handle(")", filter=vi_navigation_mode) def _end_of_sentence(event: E) -> None: # TODO: go to end of sentence. # XXX: should become text_object. pass operator = create_operator_decorator(key_bindings) text_object = create_text_object_decorator(key_bindings) @handle(Keys.Any, filter=vi_waiting_for_text_object_mode) def _unknown_text_object(event: E) -> None: """ Unknown key binding while waiting for a text object. """ event.app.output.bell() # # *** Operators *** # def create_delete_and_change_operators( delete_only: bool, with_register: bool = False ) -> None: """ Delete and change operators. :param delete_only: Create an operator that deletes, but doesn't go to insert mode. :param with_register: Copy the deleted text to this named register instead of the clipboard. """ handler_keys: Iterable[str] if with_register: handler_keys = ('"', Keys.Any, "cd"[delete_only]) else: handler_keys = "cd"[delete_only] @operator(*handler_keys, filter=~is_read_only) def delete_or_change_operator(event: E, text_object: TextObject) -> None: clipboard_data = None buff = event.current_buffer if text_object: new_document, clipboard_data = text_object.cut(buff) buff.document = new_document # Set deleted/changed text to clipboard or named register. if clipboard_data and clipboard_data.text: if with_register: reg_name = event.key_sequence[1].data if reg_name in vi_register_names: event.app.vi_state.named_registers[reg_name] = clipboard_data else: event.app.clipboard.set_data(clipboard_data) # Only go back to insert mode in case of 'change'. if not delete_only: event.app.vi_state.input_mode = InputMode.INSERT create_delete_and_change_operators(False, False) create_delete_and_change_operators(False, True) create_delete_and_change_operators(True, False) create_delete_and_change_operators(True, True) def create_transform_handler( filter: Filter, transform_func: Callable[[str], str], *a: str ) -> None: @operator(*a, filter=filter & ~is_read_only) def _(event: E, text_object: TextObject) -> None: """ Apply transformation (uppercase, lowercase, rot13, swap case). """ buff = event.current_buffer start, end = text_object.operator_range(buff.document) if start < end: # Transform. buff.transform_region( buff.cursor_position + start, buff.cursor_position + end, transform_func, ) # Move cursor buff.cursor_position += text_object.end or text_object.start for k, f, func in vi_transform_functions: create_transform_handler(f, func, *k) @operator("y") def _yank(event: E, text_object: TextObject) -> None: """ Yank operator. (Copy text.) """ _, clipboard_data = text_object.cut(event.current_buffer) if clipboard_data.text: event.app.clipboard.set_data(clipboard_data) @operator('"', Keys.Any, "y") def _yank_to_register(event: E, text_object: TextObject) -> None: """ Yank selection to named register. """ c = event.key_sequence[1].data if c in vi_register_names: _, clipboard_data = text_object.cut(event.current_buffer) event.app.vi_state.named_registers[c] = clipboard_data @operator(">") def _indent_text_object(event: E, text_object: TextObject) -> None: """ Indent. """ buff = event.current_buffer from_, to = text_object.get_line_numbers(buff) indent(buff, from_, to + 1, count=event.arg) @operator("<") def _unindent_text_object(event: E, text_object: TextObject) -> None: """ Unindent. """ buff = event.current_buffer from_, to = text_object.get_line_numbers(buff) unindent(buff, from_, to + 1, count=event.arg) @operator("g", "q") def _reshape(event: E, text_object: TextObject) -> None: """ Reshape text. """ buff = event.current_buffer from_, to = text_object.get_line_numbers(buff) reshape_text(buff, from_, to) # # *** Text objects *** # @text_object("b") def _b(event: E) -> TextObject: """ Move one word or token left. """ return TextObject( event.current_buffer.document.find_start_of_previous_word(count=event.arg) or 0 ) @text_object("B") def _B(event: E) -> TextObject: """ Move one non-blank word left """ return TextObject( event.current_buffer.document.find_start_of_previous_word( count=event.arg, WORD=True ) or 0 ) @text_object("$") def _dollar(event: E) -> TextObject: """ 'c$', 'd$' and '$': Delete/change/move until end of line. """ return TextObject(event.current_buffer.document.get_end_of_line_position()) @text_object("w") def _word_forward(event: E) -> TextObject: """ 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word. """ return TextObject( event.current_buffer.document.find_next_word_beginning(count=event.arg) or event.current_buffer.document.get_end_of_document_position() ) @text_object("W") def _WORD_forward(event: E) -> TextObject: """ 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD. """ return TextObject( event.current_buffer.document.find_next_word_beginning( count=event.arg, WORD=True ) or event.current_buffer.document.get_end_of_document_position() ) @text_object("e") def _end_of_word(event: E) -> TextObject: """ End of 'word': 'ce', 'de', 'e' """ end = event.current_buffer.document.find_next_word_ending(count=event.arg) return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) @text_object("E") def _end_of_WORD(event: E) -> TextObject: """ End of 'WORD': 'cE', 'dE', 'E' """ end = event.current_buffer.document.find_next_word_ending( count=event.arg, WORD=True ) return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) @text_object("i", "w", no_move_handler=True) def _inner_word(event: E) -> TextObject: """ Inner 'word': ciw and diw """ start, end = event.current_buffer.document.find_boundaries_of_current_word() return TextObject(start, end) @text_object("a", "w", no_move_handler=True) def _a_word(event: E) -> TextObject: """ A 'word': caw and daw """ start, end = event.current_buffer.document.find_boundaries_of_current_word( include_trailing_whitespace=True ) return TextObject(start, end) @text_object("i", "W", no_move_handler=True) def _inner_WORD(event: E) -> TextObject: """ Inner 'WORD': ciW and diW """ start, end = event.current_buffer.document.find_boundaries_of_current_word( WORD=True ) return TextObject(start, end) @text_object("a", "W", no_move_handler=True) def _a_WORD(event: E) -> TextObject: """ A 'WORD': caw and daw """ start, end = event.current_buffer.document.find_boundaries_of_current_word( WORD=True, include_trailing_whitespace=True ) return TextObject(start, end) @text_object("a", "p", no_move_handler=True) def _paragraph(event: E) -> TextObject: """ Auto paragraph. """ start = event.current_buffer.document.start_of_paragraph() end = event.current_buffer.document.end_of_paragraph(count=event.arg) return TextObject(start, end) @text_object("^") def _start_of_line(event: E) -> TextObject: """'c^', 'd^' and '^': Soft start of line, after whitespace.""" return TextObject( event.current_buffer.document.get_start_of_line_position( after_whitespace=True ) ) @text_object("0") def _hard_start_of_line(event: E) -> TextObject: """ 'c0', 'd0': Hard start of line, before whitespace. (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.) """ return TextObject( event.current_buffer.document.get_start_of_line_position( after_whitespace=False ) ) def create_ci_ca_handles( ci_start: str, ci_end: str, inner: bool, key: str | None = None ) -> None: # TODO: 'dat', 'dit', (tags (like xml) """ Delete/Change string between this start and stop character. But keep these characters. This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations. """ def handler(event: E) -> TextObject: if ci_start == ci_end: # Quotes start = event.current_buffer.document.find_backwards( ci_start, in_current_line=False ) end = event.current_buffer.document.find(ci_end, in_current_line=False) else: # Brackets start = event.current_buffer.document.find_enclosing_bracket_left( ci_start, ci_end ) end = event.current_buffer.document.find_enclosing_bracket_right( ci_start, ci_end ) if start is not None and end is not None: offset = 0 if inner else 1 return TextObject(start + 1 - offset, end + offset) else: # Nothing found. return TextObject(0) if key is None: text_object("ai"[inner], ci_start, no_move_handler=True)(handler) text_object("ai"[inner], ci_end, no_move_handler=True)(handler) else: text_object("ai"[inner], key, no_move_handler=True)(handler) for inner in (False, True): for ci_start, ci_end in [ ('"', '"'), ("'", "'"), ("`", "`"), ("[", "]"), ("<", ">"), ("{", "}"), ("(", ")"), ]: create_ci_ca_handles(ci_start, ci_end, inner) create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib' create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB' @text_object("{") def _previous_section(event: E) -> TextObject: """ Move to previous blank-line separated section. Implements '{', 'c{', 'd{', 'y{' """ index = event.current_buffer.document.start_of_paragraph( count=event.arg, before=True ) return TextObject(index) @text_object("}") def _next_section(event: E) -> TextObject: """ Move to next blank-line separated section. Implements '}', 'c}', 'd}', 'y}' """ index = event.current_buffer.document.end_of_paragraph( count=event.arg, after=True ) return TextObject(index) @text_object("f", Keys.Any) def _find_next_occurrence(event: E) -> TextObject: """ Go to next occurrence of character. Typing 'fx' will move the cursor to the next occurrence of character. 'x'. """ event.app.vi_state.last_character_find = CharacterFind(event.data, False) match = event.current_buffer.document.find( event.data, in_current_line=True, count=event.arg ) if match: return TextObject(match, type=TextObjectType.INCLUSIVE) else: return TextObject(0) @text_object("F", Keys.Any) def _find_previous_occurrence(event: E) -> TextObject: """ Go to previous occurrence of character. Typing 'Fx' will move the cursor to the previous occurrence of character. 'x'. """ event.app.vi_state.last_character_find = CharacterFind(event.data, True) return TextObject( event.current_buffer.document.find_backwards( event.data, in_current_line=True, count=event.arg ) or 0 ) @text_object("t", Keys.Any) def _t(event: E) -> TextObject: """ Move right to the next occurrence of c, then one char backward. """ event.app.vi_state.last_character_find = CharacterFind(event.data, False) match = event.current_buffer.document.find( event.data, in_current_line=True, count=event.arg ) if match: return TextObject(match - 1, type=TextObjectType.INCLUSIVE) else: return TextObject(0) @text_object("T", Keys.Any) def _T(event: E) -> TextObject: """ Move left to the previous occurrence of c, then one char forward. """ event.app.vi_state.last_character_find = CharacterFind(event.data, True) match = event.current_buffer.document.find_backwards( event.data, in_current_line=True, count=event.arg ) return TextObject(match + 1 if match else 0) def repeat(reverse: bool) -> None: """ Create ',' and ';' commands. """ @text_object("," if reverse else ";") def _(event: E) -> TextObject: """ Repeat the last 'f'/'F'/'t'/'T' command. """ pos: int | None = 0 vi_state = event.app.vi_state type = TextObjectType.EXCLUSIVE if vi_state.last_character_find: char = vi_state.last_character_find.character backwards = vi_state.last_character_find.backwards if reverse: backwards = not backwards if backwards: pos = event.current_buffer.document.find_backwards( char, in_current_line=True, count=event.arg ) else: pos = event.current_buffer.document.find( char, in_current_line=True, count=event.arg ) type = TextObjectType.INCLUSIVE if pos: return TextObject(pos, type=type) else: return TextObject(0) repeat(True) repeat(False) @text_object("h") @text_object("left") def _left(event: E) -> TextObject: """ Implements 'ch', 'dh', 'h': Cursor left. """ return TextObject( event.current_buffer.document.get_cursor_left_position(count=event.arg) ) @text_object("j", no_move_handler=True, no_selection_handler=True) # Note: We also need `no_selection_handler`, because we in # selection mode, we prefer the other 'j' binding that keeps # `buffer.preferred_column`. def _down(event: E) -> TextObject: """ Implements 'cj', 'dj', 'j', ... Cursor up. """ return TextObject( event.current_buffer.document.get_cursor_down_position(count=event.arg), type=TextObjectType.LINEWISE, ) @text_object("k", no_move_handler=True, no_selection_handler=True) def _up(event: E) -> TextObject: """ Implements 'ck', 'dk', 'k', ... Cursor up. """ return TextObject( event.current_buffer.document.get_cursor_up_position(count=event.arg), type=TextObjectType.LINEWISE, ) @text_object("l") @text_object(" ") @text_object("right") def _right(event: E) -> TextObject: """ Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right. """ return TextObject( event.current_buffer.document.get_cursor_right_position(count=event.arg) ) @text_object("H") def _top_of_screen(event: E) -> TextObject: """ Moves to the start of the visible region. (Below the scroll offset.) Implements 'cH', 'dH', 'H'. """ w = event.app.layout.current_window b = event.current_buffer if w and w.render_info: # When we find a Window that has BufferControl showing this window, # move to the start of the visible area. pos = ( b.document.translate_row_col_to_index( w.render_info.first_visible_line(after_scroll_offset=True), 0 ) - b.cursor_position ) else: # Otherwise, move to the start of the input. pos = -len(b.document.text_before_cursor) return TextObject(pos, type=TextObjectType.LINEWISE) @text_object("M") def _middle_of_screen(event: E) -> TextObject: """ Moves cursor to the vertical center of the visible region. Implements 'cM', 'dM', 'M'. """ w = event.app.layout.current_window b = event.current_buffer if w and w.render_info: # When we find a Window that has BufferControl showing this window, # move to the center of the visible area. pos = ( b.document.translate_row_col_to_index( w.render_info.center_visible_line(), 0 ) - b.cursor_position ) else: # Otherwise, move to the start of the input. pos = -len(b.document.text_before_cursor) return TextObject(pos, type=TextObjectType.LINEWISE) @text_object("L") def _end_of_screen(event: E) -> TextObject: """ Moves to the end of the visible region. (Above the scroll offset.) """ w = event.app.layout.current_window b = event.current_buffer if w and w.render_info: # When we find a Window that has BufferControl showing this window, # move to the end of the visible area. pos = ( b.document.translate_row_col_to_index( w.render_info.last_visible_line(before_scroll_offset=True), 0 ) - b.cursor_position ) else: # Otherwise, move to the end of the input. pos = len(b.document.text_after_cursor) return TextObject(pos, type=TextObjectType.LINEWISE) @text_object("n", no_move_handler=True) def _search_next(event: E) -> TextObject: """ Search next. """ buff = event.current_buffer search_state = event.app.current_search_state cursor_position = buff.get_search_position( search_state, include_current_position=False, count=event.arg ) return TextObject(cursor_position - buff.cursor_position) @handle("n", filter=vi_navigation_mode) def _search_next2(event: E) -> None: """ Search next in navigation mode. (This goes through the history.) """ search_state = event.app.current_search_state event.current_buffer.apply_search( search_state, include_current_position=False, count=event.arg ) @text_object("N", no_move_handler=True) def _search_previous(event: E) -> TextObject: """ Search previous. """ buff = event.current_buffer search_state = event.app.current_search_state cursor_position = buff.get_search_position( ~search_state, include_current_position=False, count=event.arg ) return TextObject(cursor_position - buff.cursor_position) @handle("N", filter=vi_navigation_mode) def _search_previous2(event: E) -> None: """ Search previous in navigation mode. (This goes through the history.) """ search_state = event.app.current_search_state event.current_buffer.apply_search( ~search_state, include_current_position=False, count=event.arg ) @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode) @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode) @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode) def _scroll_top(event: E) -> None: """ Scrolls the window to makes the current line the first line in the visible region. """ b = event.current_buffer event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode) @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode) def _scroll_bottom(event: E) -> None: """ Scrolls the window to makes the current line the last line in the visible region. """ # We can safely set the scroll offset to zero; the Window will make # sure that it scrolls at least enough to make the cursor visible # again. event.app.layout.current_window.vertical_scroll = 0 @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode) def _scroll_center(event: E) -> None: """ Center Window vertically around cursor. """ w = event.app.layout.current_window b = event.current_buffer if w and w.render_info: info = w.render_info # Calculate the offset that we need in order to position the row # containing the cursor in the center. scroll_height = info.window_height // 2 y = max(0, b.document.cursor_position_row - 1) height = 0 while y > 0: line_height = info.get_height_for_line(y) if height + line_height < scroll_height: height += line_height y -= 1 else: break w.vertical_scroll = y @text_object("%") def _goto_corresponding_bracket(event: E) -> TextObject: """ Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.) If an 'arg' has been given, go this this % position in the file. """ buffer = event.current_buffer if event._arg: # If 'arg' has been given, the meaning of % is to go to the 'x%' # row in the file. if 0 < event.arg <= 100: absolute_index = buffer.document.translate_row_col_to_index( int((event.arg * buffer.document.line_count - 1) / 100), 0 ) return TextObject( absolute_index - buffer.document.cursor_position, type=TextObjectType.LINEWISE, ) else: return TextObject(0) # Do nothing. else: # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s). match = buffer.document.find_matching_bracket_position() if match: return TextObject(match, type=TextObjectType.INCLUSIVE) else: return TextObject(0) @text_object("|") def _to_column(event: E) -> TextObject: """ Move to the n-th column (you may specify the argument n by typing it on number keys, for example, 20|). """ return TextObject( event.current_buffer.document.get_column_cursor_position(event.arg - 1) ) @text_object("g", "g") def _goto_first_line(event: E) -> TextObject: """ Go to the start of the very first line. Implements 'gg', 'cgg', 'ygg' """ d = event.current_buffer.document if event._arg: # Move to the given line. return TextObject( d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position, type=TextObjectType.LINEWISE, ) else: # Move to the top of the input. return TextObject( d.get_start_of_document_position(), type=TextObjectType.LINEWISE ) @text_object("g", "_") def _goto_last_line(event: E) -> TextObject: """ Go to last non-blank of line. 'g_', 'cg_', 'yg_', etc.. """ return TextObject( event.current_buffer.document.last_non_blank_of_current_line_position(), type=TextObjectType.INCLUSIVE, ) @text_object("g", "e") def _ge(event: E) -> TextObject: """ Go to last character of previous word. 'ge', 'cge', 'yge', etc.. """ prev_end = event.current_buffer.document.find_previous_word_ending( count=event.arg ) return TextObject( prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE ) @text_object("g", "E") def _gE(event: E) -> TextObject: """ Go to last character of previous WORD. 'gE', 'cgE', 'ygE', etc.. """ prev_end = event.current_buffer.document.find_previous_word_ending( count=event.arg, WORD=True ) return TextObject( prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE ) @text_object("g", "m") def _gm(event: E) -> TextObject: """ Like g0, but half a screenwidth to the right. (Or as much as possible.) """ w = event.app.layout.current_window buff = event.current_buffer if w and w.render_info: width = w.render_info.window_width start = buff.document.get_start_of_line_position(after_whitespace=False) start += int(min(width / 2, len(buff.document.current_line))) return TextObject(start, type=TextObjectType.INCLUSIVE) return TextObject(0) @text_object("G") def _last_line(event: E) -> TextObject: """ Go to the end of the document. (If no arg has been given.) """ buf = event.current_buffer return TextObject( buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0) - buf.cursor_position, type=TextObjectType.LINEWISE, ) # # *** Other *** # @handle("G", filter=has_arg) def _to_nth_history_line(event: E) -> None: """ If an argument is given, move to this line in the history. (for example, 15G) """ event.current_buffer.go_to_history(event.arg - 1) for n in "123456789": @handle( n, filter=vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode, ) def _arg(event: E) -> None: """ Always handle numerics in navigation mode as arg. """ event.append_to_arg_count(event.data) @handle( "0", filter=( vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode ) & has_arg, ) def _0_arg(event: E) -> None: """ Zero when an argument was already give. """ event.append_to_arg_count(event.data) @handle(Keys.Any, filter=vi_replace_mode) def _insert_text(event: E) -> None: """ Insert data at cursor position. """ event.current_buffer.insert_text(event.data, overwrite=True) @handle(Keys.Any, filter=vi_replace_single_mode) def _replace_single(event: E) -> None: """ Replace single character at cursor position. """ event.current_buffer.insert_text(event.data, overwrite=True) event.current_buffer.cursor_position -= 1 event.app.vi_state.input_mode = InputMode.NAVIGATION @handle( Keys.Any, filter=vi_insert_multiple_mode, save_before=(lambda e: not e.is_repeat), ) def _insert_text_multiple_cursors(event: E) -> None: """ Insert data at multiple cursor positions at once. (Usually a result of pressing 'I' or 'A' in block-selection mode.) """ buff = event.current_buffer original_text = buff.text # Construct new text. text = [] p = 0 for p2 in buff.multiple_cursor_positions: text.append(original_text[p:p2]) text.append(event.data) p = p2 text.append(original_text[p:]) # Shift all cursor positions. new_cursor_positions = [ pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions) ] # Set result. buff.text = "".join(text) buff.multiple_cursor_positions = new_cursor_positions buff.cursor_position += 1 @handle("backspace", filter=vi_insert_multiple_mode) def _delete_before_multiple_cursors(event: E) -> None: """ Backspace, using multiple cursors. """ buff = event.current_buffer original_text = buff.text # Construct new text. deleted_something = False text = [] p = 0 for p2 in buff.multiple_cursor_positions: if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines. text.append(original_text[p : p2 - 1]) deleted_something = True else: text.append(original_text[p:p2]) p = p2 text.append(original_text[p:]) if deleted_something: # Shift all cursor positions. lengths = [len(part) for part in text[:-1]] new_cursor_positions = list(accumulate(lengths)) # Set result. buff.text = "".join(text) buff.multiple_cursor_positions = new_cursor_positions buff.cursor_position -= 1 else: event.app.output.bell() @handle("delete", filter=vi_insert_multiple_mode) def _delete_after_multiple_cursors(event: E) -> None: """ Delete, using multiple cursors. """ buff = event.current_buffer original_text = buff.text # Construct new text. deleted_something = False text = [] new_cursor_positions = [] p = 0 for p2 in buff.multiple_cursor_positions: text.append(original_text[p:p2]) if p2 >= len(original_text) or original_text[p2] == "\n": # Don't delete across lines. p = p2 else: p = p2 + 1 deleted_something = True text.append(original_text[p:]) if deleted_something: # Shift all cursor positions. lengths = [len(part) for part in text[:-1]] new_cursor_positions = list(accumulate(lengths)) # Set result. buff.text = "".join(text) buff.multiple_cursor_positions = new_cursor_positions else: event.app.output.bell() @handle("left", filter=vi_insert_multiple_mode) def _left_multiple(event: E) -> None: """ Move all cursors to the left. (But keep all cursors on the same line.) """ buff = event.current_buffer new_positions = [] for p in buff.multiple_cursor_positions: if buff.document.translate_index_to_position(p)[1] > 0: p -= 1 new_positions.append(p) buff.multiple_cursor_positions = new_positions if buff.document.cursor_position_col > 0: buff.cursor_position -= 1 @handle("right", filter=vi_insert_multiple_mode) def _right_multiple(event: E) -> None: """ Move all cursors to the right. (But keep all cursors on the same line.) """ buff = event.current_buffer new_positions = [] for p in buff.multiple_cursor_positions: row, column = buff.document.translate_index_to_position(p) if column < len(buff.document.lines[row]): p += 1 new_positions.append(p) buff.multiple_cursor_positions = new_positions if not buff.document.is_cursor_at_the_end_of_line: buff.cursor_position += 1 @handle("up", filter=vi_insert_multiple_mode) @handle("down", filter=vi_insert_multiple_mode) def _updown_multiple(event: E) -> None: """ Ignore all up/down key presses when in multiple cursor mode. """ @handle("c-x", "c-l", filter=vi_insert_mode) def _complete_line(event: E) -> None: """ Pressing the ControlX - ControlL sequence in Vi mode does line completion based on the other lines in the document and the history. """ event.current_buffer.start_history_lines_completion() @handle("c-x", "c-f", filter=vi_insert_mode) def _complete_filename(event: E) -> None: """ Complete file names. """ # TODO pass @handle("c-k", filter=vi_insert_mode | vi_replace_mode) def _digraph(event: E) -> None: """ Go into digraph mode. """ event.app.vi_state.waiting_for_digraph = True @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given) def _digraph1(event: E) -> None: """ First digraph symbol. """ event.app.vi_state.digraph_symbol1 = event.data @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given) def _create_digraph(event: E) -> None: """ Insert digraph. """ try: # Lookup. code: tuple[str, str] = ( event.app.vi_state.digraph_symbol1 or "", event.data, ) if code not in DIGRAPHS: code = code[::-1] # Try reversing. symbol = DIGRAPHS[code] except KeyError: # Unknown digraph. event.app.output.bell() else: # Insert digraph. overwrite = event.app.vi_state.input_mode == InputMode.REPLACE event.current_buffer.insert_text(chr(symbol), overwrite=overwrite) event.app.vi_state.waiting_for_digraph = False finally: event.app.vi_state.waiting_for_digraph = False event.app.vi_state.digraph_symbol1 = None @handle("c-o", filter=vi_insert_mode | vi_replace_mode) def _quick_normal_mode(event: E) -> None: """ Go into normal mode for one single action. """ event.app.vi_state.temporary_navigation_mode = True @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro) def _start_macro(event: E) -> None: """ Start recording macro. """ c = event.key_sequence[1].data if c in vi_register_names: vi_state = event.app.vi_state vi_state.recording_register = c vi_state.current_recording = "" @handle("q", filter=vi_navigation_mode & vi_recording_macro) def _stop_macro(event: E) -> None: """ Stop recording macro. """ vi_state = event.app.vi_state # Store and stop recording. if vi_state.recording_register: vi_state.named_registers[vi_state.recording_register] = ClipboardData( vi_state.current_recording ) vi_state.recording_register = None vi_state.current_recording = "" @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False) def _execute_macro(event: E) -> None: """ Execute macro. Notice that we pass `record_in_macro=False`. This ensures that the `@x` keys don't appear in the recording itself. This function inserts the body of the called macro back into the KeyProcessor, so these keys will be added later on to the macro of their handlers have `record_in_macro=True`. """ # Retrieve macro. c = event.key_sequence[1].data try: macro = event.app.vi_state.named_registers[c] except KeyError: return # Expand macro (which is a string in the register), in individual keys. # Use vt100 parser for this. keys: list[KeyPress] = [] parser = Vt100Parser(keys.append) parser.feed(macro.text) parser.flush() # Now feed keys back to the input processor. for _ in range(event.arg): event.app.key_processor.feed_multiple(keys, first=True) return ConditionalKeyBindings(key_bindings, vi_mode) def load_vi_search_bindings() -> KeyBindingsBase: key_bindings = KeyBindings() handle = key_bindings.add from . import search # Vi-style forward search. handle( "/", filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, )(search.start_forward_incremental_search) handle( "?", filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, )(search.start_forward_incremental_search) handle("c-s")(search.start_forward_incremental_search) # Vi-style backward search. handle( "?", filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, )(search.start_reverse_incremental_search) handle( "/", filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, )(search.start_reverse_incremental_search) handle("c-r")(search.start_reverse_incremental_search) # Apply the search. (At the / or ? prompt.) handle("enter", filter=is_searching)(search.accept_search) handle("c-r", filter=is_searching)(search.reverse_incremental_search) handle("c-s", filter=is_searching)(search.forward_incremental_search) handle("c-c")(search.abort_search) handle("c-g")(search.abort_search) handle("backspace", filter=search_buffer_is_empty)(search.abort_search) # Handle escape. This should accept the search, just like readline. # `abort_search` would be a meaningful alternative. handle("escape")(search.accept_search) return ConditionalKeyBindings(key_bindings, vi_mode) ================================================ FILE: src/prompt_toolkit/key_binding/defaults.py ================================================ """ Default key bindings.:: key_bindings = load_key_bindings() app = Application(key_bindings=key_bindings) """ from __future__ import annotations from prompt_toolkit.filters import buffer_has_focus from prompt_toolkit.key_binding.bindings.basic import load_basic_bindings from prompt_toolkit.key_binding.bindings.cpr import load_cpr_bindings from prompt_toolkit.key_binding.bindings.emacs import ( load_emacs_bindings, load_emacs_search_bindings, load_emacs_shift_selection_bindings, ) from prompt_toolkit.key_binding.bindings.mouse import load_mouse_bindings from prompt_toolkit.key_binding.bindings.vi import ( load_vi_bindings, load_vi_search_bindings, ) from prompt_toolkit.key_binding.key_bindings import ( ConditionalKeyBindings, KeyBindingsBase, merge_key_bindings, ) __all__ = [ "load_key_bindings", ] def load_key_bindings() -> KeyBindingsBase: """ Create a KeyBindings object that contains the default key bindings. """ all_bindings = merge_key_bindings( [ # Load basic bindings. load_basic_bindings(), # Load emacs bindings. load_emacs_bindings(), load_emacs_search_bindings(), load_emacs_shift_selection_bindings(), # Load Vi bindings. load_vi_bindings(), load_vi_search_bindings(), ] ) return merge_key_bindings( [ # Make sure that the above key bindings are only active if the # currently focused control is a `BufferControl`. For other controls, we # don't want these key bindings to intervene. (This would break "ptterm" # for instance, which handles 'Keys.Any' in the user control itself.) ConditionalKeyBindings(all_bindings, buffer_has_focus), # Active, even when no buffer has been focused. load_mouse_bindings(), load_cpr_bindings(), ] ) ================================================ FILE: src/prompt_toolkit/key_binding/digraphs.py ================================================ """ Vi Digraphs. This is a list of special characters that can be inserted in Vi insert mode by pressing Control-K followed by to normal characters. Taken from Neovim and translated to Python: https://raw.githubusercontent.com/neovim/neovim/master/src/nvim/digraph.c """ from __future__ import annotations __all__ = [ "DIGRAPHS", ] # digraphs for Unicode from RFC1345 # (also work for ISO-8859-1 aka latin1) DIGRAPHS: dict[tuple[str, str], int] = { ("N", "U"): 0x00, ("S", "H"): 0x01, ("S", "X"): 0x02, ("E", "X"): 0x03, ("E", "T"): 0x04, ("E", "Q"): 0x05, ("A", "K"): 0x06, ("B", "L"): 0x07, ("B", "S"): 0x08, ("H", "T"): 0x09, ("L", "F"): 0x0A, ("V", "T"): 0x0B, ("F", "F"): 0x0C, ("C", "R"): 0x0D, ("S", "O"): 0x0E, ("S", "I"): 0x0F, ("D", "L"): 0x10, ("D", "1"): 0x11, ("D", "2"): 0x12, ("D", "3"): 0x13, ("D", "4"): 0x14, ("N", "K"): 0x15, ("S", "Y"): 0x16, ("E", "B"): 0x17, ("C", "N"): 0x18, ("E", "M"): 0x19, ("S", "B"): 0x1A, ("E", "C"): 0x1B, ("F", "S"): 0x1C, ("G", "S"): 0x1D, ("R", "S"): 0x1E, ("U", "S"): 0x1F, ("S", "P"): 0x20, ("N", "b"): 0x23, ("D", "O"): 0x24, ("A", "t"): 0x40, ("<", "("): 0x5B, ("/", "/"): 0x5C, (")", ">"): 0x5D, ("'", ">"): 0x5E, ("'", "!"): 0x60, ("(", "!"): 0x7B, ("!", "!"): 0x7C, ("!", ")"): 0x7D, ("'", "?"): 0x7E, ("D", "T"): 0x7F, ("P", "A"): 0x80, ("H", "O"): 0x81, ("B", "H"): 0x82, ("N", "H"): 0x83, ("I", "N"): 0x84, ("N", "L"): 0x85, ("S", "A"): 0x86, ("E", "S"): 0x87, ("H", "S"): 0x88, ("H", "J"): 0x89, ("V", "S"): 0x8A, ("P", "D"): 0x8B, ("P", "U"): 0x8C, ("R", "I"): 0x8D, ("S", "2"): 0x8E, ("S", "3"): 0x8F, ("D", "C"): 0x90, ("P", "1"): 0x91, ("P", "2"): 0x92, ("T", "S"): 0x93, ("C", "C"): 0x94, ("M", "W"): 0x95, ("S", "G"): 0x96, ("E", "G"): 0x97, ("S", "S"): 0x98, ("G", "C"): 0x99, ("S", "C"): 0x9A, ("C", "I"): 0x9B, ("S", "T"): 0x9C, ("O", "C"): 0x9D, ("P", "M"): 0x9E, ("A", "C"): 0x9F, ("N", "S"): 0xA0, ("!", "I"): 0xA1, ("C", "t"): 0xA2, ("P", "d"): 0xA3, ("C", "u"): 0xA4, ("Y", "e"): 0xA5, ("B", "B"): 0xA6, ("S", "E"): 0xA7, ("'", ":"): 0xA8, ("C", "o"): 0xA9, ("-", "a"): 0xAA, ("<", "<"): 0xAB, ("N", "O"): 0xAC, ("-", "-"): 0xAD, ("R", "g"): 0xAE, ("'", "m"): 0xAF, ("D", "G"): 0xB0, ("+", "-"): 0xB1, ("2", "S"): 0xB2, ("3", "S"): 0xB3, ("'", "'"): 0xB4, ("M", "y"): 0xB5, ("P", "I"): 0xB6, (".", "M"): 0xB7, ("'", ","): 0xB8, ("1", "S"): 0xB9, ("-", "o"): 0xBA, (">", ">"): 0xBB, ("1", "4"): 0xBC, ("1", "2"): 0xBD, ("3", "4"): 0xBE, ("?", "I"): 0xBF, ("A", "!"): 0xC0, ("A", "'"): 0xC1, ("A", ">"): 0xC2, ("A", "?"): 0xC3, ("A", ":"): 0xC4, ("A", "A"): 0xC5, ("A", "E"): 0xC6, ("C", ","): 0xC7, ("E", "!"): 0xC8, ("E", "'"): 0xC9, ("E", ">"): 0xCA, ("E", ":"): 0xCB, ("I", "!"): 0xCC, ("I", "'"): 0xCD, ("I", ">"): 0xCE, ("I", ":"): 0xCF, ("D", "-"): 0xD0, ("N", "?"): 0xD1, ("O", "!"): 0xD2, ("O", "'"): 0xD3, ("O", ">"): 0xD4, ("O", "?"): 0xD5, ("O", ":"): 0xD6, ("*", "X"): 0xD7, ("O", "/"): 0xD8, ("U", "!"): 0xD9, ("U", "'"): 0xDA, ("U", ">"): 0xDB, ("U", ":"): 0xDC, ("Y", "'"): 0xDD, ("T", "H"): 0xDE, ("s", "s"): 0xDF, ("a", "!"): 0xE0, ("a", "'"): 0xE1, ("a", ">"): 0xE2, ("a", "?"): 0xE3, ("a", ":"): 0xE4, ("a", "a"): 0xE5, ("a", "e"): 0xE6, ("c", ","): 0xE7, ("e", "!"): 0xE8, ("e", "'"): 0xE9, ("e", ">"): 0xEA, ("e", ":"): 0xEB, ("i", "!"): 0xEC, ("i", "'"): 0xED, ("i", ">"): 0xEE, ("i", ":"): 0xEF, ("d", "-"): 0xF0, ("n", "?"): 0xF1, ("o", "!"): 0xF2, ("o", "'"): 0xF3, ("o", ">"): 0xF4, ("o", "?"): 0xF5, ("o", ":"): 0xF6, ("-", ":"): 0xF7, ("o", "/"): 0xF8, ("u", "!"): 0xF9, ("u", "'"): 0xFA, ("u", ">"): 0xFB, ("u", ":"): 0xFC, ("y", "'"): 0xFD, ("t", "h"): 0xFE, ("y", ":"): 0xFF, ("A", "-"): 0x0100, ("a", "-"): 0x0101, ("A", "("): 0x0102, ("a", "("): 0x0103, ("A", ";"): 0x0104, ("a", ";"): 0x0105, ("C", "'"): 0x0106, ("c", "'"): 0x0107, ("C", ">"): 0x0108, ("c", ">"): 0x0109, ("C", "."): 0x010A, ("c", "."): 0x010B, ("C", "<"): 0x010C, ("c", "<"): 0x010D, ("D", "<"): 0x010E, ("d", "<"): 0x010F, ("D", "/"): 0x0110, ("d", "/"): 0x0111, ("E", "-"): 0x0112, ("e", "-"): 0x0113, ("E", "("): 0x0114, ("e", "("): 0x0115, ("E", "."): 0x0116, ("e", "."): 0x0117, ("E", ";"): 0x0118, ("e", ";"): 0x0119, ("E", "<"): 0x011A, ("e", "<"): 0x011B, ("G", ">"): 0x011C, ("g", ">"): 0x011D, ("G", "("): 0x011E, ("g", "("): 0x011F, ("G", "."): 0x0120, ("g", "."): 0x0121, ("G", ","): 0x0122, ("g", ","): 0x0123, ("H", ">"): 0x0124, ("h", ">"): 0x0125, ("H", "/"): 0x0126, ("h", "/"): 0x0127, ("I", "?"): 0x0128, ("i", "?"): 0x0129, ("I", "-"): 0x012A, ("i", "-"): 0x012B, ("I", "("): 0x012C, ("i", "("): 0x012D, ("I", ";"): 0x012E, ("i", ";"): 0x012F, ("I", "."): 0x0130, ("i", "."): 0x0131, ("I", "J"): 0x0132, ("i", "j"): 0x0133, ("J", ">"): 0x0134, ("j", ">"): 0x0135, ("K", ","): 0x0136, ("k", ","): 0x0137, ("k", "k"): 0x0138, ("L", "'"): 0x0139, ("l", "'"): 0x013A, ("L", ","): 0x013B, ("l", ","): 0x013C, ("L", "<"): 0x013D, ("l", "<"): 0x013E, ("L", "."): 0x013F, ("l", "."): 0x0140, ("L", "/"): 0x0141, ("l", "/"): 0x0142, ("N", "'"): 0x0143, ("n", "'"): 0x0144, ("N", ","): 0x0145, ("n", ","): 0x0146, ("N", "<"): 0x0147, ("n", "<"): 0x0148, ("'", "n"): 0x0149, ("N", "G"): 0x014A, ("n", "g"): 0x014B, ("O", "-"): 0x014C, ("o", "-"): 0x014D, ("O", "("): 0x014E, ("o", "("): 0x014F, ("O", '"'): 0x0150, ("o", '"'): 0x0151, ("O", "E"): 0x0152, ("o", "e"): 0x0153, ("R", "'"): 0x0154, ("r", "'"): 0x0155, ("R", ","): 0x0156, ("r", ","): 0x0157, ("R", "<"): 0x0158, ("r", "<"): 0x0159, ("S", "'"): 0x015A, ("s", "'"): 0x015B, ("S", ">"): 0x015C, ("s", ">"): 0x015D, ("S", ","): 0x015E, ("s", ","): 0x015F, ("S", "<"): 0x0160, ("s", "<"): 0x0161, ("T", ","): 0x0162, ("t", ","): 0x0163, ("T", "<"): 0x0164, ("t", "<"): 0x0165, ("T", "/"): 0x0166, ("t", "/"): 0x0167, ("U", "?"): 0x0168, ("u", "?"): 0x0169, ("U", "-"): 0x016A, ("u", "-"): 0x016B, ("U", "("): 0x016C, ("u", "("): 0x016D, ("U", "0"): 0x016E, ("u", "0"): 0x016F, ("U", '"'): 0x0170, ("u", '"'): 0x0171, ("U", ";"): 0x0172, ("u", ";"): 0x0173, ("W", ">"): 0x0174, ("w", ">"): 0x0175, ("Y", ">"): 0x0176, ("y", ">"): 0x0177, ("Y", ":"): 0x0178, ("Z", "'"): 0x0179, ("z", "'"): 0x017A, ("Z", "."): 0x017B, ("z", "."): 0x017C, ("Z", "<"): 0x017D, ("z", "<"): 0x017E, ("O", "9"): 0x01A0, ("o", "9"): 0x01A1, ("O", "I"): 0x01A2, ("o", "i"): 0x01A3, ("y", "r"): 0x01A6, ("U", "9"): 0x01AF, ("u", "9"): 0x01B0, ("Z", "/"): 0x01B5, ("z", "/"): 0x01B6, ("E", "D"): 0x01B7, ("A", "<"): 0x01CD, ("a", "<"): 0x01CE, ("I", "<"): 0x01CF, ("i", "<"): 0x01D0, ("O", "<"): 0x01D1, ("o", "<"): 0x01D2, ("U", "<"): 0x01D3, ("u", "<"): 0x01D4, ("A", "1"): 0x01DE, ("a", "1"): 0x01DF, ("A", "7"): 0x01E0, ("a", "7"): 0x01E1, ("A", "3"): 0x01E2, ("a", "3"): 0x01E3, ("G", "/"): 0x01E4, ("g", "/"): 0x01E5, ("G", "<"): 0x01E6, ("g", "<"): 0x01E7, ("K", "<"): 0x01E8, ("k", "<"): 0x01E9, ("O", ";"): 0x01EA, ("o", ";"): 0x01EB, ("O", "1"): 0x01EC, ("o", "1"): 0x01ED, ("E", "Z"): 0x01EE, ("e", "z"): 0x01EF, ("j", "<"): 0x01F0, ("G", "'"): 0x01F4, ("g", "'"): 0x01F5, (";", "S"): 0x02BF, ("'", "<"): 0x02C7, ("'", "("): 0x02D8, ("'", "."): 0x02D9, ("'", "0"): 0x02DA, ("'", ";"): 0x02DB, ("'", '"'): 0x02DD, ("A", "%"): 0x0386, ("E", "%"): 0x0388, ("Y", "%"): 0x0389, ("I", "%"): 0x038A, ("O", "%"): 0x038C, ("U", "%"): 0x038E, ("W", "%"): 0x038F, ("i", "3"): 0x0390, ("A", "*"): 0x0391, ("B", "*"): 0x0392, ("G", "*"): 0x0393, ("D", "*"): 0x0394, ("E", "*"): 0x0395, ("Z", "*"): 0x0396, ("Y", "*"): 0x0397, ("H", "*"): 0x0398, ("I", "*"): 0x0399, ("K", "*"): 0x039A, ("L", "*"): 0x039B, ("M", "*"): 0x039C, ("N", "*"): 0x039D, ("C", "*"): 0x039E, ("O", "*"): 0x039F, ("P", "*"): 0x03A0, ("R", "*"): 0x03A1, ("S", "*"): 0x03A3, ("T", "*"): 0x03A4, ("U", "*"): 0x03A5, ("F", "*"): 0x03A6, ("X", "*"): 0x03A7, ("Q", "*"): 0x03A8, ("W", "*"): 0x03A9, ("J", "*"): 0x03AA, ("V", "*"): 0x03AB, ("a", "%"): 0x03AC, ("e", "%"): 0x03AD, ("y", "%"): 0x03AE, ("i", "%"): 0x03AF, ("u", "3"): 0x03B0, ("a", "*"): 0x03B1, ("b", "*"): 0x03B2, ("g", "*"): 0x03B3, ("d", "*"): 0x03B4, ("e", "*"): 0x03B5, ("z", "*"): 0x03B6, ("y", "*"): 0x03B7, ("h", "*"): 0x03B8, ("i", "*"): 0x03B9, ("k", "*"): 0x03BA, ("l", "*"): 0x03BB, ("m", "*"): 0x03BC, ("n", "*"): 0x03BD, ("c", "*"): 0x03BE, ("o", "*"): 0x03BF, ("p", "*"): 0x03C0, ("r", "*"): 0x03C1, ("*", "s"): 0x03C2, ("s", "*"): 0x03C3, ("t", "*"): 0x03C4, ("u", "*"): 0x03C5, ("f", "*"): 0x03C6, ("x", "*"): 0x03C7, ("q", "*"): 0x03C8, ("w", "*"): 0x03C9, ("j", "*"): 0x03CA, ("v", "*"): 0x03CB, ("o", "%"): 0x03CC, ("u", "%"): 0x03CD, ("w", "%"): 0x03CE, ("'", "G"): 0x03D8, (",", "G"): 0x03D9, ("T", "3"): 0x03DA, ("t", "3"): 0x03DB, ("M", "3"): 0x03DC, ("m", "3"): 0x03DD, ("K", "3"): 0x03DE, ("k", "3"): 0x03DF, ("P", "3"): 0x03E0, ("p", "3"): 0x03E1, ("'", "%"): 0x03F4, ("j", "3"): 0x03F5, ("I", "O"): 0x0401, ("D", "%"): 0x0402, ("G", "%"): 0x0403, ("I", "E"): 0x0404, ("D", "S"): 0x0405, ("I", "I"): 0x0406, ("Y", "I"): 0x0407, ("J", "%"): 0x0408, ("L", "J"): 0x0409, ("N", "J"): 0x040A, ("T", "s"): 0x040B, ("K", "J"): 0x040C, ("V", "%"): 0x040E, ("D", "Z"): 0x040F, ("A", "="): 0x0410, ("B", "="): 0x0411, ("V", "="): 0x0412, ("G", "="): 0x0413, ("D", "="): 0x0414, ("E", "="): 0x0415, ("Z", "%"): 0x0416, ("Z", "="): 0x0417, ("I", "="): 0x0418, ("J", "="): 0x0419, ("K", "="): 0x041A, ("L", "="): 0x041B, ("M", "="): 0x041C, ("N", "="): 0x041D, ("O", "="): 0x041E, ("P", "="): 0x041F, ("R", "="): 0x0420, ("S", "="): 0x0421, ("T", "="): 0x0422, ("U", "="): 0x0423, ("F", "="): 0x0424, ("H", "="): 0x0425, ("C", "="): 0x0426, ("C", "%"): 0x0427, ("S", "%"): 0x0428, ("S", "c"): 0x0429, ("=", '"'): 0x042A, ("Y", "="): 0x042B, ("%", '"'): 0x042C, ("J", "E"): 0x042D, ("J", "U"): 0x042E, ("J", "A"): 0x042F, ("a", "="): 0x0430, ("b", "="): 0x0431, ("v", "="): 0x0432, ("g", "="): 0x0433, ("d", "="): 0x0434, ("e", "="): 0x0435, ("z", "%"): 0x0436, ("z", "="): 0x0437, ("i", "="): 0x0438, ("j", "="): 0x0439, ("k", "="): 0x043A, ("l", "="): 0x043B, ("m", "="): 0x043C, ("n", "="): 0x043D, ("o", "="): 0x043E, ("p", "="): 0x043F, ("r", "="): 0x0440, ("s", "="): 0x0441, ("t", "="): 0x0442, ("u", "="): 0x0443, ("f", "="): 0x0444, ("h", "="): 0x0445, ("c", "="): 0x0446, ("c", "%"): 0x0447, ("s", "%"): 0x0448, ("s", "c"): 0x0449, ("=", "'"): 0x044A, ("y", "="): 0x044B, ("%", "'"): 0x044C, ("j", "e"): 0x044D, ("j", "u"): 0x044E, ("j", "a"): 0x044F, ("i", "o"): 0x0451, ("d", "%"): 0x0452, ("g", "%"): 0x0453, ("i", "e"): 0x0454, ("d", "s"): 0x0455, ("i", "i"): 0x0456, ("y", "i"): 0x0457, ("j", "%"): 0x0458, ("l", "j"): 0x0459, ("n", "j"): 0x045A, ("t", "s"): 0x045B, ("k", "j"): 0x045C, ("v", "%"): 0x045E, ("d", "z"): 0x045F, ("Y", "3"): 0x0462, ("y", "3"): 0x0463, ("O", "3"): 0x046A, ("o", "3"): 0x046B, ("F", "3"): 0x0472, ("f", "3"): 0x0473, ("V", "3"): 0x0474, ("v", "3"): 0x0475, ("C", "3"): 0x0480, ("c", "3"): 0x0481, ("G", "3"): 0x0490, ("g", "3"): 0x0491, ("A", "+"): 0x05D0, ("B", "+"): 0x05D1, ("G", "+"): 0x05D2, ("D", "+"): 0x05D3, ("H", "+"): 0x05D4, ("W", "+"): 0x05D5, ("Z", "+"): 0x05D6, ("X", "+"): 0x05D7, ("T", "j"): 0x05D8, ("J", "+"): 0x05D9, ("K", "%"): 0x05DA, ("K", "+"): 0x05DB, ("L", "+"): 0x05DC, ("M", "%"): 0x05DD, ("M", "+"): 0x05DE, ("N", "%"): 0x05DF, ("N", "+"): 0x05E0, ("S", "+"): 0x05E1, ("E", "+"): 0x05E2, ("P", "%"): 0x05E3, ("P", "+"): 0x05E4, ("Z", "j"): 0x05E5, ("Z", "J"): 0x05E6, ("Q", "+"): 0x05E7, ("R", "+"): 0x05E8, ("S", "h"): 0x05E9, ("T", "+"): 0x05EA, (",", "+"): 0x060C, (";", "+"): 0x061B, ("?", "+"): 0x061F, ("H", "'"): 0x0621, ("a", "M"): 0x0622, ("a", "H"): 0x0623, ("w", "H"): 0x0624, ("a", "h"): 0x0625, ("y", "H"): 0x0626, ("a", "+"): 0x0627, ("b", "+"): 0x0628, ("t", "m"): 0x0629, ("t", "+"): 0x062A, ("t", "k"): 0x062B, ("g", "+"): 0x062C, ("h", "k"): 0x062D, ("x", "+"): 0x062E, ("d", "+"): 0x062F, ("d", "k"): 0x0630, ("r", "+"): 0x0631, ("z", "+"): 0x0632, ("s", "+"): 0x0633, ("s", "n"): 0x0634, ("c", "+"): 0x0635, ("d", "d"): 0x0636, ("t", "j"): 0x0637, ("z", "H"): 0x0638, ("e", "+"): 0x0639, ("i", "+"): 0x063A, ("+", "+"): 0x0640, ("f", "+"): 0x0641, ("q", "+"): 0x0642, ("k", "+"): 0x0643, ("l", "+"): 0x0644, ("m", "+"): 0x0645, ("n", "+"): 0x0646, ("h", "+"): 0x0647, ("w", "+"): 0x0648, ("j", "+"): 0x0649, ("y", "+"): 0x064A, (":", "+"): 0x064B, ('"', "+"): 0x064C, ("=", "+"): 0x064D, ("/", "+"): 0x064E, ("'", "+"): 0x064F, ("1", "+"): 0x0650, ("3", "+"): 0x0651, ("0", "+"): 0x0652, ("a", "S"): 0x0670, ("p", "+"): 0x067E, ("v", "+"): 0x06A4, ("g", "f"): 0x06AF, ("0", "a"): 0x06F0, ("1", "a"): 0x06F1, ("2", "a"): 0x06F2, ("3", "a"): 0x06F3, ("4", "a"): 0x06F4, ("5", "a"): 0x06F5, ("6", "a"): 0x06F6, ("7", "a"): 0x06F7, ("8", "a"): 0x06F8, ("9", "a"): 0x06F9, ("B", "."): 0x1E02, ("b", "."): 0x1E03, ("B", "_"): 0x1E06, ("b", "_"): 0x1E07, ("D", "."): 0x1E0A, ("d", "."): 0x1E0B, ("D", "_"): 0x1E0E, ("d", "_"): 0x1E0F, ("D", ","): 0x1E10, ("d", ","): 0x1E11, ("F", "."): 0x1E1E, ("f", "."): 0x1E1F, ("G", "-"): 0x1E20, ("g", "-"): 0x1E21, ("H", "."): 0x1E22, ("h", "."): 0x1E23, ("H", ":"): 0x1E26, ("h", ":"): 0x1E27, ("H", ","): 0x1E28, ("h", ","): 0x1E29, ("K", "'"): 0x1E30, ("k", "'"): 0x1E31, ("K", "_"): 0x1E34, ("k", "_"): 0x1E35, ("L", "_"): 0x1E3A, ("l", "_"): 0x1E3B, ("M", "'"): 0x1E3E, ("m", "'"): 0x1E3F, ("M", "."): 0x1E40, ("m", "."): 0x1E41, ("N", "."): 0x1E44, ("n", "."): 0x1E45, ("N", "_"): 0x1E48, ("n", "_"): 0x1E49, ("P", "'"): 0x1E54, ("p", "'"): 0x1E55, ("P", "."): 0x1E56, ("p", "."): 0x1E57, ("R", "."): 0x1E58, ("r", "."): 0x1E59, ("R", "_"): 0x1E5E, ("r", "_"): 0x1E5F, ("S", "."): 0x1E60, ("s", "."): 0x1E61, ("T", "."): 0x1E6A, ("t", "."): 0x1E6B, ("T", "_"): 0x1E6E, ("t", "_"): 0x1E6F, ("V", "?"): 0x1E7C, ("v", "?"): 0x1E7D, ("W", "!"): 0x1E80, ("w", "!"): 0x1E81, ("W", "'"): 0x1E82, ("w", "'"): 0x1E83, ("W", ":"): 0x1E84, ("w", ":"): 0x1E85, ("W", "."): 0x1E86, ("w", "."): 0x1E87, ("X", "."): 0x1E8A, ("x", "."): 0x1E8B, ("X", ":"): 0x1E8C, ("x", ":"): 0x1E8D, ("Y", "."): 0x1E8E, ("y", "."): 0x1E8F, ("Z", ">"): 0x1E90, ("z", ">"): 0x1E91, ("Z", "_"): 0x1E94, ("z", "_"): 0x1E95, ("h", "_"): 0x1E96, ("t", ":"): 0x1E97, ("w", "0"): 0x1E98, ("y", "0"): 0x1E99, ("A", "2"): 0x1EA2, ("a", "2"): 0x1EA3, ("E", "2"): 0x1EBA, ("e", "2"): 0x1EBB, ("E", "?"): 0x1EBC, ("e", "?"): 0x1EBD, ("I", "2"): 0x1EC8, ("i", "2"): 0x1EC9, ("O", "2"): 0x1ECE, ("o", "2"): 0x1ECF, ("U", "2"): 0x1EE6, ("u", "2"): 0x1EE7, ("Y", "!"): 0x1EF2, ("y", "!"): 0x1EF3, ("Y", "2"): 0x1EF6, ("y", "2"): 0x1EF7, ("Y", "?"): 0x1EF8, ("y", "?"): 0x1EF9, (";", "'"): 0x1F00, (",", "'"): 0x1F01, (";", "!"): 0x1F02, (",", "!"): 0x1F03, ("?", ";"): 0x1F04, ("?", ","): 0x1F05, ("!", ":"): 0x1F06, ("?", ":"): 0x1F07, ("1", "N"): 0x2002, ("1", "M"): 0x2003, ("3", "M"): 0x2004, ("4", "M"): 0x2005, ("6", "M"): 0x2006, ("1", "T"): 0x2009, ("1", "H"): 0x200A, ("-", "1"): 0x2010, ("-", "N"): 0x2013, ("-", "M"): 0x2014, ("-", "3"): 0x2015, ("!", "2"): 0x2016, ("=", "2"): 0x2017, ("'", "6"): 0x2018, ("'", "9"): 0x2019, (".", "9"): 0x201A, ("9", "'"): 0x201B, ('"', "6"): 0x201C, ('"', "9"): 0x201D, (":", "9"): 0x201E, ("9", '"'): 0x201F, ("/", "-"): 0x2020, ("/", "="): 0x2021, (".", "."): 0x2025, ("%", "0"): 0x2030, ("1", "'"): 0x2032, ("2", "'"): 0x2033, ("3", "'"): 0x2034, ("1", '"'): 0x2035, ("2", '"'): 0x2036, ("3", '"'): 0x2037, ("C", "a"): 0x2038, ("<", "1"): 0x2039, (">", "1"): 0x203A, (":", "X"): 0x203B, ("'", "-"): 0x203E, ("/", "f"): 0x2044, ("0", "S"): 0x2070, ("4", "S"): 0x2074, ("5", "S"): 0x2075, ("6", "S"): 0x2076, ("7", "S"): 0x2077, ("8", "S"): 0x2078, ("9", "S"): 0x2079, ("+", "S"): 0x207A, ("-", "S"): 0x207B, ("=", "S"): 0x207C, ("(", "S"): 0x207D, (")", "S"): 0x207E, ("n", "S"): 0x207F, ("0", "s"): 0x2080, ("1", "s"): 0x2081, ("2", "s"): 0x2082, ("3", "s"): 0x2083, ("4", "s"): 0x2084, ("5", "s"): 0x2085, ("6", "s"): 0x2086, ("7", "s"): 0x2087, ("8", "s"): 0x2088, ("9", "s"): 0x2089, ("+", "s"): 0x208A, ("-", "s"): 0x208B, ("=", "s"): 0x208C, ("(", "s"): 0x208D, (")", "s"): 0x208E, ("L", "i"): 0x20A4, ("P", "t"): 0x20A7, ("W", "="): 0x20A9, ("=", "e"): 0x20AC, # euro ("E", "u"): 0x20AC, # euro ("=", "R"): 0x20BD, # rouble ("=", "P"): 0x20BD, # rouble ("o", "C"): 0x2103, ("c", "o"): 0x2105, ("o", "F"): 0x2109, ("N", "0"): 0x2116, ("P", "O"): 0x2117, ("R", "x"): 0x211E, ("S", "M"): 0x2120, ("T", "M"): 0x2122, ("O", "m"): 0x2126, ("A", "O"): 0x212B, ("1", "3"): 0x2153, ("2", "3"): 0x2154, ("1", "5"): 0x2155, ("2", "5"): 0x2156, ("3", "5"): 0x2157, ("4", "5"): 0x2158, ("1", "6"): 0x2159, ("5", "6"): 0x215A, ("1", "8"): 0x215B, ("3", "8"): 0x215C, ("5", "8"): 0x215D, ("7", "8"): 0x215E, ("1", "R"): 0x2160, ("2", "R"): 0x2161, ("3", "R"): 0x2162, ("4", "R"): 0x2163, ("5", "R"): 0x2164, ("6", "R"): 0x2165, ("7", "R"): 0x2166, ("8", "R"): 0x2167, ("9", "R"): 0x2168, ("a", "R"): 0x2169, ("b", "R"): 0x216A, ("c", "R"): 0x216B, ("1", "r"): 0x2170, ("2", "r"): 0x2171, ("3", "r"): 0x2172, ("4", "r"): 0x2173, ("5", "r"): 0x2174, ("6", "r"): 0x2175, ("7", "r"): 0x2176, ("8", "r"): 0x2177, ("9", "r"): 0x2178, ("a", "r"): 0x2179, ("b", "r"): 0x217A, ("c", "r"): 0x217B, ("<", "-"): 0x2190, ("-", "!"): 0x2191, ("-", ">"): 0x2192, ("-", "v"): 0x2193, ("<", ">"): 0x2194, ("U", "D"): 0x2195, ("<", "="): 0x21D0, ("=", ">"): 0x21D2, ("=", "="): 0x21D4, ("F", "A"): 0x2200, ("d", "P"): 0x2202, ("T", "E"): 0x2203, ("/", "0"): 0x2205, ("D", "E"): 0x2206, ("N", "B"): 0x2207, ("(", "-"): 0x2208, ("-", ")"): 0x220B, ("*", "P"): 0x220F, ("+", "Z"): 0x2211, ("-", "2"): 0x2212, ("-", "+"): 0x2213, ("*", "-"): 0x2217, ("O", "b"): 0x2218, ("S", "b"): 0x2219, ("R", "T"): 0x221A, ("0", "("): 0x221D, ("0", "0"): 0x221E, ("-", "L"): 0x221F, ("-", "V"): 0x2220, ("P", "P"): 0x2225, ("A", "N"): 0x2227, ("O", "R"): 0x2228, ("(", "U"): 0x2229, (")", "U"): 0x222A, ("I", "n"): 0x222B, ("D", "I"): 0x222C, ("I", "o"): 0x222E, (".", ":"): 0x2234, (":", "."): 0x2235, (":", "R"): 0x2236, (":", ":"): 0x2237, ("?", "1"): 0x223C, ("C", "G"): 0x223E, ("?", "-"): 0x2243, ("?", "="): 0x2245, ("?", "2"): 0x2248, ("=", "?"): 0x224C, ("H", "I"): 0x2253, ("!", "="): 0x2260, ("=", "3"): 0x2261, ("=", "<"): 0x2264, (">", "="): 0x2265, ("<", "*"): 0x226A, ("*", ">"): 0x226B, ("!", "<"): 0x226E, ("!", ">"): 0x226F, ("(", "C"): 0x2282, (")", "C"): 0x2283, ("(", "_"): 0x2286, (")", "_"): 0x2287, ("0", "."): 0x2299, ("0", "2"): 0x229A, ("-", "T"): 0x22A5, (".", "P"): 0x22C5, (":", "3"): 0x22EE, (".", "3"): 0x22EF, ("E", "h"): 0x2302, ("<", "7"): 0x2308, (">", "7"): 0x2309, ("7", "<"): 0x230A, ("7", ">"): 0x230B, ("N", "I"): 0x2310, ("(", "A"): 0x2312, ("T", "R"): 0x2315, ("I", "u"): 0x2320, ("I", "l"): 0x2321, ("<", "/"): 0x2329, ("/", ">"): 0x232A, ("V", "s"): 0x2423, ("1", "h"): 0x2440, ("3", "h"): 0x2441, ("2", "h"): 0x2442, ("4", "h"): 0x2443, ("1", "j"): 0x2446, ("2", "j"): 0x2447, ("3", "j"): 0x2448, ("4", "j"): 0x2449, ("1", "."): 0x2488, ("2", "."): 0x2489, ("3", "."): 0x248A, ("4", "."): 0x248B, ("5", "."): 0x248C, ("6", "."): 0x248D, ("7", "."): 0x248E, ("8", "."): 0x248F, ("9", "."): 0x2490, ("h", "h"): 0x2500, ("H", "H"): 0x2501, ("v", "v"): 0x2502, ("V", "V"): 0x2503, ("3", "-"): 0x2504, ("3", "_"): 0x2505, ("3", "!"): 0x2506, ("3", "/"): 0x2507, ("4", "-"): 0x2508, ("4", "_"): 0x2509, ("4", "!"): 0x250A, ("4", "/"): 0x250B, ("d", "r"): 0x250C, ("d", "R"): 0x250D, ("D", "r"): 0x250E, ("D", "R"): 0x250F, ("d", "l"): 0x2510, ("d", "L"): 0x2511, ("D", "l"): 0x2512, ("L", "D"): 0x2513, ("u", "r"): 0x2514, ("u", "R"): 0x2515, ("U", "r"): 0x2516, ("U", "R"): 0x2517, ("u", "l"): 0x2518, ("u", "L"): 0x2519, ("U", "l"): 0x251A, ("U", "L"): 0x251B, ("v", "r"): 0x251C, ("v", "R"): 0x251D, ("V", "r"): 0x2520, ("V", "R"): 0x2523, ("v", "l"): 0x2524, ("v", "L"): 0x2525, ("V", "l"): 0x2528, ("V", "L"): 0x252B, ("d", "h"): 0x252C, ("d", "H"): 0x252F, ("D", "h"): 0x2530, ("D", "H"): 0x2533, ("u", "h"): 0x2534, ("u", "H"): 0x2537, ("U", "h"): 0x2538, ("U", "H"): 0x253B, ("v", "h"): 0x253C, ("v", "H"): 0x253F, ("V", "h"): 0x2542, ("V", "H"): 0x254B, ("F", "D"): 0x2571, ("B", "D"): 0x2572, ("T", "B"): 0x2580, ("L", "B"): 0x2584, ("F", "B"): 0x2588, ("l", "B"): 0x258C, ("R", "B"): 0x2590, (".", "S"): 0x2591, (":", "S"): 0x2592, ("?", "S"): 0x2593, ("f", "S"): 0x25A0, ("O", "S"): 0x25A1, ("R", "O"): 0x25A2, ("R", "r"): 0x25A3, ("R", "F"): 0x25A4, ("R", "Y"): 0x25A5, ("R", "H"): 0x25A6, ("R", "Z"): 0x25A7, ("R", "K"): 0x25A8, ("R", "X"): 0x25A9, ("s", "B"): 0x25AA, ("S", "R"): 0x25AC, ("O", "r"): 0x25AD, ("U", "T"): 0x25B2, ("u", "T"): 0x25B3, ("P", "R"): 0x25B6, ("T", "r"): 0x25B7, ("D", "t"): 0x25BC, ("d", "T"): 0x25BD, ("P", "L"): 0x25C0, ("T", "l"): 0x25C1, ("D", "b"): 0x25C6, ("D", "w"): 0x25C7, ("L", "Z"): 0x25CA, ("0", "m"): 0x25CB, ("0", "o"): 0x25CE, ("0", "M"): 0x25CF, ("0", "L"): 0x25D0, ("0", "R"): 0x25D1, ("S", "n"): 0x25D8, ("I", "c"): 0x25D9, ("F", "d"): 0x25E2, ("B", "d"): 0x25E3, ("*", "2"): 0x2605, ("*", "1"): 0x2606, ("<", "H"): 0x261C, (">", "H"): 0x261E, ("0", "u"): 0x263A, ("0", "U"): 0x263B, ("S", "U"): 0x263C, ("F", "m"): 0x2640, ("M", "l"): 0x2642, ("c", "S"): 0x2660, ("c", "H"): 0x2661, ("c", "D"): 0x2662, ("c", "C"): 0x2663, ("M", "d"): 0x2669, ("M", "8"): 0x266A, ("M", "2"): 0x266B, ("M", "b"): 0x266D, ("M", "x"): 0x266E, ("M", "X"): 0x266F, ("O", "K"): 0x2713, ("X", "X"): 0x2717, ("-", "X"): 0x2720, ("I", "S"): 0x3000, (",", "_"): 0x3001, (".", "_"): 0x3002, ("+", '"'): 0x3003, ("+", "_"): 0x3004, ("*", "_"): 0x3005, (";", "_"): 0x3006, ("0", "_"): 0x3007, ("<", "+"): 0x300A, (">", "+"): 0x300B, ("<", "'"): 0x300C, (">", "'"): 0x300D, ("<", '"'): 0x300E, (">", '"'): 0x300F, ("(", '"'): 0x3010, (")", '"'): 0x3011, ("=", "T"): 0x3012, ("=", "_"): 0x3013, ("(", "'"): 0x3014, (")", "'"): 0x3015, ("(", "I"): 0x3016, (")", "I"): 0x3017, ("-", "?"): 0x301C, ("A", "5"): 0x3041, ("a", "5"): 0x3042, ("I", "5"): 0x3043, ("i", "5"): 0x3044, ("U", "5"): 0x3045, ("u", "5"): 0x3046, ("E", "5"): 0x3047, ("e", "5"): 0x3048, ("O", "5"): 0x3049, ("o", "5"): 0x304A, ("k", "a"): 0x304B, ("g", "a"): 0x304C, ("k", "i"): 0x304D, ("g", "i"): 0x304E, ("k", "u"): 0x304F, ("g", "u"): 0x3050, ("k", "e"): 0x3051, ("g", "e"): 0x3052, ("k", "o"): 0x3053, ("g", "o"): 0x3054, ("s", "a"): 0x3055, ("z", "a"): 0x3056, ("s", "i"): 0x3057, ("z", "i"): 0x3058, ("s", "u"): 0x3059, ("z", "u"): 0x305A, ("s", "e"): 0x305B, ("z", "e"): 0x305C, ("s", "o"): 0x305D, ("z", "o"): 0x305E, ("t", "a"): 0x305F, ("d", "a"): 0x3060, ("t", "i"): 0x3061, ("d", "i"): 0x3062, ("t", "U"): 0x3063, ("t", "u"): 0x3064, ("d", "u"): 0x3065, ("t", "e"): 0x3066, ("d", "e"): 0x3067, ("t", "o"): 0x3068, ("d", "o"): 0x3069, ("n", "a"): 0x306A, ("n", "i"): 0x306B, ("n", "u"): 0x306C, ("n", "e"): 0x306D, ("n", "o"): 0x306E, ("h", "a"): 0x306F, ("b", "a"): 0x3070, ("p", "a"): 0x3071, ("h", "i"): 0x3072, ("b", "i"): 0x3073, ("p", "i"): 0x3074, ("h", "u"): 0x3075, ("b", "u"): 0x3076, ("p", "u"): 0x3077, ("h", "e"): 0x3078, ("b", "e"): 0x3079, ("p", "e"): 0x307A, ("h", "o"): 0x307B, ("b", "o"): 0x307C, ("p", "o"): 0x307D, ("m", "a"): 0x307E, ("m", "i"): 0x307F, ("m", "u"): 0x3080, ("m", "e"): 0x3081, ("m", "o"): 0x3082, ("y", "A"): 0x3083, ("y", "a"): 0x3084, ("y", "U"): 0x3085, ("y", "u"): 0x3086, ("y", "O"): 0x3087, ("y", "o"): 0x3088, ("r", "a"): 0x3089, ("r", "i"): 0x308A, ("r", "u"): 0x308B, ("r", "e"): 0x308C, ("r", "o"): 0x308D, ("w", "A"): 0x308E, ("w", "a"): 0x308F, ("w", "i"): 0x3090, ("w", "e"): 0x3091, ("w", "o"): 0x3092, ("n", "5"): 0x3093, ("v", "u"): 0x3094, ('"', "5"): 0x309B, ("0", "5"): 0x309C, ("*", "5"): 0x309D, ("+", "5"): 0x309E, ("a", "6"): 0x30A1, ("A", "6"): 0x30A2, ("i", "6"): 0x30A3, ("I", "6"): 0x30A4, ("u", "6"): 0x30A5, ("U", "6"): 0x30A6, ("e", "6"): 0x30A7, ("E", "6"): 0x30A8, ("o", "6"): 0x30A9, ("O", "6"): 0x30AA, ("K", "a"): 0x30AB, ("G", "a"): 0x30AC, ("K", "i"): 0x30AD, ("G", "i"): 0x30AE, ("K", "u"): 0x30AF, ("G", "u"): 0x30B0, ("K", "e"): 0x30B1, ("G", "e"): 0x30B2, ("K", "o"): 0x30B3, ("G", "o"): 0x30B4, ("S", "a"): 0x30B5, ("Z", "a"): 0x30B6, ("S", "i"): 0x30B7, ("Z", "i"): 0x30B8, ("S", "u"): 0x30B9, ("Z", "u"): 0x30BA, ("S", "e"): 0x30BB, ("Z", "e"): 0x30BC, ("S", "o"): 0x30BD, ("Z", "o"): 0x30BE, ("T", "a"): 0x30BF, ("D", "a"): 0x30C0, ("T", "i"): 0x30C1, ("D", "i"): 0x30C2, ("T", "U"): 0x30C3, ("T", "u"): 0x30C4, ("D", "u"): 0x30C5, ("T", "e"): 0x30C6, ("D", "e"): 0x30C7, ("T", "o"): 0x30C8, ("D", "o"): 0x30C9, ("N", "a"): 0x30CA, ("N", "i"): 0x30CB, ("N", "u"): 0x30CC, ("N", "e"): 0x30CD, ("N", "o"): 0x30CE, ("H", "a"): 0x30CF, ("B", "a"): 0x30D0, ("P", "a"): 0x30D1, ("H", "i"): 0x30D2, ("B", "i"): 0x30D3, ("P", "i"): 0x30D4, ("H", "u"): 0x30D5, ("B", "u"): 0x30D6, ("P", "u"): 0x30D7, ("H", "e"): 0x30D8, ("B", "e"): 0x30D9, ("P", "e"): 0x30DA, ("H", "o"): 0x30DB, ("B", "o"): 0x30DC, ("P", "o"): 0x30DD, ("M", "a"): 0x30DE, ("M", "i"): 0x30DF, ("M", "u"): 0x30E0, ("M", "e"): 0x30E1, ("M", "o"): 0x30E2, ("Y", "A"): 0x30E3, ("Y", "a"): 0x30E4, ("Y", "U"): 0x30E5, ("Y", "u"): 0x30E6, ("Y", "O"): 0x30E7, ("Y", "o"): 0x30E8, ("R", "a"): 0x30E9, ("R", "i"): 0x30EA, ("R", "u"): 0x30EB, ("R", "e"): 0x30EC, ("R", "o"): 0x30ED, ("W", "A"): 0x30EE, ("W", "a"): 0x30EF, ("W", "i"): 0x30F0, ("W", "e"): 0x30F1, ("W", "o"): 0x30F2, ("N", "6"): 0x30F3, ("V", "u"): 0x30F4, ("K", "A"): 0x30F5, ("K", "E"): 0x30F6, ("V", "a"): 0x30F7, ("V", "i"): 0x30F8, ("V", "e"): 0x30F9, ("V", "o"): 0x30FA, (".", "6"): 0x30FB, ("-", "6"): 0x30FC, ("*", "6"): 0x30FD, ("+", "6"): 0x30FE, ("b", "4"): 0x3105, ("p", "4"): 0x3106, ("m", "4"): 0x3107, ("f", "4"): 0x3108, ("d", "4"): 0x3109, ("t", "4"): 0x310A, ("n", "4"): 0x310B, ("l", "4"): 0x310C, ("g", "4"): 0x310D, ("k", "4"): 0x310E, ("h", "4"): 0x310F, ("j", "4"): 0x3110, ("q", "4"): 0x3111, ("x", "4"): 0x3112, ("z", "h"): 0x3113, ("c", "h"): 0x3114, ("s", "h"): 0x3115, ("r", "4"): 0x3116, ("z", "4"): 0x3117, ("c", "4"): 0x3118, ("s", "4"): 0x3119, ("a", "4"): 0x311A, ("o", "4"): 0x311B, ("e", "4"): 0x311C, ("a", "i"): 0x311E, ("e", "i"): 0x311F, ("a", "u"): 0x3120, ("o", "u"): 0x3121, ("a", "n"): 0x3122, ("e", "n"): 0x3123, ("a", "N"): 0x3124, ("e", "N"): 0x3125, ("e", "r"): 0x3126, ("i", "4"): 0x3127, ("u", "4"): 0x3128, ("i", "u"): 0x3129, ("v", "4"): 0x312A, ("n", "G"): 0x312B, ("g", "n"): 0x312C, ("1", "c"): 0x3220, ("2", "c"): 0x3221, ("3", "c"): 0x3222, ("4", "c"): 0x3223, ("5", "c"): 0x3224, ("6", "c"): 0x3225, ("7", "c"): 0x3226, ("8", "c"): 0x3227, ("9", "c"): 0x3228, # code points 0xe000 - 0xefff excluded, they have no assigned # characters, only used in proposals. ("f", "f"): 0xFB00, ("f", "i"): 0xFB01, ("f", "l"): 0xFB02, ("f", "t"): 0xFB05, ("s", "t"): 0xFB06, # Vim 5.x compatible digraphs that don't conflict with the above ("~", "!"): 161, ("c", "|"): 162, ("$", "$"): 163, ("o", "x"): 164, # currency symbol in ISO 8859-1 ("Y", "-"): 165, ("|", "|"): 166, ("c", "O"): 169, ("-", ","): 172, ("-", "="): 175, ("~", "o"): 176, ("2", "2"): 178, ("3", "3"): 179, ("p", "p"): 182, ("~", "."): 183, ("1", "1"): 185, ("~", "?"): 191, ("A", "`"): 192, ("A", "^"): 194, ("A", "~"): 195, ("A", '"'): 196, ("A", "@"): 197, ("E", "`"): 200, ("E", "^"): 202, ("E", '"'): 203, ("I", "`"): 204, ("I", "^"): 206, ("I", '"'): 207, ("N", "~"): 209, ("O", "`"): 210, ("O", "^"): 212, ("O", "~"): 213, ("/", "\\"): 215, # multiplication symbol in ISO 8859-1 ("U", "`"): 217, ("U", "^"): 219, ("I", "p"): 222, ("a", "`"): 224, ("a", "^"): 226, ("a", "~"): 227, ("a", '"'): 228, ("a", "@"): 229, ("e", "`"): 232, ("e", "^"): 234, ("e", '"'): 235, ("i", "`"): 236, ("i", "^"): 238, ("n", "~"): 241, ("o", "`"): 242, ("o", "^"): 244, ("o", "~"): 245, ("u", "`"): 249, ("u", "^"): 251, ("y", '"'): 255, } ================================================ FILE: src/prompt_toolkit/key_binding/emacs_state.py ================================================ from __future__ import annotations from .key_processor import KeyPress __all__ = [ "EmacsState", ] class EmacsState: """ Mutable class to hold Emacs specific state. """ def __init__(self) -> None: # Simple macro recording. (Like Readline does.) # (For Emacs mode.) self.macro: list[KeyPress] | None = [] self.current_recording: list[KeyPress] | None = None def reset(self) -> None: self.current_recording = None @property def is_recording(self) -> bool: "Tell whether we are recording a macro." return self.current_recording is not None def start_macro(self) -> None: "Start recording macro." self.current_recording = [] def end_macro(self) -> None: "End recording macro." self.macro = self.current_recording self.current_recording = None ================================================ FILE: src/prompt_toolkit/key_binding/key_bindings.py ================================================ """ Key bindings registry. A `KeyBindings` object is a container that holds a list of key bindings. It has a very efficient internal data structure for checking which key bindings apply for a pressed key. Typical usage:: kb = KeyBindings() @kb.add(Keys.ControlX, Keys.ControlC, filter=INSERT) def handler(event): # Handle ControlX-ControlC key sequence. pass It is also possible to combine multiple KeyBindings objects. We do this in the default key bindings. There are some KeyBindings objects that contain the Emacs bindings, while others contain the Vi bindings. They are merged together using `merge_key_bindings`. We also have a `ConditionalKeyBindings` object that can enable/disable a group of key bindings at once. It is also possible to add a filter to a function, before a key binding has been assigned, through the `key_binding` decorator.:: # First define a key handler with the `filter`. @key_binding(filter=condition) def my_key_binding(event): ... # Later, add it to the key bindings. kb.add(Keys.A, my_key_binding) """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable, Coroutine, Hashable, Sequence from inspect import isawaitable from typing import ( TYPE_CHECKING, Any, TypeVar, Union, cast, ) from prompt_toolkit.cache import SimpleCache from prompt_toolkit.filters import FilterOrBool, Never, to_filter from prompt_toolkit.keys import KEY_ALIASES, Keys if TYPE_CHECKING: # Avoid circular imports. from .key_processor import KeyPressEvent # The only two return values for a mouse handler (and key bindings) are # `None` and `NotImplemented`. For the type checker it's best to annotate # this as `object`. (The consumer never expects a more specific instance: # checking for NotImplemented can be done using `is NotImplemented`.) NotImplementedOrNone = object # Other non-working options are: # * Optional[Literal[NotImplemented]] # --> Doesn't work, Literal can't take an Any. # * None # --> Doesn't work. We can't assign the result of a function that # returns `None` to a variable. # * Any # --> Works, but too broad. __all__ = [ "NotImplementedOrNone", "Binding", "KeyBindingsBase", "KeyBindings", "ConditionalKeyBindings", "merge_key_bindings", "DynamicKeyBindings", "GlobalOnlyKeyBindings", ] # Key bindings can be regular functions or coroutines. # In both cases, if they return `NotImplemented`, the UI won't be invalidated. # This is mainly used in case of mouse move events, to prevent excessive # repainting during mouse move events. KeyHandlerCallable = Callable[ ["KeyPressEvent"], Union["NotImplementedOrNone", Coroutine[Any, Any, "NotImplementedOrNone"]], ] class Binding: """ Key binding: (key sequence + handler + filter). (Immutable binding class.) :param record_in_macro: When True, don't record this key binding when a macro is recorded. """ def __init__( self, keys: tuple[Keys | str, ...], handler: KeyHandlerCallable, filter: FilterOrBool = True, eager: FilterOrBool = False, is_global: FilterOrBool = False, save_before: Callable[[KeyPressEvent], bool] = (lambda e: True), record_in_macro: FilterOrBool = True, ) -> None: self.keys = keys self.handler = handler self.filter = to_filter(filter) self.eager = to_filter(eager) self.is_global = to_filter(is_global) self.save_before = save_before self.record_in_macro = to_filter(record_in_macro) def call(self, event: KeyPressEvent) -> None: result = self.handler(event) # If the handler is a coroutine, create an asyncio task. if isawaitable(result): awaitable = cast(Coroutine[Any, Any, "NotImplementedOrNone"], result) async def bg_task() -> None: result = await awaitable if result != NotImplemented: event.app.invalidate() event.app.create_background_task(bg_task()) elif result != NotImplemented: event.app.invalidate() def __repr__(self) -> str: return ( f"{self.__class__.__name__}(keys={self.keys!r}, handler={self.handler!r})" ) # Sequence of keys presses. KeysTuple = tuple[Keys | str, ...] class KeyBindingsBase(metaclass=ABCMeta): """ Interface for a KeyBindings. """ @property @abstractmethod def _version(self) -> Hashable: """ For cache invalidation. - This should increase every time that something changes. """ return 0 @abstractmethod def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]: """ Return a list of key bindings that can handle these keys. (This return also inactive bindings, so the `filter` still has to be called, for checking it.) :param keys: tuple of keys. """ return [] @abstractmethod def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]: """ Return a list of key bindings that handle a key sequence starting with `keys`. (It does only return bindings for which the sequences are longer than `keys`. And like `get_bindings_for_keys`, it also includes inactive bindings.) :param keys: tuple of keys. """ return [] @property @abstractmethod def bindings(self) -> list[Binding]: """ List of `Binding` objects. (These need to be exposed, so that `KeyBindings` objects can be merged together.) """ return [] # `add` and `remove` don't have to be part of this interface. T = TypeVar("T", bound=KeyHandlerCallable | Binding) class KeyBindings(KeyBindingsBase): """ A container for a set of key bindings. Example usage:: kb = KeyBindings() @kb.add('c-t') def _(event): print('Control-T pressed') @kb.add('c-a', 'c-b') def _(event): print('Control-A pressed, followed by Control-B') @kb.add('c-x', filter=is_searching) def _(event): print('Control-X pressed') # Works only if we are searching. """ def __init__(self) -> None: self._bindings: list[Binding] = [] self._get_bindings_for_keys_cache: SimpleCache[KeysTuple, list[Binding]] = ( SimpleCache(maxsize=10000) ) self._get_bindings_starting_with_keys_cache: SimpleCache[ KeysTuple, list[Binding] ] = SimpleCache(maxsize=1000) self.__version = 0 # For cache invalidation. def _clear_cache(self) -> None: self.__version += 1 self._get_bindings_for_keys_cache.clear() self._get_bindings_starting_with_keys_cache.clear() @property def bindings(self) -> list[Binding]: return self._bindings @property def _version(self) -> Hashable: return self.__version def add( self, *keys: Keys | str, filter: FilterOrBool = True, eager: FilterOrBool = False, is_global: FilterOrBool = False, save_before: Callable[[KeyPressEvent], bool] = (lambda e: True), record_in_macro: FilterOrBool = True, ) -> Callable[[T], T]: """ Decorator for adding a key bindings. :param filter: :class:`~prompt_toolkit.filters.Filter` to determine when this key binding is active. :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`. When True, ignore potential longer matches when this key binding is hit. E.g. when there is an active eager key binding for Ctrl-X, execute the handler immediately and ignore the key binding for Ctrl-X Ctrl-E of which it is a prefix. :param is_global: When this key bindings is added to a `Container` or `Control`, make it a global (always active) binding. :param save_before: Callable that takes an `Event` and returns True if we should save the current buffer, before handling the event. (That's the default.) :param record_in_macro: Record these key bindings when a macro is being recorded. (True by default.) """ assert keys keys = tuple(_parse_key(k) for k in keys) if isinstance(filter, Never): # When a filter is Never, it will always stay disabled, so in that # case don't bother putting it in the key bindings. It will slow # down every key press otherwise. def decorator(func: T) -> T: return func else: def decorator(func: T) -> T: if isinstance(func, Binding): # We're adding an existing Binding object. self.bindings.append( Binding( keys, func.handler, filter=func.filter & to_filter(filter), eager=to_filter(eager) | func.eager, is_global=to_filter(is_global) | func.is_global, save_before=func.save_before, record_in_macro=func.record_in_macro, ) ) else: self.bindings.append( Binding( keys, cast(KeyHandlerCallable, func), filter=filter, eager=eager, is_global=is_global, save_before=save_before, record_in_macro=record_in_macro, ) ) self._clear_cache() return func return decorator def remove(self, *args: Keys | str | KeyHandlerCallable) -> None: """ Remove a key binding. This expects either a function that was given to `add` method as parameter or a sequence of key bindings. Raises `ValueError` when no bindings was found. Usage:: remove(handler) # Pass handler. remove('c-x', 'c-a') # Or pass the key bindings. """ found = False if callable(args[0]): assert len(args) == 1 function = args[0] # Remove the given function. for b in self.bindings: if b.handler == function: self.bindings.remove(b) found = True else: assert len(args) > 0 args = cast(tuple[Keys | str], args) # Remove this sequence of key bindings. keys = tuple(_parse_key(k) for k in args) for b in self.bindings: if b.keys == keys: self.bindings.remove(b) found = True if found: self._clear_cache() else: # No key binding found for this function. Raise ValueError. raise ValueError(f"Binding not found: {function!r}") # For backwards-compatibility. add_binding = add remove_binding = remove def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]: """ Return a list of key bindings that can handle this key. (This return also inactive bindings, so the `filter` still has to be called, for checking it.) :param keys: tuple of keys. """ def get() -> list[Binding]: result: list[tuple[int, Binding]] = [] for b in self.bindings: if len(keys) == len(b.keys): match = True any_count = 0 for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if i == Keys.Any: any_count += 1 if match: result.append((any_count, b)) # Place bindings that have more 'Any' occurrences in them at the end. result = sorted(result, key=lambda item: -item[0]) return [item[1] for item in result] return self._get_bindings_for_keys_cache.get(keys, get) def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]: """ Return a list of key bindings that handle a key sequence starting with `keys`. (It does only return bindings for which the sequences are longer than `keys`. And like `get_bindings_for_keys`, it also includes inactive bindings.) :param keys: tuple of keys. """ def get() -> list[Binding]: result = [] for b in self.bindings: if len(keys) < len(b.keys): match = True for i, j in zip(b.keys, keys): if i != j and i != Keys.Any: match = False break if match: result.append(b) return result return self._get_bindings_starting_with_keys_cache.get(keys, get) def _parse_key(key: Keys | str) -> str | Keys: """ Replace key by alias and verify whether it's a valid one. """ # Already a parse key? -> Return it. if isinstance(key, Keys): return key # Lookup aliases. key = KEY_ALIASES.get(key, key) # Replace 'space' by ' ' if key == "space": key = " " # Return as `Key` object when it's a special key. try: return Keys(key) except ValueError: pass # Final validation. if len(key) != 1: raise ValueError(f"Invalid key: {key}") return key def key_binding( filter: FilterOrBool = True, eager: FilterOrBool = False, is_global: FilterOrBool = False, save_before: Callable[[KeyPressEvent], bool] = (lambda event: True), record_in_macro: FilterOrBool = True, ) -> Callable[[KeyHandlerCallable], Binding]: """ Decorator that turn a function into a `Binding` object. This can be added to a `KeyBindings` object when a key binding is assigned. """ assert save_before is None or callable(save_before) filter = to_filter(filter) eager = to_filter(eager) is_global = to_filter(is_global) save_before = save_before record_in_macro = to_filter(record_in_macro) keys = () def decorator(function: KeyHandlerCallable) -> Binding: return Binding( keys, function, filter=filter, eager=eager, is_global=is_global, save_before=save_before, record_in_macro=record_in_macro, ) return decorator class _Proxy(KeyBindingsBase): """ Common part for ConditionalKeyBindings and _MergedKeyBindings. """ def __init__(self) -> None: # `KeyBindings` to be synchronized with all the others. self._bindings2: KeyBindingsBase = KeyBindings() self._last_version: Hashable = () def _update_cache(self) -> None: """ If `self._last_version` is outdated, then this should update the version and `self._bindings2`. """ raise NotImplementedError # Proxy methods to self._bindings2. @property def bindings(self) -> list[Binding]: self._update_cache() return self._bindings2.bindings @property def _version(self) -> Hashable: self._update_cache() return self._last_version def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]: self._update_cache() return self._bindings2.get_bindings_for_keys(keys) def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]: self._update_cache() return self._bindings2.get_bindings_starting_with_keys(keys) class ConditionalKeyBindings(_Proxy): """ Wraps around a `KeyBindings`. Disable/enable all the key bindings according to the given (additional) filter.:: @Condition def setting_is_true(): return True # or False registry = ConditionalKeyBindings(key_bindings, setting_is_true) When new key bindings are added to this object. They are also enable/disabled according to the given `filter`. :param registries: List of :class:`.KeyBindings` objects. :param filter: :class:`~prompt_toolkit.filters.Filter` object. """ def __init__( self, key_bindings: KeyBindingsBase, filter: FilterOrBool = True ) -> None: _Proxy.__init__(self) self.key_bindings = key_bindings self.filter = to_filter(filter) def _update_cache(self) -> None: "If the original key bindings was changed. Update our copy version." expected_version = self.key_bindings._version if self._last_version != expected_version: bindings2 = KeyBindings() # Copy all bindings from `self.key_bindings`, adding our condition. for b in self.key_bindings.bindings: bindings2.bindings.append( Binding( keys=b.keys, handler=b.handler, filter=self.filter & b.filter, eager=b.eager, is_global=b.is_global, save_before=b.save_before, record_in_macro=b.record_in_macro, ) ) self._bindings2 = bindings2 self._last_version = expected_version class _MergedKeyBindings(_Proxy): """ Merge multiple registries of key bindings into one. This class acts as a proxy to multiple :class:`.KeyBindings` objects, but behaves as if this is just one bigger :class:`.KeyBindings`. :param registries: List of :class:`.KeyBindings` objects. """ def __init__(self, registries: Sequence[KeyBindingsBase]) -> None: _Proxy.__init__(self) self.registries = registries def _update_cache(self) -> None: """ If one of the original registries was changed. Update our merged version. """ expected_version = tuple(r._version for r in self.registries) if self._last_version != expected_version: bindings2 = KeyBindings() for reg in self.registries: bindings2.bindings.extend(reg.bindings) self._bindings2 = bindings2 self._last_version = expected_version def merge_key_bindings(bindings: Sequence[KeyBindingsBase]) -> _MergedKeyBindings: """ Merge multiple :class:`.Keybinding` objects together. Usage:: bindings = merge_key_bindings([bindings1, bindings2, ...]) """ return _MergedKeyBindings(bindings) class DynamicKeyBindings(_Proxy): """ KeyBindings class that can dynamically returns any KeyBindings. :param get_key_bindings: Callable that returns a :class:`.KeyBindings` instance. """ def __init__(self, get_key_bindings: Callable[[], KeyBindingsBase | None]) -> None: self.get_key_bindings = get_key_bindings self.__version = 0 self._last_child_version = None self._dummy = KeyBindings() # Empty key bindings. def _update_cache(self) -> None: key_bindings = self.get_key_bindings() or self._dummy assert isinstance(key_bindings, KeyBindingsBase) version = id(key_bindings), key_bindings._version self._bindings2 = key_bindings self._last_version = version class GlobalOnlyKeyBindings(_Proxy): """ Wrapper around a :class:`.KeyBindings` object that only exposes the global key bindings. """ def __init__(self, key_bindings: KeyBindingsBase) -> None: _Proxy.__init__(self) self.key_bindings = key_bindings def _update_cache(self) -> None: """ If one of the original registries was changed. Update our merged version. """ expected_version = self.key_bindings._version if self._last_version != expected_version: bindings2 = KeyBindings() for b in self.key_bindings.bindings: if b.is_global(): bindings2.bindings.append(b) self._bindings2 = bindings2 self._last_version = expected_version ================================================ FILE: src/prompt_toolkit/key_binding/key_processor.py ================================================ """ An :class:`~.KeyProcessor` receives callbacks for the keystrokes parsed from the input in the :class:`~prompt_toolkit.inputstream.InputStream` instance. The `KeyProcessor` will according to the implemented keybindings call the correct callbacks when new key presses are feed through `feed`. """ from __future__ import annotations import weakref from asyncio import Task, sleep from collections import deque from collections.abc import Generator from typing import TYPE_CHECKING, Any from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import EditingMode from prompt_toolkit.filters.app import vi_navigation_mode from prompt_toolkit.keys import Keys from prompt_toolkit.utils import Event from .key_bindings import Binding, KeyBindingsBase if TYPE_CHECKING: from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer __all__ = [ "KeyProcessor", "KeyPress", "KeyPressEvent", ] class KeyPress: """ :param key: A `Keys` instance or text (one character). :param data: The received string on stdin. (Often vt100 escape codes.) """ def __init__(self, key: Keys | str, data: str | None = None) -> None: assert isinstance(key, Keys) or len(key) == 1 if data is None: if isinstance(key, Keys): data = key.value else: data = key # 'key' is a one character string. self.key = key self.data = data def __repr__(self) -> str: return f"{self.__class__.__name__}(key={self.key!r}, data={self.data!r})" def __eq__(self, other: object) -> bool: if not isinstance(other, KeyPress): return False return self.key == other.key and self.data == other.data """ Helper object to indicate flush operation in the KeyProcessor. NOTE: the implementation is very similar to the VT100 parser. """ _Flush = KeyPress("?", data="_Flush") class KeyProcessor: """ Statemachine that receives :class:`KeyPress` instances and according to the key bindings in the given :class:`KeyBindings`, calls the matching handlers. :: p = KeyProcessor(key_bindings) # Send keys into the processor. p.feed(KeyPress(Keys.ControlX, '\x18')) p.feed(KeyPress(Keys.ControlC, '\x03') # Process all the keys in the queue. p.process_keys() # Now the ControlX-ControlC callback will be called if this sequence is # registered in the key bindings. :param key_bindings: `KeyBindingsBase` instance. """ def __init__(self, key_bindings: KeyBindingsBase) -> None: self._bindings = key_bindings self.before_key_press = Event(self) self.after_key_press = Event(self) self._flush_wait_task: Task[None] | None = None self.reset() def reset(self) -> None: self._previous_key_sequence: list[KeyPress] = [] self._previous_handler: Binding | None = None # The queue of keys not yet send to our _process generator/state machine. self.input_queue: deque[KeyPress] = deque() # The key buffer that is matched in the generator state machine. # (This is at at most the amount of keys that make up for one key binding.) self.key_buffer: list[KeyPress] = [] #: Readline argument (for repetition of commands.) #: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html self.arg: str | None = None # Start the processor coroutine. self._process_coroutine = self._process() self._process_coroutine.send(None) # type: ignore def _get_matches(self, key_presses: list[KeyPress]) -> list[Binding]: """ For a list of :class:`KeyPress` instances. Give the matching handlers that would handle this. """ keys = tuple(k.key for k in key_presses) # Try match, with mode flag return [b for b in self._bindings.get_bindings_for_keys(keys) if b.filter()] def _is_prefix_of_longer_match(self, key_presses: list[KeyPress]) -> bool: """ For a list of :class:`KeyPress` instances. Return True if there is any handler that is bound to a suffix of this keys. """ keys = tuple(k.key for k in key_presses) # Get the filters for all the key bindings that have a longer match. # Note that we transform it into a `set`, because we don't care about # the actual bindings and executing it more than once doesn't make # sense. (Many key bindings share the same filter.) filters = { b.filter for b in self._bindings.get_bindings_starting_with_keys(keys) } # When any key binding is active, return True. return any(f() for f in filters) def _process(self) -> Generator[None, KeyPress, None]: """ Coroutine implementing the key match algorithm. Key strokes are sent into this generator, and it calls the appropriate handlers. """ buffer = self.key_buffer retry = False while True: flush = False if retry: retry = False else: key = yield if key is _Flush: flush = True else: buffer.append(key) # If we have some key presses, check for matches. if buffer: matches = self._get_matches(buffer) if flush: is_prefix_of_longer_match = False else: is_prefix_of_longer_match = self._is_prefix_of_longer_match(buffer) # When eager matches were found, give priority to them and also # ignore all the longer matches. eager_matches = [m for m in matches if m.eager()] if eager_matches: matches = eager_matches is_prefix_of_longer_match = False # Exact matches found, call handler. if not is_prefix_of_longer_match and matches: self._call_handler(matches[-1], key_sequence=buffer[:]) del buffer[:] # Keep reference. # No match found. elif not is_prefix_of_longer_match and not matches: retry = True found = False # Loop over the input, try longest match first and shift. for i in range(len(buffer), 0, -1): matches = self._get_matches(buffer[:i]) if matches: self._call_handler(matches[-1], key_sequence=buffer[:i]) del buffer[:i] found = True break if not found: del buffer[:1] def feed(self, key_press: KeyPress, first: bool = False) -> None: """ Add a new :class:`KeyPress` to the input queue. (Don't forget to call `process_keys` in order to process the queue.) :param first: If true, insert before everything else. """ if first: self.input_queue.appendleft(key_press) else: self.input_queue.append(key_press) def feed_multiple(self, key_presses: list[KeyPress], first: bool = False) -> None: """ :param first: If true, insert before everything else. """ if first: self.input_queue.extendleft(reversed(key_presses)) else: self.input_queue.extend(key_presses) def process_keys(self) -> None: """ Process all the keys in the `input_queue`. (To be called after `feed`.) Note: because of the `feed`/`process_keys` separation, it is possible to call `feed` from inside a key binding. This function keeps looping until the queue is empty. """ app = get_app() def not_empty() -> bool: # When the application result is set, stop processing keys. (E.g. # if ENTER was received, followed by a few additional key strokes, # leave the other keys in the queue.) if app.is_done: # But if there are still CPRResponse keys in the queue, these # need to be processed. return any(k for k in self.input_queue if k.key == Keys.CPRResponse) else: return bool(self.input_queue) def get_next() -> KeyPress: if app.is_done: # Only process CPR responses. Everything else is typeahead. cpr = [k for k in self.input_queue if k.key == Keys.CPRResponse][0] self.input_queue.remove(cpr) return cpr else: return self.input_queue.popleft() is_flush = False while not_empty(): # Process next key. key_press = get_next() is_flush = key_press is _Flush is_cpr = key_press.key == Keys.CPRResponse if not is_flush and not is_cpr: self.before_key_press.fire() try: self._process_coroutine.send(key_press) except Exception: # If for some reason something goes wrong in the parser, (maybe # an exception was raised) restart the processor for next time. self.reset() self.empty_queue() raise if not is_flush and not is_cpr: self.after_key_press.fire() # Skip timeout if the last key was flush. if not is_flush: self._start_timeout() def empty_queue(self) -> list[KeyPress]: """ Empty the input queue. Return the unprocessed input. """ key_presses = list(self.input_queue) self.input_queue.clear() # Filter out CPRs. We don't want to return these. key_presses = [k for k in key_presses if k.key != Keys.CPRResponse] return key_presses def _call_handler(self, handler: Binding, key_sequence: list[KeyPress]) -> None: app = get_app() was_recording_emacs = app.emacs_state.is_recording was_recording_vi = bool(app.vi_state.recording_register) was_temporary_navigation_mode = app.vi_state.temporary_navigation_mode arg = self.arg self.arg = None event = KeyPressEvent( weakref.ref(self), arg=arg, key_sequence=key_sequence, previous_key_sequence=self._previous_key_sequence, is_repeat=(handler == self._previous_handler), ) # Save the state of the current buffer. if handler.save_before(event): event.app.current_buffer.save_to_undo_stack() # Call handler. from prompt_toolkit.buffer import EditReadOnlyBuffer try: handler.call(event) self._fix_vi_cursor_position(event) except EditReadOnlyBuffer: # When a key binding does an attempt to change a buffer which is # read-only, we can ignore that. We sound a bell and go on. app.output.bell() if was_temporary_navigation_mode: self._leave_vi_temp_navigation_mode(event) self._previous_key_sequence = key_sequence self._previous_handler = handler # Record the key sequence in our macro. (Only if we're in macro mode # before and after executing the key.) if handler.record_in_macro(): if app.emacs_state.is_recording and was_recording_emacs: recording = app.emacs_state.current_recording if recording is not None: # Should always be true, given that # `was_recording_emacs` is set. recording.extend(key_sequence) if app.vi_state.recording_register and was_recording_vi: for k in key_sequence: app.vi_state.current_recording += k.data def _fix_vi_cursor_position(self, event: KeyPressEvent) -> None: """ After every command, make sure that if we are in Vi navigation mode, we never put the cursor after the last character of a line. (Unless it's an empty line.) """ app = event.app buff = app.current_buffer preferred_column = buff.preferred_column if ( vi_navigation_mode() and buff.document.is_cursor_at_the_end_of_line and len(buff.document.current_line) > 0 ): buff.cursor_position -= 1 # Set the preferred_column for arrow up/down again. # (This was cleared after changing the cursor position.) buff.preferred_column = preferred_column def _leave_vi_temp_navigation_mode(self, event: KeyPressEvent) -> None: """ If we're in Vi temporary navigation (normal) mode, return to insert/replace mode after executing one action. """ app = event.app if app.editing_mode == EditingMode.VI: # Not waiting for a text object and no argument has been given. if app.vi_state.operator_func is None and self.arg is None: app.vi_state.temporary_navigation_mode = False def _start_timeout(self) -> None: """ Start auto flush timeout. Similar to Vim's `timeoutlen` option. Start a background coroutine with a timer. When this timeout expires and no key was pressed in the meantime, we flush all data in the queue and call the appropriate key binding handlers. """ app = get_app() timeout = app.timeoutlen if timeout is None: return async def wait() -> None: "Wait for timeout." # This sleep can be cancelled. In that case we don't flush. await sleep(timeout) if len(self.key_buffer) > 0: # (No keys pressed in the meantime.) flush_keys() def flush_keys() -> None: "Flush keys." self.feed(_Flush) self.process_keys() # Automatically flush keys. if self._flush_wait_task: self._flush_wait_task.cancel() self._flush_wait_task = app.create_background_task(wait()) def send_sigint(self) -> None: """ Send SIGINT. Immediately call the SIGINT key handler. """ self.feed(KeyPress(key=Keys.SIGINT), first=True) self.process_keys() class KeyPressEvent: """ Key press event, delivered to key bindings. :param key_processor_ref: Weak reference to the `KeyProcessor`. :param arg: Repetition argument. :param key_sequence: List of `KeyPress` instances. :param previouskey_sequence: Previous list of `KeyPress` instances. :param is_repeat: True when the previous event was delivered to the same handler. """ def __init__( self, key_processor_ref: weakref.ReferenceType[KeyProcessor], arg: str | None, key_sequence: list[KeyPress], previous_key_sequence: list[KeyPress], is_repeat: bool, ) -> None: self._key_processor_ref = key_processor_ref self.key_sequence = key_sequence self.previous_key_sequence = previous_key_sequence #: True when the previous key sequence was handled by the same handler. self.is_repeat = is_repeat self._arg = arg self._app = get_app() def __repr__(self) -> str: return f"KeyPressEvent(arg={self.arg!r}, key_sequence={self.key_sequence!r}, is_repeat={self.is_repeat!r})" @property def data(self) -> str: return self.key_sequence[-1].data @property def key_processor(self) -> KeyProcessor: processor = self._key_processor_ref() if processor is None: raise Exception("KeyProcessor was lost. This should not happen.") return processor @property def app(self) -> Application[Any]: """ The current `Application` object. """ return self._app @property def current_buffer(self) -> Buffer: """ The current buffer. """ return self.app.current_buffer @property def arg(self) -> int: """ Repetition argument. """ if self._arg == "-": return -1 result = int(self._arg or 1) # Don't exceed a million. if int(result) >= 1000000: result = 1 return result @property def arg_present(self) -> bool: """ True if repetition argument was explicitly provided. """ return self._arg is not None def append_to_arg_count(self, data: str) -> None: """ Add digit to the input argument. :param data: the typed digit as string """ assert data in "-0123456789" current = self._arg if data == "-": assert current is None or current == "-" result = data elif current is None: result = data else: result = f"{current}{data}" self.key_processor.arg = result @property def cli(self) -> Application[Any]: "For backward-compatibility." return self.app ================================================ FILE: src/prompt_toolkit/key_binding/vi_state.py ================================================ from __future__ import annotations from collections.abc import Callable from enum import Enum from typing import TYPE_CHECKING from prompt_toolkit.clipboard import ClipboardData if TYPE_CHECKING: from .bindings.vi import TextObject from .key_processor import KeyPressEvent __all__ = [ "InputMode", "CharacterFind", "ViState", ] class InputMode(str, Enum): value: str INSERT = "vi-insert" INSERT_MULTIPLE = "vi-insert-multiple" NAVIGATION = "vi-navigation" # Normal mode. REPLACE = "vi-replace" REPLACE_SINGLE = "vi-replace-single" class CharacterFind: def __init__(self, character: str, backwards: bool = False) -> None: self.character = character self.backwards = backwards class ViState: """ Mutable class to hold the state of the Vi navigation. """ def __init__(self) -> None: #: None or CharacterFind instance. (This is used to repeat the last #: search in Vi mode, by pressing the 'n' or 'N' in navigation mode.) self.last_character_find: CharacterFind | None = None # When an operator is given and we are waiting for text object, # -- e.g. in the case of 'dw', after the 'd' --, an operator callback # is set here. self.operator_func: None | (Callable[[KeyPressEvent, TextObject], None]) = None self.operator_arg: int | None = None #: Named registers. Maps register name (e.g. 'a') to #: :class:`ClipboardData` instances. self.named_registers: dict[str, ClipboardData] = {} #: The Vi mode we're currently in to. self.__input_mode = InputMode.INSERT #: Waiting for digraph. self.waiting_for_digraph = False self.digraph_symbol1: str | None = None # (None or a symbol.) #: When true, make ~ act as an operator. self.tilde_operator = False #: Register in which we are recording a macro. #: `None` when not recording anything. # Note that the recording is only stored in the register after the # recording is stopped. So we record in a separate `current_recording` # variable. self.recording_register: str | None = None self.current_recording: str = "" # Temporary navigation (normal) mode. # This happens when control-o has been pressed in insert or replace # mode. The user can now do one navigation action and we'll return back # to insert/replace. self.temporary_navigation_mode = False @property def input_mode(self) -> InputMode: "Get `InputMode`." return self.__input_mode @input_mode.setter def input_mode(self, value: InputMode) -> None: "Set `InputMode`." if value == InputMode.NAVIGATION: self.waiting_for_digraph = False self.operator_func = None self.operator_arg = None self.__input_mode = value def reset(self) -> None: """ Reset state, go back to the given mode. INSERT by default. """ # Go back to insert mode. self.input_mode = InputMode.INSERT self.waiting_for_digraph = False self.operator_func = None self.operator_arg = None # Reset recording state. self.recording_register = None self.current_recording = "" ================================================ FILE: src/prompt_toolkit/keys.py ================================================ from __future__ import annotations from enum import Enum __all__ = [ "Keys", "ALL_KEYS", ] class Keys(str, Enum): """ List of keys for use in key bindings. Note that this is an "StrEnum", all values can be compared against strings. """ value: str Escape = "escape" # Also Control-[ ShiftEscape = "s-escape" ControlAt = "c-@" # Also Control-Space. ControlA = "c-a" ControlB = "c-b" ControlC = "c-c" ControlD = "c-d" ControlE = "c-e" ControlF = "c-f" ControlG = "c-g" ControlH = "c-h" ControlI = "c-i" # Tab ControlJ = "c-j" # Newline ControlK = "c-k" ControlL = "c-l" ControlM = "c-m" # Carriage return ControlN = "c-n" ControlO = "c-o" ControlP = "c-p" ControlQ = "c-q" ControlR = "c-r" ControlS = "c-s" ControlT = "c-t" ControlU = "c-u" ControlV = "c-v" ControlW = "c-w" ControlX = "c-x" ControlY = "c-y" ControlZ = "c-z" Control1 = "c-1" Control2 = "c-2" Control3 = "c-3" Control4 = "c-4" Control5 = "c-5" Control6 = "c-6" Control7 = "c-7" Control8 = "c-8" Control9 = "c-9" Control0 = "c-0" ControlShift1 = "c-s-1" ControlShift2 = "c-s-2" ControlShift3 = "c-s-3" ControlShift4 = "c-s-4" ControlShift5 = "c-s-5" ControlShift6 = "c-s-6" ControlShift7 = "c-s-7" ControlShift8 = "c-s-8" ControlShift9 = "c-s-9" ControlShift0 = "c-s-0" ControlBackslash = "c-\\" ControlSquareClose = "c-]" ControlCircumflex = "c-^" ControlUnderscore = "c-_" Left = "left" Right = "right" Up = "up" Down = "down" Home = "home" End = "end" Insert = "insert" Delete = "delete" PageUp = "pageup" PageDown = "pagedown" ControlLeft = "c-left" ControlRight = "c-right" ControlUp = "c-up" ControlDown = "c-down" ControlHome = "c-home" ControlEnd = "c-end" ControlInsert = "c-insert" ControlDelete = "c-delete" ControlPageUp = "c-pageup" ControlPageDown = "c-pagedown" ShiftLeft = "s-left" ShiftRight = "s-right" ShiftUp = "s-up" ShiftDown = "s-down" ShiftHome = "s-home" ShiftEnd = "s-end" ShiftInsert = "s-insert" ShiftDelete = "s-delete" ShiftPageUp = "s-pageup" ShiftPageDown = "s-pagedown" ControlShiftLeft = "c-s-left" ControlShiftRight = "c-s-right" ControlShiftUp = "c-s-up" ControlShiftDown = "c-s-down" ControlShiftHome = "c-s-home" ControlShiftEnd = "c-s-end" ControlShiftInsert = "c-s-insert" ControlShiftDelete = "c-s-delete" ControlShiftPageUp = "c-s-pageup" ControlShiftPageDown = "c-s-pagedown" BackTab = "s-tab" # shift + tab F1 = "f1" F2 = "f2" F3 = "f3" F4 = "f4" F5 = "f5" F6 = "f6" F7 = "f7" F8 = "f8" F9 = "f9" F10 = "f10" F11 = "f11" F12 = "f12" F13 = "f13" F14 = "f14" F15 = "f15" F16 = "f16" F17 = "f17" F18 = "f18" F19 = "f19" F20 = "f20" F21 = "f21" F22 = "f22" F23 = "f23" F24 = "f24" ControlF1 = "c-f1" ControlF2 = "c-f2" ControlF3 = "c-f3" ControlF4 = "c-f4" ControlF5 = "c-f5" ControlF6 = "c-f6" ControlF7 = "c-f7" ControlF8 = "c-f8" ControlF9 = "c-f9" ControlF10 = "c-f10" ControlF11 = "c-f11" ControlF12 = "c-f12" ControlF13 = "c-f13" ControlF14 = "c-f14" ControlF15 = "c-f15" ControlF16 = "c-f16" ControlF17 = "c-f17" ControlF18 = "c-f18" ControlF19 = "c-f19" ControlF20 = "c-f20" ControlF21 = "c-f21" ControlF22 = "c-f22" ControlF23 = "c-f23" ControlF24 = "c-f24" # Matches any key. Any = "<any>" # Special. ScrollUp = "<scroll-up>" ScrollDown = "<scroll-down>" CPRResponse = "<cursor-position-response>" Vt100MouseEvent = "<vt100-mouse-event>" WindowsMouseEvent = "<windows-mouse-event>" BracketedPaste = "<bracketed-paste>" SIGINT = "<sigint>" # For internal use: key which is ignored. # (The key binding for this key should not do anything.) Ignore = "<ignore>" # Some 'Key' aliases (for backwards-compatibility). ControlSpace = ControlAt Tab = ControlI Enter = ControlM Backspace = ControlH # ShiftControl was renamed to ControlShift in # 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020). ShiftControlLeft = ControlShiftLeft ShiftControlRight = ControlShiftRight ShiftControlHome = ControlShiftHome ShiftControlEnd = ControlShiftEnd ALL_KEYS: list[str] = [k.value for k in Keys] # Aliases. KEY_ALIASES: dict[str, str] = { "backspace": "c-h", "c-space": "c-@", "enter": "c-m", "tab": "c-i", # ShiftControl was renamed to ControlShift. "s-c-left": "c-s-left", "s-c-right": "c-s-right", "s-c-home": "c-s-home", "s-c-end": "c-s-end", } ================================================ FILE: src/prompt_toolkit/layout/__init__.py ================================================ """ Command line layout definitions ------------------------------- The layout of a command line interface is defined by a Container instance. There are two main groups of classes here. Containers and controls: - A container can contain other containers or controls, it can have multiple children and it decides about the dimensions. - A control is responsible for rendering the actual content to a screen. A control can propose some dimensions, but it's the container who decides about the dimensions -- or when the control consumes more space -- which part of the control will be visible. Container classes:: - Container (Abstract base class) |- HSplit (Horizontal split) |- VSplit (Vertical split) |- FloatContainer (Container which can also contain menus and other floats) `- Window (Container which contains one actual control Control classes:: - UIControl (Abstract base class) |- FormattedTextControl (Renders formatted text, or a simple list of text fragments) `- BufferControl (Renders an input buffer.) Usually, you end up wrapping every control inside a `Window` object, because that's the only way to render it in a layout. There are some prepared toolbars which are ready to use:: - SystemToolbar (Shows the 'system' input buffer, for entering system commands.) - ArgToolbar (Shows the input 'arg', for repetition of input commands.) - SearchToolbar (Shows the 'search' input buffer, for incremental search.) - CompletionsToolbar (Shows the completions of the current buffer.) - ValidationToolbar (Shows validation errors of the current buffer.) And one prepared menu: - CompletionsMenu """ from __future__ import annotations from .containers import ( AnyContainer, ColorColumn, ConditionalContainer, Container, DynamicContainer, Float, FloatContainer, HorizontalAlign, HSplit, ScrollOffsets, VerticalAlign, VSplit, Window, WindowAlign, WindowRenderInfo, is_container, to_container, to_window, ) from .controls import ( BufferControl, DummyControl, FormattedTextControl, SearchBufferControl, UIContent, UIControl, ) from .dimension import ( AnyDimension, D, Dimension, is_dimension, max_layout_dimensions, sum_layout_dimensions, to_dimension, ) from .layout import InvalidLayoutError, Layout, walk from .margins import ( ConditionalMargin, Margin, NumberedMargin, PromptMargin, ScrollbarMargin, ) from .menus import CompletionsMenu, MultiColumnCompletionsMenu from .scrollable_pane import ScrollablePane __all__ = [ # Layout. "Layout", "InvalidLayoutError", "walk", # Dimensions. "AnyDimension", "Dimension", "D", "sum_layout_dimensions", "max_layout_dimensions", "to_dimension", "is_dimension", # Containers. "AnyContainer", "Container", "HorizontalAlign", "VerticalAlign", "HSplit", "VSplit", "FloatContainer", "Float", "WindowAlign", "Window", "WindowRenderInfo", "ConditionalContainer", "ScrollOffsets", "ColorColumn", "to_container", "to_window", "is_container", "DynamicContainer", "ScrollablePane", # Controls. "BufferControl", "SearchBufferControl", "DummyControl", "FormattedTextControl", "UIControl", "UIContent", # Margins. "Margin", "NumberedMargin", "ScrollbarMargin", "ConditionalMargin", "PromptMargin", # Menus. "CompletionsMenu", "MultiColumnCompletionsMenu", ] ================================================ FILE: src/prompt_toolkit/layout/containers.py ================================================ """ Container for the layout. (Containers can contain other containers or user interface controls.) """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable, Sequence from enum import Enum from functools import partial from typing import TYPE_CHECKING, Union, cast from prompt_toolkit.application.current import get_app from prompt_toolkit.cache import SimpleCache from prompt_toolkit.data_structures import Point from prompt_toolkit.filters import ( FilterOrBool, emacs_insert_mode, to_filter, vi_insert_mode, ) from prompt_toolkit.formatted_text import ( AnyFormattedText, StyleAndTextTuples, to_formatted_text, ) from prompt_toolkit.formatted_text.utils import ( fragment_list_to_text, fragment_list_width, ) from prompt_toolkit.key_binding import KeyBindingsBase from prompt_toolkit.mouse_events import MouseEvent, MouseEventType from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str from .controls import ( DummyControl, FormattedTextControl, GetLinePrefixCallable, UIContent, UIControl, ) from .dimension import ( AnyDimension, Dimension, max_layout_dimensions, sum_layout_dimensions, to_dimension, ) from .margins import Margin from .mouse_handlers import MouseHandlers from .screen import _CHAR_CACHE, Screen, WritePosition from .utils import explode_text_fragments if TYPE_CHECKING: from typing import Protocol, TypeGuard from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone __all__ = [ "AnyContainer", "Container", "HorizontalAlign", "VerticalAlign", "HSplit", "VSplit", "FloatContainer", "Float", "WindowAlign", "Window", "WindowRenderInfo", "ConditionalContainer", "ScrollOffsets", "ColorColumn", "to_container", "to_window", "is_container", "DynamicContainer", ] class Container(metaclass=ABCMeta): """ Base class for user interface layout. """ @abstractmethod def reset(self) -> None: """ Reset the state of this container and all the children. (E.g. reset scroll offsets, etc...) """ @abstractmethod def preferred_width(self, max_available_width: int) -> Dimension: """ Return a :class:`~prompt_toolkit.layout.Dimension` that represents the desired width for this container. """ @abstractmethod def preferred_height(self, width: int, max_available_height: int) -> Dimension: """ Return a :class:`~prompt_toolkit.layout.Dimension` that represents the desired height for this container. """ @abstractmethod def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: """ Write the actual content to the screen. :param screen: :class:`~prompt_toolkit.layout.screen.Screen` :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`. :param parent_style: Style string to pass to the :class:`.Window` object. This will be applied to all content of the windows. :class:`.VSplit` and :class:`.HSplit` can use it to pass their style down to the windows that they contain. :param z_index: Used for propagating z_index from parent to child. """ def is_modal(self) -> bool: """ When this container is modal, key bindings from parent containers are not taken into account if a user control in this container is focused. """ return False def get_key_bindings(self) -> KeyBindingsBase | None: """ Returns a :class:`.KeyBindings` object. These bindings become active when any user control in this container has the focus, except if any containers between this container and the focused user control is modal. """ return None @abstractmethod def get_children(self) -> list[Container]: """ Return the list of child :class:`.Container` objects. """ return [] if TYPE_CHECKING: class MagicContainer(Protocol): """ Any object that implements ``__pt_container__`` represents a container. """ def __pt_container__(self) -> AnyContainer: ... AnyContainer = Union[Container, "MagicContainer"] def _window_too_small() -> Window: "Create a `Window` that displays the 'Window too small' text." return Window( FormattedTextControl(text=[("class:window-too-small", " Window too small... ")]) ) class VerticalAlign(Enum): "Alignment for `HSplit`." TOP = "TOP" CENTER = "CENTER" BOTTOM = "BOTTOM" JUSTIFY = "JUSTIFY" class HorizontalAlign(Enum): "Alignment for `VSplit`." LEFT = "LEFT" CENTER = "CENTER" RIGHT = "RIGHT" JUSTIFY = "JUSTIFY" class _Split(Container): """ The common parts of `VSplit` and `HSplit`. """ def __init__( self, children: Sequence[AnyContainer], window_too_small: Container | None = None, padding: AnyDimension = Dimension.exact(0), padding_char: str | None = None, padding_style: str = "", width: AnyDimension = None, height: AnyDimension = None, z_index: int | None = None, modal: bool = False, key_bindings: KeyBindingsBase | None = None, style: str | Callable[[], str] = "", ) -> None: self.children = [to_container(c) for c in children] self.window_too_small = window_too_small or _window_too_small() self.padding = padding self.padding_char = padding_char self.padding_style = padding_style self.width = width self.height = height self.z_index = z_index self.modal = modal self.key_bindings = key_bindings self.style = style def is_modal(self) -> bool: return self.modal def get_key_bindings(self) -> KeyBindingsBase | None: return self.key_bindings def get_children(self) -> list[Container]: return self.children class HSplit(_Split): """ Several layouts, one stacked above/under the other. :: +--------------------+ | | +--------------------+ | | +--------------------+ By default, this doesn't display a horizontal line between the children, but if this is something you need, then create a HSplit as follows:: HSplit(children=[ ... ], padding_char='-', padding=1, padding_style='#ffff00') :param children: List of child :class:`.Container` objects. :param window_too_small: A :class:`.Container` object that is displayed if there is not enough space for all the children. By default, this is a "Window too small" message. :param align: `VerticalAlign` value. :param width: When given, use this width instead of looking at the children. :param height: When given, use this height instead of looking at the children. :param z_index: (int or None) When specified, this can be used to bring element in front of floating elements. `None` means: inherit from parent. :param style: A style string. :param modal: ``True`` or ``False``. :param key_bindings: ``None`` or a :class:`.KeyBindings` object. :param padding: (`Dimension` or int), size to be used for the padding. :param padding_char: Character to be used for filling in the padding. :param padding_style: Style to applied to the padding. """ def __init__( self, children: Sequence[AnyContainer], window_too_small: Container | None = None, align: VerticalAlign = VerticalAlign.JUSTIFY, padding: AnyDimension = 0, padding_char: str | None = None, padding_style: str = "", width: AnyDimension = None, height: AnyDimension = None, z_index: int | None = None, modal: bool = False, key_bindings: KeyBindingsBase | None = None, style: str | Callable[[], str] = "", ) -> None: super().__init__( children=children, window_too_small=window_too_small, padding=padding, padding_char=padding_char, padding_style=padding_style, width=width, height=height, z_index=z_index, modal=modal, key_bindings=key_bindings, style=style, ) self.align = align self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = ( SimpleCache(maxsize=1) ) self._remaining_space_window = Window() # Dummy window. def preferred_width(self, max_available_width: int) -> Dimension: if self.width is not None: return to_dimension(self.width) if self.children: dimensions = [c.preferred_width(max_available_width) for c in self.children] return max_layout_dimensions(dimensions) else: return Dimension() def preferred_height(self, width: int, max_available_height: int) -> Dimension: if self.height is not None: return to_dimension(self.height) dimensions = [ c.preferred_height(width, max_available_height) for c in self._all_children ] return sum_layout_dimensions(dimensions) def reset(self) -> None: for c in self.children: c.reset() @property def _all_children(self) -> list[Container]: """ List of child objects, including padding. """ def get() -> list[Container]: result: list[Container] = [] # Padding Top. if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM): result.append(Window(width=Dimension(preferred=0))) # The children with padding. for child in self.children: result.append(child) result.append( Window( height=self.padding, char=self.padding_char, style=self.padding_style, ) ) if result: result.pop() # Padding right. if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP): result.append(Window(width=Dimension(preferred=0))) return result return self._children_cache.get(tuple(self.children), get) def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: """ Render the prompt to a `Screen` instance. :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class to which the output has to be written. """ sizes = self._divide_heights(write_position) style = parent_style + " " + to_str(self.style) z_index = z_index if self.z_index is None else self.z_index if sizes is None: self.window_too_small.write_to_screen( screen, mouse_handlers, write_position, style, erase_bg, z_index ) else: # ypos = write_position.ypos xpos = write_position.xpos width = write_position.width # Draw child panes. for s, c in zip(sizes, self._all_children): c.write_to_screen( screen, mouse_handlers, WritePosition(xpos, ypos, width, s), style, erase_bg, z_index, ) ypos += s # Fill in the remaining space. This happens when a child control # refuses to take more space and we don't have any padding. Adding a # dummy child control for this (in `self._all_children`) is not # desired, because in some situations, it would take more space, even # when it's not required. This is required to apply the styling. remaining_height = write_position.ypos + write_position.height - ypos if remaining_height > 0: self._remaining_space_window.write_to_screen( screen, mouse_handlers, WritePosition(xpos, ypos, width, remaining_height), style, erase_bg, z_index, ) def _divide_heights(self, write_position: WritePosition) -> list[int] | None: """ Return the heights for all rows. Or None when there is not enough space. """ if not self.children: return [] width = write_position.width height = write_position.height # Calculate heights. dimensions = [c.preferred_height(width, height) for c in self._all_children] # Sum dimensions sum_dimensions = sum_layout_dimensions(dimensions) # If there is not enough space for both. # Don't do anything. if sum_dimensions.min > height: return None # Find optimal sizes. (Start with minimal size, increase until we cover # the whole height.) sizes = [d.min for d in dimensions] child_generator = take_using_weights( items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] ) i = next(child_generator) # Increase until we meet at least the 'preferred' size. preferred_stop = min(height, sum_dimensions.preferred) preferred_dimensions = [d.preferred for d in dimensions] while sum(sizes) < preferred_stop: if sizes[i] < preferred_dimensions[i]: sizes[i] += 1 i = next(child_generator) # Increase until we use all the available space. (or until "max") if not get_app().is_done: max_stop = min(height, sum_dimensions.max) max_dimensions = [d.max for d in dimensions] while sum(sizes) < max_stop: if sizes[i] < max_dimensions[i]: sizes[i] += 1 i = next(child_generator) return sizes class VSplit(_Split): """ Several layouts, one stacked left/right of the other. :: +---------+----------+ | | | | | | +---------+----------+ By default, this doesn't display a vertical line between the children, but if this is something you need, then create a HSplit as follows:: VSplit(children=[ ... ], padding_char='|', padding=1, padding_style='#ffff00') :param children: List of child :class:`.Container` objects. :param window_too_small: A :class:`.Container` object that is displayed if there is not enough space for all the children. By default, this is a "Window too small" message. :param align: `HorizontalAlign` value. :param width: When given, use this width instead of looking at the children. :param height: When given, use this height instead of looking at the children. :param z_index: (int or None) When specified, this can be used to bring element in front of floating elements. `None` means: inherit from parent. :param style: A style string. :param modal: ``True`` or ``False``. :param key_bindings: ``None`` or a :class:`.KeyBindings` object. :param padding: (`Dimension` or int), size to be used for the padding. :param padding_char: Character to be used for filling in the padding. :param padding_style: Style to applied to the padding. """ def __init__( self, children: Sequence[AnyContainer], window_too_small: Container | None = None, align: HorizontalAlign = HorizontalAlign.JUSTIFY, padding: AnyDimension = 0, padding_char: str | None = None, padding_style: str = "", width: AnyDimension = None, height: AnyDimension = None, z_index: int | None = None, modal: bool = False, key_bindings: KeyBindingsBase | None = None, style: str | Callable[[], str] = "", ) -> None: super().__init__( children=children, window_too_small=window_too_small, padding=padding, padding_char=padding_char, padding_style=padding_style, width=width, height=height, z_index=z_index, modal=modal, key_bindings=key_bindings, style=style, ) self.align = align self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = ( SimpleCache(maxsize=1) ) self._remaining_space_window = Window() # Dummy window. def preferred_width(self, max_available_width: int) -> Dimension: if self.width is not None: return to_dimension(self.width) dimensions = [ c.preferred_width(max_available_width) for c in self._all_children ] return sum_layout_dimensions(dimensions) def preferred_height(self, width: int, max_available_height: int) -> Dimension: if self.height is not None: return to_dimension(self.height) # At the point where we want to calculate the heights, the widths have # already been decided. So we can trust `width` to be the actual # `width` that's going to be used for the rendering. So, # `divide_widths` is supposed to use all of the available width. # Using only the `preferred` width caused a bug where the reported # height was more than required. (we had a `BufferControl` which did # wrap lines because of the smaller width returned by `_divide_widths`. sizes = self._divide_widths(width) children = self._all_children if sizes is None: return Dimension() else: dimensions = [ c.preferred_height(s, max_available_height) for s, c in zip(sizes, children) ] return max_layout_dimensions(dimensions) def reset(self) -> None: for c in self.children: c.reset() @property def _all_children(self) -> list[Container]: """ List of child objects, including padding. """ def get() -> list[Container]: result: list[Container] = [] # Padding left. if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT): result.append(Window(width=Dimension(preferred=0))) # The children with padding. for child in self.children: result.append(child) result.append( Window( width=self.padding, char=self.padding_char, style=self.padding_style, ) ) if result: result.pop() # Padding right. if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT): result.append(Window(width=Dimension(preferred=0))) return result return self._children_cache.get(tuple(self.children), get) def _divide_widths(self, width: int) -> list[int] | None: """ Return the widths for all columns. Or None when there is not enough space. """ children = self._all_children if not children: return [] # Calculate widths. dimensions = [c.preferred_width(width) for c in children] preferred_dimensions = [d.preferred for d in dimensions] # Sum dimensions sum_dimensions = sum_layout_dimensions(dimensions) # If there is not enough space for both. # Don't do anything. if sum_dimensions.min > width: return None # Find optimal sizes. (Start with minimal size, increase until we cover # the whole width.) sizes = [d.min for d in dimensions] child_generator = take_using_weights( items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] ) i = next(child_generator) # Increase until we meet at least the 'preferred' size. preferred_stop = min(width, sum_dimensions.preferred) while sum(sizes) < preferred_stop: if sizes[i] < preferred_dimensions[i]: sizes[i] += 1 i = next(child_generator) # Increase until we use all the available space. max_dimensions = [d.max for d in dimensions] max_stop = min(width, sum_dimensions.max) while sum(sizes) < max_stop: if sizes[i] < max_dimensions[i]: sizes[i] += 1 i = next(child_generator) return sizes def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: """ Render the prompt to a `Screen` instance. :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class to which the output has to be written. """ if not self.children: return children = self._all_children sizes = self._divide_widths(write_position.width) style = parent_style + " " + to_str(self.style) z_index = z_index if self.z_index is None else self.z_index # If there is not enough space. if sizes is None: self.window_too_small.write_to_screen( screen, mouse_handlers, write_position, style, erase_bg, z_index ) return # Calculate heights, take the largest possible, but not larger than # write_position.height. heights = [ child.preferred_height(width, write_position.height).preferred for width, child in zip(sizes, children) ] height = max(write_position.height, min(write_position.height, max(heights))) # ypos = write_position.ypos xpos = write_position.xpos # Draw all child panes. for s, c in zip(sizes, children): c.write_to_screen( screen, mouse_handlers, WritePosition(xpos, ypos, s, height), style, erase_bg, z_index, ) xpos += s # Fill in the remaining space. This happens when a child control # refuses to take more space and we don't have any padding. Adding a # dummy child control for this (in `self._all_children`) is not # desired, because in some situations, it would take more space, even # when it's not required. This is required to apply the styling. remaining_width = write_position.xpos + write_position.width - xpos if remaining_width > 0: self._remaining_space_window.write_to_screen( screen, mouse_handlers, WritePosition(xpos, ypos, remaining_width, height), style, erase_bg, z_index, ) class FloatContainer(Container): """ Container which can contain another container for the background, as well as a list of floating containers on top of it. Example Usage:: FloatContainer(content=Window(...), floats=[ Float(xcursor=True, ycursor=True, content=CompletionsMenu(...)) ]) :param z_index: (int or None) When specified, this can be used to bring element in front of floating elements. `None` means: inherit from parent. This is the z_index for the whole `Float` container as a whole. """ def __init__( self, content: AnyContainer, floats: list[Float], modal: bool = False, key_bindings: KeyBindingsBase | None = None, style: str | Callable[[], str] = "", z_index: int | None = None, ) -> None: self.content = to_container(content) self.floats = floats self.modal = modal self.key_bindings = key_bindings self.style = style self.z_index = z_index def reset(self) -> None: self.content.reset() for f in self.floats: f.content.reset() def preferred_width(self, max_available_width: int) -> Dimension: return self.content.preferred_width(max_available_width) def preferred_height(self, width: int, max_available_height: int) -> Dimension: """ Return the preferred height of the float container. (We don't care about the height of the floats, they should always fit into the dimensions provided by the container.) """ return self.content.preferred_height(width, max_available_height) def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: style = parent_style + " " + to_str(self.style) z_index = z_index if self.z_index is None else self.z_index self.content.write_to_screen( screen, mouse_handlers, write_position, style, erase_bg, z_index ) for number, fl in enumerate(self.floats): # z_index of a Float is computed by summing the z_index of the # container and the `Float`. new_z_index = (z_index or 0) + fl.z_index style = parent_style + " " + to_str(self.style) # If the float that we have here, is positioned relative to the # cursor position, but the Window that specifies the cursor # position is not drawn yet, because it's a Float itself, we have # to postpone this calculation. (This is a work-around, but good # enough for now.) postpone = fl.xcursor is not None or fl.ycursor is not None if postpone: new_z_index = ( number + 10**8 ) # Draw as late as possible, but keep the order. screen.draw_with_z_index( z_index=new_z_index, draw_func=partial( self._draw_float, fl, screen, mouse_handlers, write_position, style, erase_bg, new_z_index, ), ) else: self._draw_float( fl, screen, mouse_handlers, write_position, style, erase_bg, new_z_index, ) def _draw_float( self, fl: Float, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, style: str, erase_bg: bool, z_index: int | None, ) -> None: "Draw a single Float." # When a menu_position was given, use this instead of the cursor # position. (These cursor positions are absolute, translate again # relative to the write_position.) # Note: This should be inside the for-loop, because one float could # set the cursor position to be used for the next one. cpos = screen.get_menu_position( fl.attach_to_window or get_app().layout.current_window ) cursor_position = Point( x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos ) fl_width = fl.get_width() fl_height = fl.get_height() width: int height: int xpos: int ypos: int # Left & width given. if fl.left is not None and fl_width is not None: xpos = fl.left width = fl_width # Left & right given -> calculate width. elif fl.left is not None and fl.right is not None: xpos = fl.left width = write_position.width - fl.left - fl.right # Width & right given -> calculate left. elif fl_width is not None and fl.right is not None: xpos = write_position.width - fl.right - fl_width width = fl_width # Near x position of cursor. elif fl.xcursor: if fl_width is None: width = fl.content.preferred_width(write_position.width).preferred width = min(write_position.width, width) else: width = fl_width xpos = cursor_position.x if xpos + width > write_position.width: xpos = max(0, write_position.width - width) # Only width given -> center horizontally. elif fl_width: xpos = int((write_position.width - fl_width) / 2) width = fl_width # Otherwise, take preferred width from float content. else: width = fl.content.preferred_width(write_position.width).preferred if fl.left is not None: xpos = fl.left elif fl.right is not None: xpos = max(0, write_position.width - width - fl.right) else: # Center horizontally. xpos = max(0, int((write_position.width - width) / 2)) # Trim. width = min(width, write_position.width - xpos) # Top & height given. if fl.top is not None and fl_height is not None: ypos = fl.top height = fl_height # Top & bottom given -> calculate height. elif fl.top is not None and fl.bottom is not None: ypos = fl.top height = write_position.height - fl.top - fl.bottom # Height & bottom given -> calculate top. elif fl_height is not None and fl.bottom is not None: ypos = write_position.height - fl_height - fl.bottom height = fl_height # Near cursor. elif fl.ycursor: ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1) if fl_height is None: height = fl.content.preferred_height( width, write_position.height ).preferred else: height = fl_height # Reduce height if not enough space. (We can use the height # when the content requires it.) if height > write_position.height - ypos: if write_position.height - ypos + 1 >= ypos: # When the space below the cursor is more than # the space above, just reduce the height. height = write_position.height - ypos else: # Otherwise, fit the float above the cursor. height = min(height, cursor_position.y) ypos = cursor_position.y - height # Only height given -> center vertically. elif fl_height: ypos = int((write_position.height - fl_height) / 2) height = fl_height # Otherwise, take preferred height from content. else: height = fl.content.preferred_height(width, write_position.height).preferred if fl.top is not None: ypos = fl.top elif fl.bottom is not None: ypos = max(0, write_position.height - height - fl.bottom) else: # Center vertically. ypos = max(0, int((write_position.height - height) / 2)) # Trim. height = min(height, write_position.height - ypos) # Write float. # (xpos and ypos can be negative: a float can be partially visible.) if height > 0 and width > 0: wp = WritePosition( xpos=xpos + write_position.xpos, ypos=ypos + write_position.ypos, width=width, height=height, ) if not fl.hide_when_covering_content or self._area_is_empty(screen, wp): fl.content.write_to_screen( screen, mouse_handlers, wp, style, erase_bg=not fl.transparent(), z_index=z_index, ) def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool: """ Return True when the area below the write position is still empty. (For floats that should not hide content underneath.) """ wp = write_position for y in range(wp.ypos, wp.ypos + wp.height): if y in screen.data_buffer: row = screen.data_buffer[y] for x in range(wp.xpos, wp.xpos + wp.width): c = row[x] if c.char != " ": return False return True def is_modal(self) -> bool: return self.modal def get_key_bindings(self) -> KeyBindingsBase | None: return self.key_bindings def get_children(self) -> list[Container]: children = [self.content] children.extend(f.content for f in self.floats) return children class Float: """ Float for use in a :class:`.FloatContainer`. Except for the `content` parameter, all other options are optional. :param content: :class:`.Container` instance. :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`. :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`. :param left: Distance to the left edge of the :class:`.FloatContainer`. :param right: Distance to the right edge of the :class:`.FloatContainer`. :param top: Distance to the top of the :class:`.FloatContainer`. :param bottom: Distance to the bottom of the :class:`.FloatContainer`. :param attach_to_window: Attach to the cursor from this window, instead of the current window. :param hide_when_covering_content: Hide the float when it covers content underneath. :param allow_cover_cursor: When `False`, make sure to display the float below the cursor. Not on top of the indicated position. :param z_index: Z-index position. For a Float, this needs to be at least one. It is relative to the z_index of the parent container. :param transparent: :class:`.Filter` indicating whether this float needs to be drawn transparently. """ def __init__( self, content: AnyContainer, top: int | None = None, right: int | None = None, bottom: int | None = None, left: int | None = None, width: int | Callable[[], int] | None = None, height: int | Callable[[], int] | None = None, xcursor: bool = False, ycursor: bool = False, attach_to_window: AnyContainer | None = None, hide_when_covering_content: bool = False, allow_cover_cursor: bool = False, z_index: int = 1, transparent: bool = False, ) -> None: assert z_index >= 1 self.left = left self.right = right self.top = top self.bottom = bottom self.width = width self.height = height self.xcursor = xcursor self.ycursor = ycursor self.attach_to_window = ( to_window(attach_to_window) if attach_to_window else None ) self.content = to_container(content) self.hide_when_covering_content = hide_when_covering_content self.allow_cover_cursor = allow_cover_cursor self.z_index = z_index self.transparent = to_filter(transparent) def get_width(self) -> int | None: if callable(self.width): return self.width() return self.width def get_height(self) -> int | None: if callable(self.height): return self.height() return self.height def __repr__(self) -> str: return f"Float(content={self.content!r})" class WindowRenderInfo: """ Render information for the last render time of this control. It stores mapping information between the input buffers (in case of a :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual render position on the output screen. (Could be used for implementation of the Vi 'H' and 'L' key bindings as well as implementing mouse support.) :param ui_content: The original :class:`.UIContent` instance that contains the whole input, without clipping. (ui_content) :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance. :param vertical_scroll: The vertical scroll of the :class:`.Window` instance. :param window_width: The width of the window that displays the content, without the margins. :param window_height: The height of the window that displays the content. :param configured_scroll_offsets: The scroll offsets as configured for the :class:`Window` instance. :param visible_line_to_row_col: Mapping that maps the row numbers on the displayed screen (starting from zero for the first visible line) to (row, col) tuples pointing to the row and column of the :class:`.UIContent`. :param rowcol_to_yx: Mapping that maps (row, column) tuples representing coordinates of the :class:`UIContent` to (y, x) absolute coordinates at the rendered screen. """ def __init__( self, window: Window, ui_content: UIContent, horizontal_scroll: int, vertical_scroll: int, window_width: int, window_height: int, configured_scroll_offsets: ScrollOffsets, visible_line_to_row_col: dict[int, tuple[int, int]], rowcol_to_yx: dict[tuple[int, int], tuple[int, int]], x_offset: int, y_offset: int, wrap_lines: bool, ) -> None: self.window = window self.ui_content = ui_content self.vertical_scroll = vertical_scroll self.window_width = window_width # Width without margins. self.window_height = window_height self.configured_scroll_offsets = configured_scroll_offsets self.visible_line_to_row_col = visible_line_to_row_col self.wrap_lines = wrap_lines self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x # screen coordinates. self._x_offset = x_offset self._y_offset = y_offset @property def visible_line_to_input_line(self) -> dict[int, int]: return { visible_line: rowcol[0] for visible_line, rowcol in self.visible_line_to_row_col.items() } @property def cursor_position(self) -> Point: """ Return the cursor position coordinates, relative to the left/top corner of the rendered screen. """ cpos = self.ui_content.cursor_position try: y, x = self._rowcol_to_yx[cpos.y, cpos.x] except KeyError: # For `DummyControl` for instance, the content can be empty, and so # will `_rowcol_to_yx` be. Return 0/0 by default. return Point(x=0, y=0) else: return Point(x=x - self._x_offset, y=y - self._y_offset) @property def applied_scroll_offsets(self) -> ScrollOffsets: """ Return a :class:`.ScrollOffsets` instance that indicates the actual offset. This can be less than or equal to what's configured. E.g, when the cursor is completely at the top, the top offset will be zero rather than what's configured. """ if self.displayed_lines[0] == 0: top = 0 else: # Get row where the cursor is displayed. y = self.input_line_to_visible_line[self.ui_content.cursor_position.y] top = min(y, self.configured_scroll_offsets.top) return ScrollOffsets( top=top, bottom=min( self.ui_content.line_count - self.displayed_lines[-1] - 1, self.configured_scroll_offsets.bottom, ), # For left/right, it probably doesn't make sense to return something. # (We would have to calculate the widths of all the lines and keep # double width characters in mind.) left=0, right=0, ) @property def displayed_lines(self) -> list[int]: """ List of all the visible rows. (Line numbers of the input buffer.) The last line may not be entirely visible. """ return sorted(row for row, col in self.visible_line_to_row_col.values()) @property def input_line_to_visible_line(self) -> dict[int, int]: """ Return the dictionary mapping the line numbers of the input buffer to the lines of the screen. When a line spans several rows at the screen, the first row appears in the dictionary. """ result: dict[int, int] = {} for k, v in self.visible_line_to_input_line.items(): if v in result: result[v] = min(result[v], k) else: result[v] = k return result def first_visible_line(self, after_scroll_offset: bool = False) -> int: """ Return the line number (0 based) of the input document that corresponds with the first visible line. """ if after_scroll_offset: return self.displayed_lines[self.applied_scroll_offsets.top] else: return self.displayed_lines[0] def last_visible_line(self, before_scroll_offset: bool = False) -> int: """ Like `first_visible_line`, but for the last visible line. """ if before_scroll_offset: return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom] else: return self.displayed_lines[-1] def center_visible_line( self, before_scroll_offset: bool = False, after_scroll_offset: bool = False ) -> int: """ Like `first_visible_line`, but for the center visible line. """ return ( self.first_visible_line(after_scroll_offset) + ( self.last_visible_line(before_scroll_offset) - self.first_visible_line(after_scroll_offset) ) // 2 ) @property def content_height(self) -> int: """ The full height of the user control. """ return self.ui_content.line_count @property def full_height_visible(self) -> bool: """ True when the full height is visible (There is no vertical scroll.) """ return ( self.vertical_scroll == 0 and self.last_visible_line() == self.content_height ) @property def top_visible(self) -> bool: """ True when the top of the buffer is visible. """ return self.vertical_scroll == 0 @property def bottom_visible(self) -> bool: """ True when the bottom of the buffer is visible. """ return self.last_visible_line() == self.content_height - 1 @property def vertical_scroll_percentage(self) -> int: """ Vertical scroll as a percentage. (0 means: the top is visible, 100 means: the bottom is visible.) """ if self.bottom_visible: return 100 else: return 100 * self.vertical_scroll // self.content_height def get_height_for_line(self, lineno: int) -> int: """ Return the height of the given line. (The height that it would take, if this line became visible.) """ if self.wrap_lines: return self.ui_content.get_height_for_line( lineno, self.window_width, self.window.get_line_prefix ) else: return 1 class ScrollOffsets: """ Scroll offsets for the :class:`.Window` class. Note that left/right offsets only make sense if line wrapping is disabled. """ def __init__( self, top: int | Callable[[], int] = 0, bottom: int | Callable[[], int] = 0, left: int | Callable[[], int] = 0, right: int | Callable[[], int] = 0, ) -> None: self._top = top self._bottom = bottom self._left = left self._right = right @property def top(self) -> int: return to_int(self._top) @property def bottom(self) -> int: return to_int(self._bottom) @property def left(self) -> int: return to_int(self._left) @property def right(self) -> int: return to_int(self._right) def __repr__(self) -> str: return f"ScrollOffsets(top={self._top!r}, bottom={self._bottom!r}, left={self._left!r}, right={self._right!r})" class ColorColumn: """ Column for a :class:`.Window` to be colored. """ def __init__(self, position: int, style: str = "class:color-column") -> None: self.position = position self.style = style _in_insert_mode = vi_insert_mode | emacs_insert_mode class WindowAlign(Enum): """ Alignment of the Window content. Note that this is different from `HorizontalAlign` and `VerticalAlign`, which are used for the alignment of the child containers in respectively `VSplit` and `HSplit`. """ LEFT = "LEFT" RIGHT = "RIGHT" CENTER = "CENTER" class Window(Container): """ Container that holds a control. :param content: :class:`.UIControl` instance. :param width: :class:`.Dimension` instance or callable. :param height: :class:`.Dimension` instance or callable. :param z_index: When specified, this can be used to bring element in front of floating elements. :param dont_extend_width: When `True`, don't take up more width then the preferred width reported by the control. :param dont_extend_height: When `True`, don't take up more width then the preferred height reported by the control. :param ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore the :class:`.UIContent` width when calculating the dimensions. :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore the :class:`.UIContent` height when calculating the dimensions. :param left_margins: A list of :class:`.Margin` instance to be displayed on the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin` can be one of them in order to show line numbers. :param right_margins: Like `left_margins`, but on the other side. :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the preferred amount of lines/columns to be always visible before/after the cursor. When both top and bottom are a very high number, the cursor will be centered vertically most of the time. :param allow_scroll_beyond_bottom: A `bool` or :class:`.Filter` instance. When True, allow scrolling so far, that the top part of the content is not visible anymore, while there is still empty space available at the bottom of the window. In the Vi editor for instance, this is possible. You will see tildes while the top part of the body is hidden. :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't scroll horizontally, but wrap lines instead. :param get_vertical_scroll: Callable that takes this window instance as input and returns a preferred vertical scroll. (When this is `None`, the scroll is only determined by the last and current cursor position.) :param get_horizontal_scroll: Callable that takes this window instance as input and returns a preferred vertical scroll. :param always_hide_cursor: A `bool` or :class:`.Filter` instance. When True, never display the cursor, even when the user control specifies a cursor position. :param cursorline: A `bool` or :class:`.Filter` instance. When True, display a cursorline. :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True, display a cursorcolumn. :param colorcolumns: A list of :class:`.ColorColumn` instances that describe the columns to be highlighted, or a callable that returns such a list. :param align: :class:`.WindowAlign` value or callable that returns an :class:`.WindowAlign` value. alignment of content. :param style: A style string. Style to be applied to all the cells in this window. (This can be a callable that returns a string.) :param char: (string) Character to be used for filling the background. This can also be a callable that returns a character. :param get_line_prefix: None or a callable that returns formatted text to be inserted before a line. It takes a line number (int) and a wrap_count and returns formatted text. This can be used for implementation of line continuations, things like Vim "breakindent" and so on. """ def __init__( self, content: UIControl | None = None, width: AnyDimension = None, height: AnyDimension = None, z_index: int | None = None, dont_extend_width: FilterOrBool = False, dont_extend_height: FilterOrBool = False, ignore_content_width: FilterOrBool = False, ignore_content_height: FilterOrBool = False, left_margins: Sequence[Margin] | None = None, right_margins: Sequence[Margin] | None = None, scroll_offsets: ScrollOffsets | None = None, allow_scroll_beyond_bottom: FilterOrBool = False, wrap_lines: FilterOrBool = False, get_vertical_scroll: Callable[[Window], int] | None = None, get_horizontal_scroll: Callable[[Window], int] | None = None, always_hide_cursor: FilterOrBool = False, cursorline: FilterOrBool = False, cursorcolumn: FilterOrBool = False, colorcolumns: ( None | list[ColorColumn] | Callable[[], list[ColorColumn]] ) = None, align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, style: str | Callable[[], str] = "", char: None | str | Callable[[], str] = None, get_line_prefix: GetLinePrefixCallable | None = None, ) -> None: self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) self.always_hide_cursor = to_filter(always_hide_cursor) self.wrap_lines = to_filter(wrap_lines) self.cursorline = to_filter(cursorline) self.cursorcolumn = to_filter(cursorcolumn) self.content = content or DummyControl() self.dont_extend_width = to_filter(dont_extend_width) self.dont_extend_height = to_filter(dont_extend_height) self.ignore_content_width = to_filter(ignore_content_width) self.ignore_content_height = to_filter(ignore_content_height) self.left_margins = left_margins or [] self.right_margins = right_margins or [] self.scroll_offsets = scroll_offsets or ScrollOffsets() self.get_vertical_scroll = get_vertical_scroll self.get_horizontal_scroll = get_horizontal_scroll self.colorcolumns = colorcolumns or [] self.align = align self.style = style self.char = char self.get_line_prefix = get_line_prefix self.width = width self.height = height self.z_index = z_index # Cache for the screens generated by the margin. self._ui_content_cache: SimpleCache[tuple[int, int, int], UIContent] = ( SimpleCache(maxsize=8) ) self._margin_width_cache: SimpleCache[tuple[Margin, int], int] = SimpleCache( maxsize=1 ) self.reset() def __repr__(self) -> str: return f"Window(content={self.content!r})" def reset(self) -> None: self.content.reset() #: Scrolling position of the main content. self.vertical_scroll = 0 self.horizontal_scroll = 0 # Vertical scroll 2: this is the vertical offset that a line is # scrolled if a single line (the one that contains the cursor) consumes # all of the vertical space. self.vertical_scroll_2 = 0 #: Keep render information (mappings between buffer input and render #: output.) self.render_info: WindowRenderInfo | None = None def _get_margin_width(self, margin: Margin) -> int: """ Return the width for this margin. (Calculate only once per render time.) """ # Margin.get_width, needs to have a UIContent instance. def get_ui_content() -> UIContent: return self._get_ui_content(width=0, height=0) def get_width() -> int: return margin.get_width(get_ui_content) key = (margin, get_app().render_counter) return self._margin_width_cache.get(key, get_width) def _get_total_margin_width(self) -> int: """ Calculate and return the width of the margin (left + right). """ return sum(self._get_margin_width(m) for m in self.left_margins) + sum( self._get_margin_width(m) for m in self.right_margins ) def preferred_width(self, max_available_width: int) -> Dimension: """ Calculate the preferred width for this window. """ def preferred_content_width() -> int | None: """Content width: is only calculated if no exact width for the window was given.""" if self.ignore_content_width(): return None # Calculate the width of the margin. total_margin_width = self._get_total_margin_width() # Window of the content. (Can be `None`.) preferred_width = self.content.preferred_width( max_available_width - total_margin_width ) if preferred_width is not None: # Include width of the margins. preferred_width += total_margin_width return preferred_width # Merge. return self._merge_dimensions( dimension=to_dimension(self.width), get_preferred=preferred_content_width, dont_extend=self.dont_extend_width(), ) def preferred_height(self, width: int, max_available_height: int) -> Dimension: """ Calculate the preferred height for this window. """ def preferred_content_height() -> int | None: """Content height: is only calculated if no exact height for the window was given.""" if self.ignore_content_height(): return None total_margin_width = self._get_total_margin_width() wrap_lines = self.wrap_lines() return self.content.preferred_height( width - total_margin_width, max_available_height, wrap_lines, self.get_line_prefix, ) return self._merge_dimensions( dimension=to_dimension(self.height), get_preferred=preferred_content_height, dont_extend=self.dont_extend_height(), ) @staticmethod def _merge_dimensions( dimension: Dimension | None, get_preferred: Callable[[], int | None], dont_extend: bool = False, ) -> Dimension: """ Take the Dimension from this `Window` class and the received preferred size from the `UIControl` and return a `Dimension` to report to the parent container. """ dimension = dimension or Dimension() # When a preferred dimension was explicitly given to the Window, # ignore the UIControl. preferred: int | None if dimension.preferred_specified: preferred = dimension.preferred else: # Otherwise, calculate the preferred dimension from the UI control # content. preferred = get_preferred() # When a 'preferred' dimension is given by the UIControl, make sure # that it stays within the bounds of the Window. if preferred is not None: if dimension.max_specified: preferred = min(preferred, dimension.max) if dimension.min_specified: preferred = max(preferred, dimension.min) # When a `dont_extend` flag has been given, use the preferred dimension # also as the max dimension. max_: int | None min_: int | None if dont_extend and preferred is not None: max_ = min(dimension.max, preferred) else: max_ = dimension.max if dimension.max_specified else None min_ = dimension.min if dimension.min_specified else None return Dimension( min=min_, max=max_, preferred=preferred, weight=dimension.weight ) def _get_ui_content(self, width: int, height: int) -> UIContent: """ Create a `UIContent` instance. """ def get_content() -> UIContent: return self.content.create_content(width=width, height=height) key = (get_app().render_counter, width, height) return self._ui_content_cache.get(key, get_content) def _get_digraph_char(self) -> str | None: "Return `False`, or the Digraph symbol to be used." app = get_app() if app.quoted_insert: return "^" if app.vi_state.waiting_for_digraph: if app.vi_state.digraph_symbol1: return app.vi_state.digraph_symbol1 return "?" return None def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: """ Write window to screen. This renders the user control, the margins and copies everything over to the absolute position at the given screen. """ # If dont_extend_width/height was given. Then reduce width/height in # WritePosition if the parent wanted us to paint in a bigger area. # (This happens if this window is bundled with another window in a # HSplit/VSplit, but with different size requirements.) write_position = WritePosition( xpos=write_position.xpos, ypos=write_position.ypos, width=write_position.width, height=write_position.height, ) if self.dont_extend_width(): write_position.width = min( write_position.width, self.preferred_width(write_position.width).preferred, ) if self.dont_extend_height(): write_position.height = min( write_position.height, self.preferred_height( write_position.width, write_position.height ).preferred, ) # Draw z_index = z_index if self.z_index is None else self.z_index draw_func = partial( self._write_to_screen_at_index, screen, mouse_handlers, write_position, parent_style, erase_bg, ) if z_index is None or z_index <= 0: # When no z_index is given, draw right away. draw_func() else: # Otherwise, postpone. screen.draw_with_z_index(z_index=z_index, draw_func=draw_func) def _write_to_screen_at_index( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, ) -> None: # Don't bother writing invisible windows. # (We save some time, but also avoid applying last-line styling.) if write_position.height <= 0 or write_position.width <= 0: return # Calculate margin sizes. left_margin_widths = [self._get_margin_width(m) for m in self.left_margins] right_margin_widths = [self._get_margin_width(m) for m in self.right_margins] total_margin_width = sum(left_margin_widths + right_margin_widths) # Render UserControl. ui_content = self.content.create_content( write_position.width - total_margin_width, write_position.height ) assert isinstance(ui_content, UIContent) # Scroll content. wrap_lines = self.wrap_lines() self._scroll( ui_content, write_position.width - total_margin_width, write_position.height ) # Erase background and fill with `char`. self._fill_bg(screen, write_position, erase_bg) # Resolve `align` attribute. align = self.align() if callable(self.align) else self.align # Write body visible_line_to_row_col, rowcol_to_yx = self._copy_body( ui_content, screen, write_position, sum(left_margin_widths), write_position.width - total_margin_width, self.vertical_scroll, self.horizontal_scroll, wrap_lines=wrap_lines, highlight_lines=True, vertical_scroll_2=self.vertical_scroll_2, always_hide_cursor=self.always_hide_cursor(), has_focus=get_app().layout.current_control == self.content, align=align, get_line_prefix=self.get_line_prefix, ) # Remember render info. (Set before generating the margins. They need this.) x_offset = write_position.xpos + sum(left_margin_widths) y_offset = write_position.ypos render_info = WindowRenderInfo( window=self, ui_content=ui_content, horizontal_scroll=self.horizontal_scroll, vertical_scroll=self.vertical_scroll, window_width=write_position.width - total_margin_width, window_height=write_position.height, configured_scroll_offsets=self.scroll_offsets, visible_line_to_row_col=visible_line_to_row_col, rowcol_to_yx=rowcol_to_yx, x_offset=x_offset, y_offset=y_offset, wrap_lines=wrap_lines, ) self.render_info = render_info # Set mouse handlers. def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone: """ Wrapper around the mouse_handler of the `UIControl` that turns screen coordinates into line coordinates. Returns `NotImplemented` if no UI invalidation should be done. """ # Don't handle mouse events outside of the current modal part of # the UI. if self not in get_app().layout.walk_through_modal_area(): return NotImplemented # Find row/col position first. yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()} y = mouse_event.position.y x = mouse_event.position.x # If clicked below the content area, look for a position in the # last line instead. max_y = write_position.ypos + len(visible_line_to_row_col) - 1 y = min(max_y, y) result: NotImplementedOrNone while x >= 0: try: row, col = yx_to_rowcol[y, x] except KeyError: # Try again. (When clicking on the right side of double # width characters, or on the right side of the input.) x -= 1 else: # Found position, call handler of UIControl. result = self.content.mouse_handler( MouseEvent( position=Point(x=col, y=row), event_type=mouse_event.event_type, button=mouse_event.button, modifiers=mouse_event.modifiers, ) ) break else: # nobreak. # (No x/y coordinate found for the content. This happens in # case of a DummyControl, that does not have any content. # Report (0,0) instead.) result = self.content.mouse_handler( MouseEvent( position=Point(x=0, y=0), event_type=mouse_event.event_type, button=mouse_event.button, modifiers=mouse_event.modifiers, ) ) # If it returns NotImplemented, handle it here. if result == NotImplemented: result = self._mouse_handler(mouse_event) return result mouse_handlers.set_mouse_handler_for_range( x_min=write_position.xpos + sum(left_margin_widths), x_max=write_position.xpos + write_position.width - total_margin_width, y_min=write_position.ypos, y_max=write_position.ypos + write_position.height, handler=mouse_handler, ) # Render and copy margins. move_x = 0 def render_margin(m: Margin, width: int) -> UIContent: "Render margin. Return `Screen`." # Retrieve margin fragments. fragments = m.create_margin(render_info, width, write_position.height) # Turn it into a UIContent object. # already rendered those fragments using this size.) return FormattedTextControl(fragments).create_content( width + 1, write_position.height ) for m, width in zip(self.left_margins, left_margin_widths): if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.) # Create screen for margin. margin_content = render_margin(m, width) # Copy and shift X. self._copy_margin(margin_content, screen, write_position, move_x, width) move_x += width move_x = write_position.width - sum(right_margin_widths) for m, width in zip(self.right_margins, right_margin_widths): # Create screen for margin. margin_content = render_margin(m, width) # Copy and shift X. self._copy_margin(margin_content, screen, write_position, move_x, width) move_x += width # Apply 'self.style' self._apply_style(screen, write_position, parent_style) # Tell the screen that this user control has been painted at this # position. screen.visible_windows_to_write_positions[self] = write_position def _copy_body( self, ui_content: UIContent, new_screen: Screen, write_position: WritePosition, move_x: int, width: int, vertical_scroll: int = 0, horizontal_scroll: int = 0, wrap_lines: bool = False, highlight_lines: bool = False, vertical_scroll_2: int = 0, always_hide_cursor: bool = False, has_focus: bool = False, align: WindowAlign = WindowAlign.LEFT, get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None, ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]: """ Copy the UIContent into the output screen. Return (visible_line_to_row_col, rowcol_to_yx) tuple. :param get_line_prefix: None or a callable that takes a line number (int) and a wrap_count (int) and returns formatted text. """ xpos = write_position.xpos + move_x ypos = write_position.ypos line_count = ui_content.line_count new_buffer = new_screen.data_buffer empty_char = _CHAR_CACHE["", ""] # Map visible line number to (row, col) of input. # 'col' will always be zero if line wrapping is off. visible_line_to_row_col: dict[int, tuple[int, int]] = {} # Maps (row, col) from the input to (y, x) screen coordinates. rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {} def copy_line( line: StyleAndTextTuples, lineno: int, x: int, y: int, is_input: bool = False, ) -> tuple[int, int]: """ Copy over a single line to the output screen. This can wrap over multiple lines in the output. It will call the prefix (prompt) function before every line. """ if is_input: current_rowcol_to_yx = rowcol_to_yx else: current_rowcol_to_yx = {} # Throwaway dictionary. # Draw line prefix. if is_input and get_line_prefix: prompt = to_formatted_text(get_line_prefix(lineno, 0)) x, y = copy_line(prompt, lineno, x, y, is_input=False) # Scroll horizontally. skipped = 0 # Characters skipped because of horizontal scrolling. if horizontal_scroll and is_input: h_scroll = horizontal_scroll line = explode_text_fragments(line) while h_scroll > 0 and line: h_scroll -= get_cwidth(line[0][1]) skipped += 1 del line[:1] # Remove first character. x -= h_scroll # When scrolling over double width character, # this can end up being negative. # Align this line. (Note that this doesn't work well when we use # get_line_prefix and that function returns variable width prefixes.) if align == WindowAlign.CENTER: line_width = fragment_list_width(line) if line_width < width: x += (width - line_width) // 2 elif align == WindowAlign.RIGHT: line_width = fragment_list_width(line) if line_width < width: x += width - line_width col = 0 wrap_count = 0 for style, text, *_ in line: new_buffer_row = new_buffer[y + ypos] # Remember raw VT escape sequences. (E.g. FinalTerm's # escape sequences.) if "[ZeroWidthEscape]" in style: new_screen.zero_width_escapes[y + ypos][x + xpos] += text continue for c in text: char = _CHAR_CACHE[c, style] char_width = char.width # Wrap when the line width is exceeded. if wrap_lines and x + char_width > width: visible_line_to_row_col[y + 1] = ( lineno, visible_line_to_row_col[y][1] + x, ) y += 1 wrap_count += 1 x = 0 # Insert line prefix (continuation prompt). if is_input and get_line_prefix: prompt = to_formatted_text( get_line_prefix(lineno, wrap_count) ) x, y = copy_line(prompt, lineno, x, y, is_input=False) new_buffer_row = new_buffer[y + ypos] if y >= write_position.height: return x, y # Break out of all for loops. # Set character in screen and shift 'x'. if x >= 0 and y >= 0 and x < width: new_buffer_row[x + xpos] = char # When we print a multi width character, make sure # to erase the neighbors positions in the screen. # (The empty string if different from everything, # so next redraw this cell will repaint anyway.) if char_width > 1: for i in range(1, char_width): new_buffer_row[x + xpos + i] = empty_char # If this is a zero width characters, then it's # probably part of a decomposed unicode character. # See: https://en.wikipedia.org/wiki/Unicode_equivalence # Merge it in the previous cell. elif char_width == 0: # Handle all character widths. If the previous # character is a multiwidth character, then # merge it two positions back. for pw in [2, 1]: # Previous character width. if ( x - pw >= 0 and new_buffer_row[x + xpos - pw].width == pw ): prev_char = new_buffer_row[x + xpos - pw] char2 = _CHAR_CACHE[ prev_char.char + c, prev_char.style ] new_buffer_row[x + xpos - pw] = char2 # Keep track of write position for each character. current_rowcol_to_yx[lineno, col + skipped] = ( y + ypos, x + xpos, ) col += 1 x += char_width return x, y # Copy content. def copy() -> int: y = -vertical_scroll_2 lineno = vertical_scroll while y < write_position.height and lineno < line_count: # Take the next line and copy it in the real screen. line = ui_content.get_line(lineno) visible_line_to_row_col[y] = (lineno, horizontal_scroll) # Copy margin and actual line. x = 0 x, y = copy_line(line, lineno, x, y, is_input=True) lineno += 1 y += 1 return y copy() def cursor_pos_to_screen_pos(row: int, col: int) -> Point: "Translate row/col from UIContent to real Screen coordinates." try: y, x = rowcol_to_yx[row, col] except KeyError: # Normally this should never happen. (It is a bug, if it happens.) # But to be sure, return (0, 0) return Point(x=0, y=0) # raise ValueError( # 'Invalid position. row=%r col=%r, vertical_scroll=%r, ' # 'horizontal_scroll=%r, height=%r' % # (row, col, vertical_scroll, horizontal_scroll, write_position.height)) else: return Point(x=x, y=y) # Set cursor and menu positions. if ui_content.cursor_position: screen_cursor_position = cursor_pos_to_screen_pos( ui_content.cursor_position.y, ui_content.cursor_position.x ) if has_focus: new_screen.set_cursor_position(self, screen_cursor_position) if always_hide_cursor: new_screen.show_cursor = False else: new_screen.show_cursor = ui_content.show_cursor self._highlight_digraph(new_screen) if highlight_lines: self._highlight_cursorlines( new_screen, screen_cursor_position, xpos, ypos, width, write_position.height, ) # Draw input characters from the input processor queue. if has_focus and ui_content.cursor_position: self._show_key_processor_key_buffer(new_screen) # Set menu position. if ui_content.menu_position: new_screen.set_menu_position( self, cursor_pos_to_screen_pos( ui_content.menu_position.y, ui_content.menu_position.x ), ) # Update output screen height. new_screen.height = max(new_screen.height, ypos + write_position.height) return visible_line_to_row_col, rowcol_to_yx def _fill_bg( self, screen: Screen, write_position: WritePosition, erase_bg: bool ) -> None: """ Erase/fill the background. (Useful for floats and when a `char` has been given.) """ char: str | None if callable(self.char): char = self.char() else: char = self.char if erase_bg or char: wp = write_position char_obj = _CHAR_CACHE[char or " ", ""] for y in range(wp.ypos, wp.ypos + wp.height): row = screen.data_buffer[y] for x in range(wp.xpos, wp.xpos + wp.width): row[x] = char_obj def _apply_style( self, new_screen: Screen, write_position: WritePosition, parent_style: str ) -> None: # Apply `self.style`. style = parent_style + " " + to_str(self.style) new_screen.fill_area(write_position, style=style, after=False) # Apply the 'last-line' class to the last line of each Window. This can # be used to apply an 'underline' to the user control. wp = WritePosition( write_position.xpos, write_position.ypos + write_position.height - 1, write_position.width, 1, ) new_screen.fill_area(wp, "class:last-line", after=True) def _highlight_digraph(self, new_screen: Screen) -> None: """ When we are in Vi digraph mode, put a question mark underneath the cursor. """ digraph_char = self._get_digraph_char() if digraph_char: cpos = new_screen.get_cursor_position(self) new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ digraph_char, "class:digraph" ] def _show_key_processor_key_buffer(self, new_screen: Screen) -> None: """ When the user is typing a key binding that consists of several keys, display the last pressed key if the user is in insert mode and the key is meaningful to be displayed. E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the first 'j' needs to be displayed in order to get some feedback. """ app = get_app() key_buffer = app.key_processor.key_buffer if key_buffer and _in_insert_mode() and not app.is_done: # The textual data for the given key. (Can be a VT100 escape # sequence.) data = key_buffer[-1].data # Display only if this is a 1 cell width character. if get_cwidth(data) == 1: cpos = new_screen.get_cursor_position(self) new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ data, "class:partial-key-binding" ] def _highlight_cursorlines( self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int ) -> None: """ Highlight cursor row/column. """ cursor_line_style = " class:cursor-line " cursor_column_style = " class:cursor-column " data_buffer = new_screen.data_buffer # Highlight cursor line. if self.cursorline(): row = data_buffer[cpos.y] for x in range(x, x + width): original_char = row[x] row[x] = _CHAR_CACHE[ original_char.char, original_char.style + cursor_line_style ] # Highlight cursor column. if self.cursorcolumn(): for y2 in range(y, y + height): row = data_buffer[y2] original_char = row[cpos.x] row[cpos.x] = _CHAR_CACHE[ original_char.char, original_char.style + cursor_column_style ] # Highlight color columns colorcolumns = self.colorcolumns if callable(colorcolumns): colorcolumns = colorcolumns() for cc in colorcolumns: assert isinstance(cc, ColorColumn) column = cc.position if column < x + width: # Only draw when visible. color_column_style = " " + cc.style for y2 in range(y, y + height): row = data_buffer[y2] original_char = row[column + x] row[column + x] = _CHAR_CACHE[ original_char.char, original_char.style + color_column_style ] def _copy_margin( self, margin_content: UIContent, new_screen: Screen, write_position: WritePosition, move_x: int, width: int, ) -> None: """ Copy characters from the margin screen to the real screen. """ xpos = write_position.xpos + move_x ypos = write_position.ypos margin_write_position = WritePosition(xpos, ypos, width, write_position.height) self._copy_body(margin_content, new_screen, margin_write_position, 0, width) def _scroll(self, ui_content: UIContent, width: int, height: int) -> None: """ Scroll body. Ensure that the cursor is visible. """ if self.wrap_lines(): func = self._scroll_when_linewrapping else: func = self._scroll_without_linewrapping func(ui_content, width, height) def _scroll_when_linewrapping( self, ui_content: UIContent, width: int, height: int ) -> None: """ Scroll to make sure the cursor position is visible and that we maintain the requested scroll offset. Set `self.horizontal_scroll/vertical_scroll`. """ scroll_offsets_bottom = self.scroll_offsets.bottom scroll_offsets_top = self.scroll_offsets.top # We don't have horizontal scrolling. self.horizontal_scroll = 0 def get_line_height(lineno: int) -> int: return ui_content.get_height_for_line(lineno, width, self.get_line_prefix) # When there is no space, reset `vertical_scroll_2` to zero and abort. # This can happen if the margin is bigger than the window width. # Otherwise the text height will become "infinite" (a big number) and # the copy_line will spend a huge amount of iterations trying to render # nothing. if width <= 0: self.vertical_scroll = ui_content.cursor_position.y self.vertical_scroll_2 = 0 return # If the current line consumes more than the whole window height, # then we have to scroll vertically inside this line. (We don't take # the scroll offsets into account for this.) # Also, ignore the scroll offsets in this case. Just set the vertical # scroll to this line. line_height = get_line_height(ui_content.cursor_position.y) if line_height > height - scroll_offsets_top: # Calculate the height of the text before the cursor (including # line prefixes). text_before_height = ui_content.get_height_for_line( ui_content.cursor_position.y, width, self.get_line_prefix, slice_stop=ui_content.cursor_position.x, ) # Adjust scroll offset. self.vertical_scroll = ui_content.cursor_position.y self.vertical_scroll_2 = min( text_before_height - 1, # Keep the cursor visible. line_height - height, # Avoid blank lines at the bottom when scrolling up again. self.vertical_scroll_2, ) self.vertical_scroll_2 = max( 0, text_before_height - height, self.vertical_scroll_2 ) return else: self.vertical_scroll_2 = 0 # Current line doesn't consume the whole height. Take scroll offsets into account. def get_min_vertical_scroll() -> int: # Make sure that the cursor line is not below the bottom. # (Calculate how many lines can be shown between the cursor and the .) used_height = 0 prev_lineno = ui_content.cursor_position.y for lineno in range(ui_content.cursor_position.y, -1, -1): used_height += get_line_height(lineno) if used_height > height - scroll_offsets_bottom: return prev_lineno else: prev_lineno = lineno return 0 def get_max_vertical_scroll() -> int: # Make sure that the cursor line is not above the top. prev_lineno = ui_content.cursor_position.y used_height = 0 for lineno in range(ui_content.cursor_position.y - 1, -1, -1): used_height += get_line_height(lineno) if used_height > scroll_offsets_top: return prev_lineno else: prev_lineno = lineno return prev_lineno def get_topmost_visible() -> int: """ Calculate the upper most line that can be visible, while the bottom is still visible. We should not allow scroll more than this if `allow_scroll_beyond_bottom` is false. """ prev_lineno = ui_content.line_count - 1 used_height = 0 for lineno in range(ui_content.line_count - 1, -1, -1): used_height += get_line_height(lineno) if used_height > height: return prev_lineno else: prev_lineno = lineno return prev_lineno # Scroll vertically. (Make sure that the whole line which contains the # cursor is visible. topmost_visible = get_topmost_visible() # Note: the `min(topmost_visible, ...)` is to make sure that we # don't require scrolling up because of the bottom scroll offset, # when we are at the end of the document. self.vertical_scroll = max( self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()) ) self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll()) # Disallow scrolling beyond bottom? if not self.allow_scroll_beyond_bottom(): self.vertical_scroll = min(self.vertical_scroll, topmost_visible) def _scroll_without_linewrapping( self, ui_content: UIContent, width: int, height: int ) -> None: """ Scroll to make sure the cursor position is visible and that we maintain the requested scroll offset. Set `self.horizontal_scroll/vertical_scroll`. """ cursor_position = ui_content.cursor_position or Point(x=0, y=0) # Without line wrapping, we will never have to scroll vertically inside # a single line. self.vertical_scroll_2 = 0 if ui_content.line_count == 0: self.vertical_scroll = 0 self.horizontal_scroll = 0 return else: current_line_text = fragment_list_to_text( ui_content.get_line(cursor_position.y) ) def do_scroll( current_scroll: int, scroll_offset_start: int, scroll_offset_end: int, cursor_pos: int, window_size: int, content_size: int, ) -> int: "Scrolling algorithm. Used for both horizontal and vertical scrolling." # Calculate the scroll offset to apply. # This can obviously never be more than have the screen size. Also, when the # cursor appears at the top or bottom, we don't apply the offset. scroll_offset_start = int( min(scroll_offset_start, window_size / 2, cursor_pos) ) scroll_offset_end = int( min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos) ) # Prevent negative scroll offsets. if current_scroll < 0: current_scroll = 0 # Scroll back if we scrolled to much and there's still space to show more of the document. if ( not self.allow_scroll_beyond_bottom() and current_scroll > content_size - window_size ): current_scroll = max(0, content_size - window_size) # Scroll up if cursor is before visible part. if current_scroll > cursor_pos - scroll_offset_start: current_scroll = max(0, cursor_pos - scroll_offset_start) # Scroll down if cursor is after visible part. if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end: current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end return current_scroll # When a preferred scroll is given, take that first into account. if self.get_vertical_scroll: self.vertical_scroll = self.get_vertical_scroll(self) assert isinstance(self.vertical_scroll, int) if self.get_horizontal_scroll: self.horizontal_scroll = self.get_horizontal_scroll(self) assert isinstance(self.horizontal_scroll, int) # Update horizontal/vertical scroll to make sure that the cursor # remains visible. offsets = self.scroll_offsets self.vertical_scroll = do_scroll( current_scroll=self.vertical_scroll, scroll_offset_start=offsets.top, scroll_offset_end=offsets.bottom, cursor_pos=ui_content.cursor_position.y, window_size=height, content_size=ui_content.line_count, ) if self.get_line_prefix: current_line_prefix_width = fragment_list_width( to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0)) ) else: current_line_prefix_width = 0 self.horizontal_scroll = do_scroll( current_scroll=self.horizontal_scroll, scroll_offset_start=offsets.left, scroll_offset_end=offsets.right, cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]), window_size=width - current_line_prefix_width, # We can only analyze the current line. Calculating the width off # all the lines is too expensive. content_size=max( get_cwidth(current_line_text), self.horizontal_scroll + width ), ) def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: """ Mouse handler. Called when the UI control doesn't handle this particular event. Return `NotImplemented` if nothing was done as a consequence of this key binding (no UI invalidate required in that case). """ if mouse_event.event_type == MouseEventType.SCROLL_DOWN: self._scroll_down() return None elif mouse_event.event_type == MouseEventType.SCROLL_UP: self._scroll_up() return None return NotImplemented def _scroll_down(self) -> None: "Scroll window down." info = self.render_info if info is None: return if self.vertical_scroll < info.content_height - info.window_height: if info.cursor_position.y <= info.configured_scroll_offsets.top: self.content.move_cursor_down() self.vertical_scroll += 1 def _scroll_up(self) -> None: "Scroll window up." info = self.render_info if info is None: return if info.vertical_scroll > 0: # TODO: not entirely correct yet in case of line wrapping and long lines. if ( info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom ): self.content.move_cursor_up() self.vertical_scroll -= 1 def get_key_bindings(self) -> KeyBindingsBase | None: return self.content.get_key_bindings() def get_children(self) -> list[Container]: return [] class ConditionalContainer(Container): """ Wrapper around any other container that can change the visibility. The received `filter` determines whether the given container should be displayed or not. :param content: :class:`.Container` instance. :param filter: :class:`.Filter` instance. """ def __init__( self, content: AnyContainer, filter: FilterOrBool, alternative_content: AnyContainer | None = None, ) -> None: self.content = to_container(content) self.alternative_content = ( to_container(alternative_content) if alternative_content is not None else None ) self.filter = to_filter(filter) def __repr__(self) -> str: return f"ConditionalContainer({self.content!r}, filter={self.filter!r})" def reset(self) -> None: self.content.reset() def preferred_width(self, max_available_width: int) -> Dimension: if self.filter(): return self.content.preferred_width(max_available_width) elif self.alternative_content is not None: return self.alternative_content.preferred_width(max_available_width) else: return Dimension.zero() def preferred_height(self, width: int, max_available_height: int) -> Dimension: if self.filter(): return self.content.preferred_height(width, max_available_height) elif self.alternative_content is not None: return self.alternative_content.preferred_height( width, max_available_height ) else: return Dimension.zero() def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: if self.filter(): return self.content.write_to_screen( screen, mouse_handlers, write_position, parent_style, erase_bg, z_index ) elif self.alternative_content is not None: return self.alternative_content.write_to_screen( screen, mouse_handlers, write_position, parent_style, erase_bg, z_index, ) def get_children(self) -> list[Container]: result = [self.content] if self.alternative_content is not None: result.append(self.alternative_content) return result class DynamicContainer(Container): """ Container class that dynamically returns any Container. :param get_container: Callable that returns a :class:`.Container` instance or any widget with a ``__pt_container__`` method. """ def __init__(self, get_container: Callable[[], AnyContainer]) -> None: self.get_container = get_container def _get_container(self) -> Container: """ Return the current container object. We call `to_container`, because `get_container` can also return a widget with a ``__pt_container__`` method. """ obj = self.get_container() return to_container(obj) def reset(self) -> None: self._get_container().reset() def preferred_width(self, max_available_width: int) -> Dimension: return self._get_container().preferred_width(max_available_width) def preferred_height(self, width: int, max_available_height: int) -> Dimension: return self._get_container().preferred_height(width, max_available_height) def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: self._get_container().write_to_screen( screen, mouse_handlers, write_position, parent_style, erase_bg, z_index ) def is_modal(self) -> bool: return False def get_key_bindings(self) -> KeyBindingsBase | None: # Key bindings will be collected when `layout.walk()` finds the child # container. return None def get_children(self) -> list[Container]: # Here we have to return the current active container itself, not its # children. Otherwise, we run into issues where `layout.walk()` will # never see an object of type `Window` if this contains a window. We # can't/shouldn't proxy the "isinstance" check. return [self._get_container()] def to_container(container: AnyContainer) -> Container: """ Make sure that the given object is a :class:`.Container`. """ if isinstance(container, Container): return container elif hasattr(container, "__pt_container__"): return to_container(container.__pt_container__()) else: raise ValueError(f"Not a container object: {container!r}") def to_window(container: AnyContainer) -> Window: """ Make sure that the given argument is a :class:`.Window`. """ if isinstance(container, Window): return container elif hasattr(container, "__pt_container__"): return to_window(cast("MagicContainer", container).__pt_container__()) else: raise ValueError(f"Not a Window object: {container!r}.") def is_container(value: object) -> TypeGuard[AnyContainer]: """ Checks whether the given value is a container object (for use in assert statements). """ if isinstance(value, Container): return True if hasattr(value, "__pt_container__"): return is_container(cast("MagicContainer", value).__pt_container__()) return False ================================================ FILE: src/prompt_toolkit/layout/controls.py ================================================ """ User interface Controls for the layout. """ from __future__ import annotations import time from abc import ABCMeta, abstractmethod from collections.abc import Callable, Hashable, Iterable from typing import TYPE_CHECKING, NamedTuple from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer from prompt_toolkit.cache import SimpleCache from prompt_toolkit.data_structures import Point from prompt_toolkit.document import Document from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.formatted_text import ( AnyFormattedText, StyleAndTextTuples, to_formatted_text, ) from prompt_toolkit.formatted_text.utils import ( fragment_list_to_text, fragment_list_width, split_lines, ) from prompt_toolkit.lexers import Lexer, SimpleLexer from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType from prompt_toolkit.search import SearchState from prompt_toolkit.selection import SelectionType from prompt_toolkit.utils import get_cwidth from .processors import ( DisplayMultipleCursors, HighlightIncrementalSearchProcessor, HighlightSearchProcessor, HighlightSelectionProcessor, Processor, TransformationInput, merge_processors, ) if TYPE_CHECKING: from prompt_toolkit.key_binding.key_bindings import ( KeyBindingsBase, NotImplementedOrNone, ) from prompt_toolkit.utils import Event __all__ = [ "BufferControl", "SearchBufferControl", "DummyControl", "FormattedTextControl", "UIControl", "UIContent", ] GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] class UIControl(metaclass=ABCMeta): """ Base class for all user interface controls. """ def reset(self) -> None: # Default reset. (Doesn't have to be implemented.) pass def preferred_width(self, max_available_width: int) -> int | None: return None def preferred_height( self, width: int, max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, ) -> int | None: return None def is_focusable(self) -> bool: """ Tell whether this user control is focusable. """ return False @abstractmethod def create_content(self, width: int, height: int) -> UIContent: """ Generate the content for this user control. Returns a :class:`.UIContent` instance. """ def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: """ Handle mouse events. When `NotImplemented` is returned, it means that the given event is not handled by the `UIControl` itself. The `Window` or key bindings can decide to handle this event as scrolling or changing focus. :param mouse_event: `MouseEvent` instance. """ return NotImplemented def move_cursor_down(self) -> None: """ Request to move the cursor down. This happens when scrolling down and the cursor is completely at the top. """ def move_cursor_up(self) -> None: """ Request to move the cursor up. """ def get_key_bindings(self) -> KeyBindingsBase | None: """ The key bindings that are specific for this user control. Return a :class:`.KeyBindings` object if some key bindings are specified, or `None` otherwise. """ def get_invalidate_events(self) -> Iterable[Event[object]]: """ Return a list of `Event` objects. This can be a generator. (The application collects all these events, in order to bind redraw handlers to these events.) """ return [] class UIContent: """ Content generated by a user control. This content consists of a list of lines. :param get_line: Callable that takes a line number and returns the current line. This is a list of (style_str, text) tuples. :param line_count: The number of lines. :param cursor_position: a :class:`.Point` for the cursor position. :param menu_position: a :class:`.Point` for the menu position. :param show_cursor: Make the cursor visible. """ def __init__( self, get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []), line_count: int = 0, cursor_position: Point | None = None, menu_position: Point | None = None, show_cursor: bool = True, ): self.get_line = get_line self.line_count = line_count self.cursor_position = cursor_position or Point(x=0, y=0) self.menu_position = menu_position self.show_cursor = show_cursor # Cache for line heights. Maps cache key -> height self._line_heights_cache: dict[Hashable, int] = {} def __getitem__(self, lineno: int) -> StyleAndTextTuples: "Make it iterable (iterate line by line)." if lineno < self.line_count: return self.get_line(lineno) else: raise IndexError def get_height_for_line( self, lineno: int, width: int, get_line_prefix: GetLinePrefixCallable | None, slice_stop: int | None = None, ) -> int: """ Return the height that a given line would need if it is rendered in a space with the given width (using line wrapping). :param get_line_prefix: None or a `Window.get_line_prefix` callable that returns the prefix to be inserted before this line. :param slice_stop: Wrap only "line[:slice_stop]" and return that partial result. This is needed for scrolling the window correctly when line wrapping. :returns: The computed height. """ # Instead of using `get_line_prefix` as key, we use render_counter # instead. This is more reliable, because this function could still be # the same, while the content would change over time. key = get_app().render_counter, lineno, width, slice_stop try: return self._line_heights_cache[key] except KeyError: if width == 0: height = 10**8 else: # Calculate line width first. line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] text_width = get_cwidth(line) if get_line_prefix: # Add prefix width. text_width += fragment_list_width( to_formatted_text(get_line_prefix(lineno, 0)) ) # Slower path: compute path when there's a line prefix. height = 1 # Keep wrapping as long as the line doesn't fit. # Keep adding new prefixes for every wrapped line. while text_width > width: height += 1 text_width -= width fragments2 = to_formatted_text( get_line_prefix(lineno, height - 1) ) prefix_width = get_cwidth(fragment_list_to_text(fragments2)) if prefix_width >= width: # Prefix doesn't fit. height = 10**8 break text_width += prefix_width else: # Fast path: compute height when there's no line prefix. try: quotient, remainder = divmod(text_width, width) except ZeroDivisionError: height = 10**8 else: if remainder: quotient += 1 # Like math.ceil. height = max(1, quotient) # Cache and return self._line_heights_cache[key] = height return height class FormattedTextControl(UIControl): """ Control that displays formatted text. This can be either plain text, an :class:`~prompt_toolkit.formatted_text.HTML` object an :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str, text)`` tuples or a callable that takes no argument and returns one of those, depending on how you prefer to do the formatting. See ``prompt_toolkit.layout.formatted_text`` for more information. (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) When this UI control has the focus, the cursor will be shown in the upper left corner of this control by default. There are two ways for specifying the cursor position: - Pass a `get_cursor_position` function which returns a `Point` instance with the current cursor position. - If the (formatted) text is passed as a list of ``(style, text)`` tuples and there is one that looks like ``('[SetCursorPosition]', '')``, then this will specify the cursor position. Mouse support: The list of fragments can also contain tuples of three items, looking like: (style_str, text, handler). When mouse support is enabled and the user clicks on this fragment, then the given handler is called. That handler should accept two inputs: (Application, MouseEvent) and it should either handle the event or return `NotImplemented` in case we want the containing Window to handle this event. :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. :param text: Text or formatted text to be displayed. :param style: Style string applied to the content. (If you want to style the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the :class:`~prompt_toolkit.layout.Window` instead.) :param key_bindings: a :class:`.KeyBindings` object. :param get_cursor_position: A callable that returns the cursor position as a `Point` instance. """ def __init__( self, text: AnyFormattedText = "", style: str = "", focusable: FilterOrBool = False, key_bindings: KeyBindingsBase | None = None, show_cursor: bool = True, modal: bool = False, get_cursor_position: Callable[[], Point | None] | None = None, ) -> None: self.text = text # No type check on 'text'. This is done dynamically. self.style = style self.focusable = to_filter(focusable) # Key bindings. self.key_bindings = key_bindings self.show_cursor = show_cursor self.modal = modal self.get_cursor_position = get_cursor_position #: Cache for the content. self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18) self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache( maxsize=1 ) # Only cache one fragment list. We don't need the previous item. # Render info for the mouse support. self._fragments: StyleAndTextTuples | None = None def reset(self) -> None: self._fragments = None def is_focusable(self) -> bool: return self.focusable() def __repr__(self) -> str: return f"{self.__class__.__name__}({self.text!r})" def _get_formatted_text_cached(self) -> StyleAndTextTuples: """ Get fragments, but only retrieve fragments once during one render run. (This function is called several times during one rendering, because we also need those for calculating the dimensions.) """ return self._fragment_cache.get( get_app().render_counter, lambda: to_formatted_text(self.text, self.style) ) def preferred_width(self, max_available_width: int) -> int: """ Return the preferred width for this control. That is the width of the longest line. """ text = fragment_list_to_text(self._get_formatted_text_cached()) line_lengths = [get_cwidth(l) for l in text.split("\n")] return max(line_lengths) def preferred_height( self, width: int, max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, ) -> int | None: """ Return the preferred height for this control. """ content = self.create_content(width, None) if wrap_lines: height = 0 for i in range(content.line_count): height += content.get_height_for_line(i, width, get_line_prefix) if height >= max_available_height: return max_available_height return height else: return content.line_count def create_content(self, width: int, height: int | None) -> UIContent: # Get fragments fragments_with_mouse_handlers = self._get_formatted_text_cached() fragment_lines_with_mouse_handlers = list( split_lines(fragments_with_mouse_handlers) ) # Strip mouse handlers from fragments. fragment_lines: list[StyleAndTextTuples] = [ [(item[0], item[1]) for item in line] for line in fragment_lines_with_mouse_handlers ] # Keep track of the fragments with mouse handler, for later use in # `mouse_handler`. self._fragments = fragments_with_mouse_handlers # If there is a `[SetCursorPosition]` in the fragment list, set the # cursor position here. def get_cursor_position( fragment: str = "[SetCursorPosition]", ) -> Point | None: for y, line in enumerate(fragment_lines): x = 0 for style_str, text, *_ in line: if fragment in style_str: return Point(x=x, y=y) x += len(text) return None # If there is a `[SetMenuPosition]`, set the menu over here. def get_menu_position() -> Point | None: return get_cursor_position("[SetMenuPosition]") cursor_position = (self.get_cursor_position or get_cursor_position)() # Create content, or take it from the cache. key = (tuple(fragments_with_mouse_handlers), width, cursor_position) def get_content() -> UIContent: return UIContent( get_line=lambda i: fragment_lines[i], line_count=len(fragment_lines), show_cursor=self.show_cursor, cursor_position=cursor_position, menu_position=get_menu_position(), ) return self._content_cache.get(key, get_content) def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: """ Handle mouse events. (When the fragment list contained mouse handlers and the user clicked on on any of these, the matching handler is called. This handler can still return `NotImplemented` in case we want the :class:`~prompt_toolkit.layout.Window` to handle this particular event.) """ if self._fragments: # Read the generator. fragments_for_line = list(split_lines(self._fragments)) try: fragments = fragments_for_line[mouse_event.position.y] except IndexError: return NotImplemented else: # Find position in the fragment list. xpos = mouse_event.position.x # Find mouse handler for this character. count = 0 for item in fragments: count += len(item[1]) if count > xpos: if len(item) >= 3: # Handler found. Call it. # (Handler can return NotImplemented, so return # that result.) handler = item[2] return handler(mouse_event) else: break # Otherwise, don't handle here. return NotImplemented def is_modal(self) -> bool: return self.modal def get_key_bindings(self) -> KeyBindingsBase | None: return self.key_bindings class DummyControl(UIControl): """ A dummy control object that doesn't paint any content. Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The `fragment` and `char` attributes of the `Window` class can be used to define the filling.) """ def create_content(self, width: int, height: int) -> UIContent: def get_line(i: int) -> StyleAndTextTuples: return [] return UIContent(get_line=get_line, line_count=100**100) # Something very big. def is_focusable(self) -> bool: return False class _ProcessedLine(NamedTuple): fragments: StyleAndTextTuples source_to_display: Callable[[int], int] display_to_source: Callable[[int], int] class BufferControl(UIControl): """ Control for visualizing the content of a :class:`.Buffer`. :param buffer: The :class:`.Buffer` object to be displayed. :param input_processors: A list of :class:`~prompt_toolkit.layout.processors.Processor` objects. :param include_default_input_processors: When True, include the default processors for highlighting of selection, search and displaying of multiple cursors. :param lexer: :class:`.Lexer` instance for syntax highlighting. :param preview_search: `bool` or :class:`.Filter`: Show search while typing. When this is `True`, probably you want to add a ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the cursor position will move, but the text won't be highlighted. :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. :param focus_on_click: Focus this buffer when it's click, but not yet focused. :param key_bindings: a :class:`.KeyBindings` object. """ def __init__( self, buffer: Buffer | None = None, input_processors: list[Processor] | None = None, include_default_input_processors: bool = True, lexer: Lexer | None = None, preview_search: FilterOrBool = False, focusable: FilterOrBool = True, search_buffer_control: ( None | SearchBufferControl | Callable[[], SearchBufferControl] ) = None, menu_position: Callable[[], int | None] | None = None, focus_on_click: FilterOrBool = False, key_bindings: KeyBindingsBase | None = None, ): self.input_processors = input_processors self.include_default_input_processors = include_default_input_processors self.default_input_processors = [ HighlightSearchProcessor(), HighlightIncrementalSearchProcessor(), HighlightSelectionProcessor(), DisplayMultipleCursors(), ] self.preview_search = to_filter(preview_search) self.focusable = to_filter(focusable) self.focus_on_click = to_filter(focus_on_click) self.buffer = buffer or Buffer() self.menu_position = menu_position self.lexer = lexer or SimpleLexer() self.key_bindings = key_bindings self._search_buffer_control = search_buffer_control #: Cache for the lexer. #: Often, due to cursor movement, undo/redo and window resizing #: operations, it happens that a short time, the same document has to be #: lexed. This is a fairly easy way to cache such an expensive operation. self._fragment_cache: SimpleCache[ Hashable, Callable[[int], StyleAndTextTuples] ] = SimpleCache(maxsize=8) self._last_click_timestamp: float | None = None self._last_get_processed_line: Callable[[int], _ProcessedLine] | None = None def __repr__(self) -> str: return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>" @property def search_buffer_control(self) -> SearchBufferControl | None: result: SearchBufferControl | None if callable(self._search_buffer_control): result = self._search_buffer_control() else: result = self._search_buffer_control assert result is None or isinstance(result, SearchBufferControl) return result @property def search_buffer(self) -> Buffer | None: control = self.search_buffer_control if control is not None: return control.buffer return None @property def search_state(self) -> SearchState: """ Return the `SearchState` for searching this `BufferControl`. This is always associated with the search control. If one search bar is used for searching multiple `BufferControls`, then they share the same `SearchState`. """ search_buffer_control = self.search_buffer_control if search_buffer_control: return search_buffer_control.searcher_search_state else: return SearchState() def is_focusable(self) -> bool: return self.focusable() def preferred_width(self, max_available_width: int) -> int | None: """ This should return the preferred width. Note: We don't specify a preferred width according to the content, because it would be too expensive. Calculating the preferred width can be done by calculating the longest line, but this would require applying all the processors to each line. This is unfeasible for a larger document, and doing it for small documents only would result in inconsistent behavior. """ return None def preferred_height( self, width: int, max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, ) -> int | None: # Calculate the content height, if it was drawn on a screen with the # given width. height = 0 content = self.create_content(width, height=1) # Pass a dummy '1' as height. # When line wrapping is off, the height should be equal to the amount # of lines. if not wrap_lines: return content.line_count # When the number of lines exceeds the max_available_height, just # return max_available_height. No need to calculate anything. if content.line_count >= max_available_height: return max_available_height for i in range(content.line_count): height += content.get_height_for_line(i, width, get_line_prefix) if height >= max_available_height: return max_available_height return height def _get_formatted_text_for_line_func( self, document: Document ) -> Callable[[int], StyleAndTextTuples]: """ Create a function that returns the fragments for a given line. """ # Cache using `document.text`. def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]: return self.lexer.lex_document(document) key = (document.text, self.lexer.invalidation_hash()) return self._fragment_cache.get(key, get_formatted_text_for_line) def _create_get_processed_line_func( self, document: Document, width: int, height: int ) -> Callable[[int], _ProcessedLine]: """ Create a function that takes a line number of the current document and returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) tuple. """ # Merge all input processors together. input_processors = self.input_processors or [] if self.include_default_input_processors: input_processors = self.default_input_processors + input_processors merged_processor = merge_processors(input_processors) def transform( lineno: int, fragments: StyleAndTextTuples, get_line: Callable[[int], StyleAndTextTuples], ) -> _ProcessedLine: "Transform the fragments for a given line number." # Get cursor position at this line. def source_to_display(i: int) -> int: """X position from the buffer to the x position in the processed fragment list. By default, we start from the 'identity' operation.""" return i transformation = merged_processor.apply_transformation( TransformationInput( self, document, lineno, source_to_display, fragments, width, height, get_line, ) ) return _ProcessedLine( transformation.fragments, transformation.source_to_display, transformation.display_to_source, ) def create_func() -> Callable[[int], _ProcessedLine]: get_line = self._get_formatted_text_for_line_func(document) cache: dict[int, _ProcessedLine] = {} def get_processed_line(i: int) -> _ProcessedLine: try: return cache[i] except KeyError: processed_line = transform(i, get_line(i), get_line) cache[i] = processed_line return processed_line return get_processed_line return create_func() def create_content( self, width: int, height: int, preview_search: bool = False ) -> UIContent: """ Create a UIContent. """ buffer = self.buffer # Trigger history loading of the buffer. We do this during the # rendering of the UI here, because it needs to happen when an # `Application` with its event loop is running. During the rendering of # the buffer control is the earliest place we can achieve this, where # we're sure the right event loop is active, and don't require user # interaction (like in a key binding). buffer.load_history_if_not_yet_loaded() # Get the document to be shown. If we are currently searching (the # search buffer has focus, and the preview_search filter is enabled), # then use the search document, which has possibly a different # text/cursor position.) search_control = self.search_buffer_control preview_now = preview_search or bool( # Only if this feature is enabled. self.preview_search() and # And something was typed in the associated search field. search_control and search_control.buffer.text and # And we are searching in this control. (Many controls can point to # the same search field, like in Pyvim.) get_app().layout.search_target_buffer_control == self ) if preview_now and search_control is not None: ss = self.search_state document = buffer.document_for_search( SearchState( text=search_control.buffer.text, direction=ss.direction, ignore_case=ss.ignore_case, ) ) else: document = buffer.document get_processed_line = self._create_get_processed_line_func( document, width, height ) self._last_get_processed_line = get_processed_line def translate_rowcol(row: int, col: int) -> Point: "Return the content column for this coordinate." return Point(x=get_processed_line(row).source_to_display(col), y=row) def get_line(i: int) -> StyleAndTextTuples: "Return the fragments for a given line number." fragments = get_processed_line(i).fragments # Add a space at the end, because that is a possible cursor # position. (When inserting after the input.) We should do this on # all the lines, not just the line containing the cursor. (Because # otherwise, line wrapping/scrolling could change when moving the # cursor around.) fragments = fragments + [("", " ")] return fragments content = UIContent( get_line=get_line, line_count=document.line_count, cursor_position=translate_rowcol( document.cursor_position_row, document.cursor_position_col ), ) # If there is an auto completion going on, use that start point for a # pop-up menu position. (But only when this buffer has the focus -- # there is only one place for a menu, determined by the focused buffer.) if get_app().layout.current_control == self: menu_position = self.menu_position() if self.menu_position else None if menu_position is not None: assert isinstance(menu_position, int) menu_row, menu_col = buffer.document.translate_index_to_position( menu_position ) content.menu_position = translate_rowcol(menu_row, menu_col) elif buffer.complete_state: # Position for completion menu. # Note: We use 'min', because the original cursor position could be # behind the input string when the actual completion is for # some reason shorter than the text we had before. (A completion # can change and shorten the input.) menu_row, menu_col = buffer.document.translate_index_to_position( min( buffer.cursor_position, buffer.complete_state.original_document.cursor_position, ) ) content.menu_position = translate_rowcol(menu_row, menu_col) else: content.menu_position = None return content def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: """ Mouse handler for this control. """ buffer = self.buffer position = mouse_event.position # Focus buffer when clicked. if get_app().layout.current_control == self: if self._last_get_processed_line: processed_line = self._last_get_processed_line(position.y) # Translate coordinates back to the cursor position of the # original input. xpos = processed_line.display_to_source(position.x) index = buffer.document.translate_row_col_to_index(position.y, xpos) # Set the cursor position. if mouse_event.event_type == MouseEventType.MOUSE_DOWN: buffer.exit_selection() buffer.cursor_position = index elif ( mouse_event.event_type == MouseEventType.MOUSE_MOVE and mouse_event.button != MouseButton.NONE ): # Click and drag to highlight a selection if ( buffer.selection_state is None and abs(buffer.cursor_position - index) > 0 ): buffer.start_selection(selection_type=SelectionType.CHARACTERS) buffer.cursor_position = index elif mouse_event.event_type == MouseEventType.MOUSE_UP: # When the cursor was moved to another place, select the text. # (The >1 is actually a small but acceptable workaround for # selecting text in Vi navigation mode. In navigation mode, # the cursor can never be after the text, so the cursor # will be repositioned automatically.) if abs(buffer.cursor_position - index) > 1: if buffer.selection_state is None: buffer.start_selection( selection_type=SelectionType.CHARACTERS ) buffer.cursor_position = index # Select word around cursor on double click. # Two MOUSE_UP events in a short timespan are considered a double click. double_click = ( self._last_click_timestamp and time.time() - self._last_click_timestamp < 0.3 ) self._last_click_timestamp = time.time() if double_click: start, end = buffer.document.find_boundaries_of_current_word() buffer.cursor_position += start buffer.start_selection(selection_type=SelectionType.CHARACTERS) buffer.cursor_position += end - start else: # Don't handle scroll events here. return NotImplemented # Not focused, but focusing on click events. else: if ( self.focus_on_click() and mouse_event.event_type == MouseEventType.MOUSE_UP ): # Focus happens on mouseup. (If we did this on mousedown, the # up event will be received at the point where this widget is # focused and be handled anyway.) get_app().layout.current_control = self else: return NotImplemented return None def move_cursor_down(self) -> None: b = self.buffer b.cursor_position += b.document.get_cursor_down_position() def move_cursor_up(self) -> None: b = self.buffer b.cursor_position += b.document.get_cursor_up_position() def get_key_bindings(self) -> KeyBindingsBase | None: """ When additional key bindings are given. Return these. """ return self.key_bindings def get_invalidate_events(self) -> Iterable[Event[object]]: """ Return the Window invalidate events. """ # Whenever the buffer changes, the UI has to be updated. yield self.buffer.on_text_changed yield self.buffer.on_cursor_position_changed yield self.buffer.on_completions_changed yield self.buffer.on_suggestion_set class SearchBufferControl(BufferControl): """ :class:`.BufferControl` which is used for searching another :class:`.BufferControl`. :param ignore_case: Search case insensitive. """ def __init__( self, buffer: Buffer | None = None, input_processors: list[Processor] | None = None, lexer: Lexer | None = None, focus_on_click: FilterOrBool = False, key_bindings: KeyBindingsBase | None = None, ignore_case: FilterOrBool = False, ): super().__init__( buffer=buffer, input_processors=input_processors, lexer=lexer, focus_on_click=focus_on_click, key_bindings=key_bindings, ) # If this BufferControl is used as a search field for one or more other # BufferControls, then represents the search state. self.searcher_search_state = SearchState(ignore_case=ignore_case) ================================================ FILE: src/prompt_toolkit/layout/dimension.py ================================================ """ Layout dimensions are used to give the minimum, maximum and preferred dimensions for containers and controls. """ from __future__ import annotations from collections.abc import Callable from typing import TYPE_CHECKING __all__ = [ "Dimension", "D", "sum_layout_dimensions", "max_layout_dimensions", "AnyDimension", "to_dimension", "is_dimension", ] if TYPE_CHECKING: from typing import TypeGuard class Dimension: """ Specified dimension (width/height) of a user control or window. The layout engine tries to honor the preferred size. If that is not possible, because the terminal is larger or smaller, it tries to keep in between min and max. :param min: Minimum size. :param max: Maximum size. :param weight: For a VSplit/HSplit, the actual size will be determined by taking the proportion of weights from all the children. E.g. When there are two children, one with a weight of 1, and the other with a weight of 2, the second will always be twice as big as the first, if the min/max values allow it. :param preferred: Preferred size. """ def __init__( self, min: int | None = None, max: int | None = None, weight: int | None = None, preferred: int | None = None, ) -> None: if weight is not None: assert weight >= 0 # Also cannot be a float. assert min is None or min >= 0 assert max is None or max >= 0 assert preferred is None or preferred >= 0 self.min_specified = min is not None self.max_specified = max is not None self.preferred_specified = preferred is not None self.weight_specified = weight is not None if min is None: min = 0 # Smallest possible value. if max is None: # 0-values are allowed, so use "is None" max = 1000**10 # Something huge. if preferred is None: preferred = min if weight is None: weight = 1 self.min = min self.max = max self.preferred = preferred self.weight = weight # Don't allow situations where max < min. (This would be a bug.) if max < min: raise ValueError("Invalid Dimension: max < min.") # Make sure that the 'preferred' size is always in the min..max range. if self.preferred < self.min: self.preferred = self.min if self.preferred > self.max: self.preferred = self.max @classmethod def exact(cls, amount: int) -> Dimension: """ Return a :class:`.Dimension` with an exact size. (min, max and preferred set to ``amount``). """ return cls(min=amount, max=amount, preferred=amount) @classmethod def zero(cls) -> Dimension: """ Create a dimension that represents a zero size. (Used for 'invisible' controls.) """ return cls.exact(amount=0) def __repr__(self) -> str: fields = [] if self.min_specified: fields.append(f"min={self.min!r}") if self.max_specified: fields.append(f"max={self.max!r}") if self.preferred_specified: fields.append(f"preferred={self.preferred!r}") if self.weight_specified: fields.append(f"weight={self.weight!r}") return "Dimension({})".format(", ".join(fields)) def sum_layout_dimensions(dimensions: list[Dimension]) -> Dimension: """ Sum a list of :class:`.Dimension` instances. """ min = sum(d.min for d in dimensions) max = sum(d.max for d in dimensions) preferred = sum(d.preferred for d in dimensions) return Dimension(min=min, max=max, preferred=preferred) def max_layout_dimensions(dimensions: list[Dimension]) -> Dimension: """ Take the maximum of a list of :class:`.Dimension` instances. Used when we have a HSplit/VSplit, and we want to get the best width/height.) """ if not len(dimensions): return Dimension.zero() # If all dimensions are size zero. Return zero. # (This is important for HSplit/VSplit, to report the right values to their # parent when all children are invisible.) if all(d.preferred == 0 and d.max == 0 for d in dimensions): return Dimension.zero() # Ignore empty dimensions. (They should not reduce the size of others.) dimensions = [d for d in dimensions if d.preferred != 0 and d.max != 0] if dimensions: # Take the highest minimum dimension. min_ = max(d.min for d in dimensions) # For the maximum, we would prefer not to go larger than then smallest # 'max' value, unless other dimensions have a bigger preferred value. # This seems to work best: # - We don't want that a widget with a small height in a VSplit would # shrink other widgets in the split. # If it doesn't work well enough, then it's up to the UI designer to # explicitly pass dimensions. max_ = min(d.max for d in dimensions) max_ = max(max_, max(d.preferred for d in dimensions)) # Make sure that min>=max. In some scenarios, when certain min..max # ranges don't have any overlap, we can end up in such an impossible # situation. In that case, give priority to the max value. # E.g. taking (1..5) and (8..9) would return (8..5). Instead take (8..8). if min_ > max_: max_ = min_ preferred = max(d.preferred for d in dimensions) return Dimension(min=min_, max=max_, preferred=preferred) else: return Dimension() # Anything that can be converted to a dimension AnyDimension = None | int | Dimension | Callable[[], "AnyDimension"] def to_dimension(value: AnyDimension) -> Dimension: """ Turn the given object into a `Dimension` object. """ if value is None: return Dimension() if isinstance(value, int): return Dimension.exact(value) if isinstance(value, Dimension): return value if callable(value): return to_dimension(value()) raise ValueError("Not an integer or Dimension object.") def is_dimension(value: object) -> TypeGuard[AnyDimension]: """ Test whether the given value could be a valid dimension. (For usage in an assertion. It's not guaranteed in case of a callable.) """ if value is None: return True if callable(value): return True # Assume it's a callable that doesn't take arguments. if isinstance(value, (int, Dimension)): return True return False # Common alias. D = Dimension # For backward-compatibility. LayoutDimension = Dimension ================================================ FILE: src/prompt_toolkit/layout/dummy.py ================================================ """ Dummy layout. Used when somebody creates an `Application` without specifying a `Layout`. """ from __future__ import annotations from prompt_toolkit.formatted_text import HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent from .containers import Window from .controls import FormattedTextControl from .dimension import D from .layout import Layout __all__ = [ "create_dummy_layout", ] E = KeyPressEvent def create_dummy_layout() -> Layout: """ Create a dummy layout for use in an 'Application' that doesn't have a layout specified. When ENTER is pressed, the application quits. """ kb = KeyBindings() @kb.add("enter") def enter(event: E) -> None: event.app.exit() control = FormattedTextControl( HTML("No layout specified. Press <reverse>ENTER</reverse> to quit."), key_bindings=kb, ) window = Window(content=control, height=D(min=1)) return Layout(container=window, focused_element=window) ================================================ FILE: src/prompt_toolkit/layout/layout.py ================================================ """ Wrapper for the layout. """ from __future__ import annotations from collections.abc import Generator, Iterable from prompt_toolkit.buffer import Buffer from .containers import ( AnyContainer, ConditionalContainer, Container, Window, to_container, ) from .controls import BufferControl, SearchBufferControl, UIControl __all__ = [ "Layout", "InvalidLayoutError", "walk", ] FocusableElement = str | Buffer | UIControl | AnyContainer class Layout: """ The layout for a prompt_toolkit :class:`~prompt_toolkit.application.Application`. This also keeps track of which user control is focused. :param container: The "root" container for the layout. :param focused_element: element to be focused initially. (Can be anything the `focus` function accepts.) """ def __init__( self, container: AnyContainer, focused_element: FocusableElement | None = None, ) -> None: self.container = to_container(container) self._stack: list[Window] = [] # Map search BufferControl back to the original BufferControl. # This is used to keep track of when exactly we are searching, and for # applying the search. # When a link exists in this dictionary, that means the search is # currently active. # Map: search_buffer_control -> original buffer control. self.search_links: dict[SearchBufferControl, BufferControl] = {} # Mapping that maps the children in the layout to their parent. # This relationship is calculated dynamically, each time when the UI # is rendered. (UI elements have only references to their children.) self._child_to_parent: dict[Container, Container] = {} if focused_element is None: try: self._stack.append(next(self.find_all_windows())) except StopIteration as e: raise InvalidLayoutError( "Invalid layout. The layout does not contain any Window object." ) from e else: self.focus(focused_element) # List of visible windows. self.visible_windows: list[Window] = [] # List of `Window` objects. def __repr__(self) -> str: return f"Layout({self.container!r}, current_window={self.current_window!r})" def find_all_windows(self) -> Generator[Window, None, None]: """ Find all the :class:`.UIControl` objects in this layout. """ for item in self.walk(): if isinstance(item, Window): yield item def find_all_controls(self) -> Iterable[UIControl]: for container in self.find_all_windows(): yield container.content def focus(self, value: FocusableElement) -> None: """ Focus the given UI element. `value` can be either: - a :class:`.UIControl` - a :class:`.Buffer` instance or the name of a :class:`.Buffer` - a :class:`.Window` - Any container object. In this case we will focus the :class:`.Window` from this container that was focused most recent, or the very first focusable :class:`.Window` of the container. """ # BufferControl by buffer name. if isinstance(value, str): for control in self.find_all_controls(): if isinstance(control, BufferControl) and control.buffer.name == value: self.focus(control) return raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.") # BufferControl by buffer object. elif isinstance(value, Buffer): for control in self.find_all_controls(): if isinstance(control, BufferControl) and control.buffer == value: self.focus(control) return raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.") # Focus UIControl. elif isinstance(value, UIControl): if value not in self.find_all_controls(): raise ValueError( "Invalid value. Container does not appear in the layout." ) if not value.is_focusable(): raise ValueError("Invalid value. UIControl is not focusable.") self.current_control = value # Otherwise, expecting any Container object. else: value = to_container(value) if isinstance(value, Window): # This is a `Window`: focus that. if value not in self.find_all_windows(): raise ValueError( f"Invalid value. Window does not appear in the layout: {value!r}" ) self.current_window = value else: # Focus a window in this container. # If we have many windows as part of this container, and some # of them have been focused before, take the last focused # item. (This is very useful when the UI is composed of more # complex sub components.) windows = [] for c in walk(value, skip_hidden=True): if isinstance(c, Window) and c.content.is_focusable(): windows.append(c) # Take the first one that was focused before. for w in reversed(self._stack): if w in windows: self.current_window = w return # None was focused before: take the very first focusable window. if windows: self.current_window = windows[0] return raise ValueError( f"Invalid value. Container cannot be focused: {value!r}" ) def has_focus(self, value: FocusableElement) -> bool: """ Check whether the given control has the focus. :param value: :class:`.UIControl` or :class:`.Window` instance. """ if isinstance(value, str): if self.current_buffer is None: return False return self.current_buffer.name == value if isinstance(value, Buffer): return self.current_buffer == value if isinstance(value, UIControl): return self.current_control == value else: value = to_container(value) if isinstance(value, Window): return self.current_window == value else: # Check whether this "container" is focused. This is true if # one of the elements inside is focused. for element in walk(value): if element == self.current_window: return True return False @property def current_control(self) -> UIControl: """ Get the :class:`.UIControl` to currently has the focus. """ return self._stack[-1].content @current_control.setter def current_control(self, control: UIControl) -> None: """ Set the :class:`.UIControl` to receive the focus. """ for window in self.find_all_windows(): if window.content == control: self.current_window = window return raise ValueError("Control not found in the user interface.") @property def current_window(self) -> Window: "Return the :class:`.Window` object that is currently focused." return self._stack[-1] @current_window.setter def current_window(self, value: Window) -> None: "Set the :class:`.Window` object to be currently focused." self._stack.append(value) @property def is_searching(self) -> bool: "True if we are searching right now." return self.current_control in self.search_links @property def search_target_buffer_control(self) -> BufferControl | None: """ Return the :class:`.BufferControl` in which we are searching or `None`. """ # Not every `UIControl` is a `BufferControl`. This only applies to # `BufferControl`. control = self.current_control if isinstance(control, SearchBufferControl): return self.search_links.get(control) else: return None def get_focusable_windows(self) -> Iterable[Window]: """ Return all the :class:`.Window` objects which are focusable (in the 'modal' area). """ for w in self.walk_through_modal_area(): if isinstance(w, Window) and w.content.is_focusable(): yield w def get_visible_focusable_windows(self) -> list[Window]: """ Return a list of :class:`.Window` objects that are focusable. """ # focusable windows are windows that are visible, but also part of the # modal container. Make sure to keep the ordering. visible_windows = self.visible_windows return [w for w in self.get_focusable_windows() if w in visible_windows] @property def current_buffer(self) -> Buffer | None: """ The currently focused :class:`~.Buffer` or `None`. """ ui_control = self.current_control if isinstance(ui_control, BufferControl): return ui_control.buffer return None def get_buffer_by_name(self, buffer_name: str) -> Buffer | None: """ Look in the layout for a buffer with the given name. Return `None` when nothing was found. """ for w in self.walk(): if isinstance(w, Window) and isinstance(w.content, BufferControl): if w.content.buffer.name == buffer_name: return w.content.buffer return None @property def buffer_has_focus(self) -> bool: """ Return `True` if the currently focused control is a :class:`.BufferControl`. (For instance, used to determine whether the default key bindings should be active or not.) """ ui_control = self.current_control return isinstance(ui_control, BufferControl) @property def previous_control(self) -> UIControl: """ Get the :class:`.UIControl` to previously had the focus. """ try: return self._stack[-2].content except IndexError: return self._stack[-1].content def focus_last(self) -> None: """ Give the focus to the last focused control. """ if len(self._stack) > 1: self._stack = self._stack[:-1] def focus_next(self) -> None: """ Focus the next visible/focusable Window. """ windows = self.get_visible_focusable_windows() if len(windows) > 0: try: index = windows.index(self.current_window) except ValueError: index = 0 else: index = (index + 1) % len(windows) self.focus(windows[index]) def focus_previous(self) -> None: """ Focus the previous visible/focusable Window. """ windows = self.get_visible_focusable_windows() if len(windows) > 0: try: index = windows.index(self.current_window) except ValueError: index = 0 else: index = (index - 1) % len(windows) self.focus(windows[index]) def walk(self) -> Iterable[Container]: """ Walk through all the layout nodes (and their children) and yield them. """ yield from walk(self.container) def walk_through_modal_area(self) -> Iterable[Container]: """ Walk through all the containers which are in the current 'modal' part of the layout. """ # Go up in the tree, and find the root. (it will be a part of the # layout, if the focus is in a modal part.) root: Container = self.current_window while not root.is_modal() and root in self._child_to_parent: root = self._child_to_parent[root] yield from walk(root) def update_parents_relations(self) -> None: """ Update child->parent relationships mapping. """ parents = {} def walk(e: Container) -> None: for c in e.get_children(): parents[c] = e walk(c) walk(self.container) self._child_to_parent = parents def reset(self) -> None: # Remove all search links when the UI starts. # (Important, for instance when control-c is been pressed while # searching. The prompt cancels, but next `run()` call the search # links are still there.) self.search_links.clear() self.container.reset() def get_parent(self, container: Container) -> Container | None: """ Return the parent container for the given container, or ``None``, if it wasn't found. """ try: return self._child_to_parent[container] except KeyError: return None class InvalidLayoutError(Exception): pass def walk(container: Container, skip_hidden: bool = False) -> Iterable[Container]: """ Walk through layout, starting at this container. """ # When `skip_hidden` is set, don't go into disabled ConditionalContainer containers. if ( skip_hidden and isinstance(container, ConditionalContainer) and not container.filter() ): return yield container for c in container.get_children(): # yield from walk(c) yield from walk(c, skip_hidden=skip_hidden) ================================================ FILE: src/prompt_toolkit/layout/margins.py ================================================ """ Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable from typing import TYPE_CHECKING from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.formatted_text import ( StyleAndTextTuples, fragment_list_to_text, to_formatted_text, ) from prompt_toolkit.utils import get_cwidth from .controls import UIContent if TYPE_CHECKING: from .containers import WindowRenderInfo __all__ = [ "Margin", "NumberedMargin", "ScrollbarMargin", "ConditionalMargin", "PromptMargin", ] class Margin(metaclass=ABCMeta): """ Base interface for a margin. """ @abstractmethod def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: """ Return the width that this margin is going to consume. :param get_ui_content: Callable that asks the user control to create a :class:`.UIContent` instance. This can be used for instance to obtain the number of lines. """ return 0 @abstractmethod def create_margin( self, window_render_info: WindowRenderInfo, width: int, height: int ) -> StyleAndTextTuples: """ Creates a margin. This should return a list of (style_str, text) tuples. :param window_render_info: :class:`~prompt_toolkit.layout.containers.WindowRenderInfo` instance, generated after rendering and copying the visible part of the :class:`~prompt_toolkit.layout.controls.UIControl` into the :class:`~prompt_toolkit.layout.containers.Window`. :param width: The width that's available for this margin. (As reported by :meth:`.get_width`.) :param height: The height that's available for this margin. (The height of the :class:`~prompt_toolkit.layout.containers.Window`.) """ return [] class NumberedMargin(Margin): """ Margin that displays the line numbers. :param relative: Number relative to the cursor position. Similar to the Vi 'relativenumber' option. :param display_tildes: Display tildes after the end of the document, just like Vi does. """ def __init__( self, relative: FilterOrBool = False, display_tildes: FilterOrBool = False ) -> None: self.relative = to_filter(relative) self.display_tildes = to_filter(display_tildes) def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: line_count = get_ui_content().line_count return max(3, len(f"{line_count}") + 1) def create_margin( self, window_render_info: WindowRenderInfo, width: int, height: int ) -> StyleAndTextTuples: relative = self.relative() style = "class:line-number" style_current = "class:line-number.current" # Get current line number. current_lineno = window_render_info.ui_content.cursor_position.y # Construct margin. result: StyleAndTextTuples = [] last_lineno = None for y, lineno in enumerate(window_render_info.displayed_lines): # Only display line number if this line is not a continuation of the previous line. if lineno != last_lineno: if lineno is None: pass elif lineno == current_lineno: # Current line. if relative: # Left align current number in relative mode. result.append((style_current, "%i" % (lineno + 1))) else: result.append( (style_current, ("%i " % (lineno + 1)).rjust(width)) ) else: # Other lines. if relative: lineno = abs(lineno - current_lineno) - 1 result.append((style, ("%i " % (lineno + 1)).rjust(width))) last_lineno = lineno result.append(("", "\n")) # Fill with tildes. if self.display_tildes(): while y < window_render_info.window_height: result.append(("class:tilde", "~\n")) y += 1 return result class ConditionalMargin(Margin): """ Wrapper around other :class:`.Margin` classes to show/hide them. """ def __init__(self, margin: Margin, filter: FilterOrBool) -> None: self.margin = margin self.filter = to_filter(filter) def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: if self.filter(): return self.margin.get_width(get_ui_content) else: return 0 def create_margin( self, window_render_info: WindowRenderInfo, width: int, height: int ) -> StyleAndTextTuples: if width and self.filter(): return self.margin.create_margin(window_render_info, width, height) else: return [] class ScrollbarMargin(Margin): """ Margin displaying a scrollbar. :param display_arrows: Display scroll up/down arrows. """ def __init__( self, display_arrows: FilterOrBool = False, up_arrow_symbol: str = "^", down_arrow_symbol: str = "v", ) -> None: self.display_arrows = to_filter(display_arrows) self.up_arrow_symbol = up_arrow_symbol self.down_arrow_symbol = down_arrow_symbol def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: return 1 def create_margin( self, window_render_info: WindowRenderInfo, width: int, height: int ) -> StyleAndTextTuples: content_height = window_render_info.content_height window_height = window_render_info.window_height display_arrows = self.display_arrows() if display_arrows: window_height -= 2 try: fraction_visible = len(window_render_info.displayed_lines) / float( content_height ) fraction_above = window_render_info.vertical_scroll / float(content_height) scrollbar_height = int( min(window_height, max(1, window_height * fraction_visible)) ) scrollbar_top = int(window_height * fraction_above) except ZeroDivisionError: return [] else: def is_scroll_button(row: int) -> bool: "True if we should display a button on this row." return scrollbar_top <= row <= scrollbar_top + scrollbar_height # Up arrow. result: StyleAndTextTuples = [] if display_arrows: result.extend( [ ("class:scrollbar.arrow", self.up_arrow_symbol), ("class:scrollbar", "\n"), ] ) # Scrollbar body. scrollbar_background = "class:scrollbar.background" scrollbar_background_start = "class:scrollbar.background,scrollbar.start" scrollbar_button = "class:scrollbar.button" scrollbar_button_end = "class:scrollbar.button,scrollbar.end" for i in range(window_height): if is_scroll_button(i): if not is_scroll_button(i + 1): # Give the last cell a different style, because we # want to underline this. result.append((scrollbar_button_end, " ")) else: result.append((scrollbar_button, " ")) else: if is_scroll_button(i + 1): result.append((scrollbar_background_start, " ")) else: result.append((scrollbar_background, " ")) result.append(("", "\n")) # Down arrow if display_arrows: result.append(("class:scrollbar.arrow", self.down_arrow_symbol)) return result class PromptMargin(Margin): """ [Deprecated] Create margin that displays a prompt. This can display one prompt at the first line, and a continuation prompt (e.g, just dots) on all the following lines. This `PromptMargin` implementation has been largely superseded in favor of the `get_line_prefix` attribute of `Window`. The reason is that a margin is always a fixed width, while `get_line_prefix` can return a variable width prefix in front of every line, making it more powerful, especially for line continuations. :param get_prompt: Callable returns formatted text or a list of `(style_str, type)` tuples to be shown as the prompt at the first line. :param get_continuation: Callable that takes three inputs. The width (int), line_number (int), and is_soft_wrap (bool). It should return formatted text or a list of `(style_str, type)` tuples for the next lines of the input. """ def __init__( self, get_prompt: Callable[[], StyleAndTextTuples], get_continuation: None | (Callable[[int, int, bool], StyleAndTextTuples]) = None, ) -> None: self.get_prompt = get_prompt self.get_continuation = get_continuation def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: "Width to report to the `Window`." # Take the width from the first line. text = fragment_list_to_text(self.get_prompt()) return get_cwidth(text) def create_margin( self, window_render_info: WindowRenderInfo, width: int, height: int ) -> StyleAndTextTuples: get_continuation = self.get_continuation result: StyleAndTextTuples = [] # First line. result.extend(to_formatted_text(self.get_prompt())) # Next lines. if get_continuation: last_y = None for y in window_render_info.displayed_lines[1:]: result.append(("", "\n")) result.extend( to_formatted_text(get_continuation(width, y, y == last_y)) ) last_y = y return result ================================================ FILE: src/prompt_toolkit/layout/menus.py ================================================ from __future__ import annotations import math from collections.abc import Callable, Iterable, Sequence from itertools import zip_longest from typing import TYPE_CHECKING, TypeVar, cast from weakref import WeakKeyDictionary from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import CompletionState from prompt_toolkit.completion import Completion from prompt_toolkit.data_structures import Point from prompt_toolkit.filters import ( Condition, FilterOrBool, has_completions, is_done, to_filter, ) from prompt_toolkit.formatted_text import ( StyleAndTextTuples, fragment_list_width, to_formatted_text, ) from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.layout.utils import explode_text_fragments from prompt_toolkit.mouse_events import MouseEvent, MouseEventType from prompt_toolkit.utils import get_cwidth from .containers import ConditionalContainer, HSplit, ScrollOffsets, Window from .controls import GetLinePrefixCallable, UIContent, UIControl from .dimension import Dimension from .margins import ScrollbarMargin if TYPE_CHECKING: from prompt_toolkit.key_binding.key_bindings import ( KeyBindings, NotImplementedOrNone, ) __all__ = [ "CompletionsMenu", "MultiColumnCompletionsMenu", ] E = KeyPressEvent class CompletionsMenuControl(UIControl): """ Helper for drawing the complete menu to the screen. :param scroll_offset: Number (integer) representing the preferred amount of completions to be displayed before and after the current one. When this is a very high number, the current completion will be shown in the middle most of the time. """ # Preferred minimum size of the menu control. # The CompletionsMenu class defines a width of 8, and there is a scrollbar # of 1.) MIN_WIDTH = 7 def has_focus(self) -> bool: return False def preferred_width(self, max_available_width: int) -> int | None: complete_state = get_app().current_buffer.complete_state if complete_state: menu_width = self._get_menu_width(500, complete_state) menu_meta_width = self._get_menu_meta_width(500, complete_state) return menu_width + menu_meta_width else: return 0 def preferred_height( self, width: int, max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, ) -> int | None: complete_state = get_app().current_buffer.complete_state if complete_state: return len(complete_state.completions) else: return 0 def create_content(self, width: int, height: int) -> UIContent: """ Create a UIContent object for this control. """ complete_state = get_app().current_buffer.complete_state if complete_state: completions = complete_state.completions index = complete_state.complete_index # Can be None! # Calculate width of completions menu. menu_width = self._get_menu_width(width, complete_state) menu_meta_width = self._get_menu_meta_width( width - menu_width, complete_state ) show_meta = self._show_meta(complete_state) def get_line(i: int) -> StyleAndTextTuples: c = completions[i] is_current_completion = i == index result = _get_menu_item_fragments( c, is_current_completion, menu_width, space_after=True ) if show_meta: result += self._get_menu_item_meta_fragments( c, is_current_completion, menu_meta_width ) return result return UIContent( get_line=get_line, cursor_position=Point(x=0, y=index or 0), line_count=len(completions), ) return UIContent() def _show_meta(self, complete_state: CompletionState) -> bool: """ Return ``True`` if we need to show a column with meta information. """ return any(c.display_meta_text for c in complete_state.completions) def _get_menu_width(self, max_width: int, complete_state: CompletionState) -> int: """ Return the width of the main column. """ return min( max_width, max( self.MIN_WIDTH, max(get_cwidth(c.display_text) for c in complete_state.completions) + 2, ), ) def _get_menu_meta_width( self, max_width: int, complete_state: CompletionState ) -> int: """ Return the width of the meta column. """ def meta_width(completion: Completion) -> int: return get_cwidth(completion.display_meta_text) if self._show_meta(complete_state): # If the amount of completions is over 200, compute the width based # on the first 200 completions, otherwise this can be very slow. completions = complete_state.completions if len(completions) > 200: completions = completions[:200] return min(max_width, max(meta_width(c) for c in completions) + 2) else: return 0 def _get_menu_item_meta_fragments( self, completion: Completion, is_current_completion: bool, width: int ) -> StyleAndTextTuples: if is_current_completion: style_str = "class:completion-menu.meta.completion.current" else: style_str = "class:completion-menu.meta.completion" text, tw = _trim_formatted_text(completion.display_meta, width - 2) padding = " " * (width - 1 - tw) return to_formatted_text( cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], style=style_str, ) def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: """ Handle mouse events: clicking and scrolling. """ b = get_app().current_buffer if mouse_event.event_type == MouseEventType.MOUSE_UP: # Select completion. b.go_to_completion(mouse_event.position.y) b.complete_state = None elif mouse_event.event_type == MouseEventType.SCROLL_DOWN: # Scroll up. b.complete_next(count=3, disable_wrap_around=True) elif mouse_event.event_type == MouseEventType.SCROLL_UP: # Scroll down. b.complete_previous(count=3, disable_wrap_around=True) return None def _get_menu_item_fragments( completion: Completion, is_current_completion: bool, width: int, space_after: bool = False, ) -> StyleAndTextTuples: """ Get the style/text tuples for a menu item, styled and trimmed to the given width. """ if is_current_completion: style_str = f"class:completion-menu.completion.current {completion.style} {completion.selected_style}" else: style_str = "class:completion-menu.completion " + completion.style text, tw = _trim_formatted_text( completion.display, (width - 2 if space_after else width - 1) ) padding = " " * (width - 1 - tw) return to_formatted_text( cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], style=style_str, ) def _trim_formatted_text( formatted_text: StyleAndTextTuples, max_width: int ) -> tuple[StyleAndTextTuples, int]: """ Trim the text to `max_width`, append dots when the text is too long. Returns (text, width) tuple. """ width = fragment_list_width(formatted_text) # When the text is too wide, trim it. if width > max_width: result = [] # Text fragments. remaining_width = max_width - 3 for style_and_ch in explode_text_fragments(formatted_text): ch_width = get_cwidth(style_and_ch[1]) if ch_width <= remaining_width: result.append(style_and_ch) remaining_width -= ch_width else: break result.append(("", "...")) return result, max_width - remaining_width else: return formatted_text, width class CompletionsMenu(ConditionalContainer): # NOTE: We use a pretty big z_index by default. Menus are supposed to be # above anything else. We also want to make sure that the content is # visible at the point where we draw this menu. def __init__( self, max_height: int | None = None, scroll_offset: int | Callable[[], int] = 0, extra_filter: FilterOrBool = True, display_arrows: FilterOrBool = False, z_index: int = 10**8, ) -> None: extra_filter = to_filter(extra_filter) display_arrows = to_filter(display_arrows) super().__init__( content=Window( content=CompletionsMenuControl(), width=Dimension(min=8), height=Dimension(min=1, max=max_height), scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset), right_margins=[ScrollbarMargin(display_arrows=display_arrows)], dont_extend_width=True, style="class:completion-menu", z_index=z_index, ), # Show when there are completions but not at the point we are # returning the input. filter=extra_filter & has_completions & ~is_done, ) class MultiColumnCompletionMenuControl(UIControl): """ Completion menu that displays all the completions in several columns. When there are more completions than space for them to be displayed, an arrow is shown on the left or right side. `min_rows` indicates how many rows will be available in any possible case. When this is larger than one, it will try to use less columns and more rows until this value is reached. Be careful passing in a too big value, if less than the given amount of rows are available, more columns would have been required, but `preferred_width` doesn't know about that and reports a too small value. This results in less completions displayed and additional scrolling. (It's a limitation of how the layout engine currently works: first the widths are calculated, then the heights.) :param suggested_max_column_width: The suggested max width of a column. The column can still be bigger than this, but if there is place for two columns of this width, we will display two columns. This to avoid that if there is one very wide completion, that it doesn't significantly reduce the amount of columns. """ _required_margin = 3 # One extra padding on the right + space for arrows. def __init__(self, min_rows: int = 3, suggested_max_column_width: int = 30) -> None: assert min_rows >= 1 self.min_rows = min_rows self.suggested_max_column_width = suggested_max_column_width self.scroll = 0 # Cache for column width computations. This computation is not cheap, # so we don't want to do it over and over again while the user # navigates through the completions. # (map `completion_state` to `(completion_count, width)`. We remember # the count, because a completer can add new completions to the # `CompletionState` while loading.) self._column_width_for_completion_state: WeakKeyDictionary[ CompletionState, tuple[int, int] ] = WeakKeyDictionary() # Info of last rendering. self._rendered_rows = 0 self._rendered_columns = 0 self._total_columns = 0 self._render_pos_to_completion: dict[tuple[int, int], Completion] = {} self._render_left_arrow = False self._render_right_arrow = False self._render_width = 0 def reset(self) -> None: self.scroll = 0 def has_focus(self) -> bool: return False def preferred_width(self, max_available_width: int) -> int | None: """ Preferred width: prefer to use at least min_rows, but otherwise as much as possible horizontally. """ complete_state = get_app().current_buffer.complete_state if complete_state is None: return 0 column_width = self._get_column_width(complete_state) result = int( column_width * math.ceil(len(complete_state.completions) / float(self.min_rows)) ) # When the desired width is still more than the maximum available, # reduce by removing columns until we are less than the available # width. while ( result > column_width and result > max_available_width - self._required_margin ): result -= column_width return result + self._required_margin def preferred_height( self, width: int, max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, ) -> int | None: """ Preferred height: as much as needed in order to display all the completions. """ complete_state = get_app().current_buffer.complete_state if complete_state is None: return 0 column_width = self._get_column_width(complete_state) column_count = max(1, (width - self._required_margin) // column_width) return int(math.ceil(len(complete_state.completions) / float(column_count))) def create_content(self, width: int, height: int) -> UIContent: """ Create a UIContent object for this menu. """ complete_state = get_app().current_buffer.complete_state if complete_state is None: return UIContent() column_width = self._get_column_width(complete_state) self._render_pos_to_completion = {} _T = TypeVar("_T") def grouper( n: int, iterable: Iterable[_T], fillvalue: _T | None = None ) -> Iterable[Sequence[_T | None]]: "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" args = [iter(iterable)] * n return zip_longest(fillvalue=fillvalue, *args) def is_current_completion(completion: Completion) -> bool: "Returns True when this completion is the currently selected one." return ( complete_state is not None and complete_state.complete_index is not None and c == complete_state.current_completion ) # Space required outside of the regular columns, for displaying the # left and right arrow. HORIZONTAL_MARGIN_REQUIRED = 3 # There should be at least one column, but it cannot be wider than # the available width. column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width) # However, when the columns tend to be very wide, because there are # some very wide entries, shrink it anyway. if column_width > self.suggested_max_column_width: # `column_width` can still be bigger that `suggested_max_column_width`, # but if there is place for two columns, we divide by two. column_width //= column_width // self.suggested_max_column_width visible_columns = max(1, (width - self._required_margin) // column_width) columns_ = list(grouper(height, complete_state.completions)) rows_ = list(zip(*columns_)) # Make sure the current completion is always visible: update scroll offset. selected_column = (complete_state.complete_index or 0) // height self.scroll = min( selected_column, max(self.scroll, selected_column - visible_columns + 1) ) render_left_arrow = self.scroll > 0 render_right_arrow = self.scroll < len(rows_[0]) - visible_columns # Write completions to screen. fragments_for_line = [] for row_index, row in enumerate(rows_): fragments: StyleAndTextTuples = [] middle_row = row_index == len(rows_) // 2 # Draw left arrow if we have hidden completions on the left. if render_left_arrow: fragments.append(("class:scrollbar", "<" if middle_row else " ")) elif render_right_arrow: # Reserve one column empty space. (If there is a right # arrow right now, there can be a left arrow as well.) fragments.append(("", " ")) # Draw row content. for column_index, c in enumerate(row[self.scroll :][:visible_columns]): if c is not None: fragments += _get_menu_item_fragments( c, is_current_completion(c), column_width, space_after=False ) # Remember render position for mouse click handler. for x in range(column_width): self._render_pos_to_completion[ (column_index * column_width + x, row_index) ] = c else: fragments.append(("class:completion", " " * column_width)) # Draw trailing padding for this row. # (_get_menu_item_fragments only returns padding on the left.) if render_left_arrow or render_right_arrow: fragments.append(("class:completion", " ")) # Draw right arrow if we have hidden completions on the right. if render_right_arrow: fragments.append(("class:scrollbar", ">" if middle_row else " ")) elif render_left_arrow: fragments.append(("class:completion", " ")) # Add line. fragments_for_line.append( to_formatted_text(fragments, style="class:completion-menu") ) self._rendered_rows = height self._rendered_columns = visible_columns self._total_columns = len(columns_) self._render_left_arrow = render_left_arrow self._render_right_arrow = render_right_arrow self._render_width = ( column_width * visible_columns + render_left_arrow + render_right_arrow + 1 ) def get_line(i: int) -> StyleAndTextTuples: return fragments_for_line[i] return UIContent(get_line=get_line, line_count=len(rows_)) def _get_column_width(self, completion_state: CompletionState) -> int: """ Return the width of each column. """ try: count, width = self._column_width_for_completion_state[completion_state] if count != len(completion_state.completions): # Number of completions changed, recompute. raise KeyError return width except KeyError: result = ( max(get_cwidth(c.display_text) for c in completion_state.completions) + 1 ) self._column_width_for_completion_state[completion_state] = ( len(completion_state.completions), result, ) return result def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: """ Handle scroll and click events. """ b = get_app().current_buffer def scroll_left() -> None: b.complete_previous(count=self._rendered_rows, disable_wrap_around=True) self.scroll = max(0, self.scroll - 1) def scroll_right() -> None: b.complete_next(count=self._rendered_rows, disable_wrap_around=True) self.scroll = min( self._total_columns - self._rendered_columns, self.scroll + 1 ) if mouse_event.event_type == MouseEventType.SCROLL_DOWN: scroll_right() elif mouse_event.event_type == MouseEventType.SCROLL_UP: scroll_left() elif mouse_event.event_type == MouseEventType.MOUSE_UP: x = mouse_event.position.x y = mouse_event.position.y # Mouse click on left arrow. if x == 0: if self._render_left_arrow: scroll_left() # Mouse click on right arrow. elif x == self._render_width - 1: if self._render_right_arrow: scroll_right() # Mouse click on completion. else: completion = self._render_pos_to_completion.get((x, y)) if completion: b.apply_completion(completion) return None def get_key_bindings(self) -> KeyBindings: """ Expose key bindings that handle the left/right arrow keys when the menu is displayed. """ from prompt_toolkit.key_binding.key_bindings import KeyBindings kb = KeyBindings() @Condition def filter() -> bool: "Only handle key bindings if this menu is visible." app = get_app() complete_state = app.current_buffer.complete_state # There need to be completions, and one needs to be selected. if complete_state is None or complete_state.complete_index is None: return False # This menu needs to be visible. return any(window.content == self for window in app.layout.visible_windows) def move(right: bool = False) -> None: buff = get_app().current_buffer complete_state = buff.complete_state if complete_state is not None and complete_state.complete_index is not None: # Calculate new complete index. new_index = complete_state.complete_index if right: new_index += self._rendered_rows else: new_index -= self._rendered_rows if 0 <= new_index < len(complete_state.completions): buff.go_to_completion(new_index) # NOTE: the is_global is required because the completion menu will # never be focussed. @kb.add("left", is_global=True, filter=filter) def _left(event: E) -> None: move() @kb.add("right", is_global=True, filter=filter) def _right(event: E) -> None: move(True) return kb class MultiColumnCompletionsMenu(HSplit): """ Container that displays the completions in several columns. When `show_meta` (a :class:`~prompt_toolkit.filters.Filter`) evaluates to True, it shows the meta information at the bottom. """ def __init__( self, min_rows: int = 3, suggested_max_column_width: int = 30, show_meta: FilterOrBool = True, extra_filter: FilterOrBool = True, z_index: int = 10**8, ) -> None: show_meta = to_filter(show_meta) extra_filter = to_filter(extra_filter) # Display filter: show when there are completions but not at the point # we are returning the input. full_filter = extra_filter & has_completions & ~is_done @Condition def any_completion_has_meta() -> bool: complete_state = get_app().current_buffer.complete_state return complete_state is not None and any( c.display_meta for c in complete_state.completions ) # Create child windows. # NOTE: We don't set style='class:completion-menu' to the # `MultiColumnCompletionMenuControl`, because this is used in a # Float that is made transparent, and the size of the control # doesn't always correspond exactly with the size of the # generated content. completions_window = ConditionalContainer( content=Window( content=MultiColumnCompletionMenuControl( min_rows=min_rows, suggested_max_column_width=suggested_max_column_width, ), width=Dimension(min=8), height=Dimension(min=1), ), filter=full_filter, ) meta_window = ConditionalContainer( content=Window(content=_SelectedCompletionMetaControl()), filter=full_filter & show_meta & any_completion_has_meta, ) # Initialize split. super().__init__([completions_window, meta_window], z_index=z_index) class _SelectedCompletionMetaControl(UIControl): """ Control that shows the meta information of the selected completion. """ def preferred_width(self, max_available_width: int) -> int | None: """ Report the width of the longest meta text as the preferred width of this control. It could be that we use less width, but this way, we're sure that the layout doesn't change when we select another completion (E.g. that completions are suddenly shown in more or fewer columns.) """ app = get_app() if app.current_buffer.complete_state: state = app.current_buffer.complete_state if len(state.completions) >= 30: # When there are many completions, calling `get_cwidth` for # every `display_meta_text` is too expensive. In this case, # just return the max available width. There will be enough # columns anyway so that the whole screen is filled with # completions and `create_content` will then take up as much # space as needed. return max_available_width return 2 + max( get_cwidth(c.display_meta_text) for c in state.completions[:100] ) else: return 0 def preferred_height( self, width: int, max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, ) -> int | None: return 1 def create_content(self, width: int, height: int) -> UIContent: fragments = self._get_text_fragments() def get_line(i: int) -> StyleAndTextTuples: return fragments return UIContent(get_line=get_line, line_count=1 if fragments else 0) def _get_text_fragments(self) -> StyleAndTextTuples: style = "class:completion-menu.multi-column-meta" state = get_app().current_buffer.complete_state if ( state and state.current_completion and state.current_completion.display_meta_text ): return to_formatted_text( cast(StyleAndTextTuples, [("", " ")]) + state.current_completion.display_meta + [("", " ")], style=style, ) return [] ================================================ FILE: src/prompt_toolkit/layout/mouse_handlers.py ================================================ from __future__ import annotations from collections import defaultdict from collections.abc import Callable from typing import TYPE_CHECKING from prompt_toolkit.mouse_events import MouseEvent if TYPE_CHECKING: from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone __all__ = [ "MouseHandler", "MouseHandlers", ] MouseHandler = Callable[[MouseEvent], "NotImplementedOrNone"] class MouseHandlers: """ Two dimensional raster of callbacks for mouse events. """ def __init__(self) -> None: def dummy_callback(mouse_event: MouseEvent) -> NotImplementedOrNone: """ :param mouse_event: `MouseEvent` instance. """ return NotImplemented # NOTE: Previously, the data structure was a dictionary mapping (x,y) # to the handlers. This however would be more inefficient when copying # over the mouse handlers of the visible region in the scrollable pane. # Map y (row) to x (column) to handlers. self.mouse_handlers: defaultdict[int, defaultdict[int, MouseHandler]] = ( defaultdict(lambda: defaultdict(lambda: dummy_callback)) ) def set_mouse_handler_for_range( self, x_min: int, x_max: int, y_min: int, y_max: int, handler: Callable[[MouseEvent], NotImplementedOrNone], ) -> None: """ Set mouse handler for a region. """ for y in range(y_min, y_max): row = self.mouse_handlers[y] for x in range(x_min, x_max): row[x] = handler ================================================ FILE: src/prompt_toolkit/layout/processors.py ================================================ """ Processors are little transformation blocks that transform the fragments list from a buffer before the BufferControl will render it to the screen. They can insert fragments before or after, or highlight fragments by replacing the fragment types. """ from __future__ import annotations import re from abc import ABCMeta, abstractmethod from collections.abc import Callable, Hashable from typing import TYPE_CHECKING, cast from prompt_toolkit.application.current import get_app from prompt_toolkit.cache import SimpleCache from prompt_toolkit.document import Document from prompt_toolkit.filters import FilterOrBool, to_filter, vi_insert_multiple_mode from prompt_toolkit.formatted_text import ( AnyFormattedText, StyleAndTextTuples, to_formatted_text, ) from prompt_toolkit.formatted_text.utils import fragment_list_len, fragment_list_to_text from prompt_toolkit.search import SearchDirection from prompt_toolkit.utils import to_int, to_str from .utils import explode_text_fragments if TYPE_CHECKING: from .controls import BufferControl, UIContent __all__ = [ "Processor", "TransformationInput", "Transformation", "DummyProcessor", "HighlightSearchProcessor", "HighlightIncrementalSearchProcessor", "HighlightSelectionProcessor", "PasswordProcessor", "HighlightMatchingBracketProcessor", "DisplayMultipleCursors", "BeforeInput", "ShowArg", "AfterInput", "AppendAutoSuggestion", "ConditionalProcessor", "ShowLeadingWhiteSpaceProcessor", "ShowTrailingWhiteSpaceProcessor", "TabsProcessor", "ReverseSearchProcessor", "DynamicProcessor", "merge_processors", ] class Processor(metaclass=ABCMeta): """ Manipulate the fragments for a given line in a :class:`~prompt_toolkit.layout.controls.BufferControl`. """ @abstractmethod def apply_transformation( self, transformation_input: TransformationInput ) -> Transformation: """ Apply transformation. Returns a :class:`.Transformation` instance. :param transformation_input: :class:`.TransformationInput` object. """ return Transformation(transformation_input.fragments) SourceToDisplay = Callable[[int], int] DisplayToSource = Callable[[int], int] class TransformationInput: """ :param buffer_control: :class:`.BufferControl` instance. :param lineno: The number of the line to which we apply the processor. :param source_to_display: A function that returns the position in the `fragments` for any position in the source string. (This takes previous processors into account.) :param fragments: List of fragments that we can transform. (Received from the previous processor.) :param get_line: Optional ; a callable that returns the fragments of another line in the current buffer; This can be used to create processors capable of affecting transforms across multiple lines. """ def __init__( self, buffer_control: BufferControl, document: Document, lineno: int, source_to_display: SourceToDisplay, fragments: StyleAndTextTuples, width: int, height: int, get_line: Callable[[int], StyleAndTextTuples] | None = None, ) -> None: self.buffer_control = buffer_control self.document = document self.lineno = lineno self.source_to_display = source_to_display self.fragments = fragments self.width = width self.height = height self.get_line = get_line def unpack( self, ) -> tuple[ BufferControl, Document, int, SourceToDisplay, StyleAndTextTuples, int, int ]: return ( self.buffer_control, self.document, self.lineno, self.source_to_display, self.fragments, self.width, self.height, ) class Transformation: """ Transformation result, as returned by :meth:`.Processor.apply_transformation`. Important: Always make sure that the length of `document.text` is equal to the length of all the text in `fragments`! :param fragments: The transformed fragments. To be displayed, or to pass to the next processor. :param source_to_display: Cursor position transformation from original string to transformed string. :param display_to_source: Cursor position transformed from source string to original string. """ def __init__( self, fragments: StyleAndTextTuples, source_to_display: SourceToDisplay | None = None, display_to_source: DisplayToSource | None = None, ) -> None: self.fragments = fragments self.source_to_display = source_to_display or (lambda i: i) self.display_to_source = display_to_source or (lambda i: i) class DummyProcessor(Processor): """ A `Processor` that doesn't do anything. """ def apply_transformation( self, transformation_input: TransformationInput ) -> Transformation: return Transformation(transformation_input.fragments) class HighlightSearchProcessor(Processor): """ Processor that highlights search matches in the document. Note that this doesn't support multiline search matches yet. The style classes 'search' and 'search.current' will be applied to the content. """ _classname = "search" _classname_current = "search.current" def _get_search_text(self, buffer_control: BufferControl) -> str: """ The text we are searching for. """ return buffer_control.search_state.text def apply_transformation( self, transformation_input: TransformationInput ) -> Transformation: ( buffer_control, document, lineno, source_to_display, fragments, _, _, ) = transformation_input.unpack() search_text = self._get_search_text(buffer_control) searchmatch_fragment = f" class:{self._classname} " searchmatch_current_fragment = f" class:{self._classname_current} " if search_text and not get_app().is_done: # For each search match, replace the style string. line_text = fragment_list_to_text(fragments) fragments = explode_text_fragments(fragments) if buffer_control.search_state.ignore_case(): flags = re.IGNORECASE else: flags = re.RegexFlag(0) # Get cursor column. cursor_column: int | None if document.cursor_position_row == lineno: cursor_column = source_to_display(document.cursor_position_col) else: cursor_column = None for match in re.finditer(re.escape(search_text), line_text, flags=flags): if cursor_column is not None: on_cursor = match.start() <= cursor_column < match.end() else: on_cursor = False for i in range(match.start(), match.end()): old_fragment, text, *_ = fragments[i] if on_cursor: fragments[i] = ( old_fragment + searchmatch_current_fragment, fragments[i][1], ) else: fragments[i] = ( old_fragment + searchmatch_fragment, fragments[i][1], ) return Transformation(fragments) class HighlightIncrementalSearchProcessor(HighlightSearchProcessor): """ Highlight the search terms that are used for highlighting the incremental search. The style class 'incsearch' will be applied to the content. Important: this requires the `preview_search=True` flag to be set for the `BufferControl`. Otherwise, the cursor position won't be set to the search match while searching, and nothing happens. """ _classname = "incsearch" _classname_current = "incsearch.current" def _get_search_text(self, buffer_control: BufferControl) -> str: """ The text we are searching for. """ # When the search buffer has focus, take that text. search_buffer = buffer_control.search_buffer if search_buffer is not None and search_buffer.text: return search_buffer.text return "" class HighlightSelectionProcessor(Processor): """ Processor that highlights the selection in the document. """ def apply_transformation( self, transformation_input: TransformationInput ) -> Transformation: ( buffer_control, document, lineno, source_to_display, fragments, _, _, ) = transformation_input.unpack() selected_fragment = " class:selected " # In case of selection, highlight all matches. selection_at_line = document.selection_range_at_line(lineno) if selection_at_line: from_, to = selection_at_line from_ = source_to_display(from_) to = source_to_display(to) fragments = explode_text_fragments(fragments) if from_ == 0 and to == 0 and len(fragments) == 0: # When this is an empty line, insert a space in order to # visualize the selection. return Transformation([(selected_fragment, " ")]) else: for i in range(from_, to): if i < len(fragments): old_fragment, old_text, *_ = fragments[i] fragments[i] = (old_fragment + selected_fragment, old_text) elif i == len(fragments): fragments.append((selected_fragment, " ")) return Transformation(fragments) class PasswordProcessor(Processor): """ Processor that masks the input. (For passwords.) :param char: (string) Character to be used. "*" by default. """ def __init__(self, char: str = "*") -> None: self.char = char def apply_transformation(self, ti: TransformationInput) -> Transformation: fragments: StyleAndTextTuples = cast( StyleAndTextTuples, [ (style, self.char * len(text), *handler) for style, text, *handler in ti.fragments ], ) return Transformation(fragments) class HighlightMatchingBracketProcessor(Processor): """ When the cursor is on or right after a bracket, it highlights the matching bracket. :param max_cursor_distance: Only highlight matching brackets when the cursor is within this distance. (From inside a `Processor`, we can't know which lines will be visible on the screen. But we also don't want to scan the whole document for matching brackets on each key press, so we limit to this value.) """ _closing_braces = "])}>" def __init__( self, chars: str = "[](){}<>", max_cursor_distance: int = 1000 ) -> None: self.chars = chars self.max_cursor_distance = max_cursor_distance self._positions_cache: SimpleCache[Hashable, list[tuple[int, int]]] = ( SimpleCache(maxsize=8) ) def _get_positions_to_highlight(self, document: Document) -> list[tuple[int, int]]: """ Return a list of (row, col) tuples that need to be highlighted. """ pos: int | None # Try for the character under the cursor. if document.current_char and document.current_char in self.chars: pos = document.find_matching_bracket_position( start_pos=document.cursor_position - self.max_cursor_distance, end_pos=document.cursor_position + self.max_cursor_distance, ) # Try for the character before the cursor. elif ( document.char_before_cursor and document.char_before_cursor in self._closing_braces and document.char_before_cursor in self.chars ): document = Document(document.text, document.cursor_position - 1) pos = document.find_matching_bracket_position( start_pos=document.cursor_position - self.max_cursor_distance, end_pos=document.cursor_position + self.max_cursor_distance, ) else: pos = None # Return a list of (row, col) tuples that need to be highlighted. if pos: pos += document.cursor_position # pos is relative. row, col = document.translate_index_to_position(pos) return [ (row, col), (document.cursor_position_row, document.cursor_position_col), ] else: return [] def apply_transformation( self, transformation_input: TransformationInput ) -> Transformation: ( buffer_control, document, lineno, source_to_display, fragments, _, _, ) = transformation_input.unpack() # When the application is in the 'done' state, don't highlight. if get_app().is_done: return Transformation(fragments) # Get the highlight positions. key = (get_app().render_counter, document.text, document.cursor_position) positions = self._positions_cache.get( key, lambda: self._get_positions_to_highlight(document) ) # Apply if positions were found at this line. if positions: for row, col in positions: if row == lineno: col = source_to_display(col) fragments = explode_text_fragments(fragments) style, text, *_ = fragments[col] if col == document.cursor_position_col: style += " class:matching-bracket.cursor " else: style += " class:matching-bracket.other " fragments[col] = (style, text) return Transformation(fragments) class DisplayMultipleCursors(Processor): """ When we're in Vi block insert mode, display all the cursors. """ def apply_transformation( self, transformation_input: TransformationInput ) -> Transformation: ( buffer_control, document, lineno, source_to_display, fragments, _, _, ) = transformation_input.unpack() buff = buffer_control.buffer if vi_insert_multiple_mode(): cursor_positions = buff.multiple_cursor_positions fragments = explode_text_fragments(fragments) # If any cursor appears on the current line, highlight that. start_pos = document.translate_row_col_to_index(lineno, 0) end_pos = start_pos + len(document.lines[lineno]) fragment_suffix = " class:multiple-cursors" for p in cursor_positions: if start_pos <= p <= end_pos: column = source_to_display(p - start_pos) # Replace fragment. try: style, text, *_ = fragments[column] except IndexError: # Cursor needs to be displayed after the current text. fragments.append((fragment_suffix, " ")) else: style += fragment_suffix fragments[column] = (style, text) return Transformation(fragments) else: return Transformation(fragments) class BeforeInput(Processor): """ Insert text before the input. :param text: This can be either plain text or formatted text (or a callable that returns any of those). :param style: style to be applied to this prompt/prefix. """ def __init__(self, text: AnyFormattedText, style: str = "") -> None: self.text = text self.style = style def apply_transformation(self, ti: TransformationInput) -> Transformation: source_to_display: SourceToDisplay | None display_to_source: DisplayToSource | None if ti.lineno == 0: # Get fragments. fragments_before = to_formatted_text(self.text, self.style) fragments = fragments_before + ti.fragments shift_position = fragment_list_len(fragments_before) source_to_display = lambda i: i + shift_position display_to_source = lambda i: i - shift_position else: fragments = ti.fragments source_to_display = None display_to_source = None return Transformation( fragments, source_to_display=source_to_display, display_to_source=display_to_source, ) def __repr__(self) -> str: return f"BeforeInput({self.text!r}, {self.style!r})" class ShowArg(BeforeInput): """ Display the 'arg' in front of the input. This was used by the `PromptSession`, but now it uses the `Window.get_line_prefix` function instead. """ def __init__(self) -> None: super().__init__(self._get_text_fragments) def _get_text_fragments(self) -> StyleAndTextTuples: app = get_app() if app.key_processor.arg is None: return [] else: arg = app.key_processor.arg return [ ("class:prompt.arg", "(arg: "), ("class:prompt.arg.text", str(arg)), ("class:prompt.arg", ") "), ] def __repr__(self) -> str: return "ShowArg()" class AfterInput(Processor): """ Insert text after the input. :param text: This can be either plain text or formatted text (or a callable that returns any of those). :param style: style to be applied to this prompt/prefix. """ def __init__(self, text: AnyFormattedText, style: str = "") -> None: self.text = text self.style = style def apply_transformation(self, ti: TransformationInput) -> Transformation: # Insert fragments after the last line. if ti.lineno == ti.document.line_count - 1: # Get fragments. fragments_after = to_formatted_text(self.text, self.style) return Transformation(fragments=ti.fragments + fragments_after) else: return Transformation(fragments=ti.fragments) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.text!r}, style={self.style!r})" class AppendAutoSuggestion(Processor): """ Append the auto suggestion to the input. (The user can then press the right arrow the insert the suggestion.) """ def __init__(self, style: str = "class:auto-suggestion") -> None: self.style = style def apply_transformation(self, ti: TransformationInput) -> Transformation: # Insert fragments after the last line. if ti.lineno == ti.document.line_count - 1: buffer = ti.buffer_control.buffer if buffer.suggestion and ti.document.is_cursor_at_the_end: suggestion = buffer.suggestion.text else: suggestion = "" return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) else: return Transformation(fragments=ti.fragments) class ShowLeadingWhiteSpaceProcessor(Processor): """ Make leading whitespace visible. :param get_char: Callable that returns one character. """ def __init__( self, get_char: Callable[[], str] | None = None, style: str = "class:leading-whitespace", ) -> None: def default_get_char() -> str: if "\xb7".encode(get_app().output.encoding(), "replace") == b"?": return "." else: return "\xb7" self.style = style self.get_char = get_char or default_get_char def apply_transformation(self, ti: TransformationInput) -> Transformation: fragments = ti.fragments # Walk through all te fragments. if fragments and fragment_list_to_text(fragments).startswith(" "): t = (self.style, self.get_char()) fragments = explode_text_fragments(fragments) for i in range(len(fragments)): if fragments[i][1] == " ": fragments[i] = t else: break return Transformation(fragments) class ShowTrailingWhiteSpaceProcessor(Processor): """ Make trailing whitespace visible. :param get_char: Callable that returns one character. """ def __init__( self, get_char: Callable[[], str] | None = None, style: str = "class:training-whitespace", ) -> None: def default_get_char() -> str: if "\xb7".encode(get_app().output.encoding(), "replace") == b"?": return "." else: return "\xb7" self.style = style self.get_char = get_char or default_get_char def apply_transformation(self, ti: TransformationInput) -> Transformation: fragments = ti.fragments if fragments and fragments[-1][1].endswith(" "): t = (self.style, self.get_char()) fragments = explode_text_fragments(fragments) # Walk backwards through all te fragments and replace whitespace. for i in range(len(fragments) - 1, -1, -1): char = fragments[i][1] if char == " ": fragments[i] = t else: break return Transformation(fragments) class TabsProcessor(Processor): """ Render tabs as spaces (instead of ^I) or make them visible (for instance, by replacing them with dots.) :param tabstop: Horizontal space taken by a tab. (`int` or callable that returns an `int`). :param char1: Character or callable that returns a character (text of length one). This one is used for the first space taken by the tab. :param char2: Like `char1`, but for the rest of the space. """ def __init__( self, tabstop: int | Callable[[], int] = 4, char1: str | Callable[[], str] = "|", char2: str | Callable[[], str] = "\u2508", style: str = "class:tab", ) -> None: self.char1 = char1 self.char2 = char2 self.tabstop = tabstop self.style = style def apply_transformation(self, ti: TransformationInput) -> Transformation: tabstop = to_int(self.tabstop) style = self.style # Create separator for tabs. separator1 = to_str(self.char1) separator2 = to_str(self.char2) # Transform fragments. fragments = explode_text_fragments(ti.fragments) position_mappings = {} result_fragments: StyleAndTextTuples = [] pos = 0 for i, fragment_and_text in enumerate(fragments): position_mappings[i] = pos if fragment_and_text[1] == "\t": # Calculate how many characters we have to insert. count = tabstop - (pos % tabstop) if count == 0: count = tabstop # Insert tab. result_fragments.append((style, separator1)) result_fragments.append((style, separator2 * (count - 1))) pos += count else: result_fragments.append(fragment_and_text) pos += 1 position_mappings[len(fragments)] = pos # Add `pos+1` to mapping, because the cursor can be right after the # line as well. position_mappings[len(fragments) + 1] = pos + 1 def source_to_display(from_position: int) -> int: "Maps original cursor position to the new one." return position_mappings[from_position] def display_to_source(display_pos: int) -> int: "Maps display cursor position to the original one." position_mappings_reversed = {v: k for k, v in position_mappings.items()} while display_pos >= 0: try: return position_mappings_reversed[display_pos] except KeyError: display_pos -= 1 return 0 return Transformation( result_fragments, source_to_display=source_to_display, display_to_source=display_to_source, ) class ReverseSearchProcessor(Processor): """ Process to display the "(reverse-i-search)`...`:..." stuff around the search buffer. Note: This processor is meant to be applied to the BufferControl that contains the search buffer, it's not meant for the original input. """ _excluded_input_processors: list[type[Processor]] = [ HighlightSearchProcessor, HighlightSelectionProcessor, BeforeInput, AfterInput, ] def _get_main_buffer(self, buffer_control: BufferControl) -> BufferControl | None: from prompt_toolkit.layout.controls import BufferControl prev_control = get_app().layout.search_target_buffer_control if ( isinstance(prev_control, BufferControl) and prev_control.search_buffer_control == buffer_control ): return prev_control return None def _content( self, main_control: BufferControl, ti: TransformationInput ) -> UIContent: from prompt_toolkit.layout.controls import BufferControl # Emulate the BufferControl through which we are searching. # For this we filter out some of the input processors. excluded_processors = tuple(self._excluded_input_processors) def filter_processor(item: Processor) -> Processor | None: """Filter processors from the main control that we want to disable here. This returns either an accepted processor or None.""" # For a `_MergedProcessor`, check each individual processor, recursively. if isinstance(item, _MergedProcessor): accepted_processors = [filter_processor(p) for p in item.processors] return merge_processors( [p for p in accepted_processors if p is not None] ) # For a `ConditionalProcessor`, check the body. elif isinstance(item, ConditionalProcessor): p = filter_processor(item.processor) if p: return ConditionalProcessor(p, item.filter) # Otherwise, check the processor itself. else: if not isinstance(item, excluded_processors): return item return None filtered_processor = filter_processor( merge_processors(main_control.input_processors or []) ) highlight_processor = HighlightIncrementalSearchProcessor() if filtered_processor: new_processors = [filtered_processor, highlight_processor] else: new_processors = [highlight_processor] from .controls import SearchBufferControl assert isinstance(ti.buffer_control, SearchBufferControl) buffer_control = BufferControl( buffer=main_control.buffer, input_processors=new_processors, include_default_input_processors=False, lexer=main_control.lexer, preview_search=True, search_buffer_control=ti.buffer_control, ) return buffer_control.create_content(ti.width, ti.height, preview_search=True) def apply_transformation(self, ti: TransformationInput) -> Transformation: from .controls import SearchBufferControl assert isinstance(ti.buffer_control, SearchBufferControl), ( "`ReverseSearchProcessor` should be applied to a `SearchBufferControl` only." ) source_to_display: SourceToDisplay | None display_to_source: DisplayToSource | None main_control = self._get_main_buffer(ti.buffer_control) if ti.lineno == 0 and main_control: content = self._content(main_control, ti) # Get the line from the original document for this search. line_fragments = content.get_line(content.cursor_position.y) if main_control.search_state.direction == SearchDirection.FORWARD: direction_text = "i-search" else: direction_text = "reverse-i-search" fragments_before: StyleAndTextTuples = [ ("class:prompt.search", "("), ("class:prompt.search", direction_text), ("class:prompt.search", ")`"), ] fragments = ( fragments_before + [ ("class:prompt.search.text", fragment_list_to_text(ti.fragments)), ("", "': "), ] + line_fragments ) shift_position = fragment_list_len(fragments_before) source_to_display = lambda i: i + shift_position display_to_source = lambda i: i - shift_position else: source_to_display = None display_to_source = None fragments = ti.fragments return Transformation( fragments, source_to_display=source_to_display, display_to_source=display_to_source, ) class ConditionalProcessor(Processor): """ Processor that applies another processor, according to a certain condition. Example:: # Create a function that returns whether or not the processor should # currently be applied. def highlight_enabled(): return true_or_false # Wrapped it in a `ConditionalProcessor` for usage in a `BufferControl`. BufferControl(input_processors=[ ConditionalProcessor(HighlightSearchProcessor(), Condition(highlight_enabled))]) :param processor: :class:`.Processor` instance. :param filter: :class:`~prompt_toolkit.filters.Filter` instance. """ def __init__(self, processor: Processor, filter: FilterOrBool) -> None: self.processor = processor self.filter = to_filter(filter) def apply_transformation( self, transformation_input: TransformationInput ) -> Transformation: # Run processor when enabled. if self.filter(): return self.processor.apply_transformation(transformation_input) else: return Transformation(transformation_input.fragments) def __repr__(self) -> str: return f"{self.__class__.__name__}(processor={self.processor!r}, filter={self.filter!r})" class DynamicProcessor(Processor): """ Processor class that dynamically returns any Processor. :param get_processor: Callable that returns a :class:`.Processor` instance. """ def __init__(self, get_processor: Callable[[], Processor | None]) -> None: self.get_processor = get_processor def apply_transformation(self, ti: TransformationInput) -> Transformation: processor = self.get_processor() or DummyProcessor() return processor.apply_transformation(ti) def merge_processors(processors: list[Processor]) -> Processor: """ Merge multiple `Processor` objects into one. """ if len(processors) == 0: return DummyProcessor() if len(processors) == 1: return processors[0] # Nothing to merge. return _MergedProcessor(processors) class _MergedProcessor(Processor): """ Processor that groups multiple other `Processor` objects, but exposes an API as if it is one `Processor`. """ def __init__(self, processors: list[Processor]): self.processors = processors def apply_transformation(self, ti: TransformationInput) -> Transformation: source_to_display_functions = [ti.source_to_display] display_to_source_functions = [] fragments = ti.fragments def source_to_display(i: int) -> int: """Translate x position from the buffer to the x position in the processor fragments list.""" for f in source_to_display_functions: i = f(i) return i for p in self.processors: transformation = p.apply_transformation( TransformationInput( ti.buffer_control, ti.document, ti.lineno, source_to_display, fragments, ti.width, ti.height, ti.get_line, ) ) fragments = transformation.fragments display_to_source_functions.append(transformation.display_to_source) source_to_display_functions.append(transformation.source_to_display) def display_to_source(i: int) -> int: for f in reversed(display_to_source_functions): i = f(i) return i # In the case of a nested _MergedProcessor, each processor wants to # receive a 'source_to_display' function (as part of the # TransformationInput) that has everything in the chain before # included, because it can be called as part of the # `apply_transformation` function. However, this first # `source_to_display` should not be part of the output that we are # returning. (This is the most consistent with `display_to_source`.) del source_to_display_functions[:1] return Transformation(fragments, source_to_display, display_to_source) ================================================ FILE: src/prompt_toolkit/layout/screen.py ================================================ from __future__ import annotations from collections import defaultdict from collections.abc import Callable from typing import TYPE_CHECKING from prompt_toolkit.cache import FastDictCache from prompt_toolkit.data_structures import Point from prompt_toolkit.utils import get_cwidth if TYPE_CHECKING: from .containers import Window __all__ = [ "Screen", "Char", ] class Char: """ Represent a single character in a :class:`.Screen`. This should be considered immutable. :param char: A single character (can be a double-width character). :param style: A style string. (Can contain classnames.) """ __slots__ = ("char", "style", "width") # If we end up having one of these special control sequences in the input string, # we should display them as follows: # Usually this happens after a "quoted insert". display_mappings: dict[str, str] = { "\x00": "^@", # Control space "\x01": "^A", "\x02": "^B", "\x03": "^C", "\x04": "^D", "\x05": "^E", "\x06": "^F", "\x07": "^G", "\x08": "^H", "\x09": "^I", "\x0a": "^J", "\x0b": "^K", "\x0c": "^L", "\x0d": "^M", "\x0e": "^N", "\x0f": "^O", "\x10": "^P", "\x11": "^Q", "\x12": "^R", "\x13": "^S", "\x14": "^T", "\x15": "^U", "\x16": "^V", "\x17": "^W", "\x18": "^X", "\x19": "^Y", "\x1a": "^Z", "\x1b": "^[", # Escape "\x1c": "^\\", "\x1d": "^]", "\x1e": "^^", "\x1f": "^_", "\x7f": "^?", # ASCII Delete (backspace). # Special characters. All visualized like Vim does. "\x80": "<80>", "\x81": "<81>", "\x82": "<82>", "\x83": "<83>", "\x84": "<84>", "\x85": "<85>", "\x86": "<86>", "\x87": "<87>", "\x88": "<88>", "\x89": "<89>", "\x8a": "<8a>", "\x8b": "<8b>", "\x8c": "<8c>", "\x8d": "<8d>", "\x8e": "<8e>", "\x8f": "<8f>", "\x90": "<90>", "\x91": "<91>", "\x92": "<92>", "\x93": "<93>", "\x94": "<94>", "\x95": "<95>", "\x96": "<96>", "\x97": "<97>", "\x98": "<98>", "\x99": "<99>", "\x9a": "<9a>", "\x9b": "<9b>", "\x9c": "<9c>", "\x9d": "<9d>", "\x9e": "<9e>", "\x9f": "<9f>", # For the non-breaking space: visualize like Emacs does by default. # (Print a space, but attach the 'nbsp' class that applies the # underline style.) "\xa0": " ", } def __init__(self, char: str = " ", style: str = "") -> None: # If this character has to be displayed otherwise, take that one. if char in self.display_mappings: if char == "\xa0": style += " class:nbsp " # Will be underlined. else: style += " class:control-character " char = self.display_mappings[char] self.char = char self.style = style # Calculate width. (We always need this, so better to store it directly # as a member for performance.) self.width = get_cwidth(char) # In theory, `other` can be any type of object, but because of performance # we don't want to do an `isinstance` check every time. We assume "other" # is always a "Char". def _equal(self, other: Char) -> bool: return self.char == other.char and self.style == other.style def _not_equal(self, other: Char) -> bool: # Not equal: We don't do `not char.__eq__` here, because of the # performance of calling yet another function. return self.char != other.char or self.style != other.style if not TYPE_CHECKING: __eq__ = _equal __ne__ = _not_equal def __repr__(self) -> str: return f"{self.__class__.__name__}({self.char!r}, {self.style!r})" _CHAR_CACHE: FastDictCache[tuple[str, str], Char] = FastDictCache( Char, size=1000 * 1000 ) Transparent = "[transparent]" class Screen: """ Two dimensional buffer of :class:`.Char` instances. """ def __init__( self, default_char: Char | None = None, initial_width: int = 0, initial_height: int = 0, ) -> None: if default_char is None: default_char2 = _CHAR_CACHE[" ", Transparent] else: default_char2 = default_char self.data_buffer: defaultdict[int, defaultdict[int, Char]] = defaultdict( lambda: defaultdict(lambda: default_char2) ) #: Escape sequences to be injected. self.zero_width_escapes: defaultdict[int, defaultdict[int, str]] = defaultdict( lambda: defaultdict(str) ) #: Position of the cursor. self.cursor_positions: dict[ Window, Point ] = {} # Map `Window` objects to `Point` objects. #: Visibility of the cursor. self.show_cursor = True #: (Optional) Where to position the menu. E.g. at the start of a completion. #: (We can't use the cursor position, because we don't want the #: completion menu to change its position when we browse through all the #: completions.) self.menu_positions: dict[ Window, Point ] = {} # Map `Window` objects to `Point` objects. #: Currently used width/height of the screen. This will increase when #: data is written to the screen. self.width = initial_width or 0 self.height = initial_height or 0 # Windows that have been drawn. (Each `Window` class will add itself to # this list.) self.visible_windows_to_write_positions: dict[Window, WritePosition] = {} # List of (z_index, draw_func) self._draw_float_functions: list[tuple[int, Callable[[], None]]] = [] @property def visible_windows(self) -> list[Window]: return list(self.visible_windows_to_write_positions.keys()) def set_cursor_position(self, window: Window, position: Point) -> None: """ Set the cursor position for a given window. """ self.cursor_positions[window] = position def set_menu_position(self, window: Window, position: Point) -> None: """ Set the cursor position for a given window. """ self.menu_positions[window] = position def get_cursor_position(self, window: Window) -> Point: """ Get the cursor position for a given window. Returns a `Point`. """ try: return self.cursor_positions[window] except KeyError: return Point(x=0, y=0) def get_menu_position(self, window: Window) -> Point: """ Get the menu position for a given window. (This falls back to the cursor position if no menu position was set.) """ try: return self.menu_positions[window] except KeyError: try: return self.cursor_positions[window] except KeyError: return Point(x=0, y=0) def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None: """ Add a draw-function for a `Window` which has a >= 0 z_index. This will be postponed until `draw_all_floats` is called. """ self._draw_float_functions.append((z_index, draw_func)) def draw_all_floats(self) -> None: """ Draw all float functions in order of z-index. """ # We keep looping because some draw functions could add new functions # to this list. See `FloatContainer`. while self._draw_float_functions: # Sort the floats that we have so far by z_index. functions = sorted(self._draw_float_functions, key=lambda item: item[0]) # Draw only one at a time, then sort everything again. Now floats # might have been added. self._draw_float_functions = functions[1:] functions[0][1]() def append_style_to_content(self, style_str: str) -> None: """ For all the characters in the screen. Set the style string to the given `style_str`. """ b = self.data_buffer char_cache = _CHAR_CACHE append_style = " " + style_str for y, row in b.items(): for x, char in row.items(): row[x] = char_cache[char.char, char.style + append_style] def fill_area( self, write_position: WritePosition, style: str = "", after: bool = False ) -> None: """ Fill the content of this area, using the given `style`. The style is prepended before whatever was here before. """ if not style.strip(): return xmin = write_position.xpos xmax = write_position.xpos + write_position.width char_cache = _CHAR_CACHE data_buffer = self.data_buffer if after: append_style = " " + style prepend_style = "" else: append_style = "" prepend_style = style + " " for y in range( write_position.ypos, write_position.ypos + write_position.height ): row = data_buffer[y] for x in range(xmin, xmax): cell = row[x] row[x] = char_cache[ cell.char, prepend_style + cell.style + append_style ] class WritePosition: def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None: assert height >= 0 assert width >= 0 # xpos and ypos can be negative. (A float can be partially visible.) self.xpos = xpos self.ypos = ypos self.width = width self.height = height def __repr__(self) -> str: return f"{self.__class__.__name__}(x={self.xpos!r}, y={self.ypos!r}, width={self.width!r}, height={self.height!r})" ================================================ FILE: src/prompt_toolkit/layout/scrollable_pane.py ================================================ from __future__ import annotations from prompt_toolkit.data_structures import Point from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.key_binding import KeyBindingsBase from prompt_toolkit.mouse_events import MouseEvent from .containers import Container, ScrollOffsets from .dimension import AnyDimension, Dimension, sum_layout_dimensions, to_dimension from .mouse_handlers import MouseHandler, MouseHandlers from .screen import Char, Screen, WritePosition __all__ = ["ScrollablePane"] # Never go beyond this height, because performance will degrade. MAX_AVAILABLE_HEIGHT = 10_000 class ScrollablePane(Container): """ Container widget that exposes a larger virtual screen to its content and displays it in a vertical scrollbale region. Typically this is wrapped in a large `HSplit` container. Make sure in that case to not specify a `height` dimension of the `HSplit`, so that it will scale according to the content. .. note:: If you want to display a completion menu for widgets in this `ScrollablePane`, then it's still a good practice to use a `FloatContainer` with a `CompletionsMenu` in a `Float` at the top-level of the layout hierarchy, rather then nesting a `FloatContainer` in this `ScrollablePane`. (Otherwise, it's possible that the completion menu is clipped.) :param content: The content container. :param scrolloffset: Try to keep the cursor within this distance from the top/bottom (left/right offset is not used). :param keep_cursor_visible: When `True`, automatically scroll the pane so that the cursor (of the focused window) is always visible. :param keep_focused_window_visible: When `True`, automatically scroll the pane so that the focused window is visible, or as much visible as possible if it doesn't completely fit the screen. :param max_available_height: Always constraint the height to this amount for performance reasons. :param width: When given, use this width instead of looking at the children. :param height: When given, use this height instead of looking at the children. :param show_scrollbar: When `True` display a scrollbar on the right. """ def __init__( self, content: Container, scroll_offsets: ScrollOffsets | None = None, keep_cursor_visible: FilterOrBool = True, keep_focused_window_visible: FilterOrBool = True, max_available_height: int = MAX_AVAILABLE_HEIGHT, width: AnyDimension = None, height: AnyDimension = None, show_scrollbar: FilterOrBool = True, display_arrows: FilterOrBool = True, up_arrow_symbol: str = "^", down_arrow_symbol: str = "v", ) -> None: self.content = content self.scroll_offsets = scroll_offsets or ScrollOffsets(top=1, bottom=1) self.keep_cursor_visible = to_filter(keep_cursor_visible) self.keep_focused_window_visible = to_filter(keep_focused_window_visible) self.max_available_height = max_available_height self.width = width self.height = height self.show_scrollbar = to_filter(show_scrollbar) self.display_arrows = to_filter(display_arrows) self.up_arrow_symbol = up_arrow_symbol self.down_arrow_symbol = down_arrow_symbol self.vertical_scroll = 0 def __repr__(self) -> str: return f"ScrollablePane({self.content!r})" def reset(self) -> None: self.content.reset() def preferred_width(self, max_available_width: int) -> Dimension: if self.width is not None: return to_dimension(self.width) # We're only scrolling vertical. So the preferred width is equal to # that of the content. content_width = self.content.preferred_width(max_available_width) # If a scrollbar needs to be displayed, add +1 to the content width. if self.show_scrollbar(): return sum_layout_dimensions([Dimension.exact(1), content_width]) return content_width def preferred_height(self, width: int, max_available_height: int) -> Dimension: if self.height is not None: return to_dimension(self.height) # Prefer a height large enough so that it fits all the content. If not, # we'll make the pane scrollable. if self.show_scrollbar(): # If `show_scrollbar` is set. Always reserve space for the scrollbar. width -= 1 dimension = self.content.preferred_height(width, self.max_available_height) # Only take 'preferred' into account. Min/max can be anything. return Dimension(min=0, preferred=dimension.preferred) def write_to_screen( self, screen: Screen, mouse_handlers: MouseHandlers, write_position: WritePosition, parent_style: str, erase_bg: bool, z_index: int | None, ) -> None: """ Render scrollable pane content. This works by rendering on an off-screen canvas, and copying over the visible region. """ show_scrollbar = self.show_scrollbar() if show_scrollbar: virtual_width = write_position.width - 1 else: virtual_width = write_position.width # Compute preferred height again. virtual_height = self.content.preferred_height( virtual_width, self.max_available_height ).preferred # Ensure virtual height is at least the available height. virtual_height = max(virtual_height, write_position.height) virtual_height = min(virtual_height, self.max_available_height) # First, write the content to a virtual screen, then copy over the # visible part to the real screen. temp_screen = Screen(default_char=Char(char=" ", style=parent_style)) temp_screen.show_cursor = screen.show_cursor temp_write_position = WritePosition( xpos=0, ypos=0, width=virtual_width, height=virtual_height ) temp_mouse_handlers = MouseHandlers() self.content.write_to_screen( temp_screen, temp_mouse_handlers, temp_write_position, parent_style, erase_bg, z_index, ) temp_screen.draw_all_floats() # If anything in the virtual screen is focused, move vertical scroll to from prompt_toolkit.application import get_app focused_window = get_app().layout.current_window try: visible_win_write_pos = temp_screen.visible_windows_to_write_positions[ focused_window ] except KeyError: pass # No window focused here. Don't scroll. else: # Make sure this window is visible. self._make_window_visible( write_position.height, virtual_height, visible_win_write_pos, temp_screen.cursor_positions.get(focused_window), ) # Copy over virtual screen and zero width escapes to real screen. self._copy_over_screen(screen, temp_screen, write_position, virtual_width) # Copy over mouse handlers. self._copy_over_mouse_handlers( mouse_handlers, temp_mouse_handlers, write_position, virtual_width ) # Set screen.width/height. ypos = write_position.ypos xpos = write_position.xpos screen.width = max(screen.width, xpos + virtual_width) screen.height = max(screen.height, ypos + write_position.height) # Copy over window write positions. self._copy_over_write_positions(screen, temp_screen, write_position) if temp_screen.show_cursor: screen.show_cursor = True # Copy over cursor positions, if they are visible. for window, point in temp_screen.cursor_positions.items(): if ( 0 <= point.x < write_position.width and self.vertical_scroll <= point.y < write_position.height + self.vertical_scroll ): screen.cursor_positions[window] = Point( x=point.x + xpos, y=point.y + ypos - self.vertical_scroll ) # Copy over menu positions, but clip them to the visible area. for window, point in temp_screen.menu_positions.items(): screen.menu_positions[window] = self._clip_point_to_visible_area( Point(x=point.x + xpos, y=point.y + ypos - self.vertical_scroll), write_position, ) # Draw scrollbar. if show_scrollbar: self._draw_scrollbar( write_position, virtual_height, screen, ) def _clip_point_to_visible_area( self, point: Point, write_position: WritePosition ) -> Point: """ Ensure that the cursor and menu positions always are always reported """ if point.x < write_position.xpos: point = point._replace(x=write_position.xpos) if point.y < write_position.ypos: point = point._replace(y=write_position.ypos) if point.x >= write_position.xpos + write_position.width: point = point._replace(x=write_position.xpos + write_position.width - 1) if point.y >= write_position.ypos + write_position.height: point = point._replace(y=write_position.ypos + write_position.height - 1) return point def _copy_over_screen( self, screen: Screen, temp_screen: Screen, write_position: WritePosition, virtual_width: int, ) -> None: """ Copy over visible screen content and "zero width escape sequences". """ ypos = write_position.ypos xpos = write_position.xpos for y in range(write_position.height): temp_row = temp_screen.data_buffer[y + self.vertical_scroll] row = screen.data_buffer[y + ypos] temp_zero_width_escapes = temp_screen.zero_width_escapes[ y + self.vertical_scroll ] zero_width_escapes = screen.zero_width_escapes[y + ypos] for x in range(virtual_width): row[x + xpos] = temp_row[x] if x in temp_zero_width_escapes: zero_width_escapes[x + xpos] = temp_zero_width_escapes[x] def _copy_over_mouse_handlers( self, mouse_handlers: MouseHandlers, temp_mouse_handlers: MouseHandlers, write_position: WritePosition, virtual_width: int, ) -> None: """ Copy over mouse handlers from virtual screen to real screen. Note: we take `virtual_width` because we don't want to copy over mouse handlers that we possibly have behind the scrollbar. """ ypos = write_position.ypos xpos = write_position.xpos # Cache mouse handlers when wrapping them. Very often the same mouse # handler is registered for many positions. mouse_handler_wrappers: dict[MouseHandler, MouseHandler] = {} def wrap_mouse_handler(handler: MouseHandler) -> MouseHandler: "Wrap mouse handler. Translate coordinates in `MouseEvent`." if handler not in mouse_handler_wrappers: def new_handler(event: MouseEvent) -> None: new_event = MouseEvent( position=Point( x=event.position.x - xpos, y=event.position.y + self.vertical_scroll - ypos, ), event_type=event.event_type, button=event.button, modifiers=event.modifiers, ) handler(new_event) mouse_handler_wrappers[handler] = new_handler return mouse_handler_wrappers[handler] # Copy handlers. mouse_handlers_dict = mouse_handlers.mouse_handlers temp_mouse_handlers_dict = temp_mouse_handlers.mouse_handlers for y in range(write_position.height): if y in temp_mouse_handlers_dict: temp_mouse_row = temp_mouse_handlers_dict[y + self.vertical_scroll] mouse_row = mouse_handlers_dict[y + ypos] for x in range(virtual_width): if x in temp_mouse_row: mouse_row[x + xpos] = wrap_mouse_handler(temp_mouse_row[x]) def _copy_over_write_positions( self, screen: Screen, temp_screen: Screen, write_position: WritePosition ) -> None: """ Copy over window write positions. """ ypos = write_position.ypos xpos = write_position.xpos for win, write_pos in temp_screen.visible_windows_to_write_positions.items(): screen.visible_windows_to_write_positions[win] = WritePosition( xpos=write_pos.xpos + xpos, ypos=write_pos.ypos + ypos - self.vertical_scroll, # TODO: if the window is only partly visible, then truncate width/height. # This could be important if we have nested ScrollablePanes. height=write_pos.height, width=write_pos.width, ) def is_modal(self) -> bool: return self.content.is_modal() def get_key_bindings(self) -> KeyBindingsBase | None: return self.content.get_key_bindings() def get_children(self) -> list[Container]: return [self.content] def _make_window_visible( self, visible_height: int, virtual_height: int, visible_win_write_pos: WritePosition, cursor_position: Point | None, ) -> None: """ Scroll the scrollable pane, so that this window becomes visible. :param visible_height: Height of this `ScrollablePane` that is rendered. :param virtual_height: Height of the virtual, temp screen. :param visible_win_write_pos: `WritePosition` of the nested window on the temp screen. :param cursor_position: The location of the cursor position of this window on the temp screen. """ # Start with maximum allowed scroll range, and then reduce according to # the focused window and cursor position. min_scroll = 0 max_scroll = virtual_height - visible_height if self.keep_cursor_visible(): # Reduce min/max scroll according to the cursor in the focused window. if cursor_position is not None: offsets = self.scroll_offsets cpos_min_scroll = ( cursor_position.y - visible_height + 1 + offsets.bottom ) cpos_max_scroll = cursor_position.y - offsets.top min_scroll = max(min_scroll, cpos_min_scroll) max_scroll = max(0, min(max_scroll, cpos_max_scroll)) if self.keep_focused_window_visible(): # Reduce min/max scroll according to focused window position. # If the window is small enough, bot the top and bottom of the window # should be visible. if visible_win_write_pos.height <= visible_height: window_min_scroll = ( visible_win_write_pos.ypos + visible_win_write_pos.height - visible_height ) window_max_scroll = visible_win_write_pos.ypos else: # Window does not fit on the screen. Make sure at least the whole # screen is occupied with this window, and nothing else is shown. window_min_scroll = visible_win_write_pos.ypos window_max_scroll = ( visible_win_write_pos.ypos + visible_win_write_pos.height - visible_height ) min_scroll = max(min_scroll, window_min_scroll) max_scroll = min(max_scroll, window_max_scroll) if min_scroll > max_scroll: min_scroll = max_scroll # Should not happen. # Finally, properly clip the vertical scroll. if self.vertical_scroll > max_scroll: self.vertical_scroll = max_scroll if self.vertical_scroll < min_scroll: self.vertical_scroll = min_scroll def _draw_scrollbar( self, write_position: WritePosition, content_height: int, screen: Screen ) -> None: """ Draw the scrollbar on the screen. Note: There is some code duplication with the `ScrollbarMargin` implementation. """ window_height = write_position.height display_arrows = self.display_arrows() if display_arrows: window_height -= 2 try: fraction_visible = write_position.height / float(content_height) fraction_above = self.vertical_scroll / float(content_height) scrollbar_height = int( min(window_height, max(1, window_height * fraction_visible)) ) scrollbar_top = int(window_height * fraction_above) except ZeroDivisionError: return else: def is_scroll_button(row: int) -> bool: "True if we should display a button on this row." return scrollbar_top <= row <= scrollbar_top + scrollbar_height xpos = write_position.xpos + write_position.width - 1 ypos = write_position.ypos data_buffer = screen.data_buffer # Up arrow. if display_arrows: data_buffer[ypos][xpos] = Char( self.up_arrow_symbol, "class:scrollbar.arrow" ) ypos += 1 # Scrollbar body. scrollbar_background = "class:scrollbar.background" scrollbar_background_start = "class:scrollbar.background,scrollbar.start" scrollbar_button = "class:scrollbar.button" scrollbar_button_end = "class:scrollbar.button,scrollbar.end" for i in range(window_height): style = "" if is_scroll_button(i): if not is_scroll_button(i + 1): # Give the last cell a different style, because we want # to underline this. style = scrollbar_button_end else: style = scrollbar_button else: if is_scroll_button(i + 1): style = scrollbar_background_start else: style = scrollbar_background data_buffer[ypos][xpos] = Char(" ", style) ypos += 1 # Down arrow if display_arrows: data_buffer[ypos][xpos] = Char( self.down_arrow_symbol, "class:scrollbar.arrow" ) ================================================ FILE: src/prompt_toolkit/layout/utils.py ================================================ from __future__ import annotations from collections.abc import Iterable from typing import TYPE_CHECKING, TypeVar, cast, overload from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple if TYPE_CHECKING: from typing_extensions import SupportsIndex __all__ = [ "explode_text_fragments", ] _T = TypeVar("_T", bound=OneStyleAndTextTuple) class _ExplodedList(list[_T]): """ Wrapper around a list, that marks it as 'exploded'. As soon as items are added or the list is extended, the new items are automatically exploded as well. """ exploded = True def append(self, item: _T) -> None: self.extend([item]) def extend(self, lst: Iterable[_T]) -> None: super().extend(explode_text_fragments(lst)) def insert(self, index: SupportsIndex, item: _T) -> None: raise NotImplementedError # TODO # TODO: When creating a copy() or [:], return also an _ExplodedList. @overload def __setitem__(self, index: SupportsIndex, value: _T) -> None: ... @overload def __setitem__(self, index: slice, value: Iterable[_T]) -> None: ... def __setitem__( self, index: SupportsIndex | slice, value: _T | Iterable[_T] ) -> None: """ Ensure that when `(style_str, 'long string')` is set, the string will be exploded. """ if not isinstance(index, slice): int_index = index.__index__() index = slice(int_index, int_index + 1) if isinstance(value, tuple): # In case of `OneStyleAndTextTuple`. value = cast("list[_T]", [value]) super().__setitem__(index, explode_text_fragments(value)) def explode_text_fragments(fragments: Iterable[_T]) -> _ExplodedList[_T]: """ Turn a list of (style_str, text) tuples into another list where each string is exactly one character. It should be fine to call this function several times. Calling this on a list that is already exploded, is a null operation. :param fragments: List of (style, text) tuples. """ # When the fragments is already exploded, don't explode again. if isinstance(fragments, _ExplodedList): return fragments result: list[_T] = [] for style, string, *rest in fragments: for c in string: result.append((style, c, *rest)) # type: ignore return _ExplodedList(result) ================================================ FILE: src/prompt_toolkit/lexers/__init__.py ================================================ """ Lexer interface and implementations. Used for syntax highlighting. """ from __future__ import annotations from .base import DynamicLexer, Lexer, SimpleLexer from .pygments import PygmentsLexer, RegexSync, SyncFromStart, SyntaxSync __all__ = [ # Base. "Lexer", "SimpleLexer", "DynamicLexer", # Pygments. "PygmentsLexer", "RegexSync", "SyncFromStart", "SyntaxSync", ] ================================================ FILE: src/prompt_toolkit/lexers/base.py ================================================ """ Base classes for prompt_toolkit lexers. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable, Hashable from prompt_toolkit.document import Document from prompt_toolkit.formatted_text.base import StyleAndTextTuples __all__ = [ "Lexer", "SimpleLexer", "DynamicLexer", ] class Lexer(metaclass=ABCMeta): """ Base class for all lexers. """ @abstractmethod def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: """ Takes a :class:`~prompt_toolkit.document.Document` and returns a callable that takes a line number and returns a list of ``(style_str, text)`` tuples for that line. XXX: Note that in the past, this was supposed to return a list of ``(Token, text)`` tuples, just like a Pygments lexer. """ def invalidation_hash(self) -> Hashable: """ When this changes, `lex_document` could give a different output. (Only used for `DynamicLexer`.) """ return id(self) class SimpleLexer(Lexer): """ Lexer that doesn't do any tokenizing and returns the whole input as one token. :param style: The style string for this lexer. """ def __init__(self, style: str = "") -> None: self.style = style def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: lines = document.lines def get_line(lineno: int) -> StyleAndTextTuples: "Return the tokens for the given line." try: return [(self.style, lines[lineno])] except IndexError: return [] return get_line class DynamicLexer(Lexer): """ Lexer class that can dynamically returns any Lexer. :param get_lexer: Callable that returns a :class:`.Lexer` instance. """ def __init__(self, get_lexer: Callable[[], Lexer | None]) -> None: self.get_lexer = get_lexer self._dummy = SimpleLexer() def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: lexer = self.get_lexer() or self._dummy return lexer.lex_document(document) def invalidation_hash(self) -> Hashable: lexer = self.get_lexer() or self._dummy return id(lexer) ================================================ FILE: src/prompt_toolkit/lexers/pygments.py ================================================ """ Adaptor classes for using Pygments lexers within prompt_toolkit. This includes syntax synchronization code, so that we don't have to start lexing at the beginning of a document, when displaying a very large text. """ from __future__ import annotations import re from abc import ABCMeta, abstractmethod from collections.abc import Callable, Generator, Iterable from typing import TYPE_CHECKING from prompt_toolkit.document import Document from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.formatted_text.utils import split_lines from prompt_toolkit.styles.pygments import pygments_token_to_classname from .base import Lexer, SimpleLexer if TYPE_CHECKING: from pygments.lexer import Lexer as PygmentsLexerCls __all__ = [ "PygmentsLexer", "SyntaxSync", "SyncFromStart", "RegexSync", ] class SyntaxSync(metaclass=ABCMeta): """ Syntax synchronizer. This is a tool that finds a start position for the lexer. This is especially important when editing big documents; we don't want to start the highlighting by running the lexer from the beginning of the file. That is very slow when editing. """ @abstractmethod def get_sync_start_position( self, document: Document, lineno: int ) -> tuple[int, int]: """ Return the position from where we can start lexing as a (row, column) tuple. :param document: `Document` instance that contains all the lines. :param lineno: The line that we want to highlight. (We need to return this line, or an earlier position.) """ class SyncFromStart(SyntaxSync): """ Always start the syntax highlighting from the beginning. """ def get_sync_start_position( self, document: Document, lineno: int ) -> tuple[int, int]: return 0, 0 class RegexSync(SyntaxSync): """ Synchronize by starting at a line that matches the given regex pattern. """ # Never go more than this amount of lines backwards for synchronization. # That would be too CPU intensive. MAX_BACKWARDS = 500 # Start lexing at the start, if we are in the first 'n' lines and no # synchronization position was found. FROM_START_IF_NO_SYNC_POS_FOUND = 100 def __init__(self, pattern: str) -> None: self._compiled_pattern = re.compile(pattern) def get_sync_start_position( self, document: Document, lineno: int ) -> tuple[int, int]: """ Scan backwards, and find a possible position to start. """ pattern = self._compiled_pattern lines = document.lines # Scan upwards, until we find a point where we can start the syntax # synchronization. for i in range(lineno, max(-1, lineno - self.MAX_BACKWARDS), -1): match = pattern.match(lines[i]) if match: return i, match.start() # No synchronization point found. If we aren't that far from the # beginning, start at the very beginning, otherwise, just try to start # at the current line. if lineno < self.FROM_START_IF_NO_SYNC_POS_FOUND: return 0, 0 else: return lineno, 0 @classmethod def from_pygments_lexer_cls(cls, lexer_cls: type[PygmentsLexerCls]) -> RegexSync: """ Create a :class:`.RegexSync` instance for this Pygments lexer class. """ patterns = { # For Python, start highlighting at any class/def block. "Python": r"^\s*(class|def)\s+", "Python 3": r"^\s*(class|def)\s+", # For HTML, start at any open/close tag definition. "HTML": r"<[/a-zA-Z]", # For javascript, start at a function. "JavaScript": r"\bfunction\b", # TODO: Add definitions for other languages. # By default, we start at every possible line. } p = patterns.get(lexer_cls.name, "^") return cls(p) class _TokenCache(dict[tuple[str, ...], str]): """ Cache that converts Pygments tokens into `prompt_toolkit` style objects. ``Token.A.B.C`` will be converted into: ``class:pygments,pygments.A,pygments.A.B,pygments.A.B.C`` """ def __missing__(self, key: tuple[str, ...]) -> str: result = "class:" + pygments_token_to_classname(key) self[key] = result return result _token_cache = _TokenCache() class PygmentsLexer(Lexer): """ Lexer that calls a pygments lexer. Example:: from pygments.lexers.html import HtmlLexer lexer = PygmentsLexer(HtmlLexer) Note: Don't forget to also load a Pygments compatible style. E.g.:: from prompt_toolkit.styles.from_pygments import style_from_pygments_cls from pygments.styles import get_style_by_name style = style_from_pygments_cls(get_style_by_name('monokai')) :param pygments_lexer_cls: A `Lexer` from Pygments. :param sync_from_start: Start lexing at the start of the document. This will always give the best results, but it will be slow for bigger documents. (When the last part of the document is display, then the whole document will be lexed by Pygments on every key stroke.) It is recommended to disable this for inputs that are expected to be more than 1,000 lines. :param syntax_sync: `SyntaxSync` object. """ # Minimum amount of lines to go backwards when starting the parser. # This is important when the lines are retrieved in reverse order, or when # scrolling upwards. (Due to the complexity of calculating the vertical # scroll offset in the `Window` class, lines are not always retrieved in # order.) MIN_LINES_BACKWARDS = 50 # When a parser was started this amount of lines back, read the parser # until we get the current line. Otherwise, start a new parser. # (This should probably be bigger than MIN_LINES_BACKWARDS.) REUSE_GENERATOR_MAX_DISTANCE = 100 def __init__( self, pygments_lexer_cls: type[PygmentsLexerCls], sync_from_start: FilterOrBool = True, syntax_sync: SyntaxSync | None = None, ) -> None: self.pygments_lexer_cls = pygments_lexer_cls self.sync_from_start = to_filter(sync_from_start) # Instantiate the Pygments lexer. self.pygments_lexer = pygments_lexer_cls( stripnl=False, stripall=False, ensurenl=False ) # Create syntax sync instance. self.syntax_sync = syntax_sync or RegexSync.from_pygments_lexer_cls( pygments_lexer_cls ) @classmethod def from_filename( cls, filename: str, sync_from_start: FilterOrBool = True ) -> Lexer: """ Create a `Lexer` from a filename. """ # Inline imports: the Pygments dependency is optional! from pygments.lexers import get_lexer_for_filename from pygments.util import ClassNotFound try: pygments_lexer = get_lexer_for_filename(filename) except ClassNotFound: return SimpleLexer() else: return cls(pygments_lexer.__class__, sync_from_start=sync_from_start) def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: """ Create a lexer function that takes a line number and returns the list of (style_str, text) tuples as the Pygments lexer returns for that line. """ LineGenerator = Generator[tuple[int, StyleAndTextTuples], None, None] # Cache of already lexed lines. cache: dict[int, StyleAndTextTuples] = {} # Pygments generators that are currently lexing. # Map lexer generator to the line number. line_generators: dict[LineGenerator, int] = {} def get_syntax_sync() -> SyntaxSync: "The Syntax synchronization object that we currently use." if self.sync_from_start(): return SyncFromStart() else: return self.syntax_sync def find_closest_generator(i: int) -> LineGenerator | None: "Return a generator close to line 'i', or None if none was found." for generator, lineno in line_generators.items(): if lineno < i and i - lineno < self.REUSE_GENERATOR_MAX_DISTANCE: return generator return None def create_line_generator(start_lineno: int, column: int = 0) -> LineGenerator: """ Create a generator that yields the lexed lines. Each iteration it yields a (line_number, [(style_str, text), ...]) tuple. """ def get_text_fragments() -> Iterable[tuple[str, str]]: text = "\n".join(document.lines[start_lineno:])[column:] # We call `get_text_fragments_unprocessed`, because `get_tokens` will # still replace \r\n and \r by \n. (We don't want that, # Pygments should return exactly the same amount of text, as we # have given as input.) for _, t, v in self.pygments_lexer.get_tokens_unprocessed(text): # Turn Pygments `Token` object into prompt_toolkit style # strings. yield _token_cache[t], v yield from enumerate(split_lines(list(get_text_fragments())), start_lineno) def get_generator(i: int) -> LineGenerator: """ Find an already started generator that is close, or create a new one. """ # Find closest line generator. generator = find_closest_generator(i) if generator: return generator # No generator found. Determine starting point for the syntax # synchronization first. # Go at least x lines back. (Make scrolling upwards more # efficient.) i = max(0, i - self.MIN_LINES_BACKWARDS) if i == 0: row = 0 column = 0 else: row, column = get_syntax_sync().get_sync_start_position(document, i) # Find generator close to this point, or otherwise create a new one. generator = find_closest_generator(i) if generator: return generator else: generator = create_line_generator(row, column) # If the column is not 0, ignore the first line. (Which is # incomplete. This happens when the synchronization algorithm tells # us to start parsing in the middle of a line.) if column: next(generator) row += 1 line_generators[generator] = row return generator def get_line(i: int) -> StyleAndTextTuples: "Return the tokens for a given line number." try: return cache[i] except KeyError: generator = get_generator(i) # Exhaust the generator, until we find the requested line. for num, line in generator: cache[num] = line if num == i: line_generators[generator] = i # Remove the next item from the cache. # (It could happen that it's already there, because of # another generator that started filling these lines, # but we want to synchronize these lines with the # current lexer's state.) if num + 1 in cache: del cache[num + 1] return cache[num] return [] return get_line ================================================ FILE: src/prompt_toolkit/log.py ================================================ """ Logging configuration. """ from __future__ import annotations import logging __all__ = [ "logger", ] logger = logging.getLogger(__package__) ================================================ FILE: src/prompt_toolkit/mouse_events.py ================================================ """ Mouse events. How it works ------------ The renderer has a 2 dimensional grid of mouse event handlers. (`prompt_toolkit.layout.MouseHandlers`.) When the layout is rendered, the `Window` class will make sure that this grid will also be filled with callbacks. For vt100 terminals, mouse events are received through stdin, just like any other key press. There is a handler among the key bindings that catches these events and forwards them to such a mouse event handler. It passes through the `Window` class where the coordinates are translated from absolute coordinates to coordinates relative to the user control, and there `UIControl.mouse_handler` is called. """ from __future__ import annotations from enum import Enum from .data_structures import Point __all__ = ["MouseEventType", "MouseButton", "MouseModifier", "MouseEvent"] class MouseEventType(Enum): # Mouse up: This same event type is fired for all three events: left mouse # up, right mouse up, or middle mouse up MOUSE_UP = "MOUSE_UP" # Mouse down: This implicitly refers to the left mouse down (this event is # not fired upon pressing the middle or right mouse buttons). MOUSE_DOWN = "MOUSE_DOWN" SCROLL_UP = "SCROLL_UP" SCROLL_DOWN = "SCROLL_DOWN" # Triggered when the left mouse button is held down, and the mouse moves MOUSE_MOVE = "MOUSE_MOVE" class MouseButton(Enum): LEFT = "LEFT" MIDDLE = "MIDDLE" RIGHT = "RIGHT" # When we're scrolling, or just moving the mouse and not pressing a button. NONE = "NONE" # This is for when we don't know which mouse button was pressed, but we do # know that one has been pressed during this mouse event (as opposed to # scrolling, for example) UNKNOWN = "UNKNOWN" class MouseModifier(Enum): SHIFT = "SHIFT" ALT = "ALT" CONTROL = "CONTROL" class MouseEvent: """ Mouse event, sent to `UIControl.mouse_handler`. :param position: `Point` instance. :param event_type: `MouseEventType`. """ def __init__( self, position: Point, event_type: MouseEventType, button: MouseButton, modifiers: frozenset[MouseModifier], ) -> None: self.position = position self.event_type = event_type self.button = button self.modifiers = modifiers def __repr__(self) -> str: return f"MouseEvent({self.position!r},{self.event_type!r},{self.button!r},{self.modifiers!r})" ================================================ FILE: src/prompt_toolkit/output/__init__.py ================================================ from __future__ import annotations from .base import DummyOutput, Output from .color_depth import ColorDepth from .defaults import create_output __all__ = [ # Base. "Output", "DummyOutput", # Color depth. "ColorDepth", # Defaults. "create_output", ] ================================================ FILE: src/prompt_toolkit/output/base.py ================================================ """ Interface for an output. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from typing import TextIO from prompt_toolkit.cursor_shapes import CursorShape from prompt_toolkit.data_structures import Size from prompt_toolkit.styles import Attrs from .color_depth import ColorDepth __all__ = [ "Output", "DummyOutput", ] class Output(metaclass=ABCMeta): """ Base class defining the output interface for a :class:`~prompt_toolkit.renderer.Renderer`. Actual implementations are :class:`~prompt_toolkit.output.vt100.Vt100_Output` and :class:`~prompt_toolkit.output.win32.Win32Output`. """ stdout: TextIO | None = None @abstractmethod def fileno(self) -> int: "Return the file descriptor to which we can write for the output." @abstractmethod def encoding(self) -> str: """ Return the encoding for this output, e.g. 'utf-8'. (This is used mainly to know which characters are supported by the output the data, so that the UI can provide alternatives, when required.) """ @abstractmethod def write(self, data: str) -> None: "Write text (Terminal escape sequences will be removed/escaped.)" @abstractmethod def write_raw(self, data: str) -> None: "Write text." @abstractmethod def set_title(self, title: str) -> None: "Set terminal title." @abstractmethod def clear_title(self) -> None: "Clear title again. (or restore previous title.)" @abstractmethod def flush(self) -> None: "Write to output stream and flush." @abstractmethod def erase_screen(self) -> None: """ Erases the screen with the background color and moves the cursor to home. """ @abstractmethod def enter_alternate_screen(self) -> None: "Go to the alternate screen buffer. (For full screen applications)." @abstractmethod def quit_alternate_screen(self) -> None: "Leave the alternate screen buffer." @abstractmethod def enable_mouse_support(self) -> None: "Enable mouse." @abstractmethod def disable_mouse_support(self) -> None: "Disable mouse." @abstractmethod def erase_end_of_line(self) -> None: """ Erases from the current cursor position to the end of the current line. """ @abstractmethod def erase_down(self) -> None: """ Erases the screen from the current line down to the bottom of the screen. """ @abstractmethod def reset_attributes(self) -> None: "Reset color and styling attributes." @abstractmethod def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: "Set new color and styling attributes." @abstractmethod def disable_autowrap(self) -> None: "Disable auto line wrapping." @abstractmethod def enable_autowrap(self) -> None: "Enable auto line wrapping." @abstractmethod def cursor_goto(self, row: int = 0, column: int = 0) -> None: "Move cursor position." @abstractmethod def cursor_up(self, amount: int) -> None: "Move cursor `amount` place up." @abstractmethod def cursor_down(self, amount: int) -> None: "Move cursor `amount` place down." @abstractmethod def cursor_forward(self, amount: int) -> None: "Move cursor `amount` place forward." @abstractmethod def cursor_backward(self, amount: int) -> None: "Move cursor `amount` place backward." @abstractmethod def hide_cursor(self) -> None: "Hide cursor." @abstractmethod def show_cursor(self) -> None: "Show cursor." @abstractmethod def set_cursor_shape(self, cursor_shape: CursorShape) -> None: "Set cursor shape to block, beam or underline." @abstractmethod def reset_cursor_shape(self) -> None: "Reset cursor shape." def ask_for_cpr(self) -> None: """ Asks for a cursor position report (CPR). (VT100 only.) """ @property def responds_to_cpr(self) -> bool: """ `True` if the `Application` can expect to receive a CPR response after calling `ask_for_cpr` (this will come back through the corresponding `Input`). This is used to determine the amount of available rows we have below the cursor position. In the first place, we have this so that the drop down autocompletion menus are sized according to the available space. On Windows, we don't need this, there we have `get_rows_below_cursor_position`. """ return False @abstractmethod def get_size(self) -> Size: "Return the size of the output window." def bell(self) -> None: "Sound bell." def enable_bracketed_paste(self) -> None: "For vt100 only." def disable_bracketed_paste(self) -> None: "For vt100 only." def reset_cursor_key_mode(self) -> None: """ For vt100 only. Put the terminal in normal cursor mode (instead of application mode). See: https://vt100.net/docs/vt100-ug/chapter3.html """ def scroll_buffer_to_prompt(self) -> None: "For Win32 only." def get_rows_below_cursor_position(self) -> int: "For Windows only." raise NotImplementedError @abstractmethod def get_default_color_depth(self) -> ColorDepth: """ Get default color depth for this output. This value will be used if no color depth was explicitly passed to the `Application`. .. note:: If the `$PROMPT_TOOLKIT_COLOR_DEPTH` environment variable has been set, then `outputs.defaults.create_output` will pass this value to the implementation as the default_color_depth, which is returned here. (This is not used when the output corresponds to a prompt_toolkit SSH/Telnet session.) """ class DummyOutput(Output): """ For testing. An output class that doesn't render anything. """ def fileno(self) -> int: "There is no sensible default for fileno()." raise NotImplementedError def encoding(self) -> str: return "utf-8" def write(self, data: str) -> None: pass def write_raw(self, data: str) -> None: pass def set_title(self, title: str) -> None: pass def clear_title(self) -> None: pass def flush(self) -> None: pass def erase_screen(self) -> None: pass def enter_alternate_screen(self) -> None: pass def quit_alternate_screen(self) -> None: pass def enable_mouse_support(self) -> None: pass def disable_mouse_support(self) -> None: pass def erase_end_of_line(self) -> None: pass def erase_down(self) -> None: pass def reset_attributes(self) -> None: pass def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: pass def disable_autowrap(self) -> None: pass def enable_autowrap(self) -> None: pass def cursor_goto(self, row: int = 0, column: int = 0) -> None: pass def cursor_up(self, amount: int) -> None: pass def cursor_down(self, amount: int) -> None: pass def cursor_forward(self, amount: int) -> None: pass def cursor_backward(self, amount: int) -> None: pass def hide_cursor(self) -> None: pass def show_cursor(self) -> None: pass def set_cursor_shape(self, cursor_shape: CursorShape) -> None: pass def reset_cursor_shape(self) -> None: pass def ask_for_cpr(self) -> None: pass def bell(self) -> None: pass def enable_bracketed_paste(self) -> None: pass def disable_bracketed_paste(self) -> None: pass def scroll_buffer_to_prompt(self) -> None: pass def get_size(self) -> Size: return Size(rows=40, columns=80) def get_rows_below_cursor_position(self) -> int: return 40 def get_default_color_depth(self) -> ColorDepth: return ColorDepth.DEPTH_1_BIT ================================================ FILE: src/prompt_toolkit/output/color_depth.py ================================================ from __future__ import annotations import os from enum import Enum __all__ = [ "ColorDepth", ] class ColorDepth(str, Enum): """ Possible color depth values for the output. """ value: str #: One color only. DEPTH_1_BIT = "DEPTH_1_BIT" #: ANSI Colors. DEPTH_4_BIT = "DEPTH_4_BIT" #: The default. DEPTH_8_BIT = "DEPTH_8_BIT" #: 24 bit True color. DEPTH_24_BIT = "DEPTH_24_BIT" # Aliases. MONOCHROME = DEPTH_1_BIT ANSI_COLORS_ONLY = DEPTH_4_BIT DEFAULT = DEPTH_8_BIT TRUE_COLOR = DEPTH_24_BIT @classmethod def from_env(cls) -> ColorDepth | None: """ Return the color depth if the $PROMPT_TOOLKIT_COLOR_DEPTH environment variable has been set. This is a way to enforce a certain color depth in all prompt_toolkit applications. """ # Disable color if a `NO_COLOR` environment variable is set. # See: https://no-color.org/ if os.environ.get("NO_COLOR"): return cls.DEPTH_1_BIT # Check the `PROMPT_TOOLKIT_COLOR_DEPTH` environment variable. all_values = [i.value for i in ColorDepth] if os.environ.get("PROMPT_TOOLKIT_COLOR_DEPTH") in all_values: return cls(os.environ["PROMPT_TOOLKIT_COLOR_DEPTH"]) return None @classmethod def default(cls) -> ColorDepth: """ Return the default color depth for the default output. """ from .defaults import create_output return create_output().get_default_color_depth() ================================================ FILE: src/prompt_toolkit/output/conemu.py ================================================ from __future__ import annotations import sys assert sys.platform == "win32" from typing import Any, TextIO from prompt_toolkit.data_structures import Size from .base import Output from .color_depth import ColorDepth from .vt100 import Vt100_Output from .win32 import Win32Output __all__ = [ "ConEmuOutput", ] class ConEmuOutput: """ ConEmu (Windows) output abstraction. ConEmu is a Windows console application, but it also supports ANSI escape sequences. This output class is actually a proxy to both `Win32Output` and `Vt100_Output`. It uses `Win32Output` for console sizing and scrolling, but all cursor movements and scrolling happens through the `Vt100_Output`. This way, we can have 256 colors in ConEmu and Cmder. Rendering will be even a little faster as well. http://conemu.github.io/ http://gooseberrycreative.com/cmder/ """ def __init__( self, stdout: TextIO, default_color_depth: ColorDepth | None = None ) -> None: self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth) self.vt100_output = Vt100_Output( stdout, lambda: Size(0, 0), default_color_depth=default_color_depth ) @property def responds_to_cpr(self) -> bool: return False # We don't need this on Windows. def __getattr__(self, name: str) -> Any: if name in ( "get_size", "get_rows_below_cursor_position", "enable_mouse_support", "disable_mouse_support", "scroll_buffer_to_prompt", "get_win32_screen_buffer_info", "enable_bracketed_paste", "disable_bracketed_paste", ): return getattr(self.win32_output, name) else: return getattr(self.vt100_output, name) Output.register(ConEmuOutput) ================================================ FILE: src/prompt_toolkit/output/defaults.py ================================================ from __future__ import annotations import sys from typing import TYPE_CHECKING, TextIO, cast from prompt_toolkit.utils import ( get_bell_environment_variable, get_term_environment_variable, is_conemu_ansi, ) from .base import DummyOutput, Output from .color_depth import ColorDepth from .plain_text import PlainTextOutput if TYPE_CHECKING: from prompt_toolkit.patch_stdout import StdoutProxy __all__ = [ "create_output", ] def create_output( stdout: TextIO | StdoutProxy | None = None, always_prefer_tty: bool = False ) -> Output: """ Return an :class:`~prompt_toolkit.output.Output` instance for the command line. :param stdout: The stdout object :param always_prefer_tty: When set, look for `sys.stderr` if `sys.stdout` is not a TTY. Useful if `sys.stdout` is redirected to a file, but we still want user input and output on the terminal. By default, this is `False`. If `sys.stdout` is not a terminal (maybe it's redirected to a file), then a `PlainTextOutput` will be returned. That way, tools like `print_formatted_text` will write plain text into that file. """ # Consider TERM, PROMPT_TOOLKIT_BELL, and PROMPT_TOOLKIT_COLOR_DEPTH # environment variables. Notice that PROMPT_TOOLKIT_COLOR_DEPTH value is # the default that's used if the Application doesn't override it. term_from_env = get_term_environment_variable() bell_from_env = get_bell_environment_variable() color_depth_from_env = ColorDepth.from_env() if stdout is None: # By default, render to stdout. If the output is piped somewhere else, # render to stderr. stdout = sys.stdout if always_prefer_tty: for io in [sys.stdout, sys.stderr]: if io is not None and io.isatty(): # (This is `None` when using `pythonw.exe` on Windows.) stdout = io break # If the patch_stdout context manager has been used, then sys.stdout is # replaced by this proxy. For prompt_toolkit applications, we want to use # the real stdout. from prompt_toolkit.patch_stdout import StdoutProxy while isinstance(stdout, StdoutProxy): stdout = stdout.original_stdout # If the output is still `None`, use a DummyOutput. # This happens for instance on Windows, when running the application under # `pythonw.exe`. In that case, there won't be a terminal Window, and # stdin/stdout/stderr are `None`. if stdout is None: return DummyOutput() if sys.platform == "win32": from .conemu import ConEmuOutput from .win32 import Win32Output from .windows10 import Windows10_Output, is_win_vt100_enabled if is_win_vt100_enabled(): return cast( Output, Windows10_Output(stdout, default_color_depth=color_depth_from_env), ) if is_conemu_ansi(): return cast( Output, ConEmuOutput(stdout, default_color_depth=color_depth_from_env) ) else: return Win32Output(stdout, default_color_depth=color_depth_from_env) else: from .vt100 import Vt100_Output # Stdout is not a TTY? Render as plain text. # This is mostly useful if stdout is redirected to a file, and # `print_formatted_text` is used. if not stdout.isatty(): return PlainTextOutput(stdout) return Vt100_Output.from_pty( stdout, term=term_from_env, default_color_depth=color_depth_from_env, enable_bell=bell_from_env, ) ================================================ FILE: src/prompt_toolkit/output/flush_stdout.py ================================================ from __future__ import annotations import errno import os import sys from collections.abc import Iterator from contextlib import contextmanager from typing import IO, TextIO __all__ = ["flush_stdout"] def flush_stdout(stdout: TextIO, data: str) -> None: # If the IO object has an `encoding` and `buffer` attribute, it means that # we can access the underlying BinaryIO object and write into it in binary # mode. This is preferred if possible. # NOTE: When used in a Jupyter notebook, don't write binary. # `ipykernel.iostream.OutStream` has an `encoding` attribute, but not # a `buffer` attribute, so we can't write binary in it. has_binary_io = hasattr(stdout, "encoding") and hasattr(stdout, "buffer") try: # Ensure that `stdout` is made blocking when writing into it. # Otherwise, when uvloop is activated (which makes stdout # non-blocking), and we write big amounts of text, then we get a # `BlockingIOError` here. with _blocking_io(stdout): # (We try to encode ourself, because that way we can replace # characters that don't exist in the character set, avoiding # UnicodeEncodeError crashes. E.g. u'\xb7' does not appear in 'ascii'.) # My Arch Linux installation of july 2015 reported 'ANSI_X3.4-1968' # for sys.stdout.encoding in xterm. if has_binary_io: stdout.buffer.write(data.encode(stdout.encoding or "utf-8", "replace")) else: stdout.write(data) stdout.flush() except OSError as e: if e.args and e.args[0] == errno.EINTR: # Interrupted system call. Can happen in case of a window # resize signal. (Just ignore. The resize handler will render # again anyway.) pass elif e.args and e.args[0] == 0: # This can happen when there is a lot of output and the user # sends a KeyboardInterrupt by pressing Control-C. E.g. in # a Python REPL when we execute "while True: print('test')". # (The `ptpython` REPL uses this `Output` class instead of # `stdout` directly -- in order to be network transparent.) # So, just ignore. pass else: raise @contextmanager def _blocking_io(io: IO[str]) -> Iterator[None]: """ Ensure that the FD for `io` is set to blocking in here. """ if sys.platform == "win32": # On Windows, the `os` module doesn't have a `get/set_blocking` # function. yield return try: fd = io.fileno() blocking = os.get_blocking(fd) except: # noqa # Failed somewhere. # `get_blocking` can raise `OSError`. # The io object can raise `AttributeError` when no `fileno()` method is # present if we're not a real file object. blocking = True # Assume we're good, and don't do anything. try: # Make blocking if we weren't blocking yet. if not blocking: os.set_blocking(fd, True) yield finally: # Restore original blocking mode. if not blocking: os.set_blocking(fd, blocking) ================================================ FILE: src/prompt_toolkit/output/plain_text.py ================================================ from __future__ import annotations from typing import TextIO from prompt_toolkit.cursor_shapes import CursorShape from prompt_toolkit.data_structures import Size from prompt_toolkit.styles import Attrs from .base import Output from .color_depth import ColorDepth from .flush_stdout import flush_stdout __all__ = ["PlainTextOutput"] class PlainTextOutput(Output): """ Output that won't include any ANSI escape sequences. Useful when stdout is not a terminal. Maybe stdout is redirected to a file. In this case, if `print_formatted_text` is used, for instance, we don't want to include formatting. (The code is mostly identical to `Vt100_Output`, but without the formatting.) """ def __init__(self, stdout: TextIO) -> None: assert all(hasattr(stdout, a) for a in ("write", "flush")) self.stdout: TextIO = stdout self._buffer: list[str] = [] def fileno(self) -> int: "There is no sensible default for fileno()." return self.stdout.fileno() def encoding(self) -> str: return "utf-8" def write(self, data: str) -> None: self._buffer.append(data) def write_raw(self, data: str) -> None: self._buffer.append(data) def set_title(self, title: str) -> None: pass def clear_title(self) -> None: pass def flush(self) -> None: if not self._buffer: return data = "".join(self._buffer) self._buffer = [] flush_stdout(self.stdout, data) def erase_screen(self) -> None: pass def enter_alternate_screen(self) -> None: pass def quit_alternate_screen(self) -> None: pass def enable_mouse_support(self) -> None: pass def disable_mouse_support(self) -> None: pass def erase_end_of_line(self) -> None: pass def erase_down(self) -> None: pass def reset_attributes(self) -> None: pass def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: pass def disable_autowrap(self) -> None: pass def enable_autowrap(self) -> None: pass def cursor_goto(self, row: int = 0, column: int = 0) -> None: pass def cursor_up(self, amount: int) -> None: pass def cursor_down(self, amount: int) -> None: self._buffer.append("\n") def cursor_forward(self, amount: int) -> None: self._buffer.append(" " * amount) def cursor_backward(self, amount: int) -> None: pass def hide_cursor(self) -> None: pass def show_cursor(self) -> None: pass def set_cursor_shape(self, cursor_shape: CursorShape) -> None: pass def reset_cursor_shape(self) -> None: pass def ask_for_cpr(self) -> None: pass def bell(self) -> None: pass def enable_bracketed_paste(self) -> None: pass def disable_bracketed_paste(self) -> None: pass def scroll_buffer_to_prompt(self) -> None: pass def get_size(self) -> Size: return Size(rows=40, columns=80) def get_rows_below_cursor_position(self) -> int: return 8 def get_default_color_depth(self) -> ColorDepth: return ColorDepth.DEPTH_1_BIT ================================================ FILE: src/prompt_toolkit/output/vt100.py ================================================ """ Output for vt100 terminals. A lot of thanks, regarding outputting of colors, goes to the Pygments project: (We don't rely on Pygments anymore, because many things are very custom, and everything has been highly optimized.) http://pygments.org/ """ from __future__ import annotations import io import os import sys from collections.abc import Callable, Hashable, Iterable, Sequence from typing import TextIO from prompt_toolkit.cursor_shapes import CursorShape from prompt_toolkit.data_structures import Size from prompt_toolkit.output import Output from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs from prompt_toolkit.utils import is_dumb_terminal from .color_depth import ColorDepth from .flush_stdout import flush_stdout __all__ = [ "Vt100_Output", ] FG_ANSI_COLORS = { "ansidefault": 39, # Low intensity. "ansiblack": 30, "ansired": 31, "ansigreen": 32, "ansiyellow": 33, "ansiblue": 34, "ansimagenta": 35, "ansicyan": 36, "ansigray": 37, # High intensity. "ansibrightblack": 90, "ansibrightred": 91, "ansibrightgreen": 92, "ansibrightyellow": 93, "ansibrightblue": 94, "ansibrightmagenta": 95, "ansibrightcyan": 96, "ansiwhite": 97, } BG_ANSI_COLORS = { "ansidefault": 49, # Low intensity. "ansiblack": 40, "ansired": 41, "ansigreen": 42, "ansiyellow": 43, "ansiblue": 44, "ansimagenta": 45, "ansicyan": 46, "ansigray": 47, # High intensity. "ansibrightblack": 100, "ansibrightred": 101, "ansibrightgreen": 102, "ansibrightyellow": 103, "ansibrightblue": 104, "ansibrightmagenta": 105, "ansibrightcyan": 106, "ansiwhite": 107, } ANSI_COLORS_TO_RGB = { "ansidefault": ( 0x00, 0x00, 0x00, ), # Don't use, 'default' doesn't really have a value. "ansiblack": (0x00, 0x00, 0x00), "ansigray": (0xE5, 0xE5, 0xE5), "ansibrightblack": (0x7F, 0x7F, 0x7F), "ansiwhite": (0xFF, 0xFF, 0xFF), # Low intensity. "ansired": (0xCD, 0x00, 0x00), "ansigreen": (0x00, 0xCD, 0x00), "ansiyellow": (0xCD, 0xCD, 0x00), "ansiblue": (0x00, 0x00, 0xCD), "ansimagenta": (0xCD, 0x00, 0xCD), "ansicyan": (0x00, 0xCD, 0xCD), # High intensity. "ansibrightred": (0xFF, 0x00, 0x00), "ansibrightgreen": (0x00, 0xFF, 0x00), "ansibrightyellow": (0xFF, 0xFF, 0x00), "ansibrightblue": (0x00, 0x00, 0xFF), "ansibrightmagenta": (0xFF, 0x00, 0xFF), "ansibrightcyan": (0x00, 0xFF, 0xFF), } assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES) def _get_closest_ansi_color(r: int, g: int, b: int, exclude: Sequence[str] = ()) -> str: """ Find closest ANSI color. Return it by name. :param r: Red (Between 0 and 255.) :param g: Green (Between 0 and 255.) :param b: Blue (Between 0 and 255.) :param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.) """ exclude = list(exclude) # When we have a bit of saturation, avoid the gray-like colors, otherwise, # too often the distance to the gray color is less. saturation = abs(r - g) + abs(g - b) + abs(b - r) # Between 0..510 if saturation > 30: exclude.extend(["ansilightgray", "ansidarkgray", "ansiwhite", "ansiblack"]) # Take the closest color. # (Thanks to Pygments for this part.) distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) match = "ansidefault" for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items(): if name != "ansidefault" and name not in exclude: d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2 if d < distance: match = name distance = d return match _ColorCodeAndName = tuple[int, str] class _16ColorCache: """ Cache which maps (r, g, b) tuples to 16 ansi colors. :param bg: Cache for background colors, instead of foreground. """ def __init__(self, bg: bool = False) -> None: self.bg = bg self._cache: dict[Hashable, _ColorCodeAndName] = {} def get_code( self, value: tuple[int, int, int], exclude: Sequence[str] = () ) -> _ColorCodeAndName: """ Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for a given (r,g,b) value. """ key: Hashable = (value, tuple(exclude)) cache = self._cache if key not in cache: cache[key] = self._get(value, exclude) return cache[key] def _get( self, value: tuple[int, int, int], exclude: Sequence[str] = () ) -> _ColorCodeAndName: r, g, b = value match = _get_closest_ansi_color(r, g, b, exclude=exclude) # Turn color name into code. if self.bg: code = BG_ANSI_COLORS[match] else: code = FG_ANSI_COLORS[match] return code, match class _256ColorCache(dict[tuple[int, int, int], int]): """ Cache which maps (r, g, b) tuples to 256 colors. """ def __init__(self) -> None: # Build color table. colors: list[tuple[int, int, int]] = [] # colors 0..15: 16 basic colors colors.append((0x00, 0x00, 0x00)) # 0 colors.append((0xCD, 0x00, 0x00)) # 1 colors.append((0x00, 0xCD, 0x00)) # 2 colors.append((0xCD, 0xCD, 0x00)) # 3 colors.append((0x00, 0x00, 0xEE)) # 4 colors.append((0xCD, 0x00, 0xCD)) # 5 colors.append((0x00, 0xCD, 0xCD)) # 6 colors.append((0xE5, 0xE5, 0xE5)) # 7 colors.append((0x7F, 0x7F, 0x7F)) # 8 colors.append((0xFF, 0x00, 0x00)) # 9 colors.append((0x00, 0xFF, 0x00)) # 10 colors.append((0xFF, 0xFF, 0x00)) # 11 colors.append((0x5C, 0x5C, 0xFF)) # 12 colors.append((0xFF, 0x00, 0xFF)) # 13 colors.append((0x00, 0xFF, 0xFF)) # 14 colors.append((0xFF, 0xFF, 0xFF)) # 15 # colors 16..231: the 6x6x6 color cube valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF) for i in range(216): r = valuerange[(i // 36) % 6] g = valuerange[(i // 6) % 6] b = valuerange[i % 6] colors.append((r, g, b)) # colors 232..255: grayscale for i in range(24): v = 8 + i * 10 colors.append((v, v, v)) self.colors = colors def __missing__(self, value: tuple[int, int, int]) -> int: r, g, b = value # Find closest color. # (Thanks to Pygments for this!) distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) match = 0 for i, (r2, g2, b2) in enumerate(self.colors): if i >= 16: # XXX: We ignore the 16 ANSI colors when mapping RGB # to the 256 colors, because these highly depend on # the color scheme of the terminal. d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2 if d < distance: match = i distance = d # Turn color name into code. self[value] = match return match _16_fg_colors = _16ColorCache(bg=False) _16_bg_colors = _16ColorCache(bg=True) _256_colors = _256ColorCache() class _EscapeCodeCache(dict[Attrs, str]): """ Cache for VT100 escape codes. It maps (fgcolor, bgcolor, bold, underline, strike, italic, blink, reverse, hidden, dim) tuples to VT100 escape sequences. :param true_color: When True, use 24bit colors instead of 256 colors. """ def __init__(self, color_depth: ColorDepth) -> None: self.color_depth = color_depth def __missing__(self, attrs: Attrs) -> str: ( fgcolor, bgcolor, bold, underline, strike, italic, blink, reverse, hidden, dim, ) = attrs parts: list[str] = [] parts.extend(self._colors_to_code(fgcolor or "", bgcolor or "")) if bold: parts.append("1") if dim: parts.append("2") if italic: parts.append("3") if blink: parts.append("5") if underline: parts.append("4") if reverse: parts.append("7") if hidden: parts.append("8") if strike: parts.append("9") if parts: result = "\x1b[0;" + ";".join(parts) + "m" else: result = "\x1b[0m" self[attrs] = result return result def _color_name_to_rgb(self, color: str) -> tuple[int, int, int]: "Turn 'ffffff', into (0xff, 0xff, 0xff)." try: rgb = int(color, 16) except ValueError: raise else: r = (rgb >> 16) & 0xFF g = (rgb >> 8) & 0xFF b = rgb & 0xFF return r, g, b def _colors_to_code(self, fg_color: str, bg_color: str) -> Iterable[str]: """ Return a tuple with the vt100 values that represent this color. """ # When requesting ANSI colors only, and both fg/bg color were converted # to ANSI, ensure that the foreground and background color are not the # same. (Unless they were explicitly defined to be the same color.) fg_ansi = "" def get(color: str, bg: bool) -> list[int]: nonlocal fg_ansi table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS if not color or self.color_depth == ColorDepth.DEPTH_1_BIT: return [] # 16 ANSI colors. (Given by name.) elif color in table: return [table[color]] # RGB colors. (Defined as 'ffffff'.) else: try: rgb = self._color_name_to_rgb(color) except ValueError: return [] # When only 16 colors are supported, use that. if self.color_depth == ColorDepth.DEPTH_4_BIT: if bg: # Background. if fg_color != bg_color: exclude = [fg_ansi] else: exclude = [] code, name = _16_bg_colors.get_code(rgb, exclude=exclude) return [code] else: # Foreground. code, name = _16_fg_colors.get_code(rgb) fg_ansi = name return [code] # True colors. (Only when this feature is enabled.) elif self.color_depth == ColorDepth.DEPTH_24_BIT: r, g, b = rgb return [(48 if bg else 38), 2, r, g, b] # 256 RGB colors. else: return [(48 if bg else 38), 5, _256_colors[rgb]] result: list[int] = [] result.extend(get(fg_color, False)) result.extend(get(bg_color, True)) return map(str, result) def _get_size(fileno: int) -> tuple[int, int]: """ Get the size of this pseudo terminal. :param fileno: stdout.fileno() :returns: A (rows, cols) tuple. """ size = os.get_terminal_size(fileno) return size.lines, size.columns class Vt100_Output(Output): """ :param get_size: A callable which returns the `Size` of the output terminal. :param stdout: Any object with has a `write` and `flush` method + an 'encoding' property. :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...) :param enable_cpr: When `True` (the default), send "cursor position request" escape sequences to the output in order to detect the cursor position. That way, we can properly determine how much space there is available for the UI (especially for drop down menus) to render. The `Renderer` will still try to figure out whether the current terminal does respond to CPR escapes. When `False`, never attempt to send CPR requests. """ # For the error messages. Only display "Output is not a terminal" once per # file descriptor. _fds_not_a_terminal: set[int] = set() def __init__( self, stdout: TextIO, get_size: Callable[[], Size], term: str | None = None, default_color_depth: ColorDepth | None = None, enable_bell: bool = True, enable_cpr: bool = True, ) -> None: assert all(hasattr(stdout, a) for a in ("write", "flush")) self._buffer: list[str] = [] self.stdout: TextIO = stdout self.default_color_depth = default_color_depth self._get_size = get_size self.term = term self.enable_bell = enable_bell self.enable_cpr = enable_cpr # Cache for escape codes. self._escape_code_caches: dict[ColorDepth, _EscapeCodeCache] = { ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT), ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT), ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT), ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT), } # Keep track of whether the cursor shape was ever changed. # (We don't restore the cursor shape if it was never changed - by # default, we don't change them.) self._cursor_shape_changed = False # Don't hide/show the cursor when this was already done. # (`None` means that we don't know whether the cursor is visible or # not.) self._cursor_visible: bool | None = None @classmethod def from_pty( cls, stdout: TextIO, term: str | None = None, default_color_depth: ColorDepth | None = None, enable_bell: bool = True, ) -> Vt100_Output: """ Create an Output class from a pseudo terminal. (This will take the dimensions by reading the pseudo terminal attributes.) """ fd: int | None # Normally, this requires a real TTY device, but people instantiate # this class often during unit tests as well. For convenience, we print # an error message, use standard dimensions, and go on. try: fd = stdout.fileno() except io.UnsupportedOperation: fd = None if not stdout.isatty() and (fd is None or fd not in cls._fds_not_a_terminal): msg = "Warning: Output is not a terminal (fd=%r).\n" sys.stderr.write(msg % fd) sys.stderr.flush() if fd is not None: cls._fds_not_a_terminal.add(fd) def get_size() -> Size: # If terminal (incorrectly) reports its size as 0, pick a # reasonable default. See # https://github.com/ipython/ipython/issues/10071 rows, columns = (None, None) # It is possible that `stdout` is no longer a TTY device at this # point. In that case we get an `OSError` in the ioctl call in # `get_size`. See: # https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1021 try: rows, columns = _get_size(stdout.fileno()) except OSError: pass return Size(rows=rows or 24, columns=columns or 80) return cls( stdout, get_size, term=term, default_color_depth=default_color_depth, enable_bell=enable_bell, ) def get_size(self) -> Size: return self._get_size() def fileno(self) -> int: "Return file descriptor." return self.stdout.fileno() def encoding(self) -> str: "Return encoding used for stdout." return self.stdout.encoding def write_raw(self, data: str) -> None: """ Write raw data to output. """ self._buffer.append(data) def write(self, data: str) -> None: """ Write text to output. (Removes vt100 escape codes. -- used for safely writing text.) """ self._buffer.append(data.replace("\x1b", "?")) def set_title(self, title: str) -> None: """ Set terminal title. """ if self.term not in ( "linux", "eterm-color", ): # Not supported by the Linux console. self.write_raw( "\x1b]2;{}\x07".format(title.replace("\x1b", "").replace("\x07", "")) ) def clear_title(self) -> None: self.set_title("") def erase_screen(self) -> None: """ Erases the screen with the background color and moves the cursor to home. """ self.write_raw("\x1b[2J") def enter_alternate_screen(self) -> None: self.write_raw("\x1b[?1049h\x1b[H") def quit_alternate_screen(self) -> None: self.write_raw("\x1b[?1049l") def enable_mouse_support(self) -> None: self.write_raw("\x1b[?1000h") # Enable mouse-drag support. self.write_raw("\x1b[?1003h") # Enable urxvt Mouse mode. (For terminals that understand this.) self.write_raw("\x1b[?1015h") # Also enable Xterm SGR mouse mode. (For terminals that understand this.) self.write_raw("\x1b[?1006h") # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr # extensions. def disable_mouse_support(self) -> None: self.write_raw("\x1b[?1000l") self.write_raw("\x1b[?1015l") self.write_raw("\x1b[?1006l") self.write_raw("\x1b[?1003l") def erase_end_of_line(self) -> None: """ Erases from the current cursor position to the end of the current line. """ self.write_raw("\x1b[K") def erase_down(self) -> None: """ Erases the screen from the current line down to the bottom of the screen. """ self.write_raw("\x1b[J") def reset_attributes(self) -> None: self.write_raw("\x1b[0m") def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: """ Create new style and output. :param attrs: `Attrs` instance. """ # Get current depth. escape_code_cache = self._escape_code_caches[color_depth] # Write escape character. self.write_raw(escape_code_cache[attrs]) def disable_autowrap(self) -> None: self.write_raw("\x1b[?7l") def enable_autowrap(self) -> None: self.write_raw("\x1b[?7h") def enable_bracketed_paste(self) -> None: self.write_raw("\x1b[?2004h") def disable_bracketed_paste(self) -> None: self.write_raw("\x1b[?2004l") def reset_cursor_key_mode(self) -> None: """ For vt100 only. Put the terminal in cursor mode (instead of application mode). """ # Put the terminal in cursor mode. (Instead of application mode.) self.write_raw("\x1b[?1l") def cursor_goto(self, row: int = 0, column: int = 0) -> None: """ Move cursor position. """ self.write_raw("\x1b[%i;%iH" % (row, column)) def cursor_up(self, amount: int) -> None: if amount == 0: pass elif amount == 1: self.write_raw("\x1b[A") else: self.write_raw("\x1b[%iA" % amount) def cursor_down(self, amount: int) -> None: if amount == 0: pass elif amount == 1: # Note: Not the same as '\n', '\n' can cause the window content to # scroll. self.write_raw("\x1b[B") else: self.write_raw("\x1b[%iB" % amount) def cursor_forward(self, amount: int) -> None: if amount == 0: pass elif amount == 1: self.write_raw("\x1b[C") else: self.write_raw("\x1b[%iC" % amount) def cursor_backward(self, amount: int) -> None: if amount == 0: pass elif amount == 1: self.write_raw("\b") # '\x1b[D' else: self.write_raw("\x1b[%iD" % amount) def hide_cursor(self) -> None: if self._cursor_visible in (True, None): self._cursor_visible = False self.write_raw("\x1b[?25l") def show_cursor(self) -> None: if self._cursor_visible in (False, None): self._cursor_visible = True self.write_raw("\x1b[?12l\x1b[?25h") # Stop blinking cursor and show. def set_cursor_shape(self, cursor_shape: CursorShape) -> None: if cursor_shape == CursorShape._NEVER_CHANGE: return self._cursor_shape_changed = True self.write_raw( { CursorShape.BLOCK: "\x1b[2 q", CursorShape.BEAM: "\x1b[6 q", CursorShape.UNDERLINE: "\x1b[4 q", CursorShape.BLINKING_BLOCK: "\x1b[1 q", CursorShape.BLINKING_BEAM: "\x1b[5 q", CursorShape.BLINKING_UNDERLINE: "\x1b[3 q", }.get(cursor_shape, "") ) def reset_cursor_shape(self) -> None: "Reset cursor shape." # (Only reset cursor shape, if we ever changed it.) if self._cursor_shape_changed: self._cursor_shape_changed = False # Reset cursor shape. self.write_raw("\x1b[0 q") def flush(self) -> None: """ Write to output stream and flush. """ if not self._buffer: return data = "".join(self._buffer) self._buffer = [] flush_stdout(self.stdout, data) def ask_for_cpr(self) -> None: """ Asks for a cursor position report (CPR). """ self.write_raw("\x1b[6n") self.flush() @property def responds_to_cpr(self) -> bool: if not self.enable_cpr: return False # When the input is a tty, we assume that CPR is supported. # It's not when the input is piped from Pexpect. if os.environ.get("PROMPT_TOOLKIT_NO_CPR", "") == "1": return False if is_dumb_terminal(self.term): return False try: return self.stdout.isatty() except ValueError: return False # ValueError: I/O operation on closed file def bell(self) -> None: "Sound bell." if self.enable_bell: self.write_raw("\a") self.flush() def get_default_color_depth(self) -> ColorDepth: """ Return the default color depth for a vt100 terminal, according to the our term value. We prefer 256 colors almost always, because this is what most terminals support these days, and is a good default. """ if self.default_color_depth is not None: return self.default_color_depth term = self.term if term is None: return ColorDepth.DEFAULT if is_dumb_terminal(term): return ColorDepth.DEPTH_1_BIT if term in ("linux", "eterm-color"): return ColorDepth.DEPTH_4_BIT return ColorDepth.DEFAULT ================================================ FILE: src/prompt_toolkit/output/win32.py ================================================ from __future__ import annotations import sys assert sys.platform == "win32" import os from collections.abc import Callable from ctypes import ArgumentError, byref, c_char, c_long, c_uint, c_ulong, pointer from ctypes.wintypes import DWORD, HANDLE from typing import TextIO, TypeVar from prompt_toolkit.cursor_shapes import CursorShape from prompt_toolkit.data_structures import Size from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs from prompt_toolkit.utils import get_cwidth from prompt_toolkit.win32_types import ( CONSOLE_SCREEN_BUFFER_INFO, COORD, SMALL_RECT, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, ) from ..utils import SPHINX_AUTODOC_RUNNING from .base import Output from .color_depth import ColorDepth # Do not import win32-specific stuff when generating documentation. # Otherwise RTD would be unable to generate docs for this module. if not SPHINX_AUTODOC_RUNNING: from ctypes import windll __all__ = [ "Win32Output", ] def _coord_byval(coord: COORD) -> c_long: """ Turns a COORD object into a c_long. This will cause it to be passed by value instead of by reference. (That is what I think at least.) When running ``ptipython`` is run (only with IPython), we often got the following error:: Error in 'SetConsoleCursorPosition'. ArgumentError("argument 2: <class 'TypeError'>: wrong type",) argument 2: <class 'TypeError'>: wrong type It was solved by turning ``COORD`` parameters into a ``c_long`` like this. More info: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx """ return c_long(coord.Y * 0x10000 | coord.X & 0xFFFF) #: If True: write the output of the renderer also to the following file. This #: is very useful for debugging. (e.g.: to see that we don't write more bytes #: than required.) _DEBUG_RENDER_OUTPUT = False _DEBUG_RENDER_OUTPUT_FILENAME = r"prompt-toolkit-windows-output.log" class NoConsoleScreenBufferError(Exception): """ Raised when the application is not running inside a Windows Console, but the user tries to instantiate Win32Output. """ def __init__(self) -> None: # Are we running in 'xterm' on Windows, like git-bash for instance? xterm = "xterm" in os.environ.get("TERM", "") if xterm: message = ( "Found {}, while expecting a Windows console. " 'Maybe try to run this program using "winpty" ' "or run it in cmd.exe instead. Or otherwise, " "in case of Cygwin, use the Python executable " "that is compiled for Cygwin.".format(os.environ["TERM"]) ) else: message = "No Windows console found. Are you running cmd.exe?" super().__init__(message) _T = TypeVar("_T") class Win32Output(Output): """ I/O abstraction for rendering to Windows consoles. (cmd.exe and similar.) """ def __init__( self, stdout: TextIO, use_complete_width: bool = False, default_color_depth: ColorDepth | None = None, ) -> None: self.use_complete_width = use_complete_width self.default_color_depth = default_color_depth self._buffer: list[str] = [] self.stdout: TextIO = stdout self.hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) self._in_alternate_screen = False self._hidden = False self.color_lookup_table = ColorLookupTable() # Remember the default console colors. info = self.get_win32_screen_buffer_info() self.default_attrs = info.wAttributes if info else 15 if _DEBUG_RENDER_OUTPUT: self.LOG = open(_DEBUG_RENDER_OUTPUT_FILENAME, "ab") def fileno(self) -> int: "Return file descriptor." return self.stdout.fileno() def encoding(self) -> str: "Return encoding used for stdout." return self.stdout.encoding def write(self, data: str) -> None: if self._hidden: data = " " * get_cwidth(data) self._buffer.append(data) def write_raw(self, data: str) -> None: "For win32, there is no difference between write and write_raw." self.write(data) def get_size(self) -> Size: info = self.get_win32_screen_buffer_info() # We take the width of the *visible* region as the size. Not the width # of the complete screen buffer. (Unless use_complete_width has been # set.) if self.use_complete_width: width = info.dwSize.X else: width = info.srWindow.Right - info.srWindow.Left height = info.srWindow.Bottom - info.srWindow.Top + 1 # We avoid the right margin, windows will wrap otherwise. maxwidth = info.dwSize.X - 1 width = min(maxwidth, width) # Create `Size` object. return Size(rows=height, columns=width) def _winapi(self, func: Callable[..., _T], *a: object, **kw: object) -> _T: """ Flush and call win API function. """ self.flush() if _DEBUG_RENDER_OUTPUT: self.LOG.write((f"{func.__name__!r}").encode() + b"\n") self.LOG.write( b" " + ", ".join([f"{i!r}" for i in a]).encode("utf-8") + b"\n" ) self.LOG.write( b" " + ", ".join([f"{type(i)!r}" for i in a]).encode("utf-8") + b"\n" ) self.LOG.flush() try: return func(*a, **kw) except ArgumentError as e: if _DEBUG_RENDER_OUTPUT: self.LOG.write((f" Error in {func.__name__!r} {e!r} {e}\n").encode()) raise def get_win32_screen_buffer_info(self) -> CONSOLE_SCREEN_BUFFER_INFO: """ Return Screen buffer info. """ # NOTE: We don't call the `GetConsoleScreenBufferInfo` API through # `self._winapi`. Doing so causes Python to crash on certain 64bit # Python versions. (Reproduced with 64bit Python 2.7.6, on Windows # 10). It is not clear why. Possibly, it has to do with passing # these objects as an argument, or through *args. # The Python documentation contains the following - possibly related - warning: # ctypes does not support passing unions or structures with # bit-fields to functions by value. While this may work on 32-bit # x86, it's not guaranteed by the library to work in the general # case. Unions and structures with bit-fields should always be # passed to functions by pointer. # Also see: # - https://github.com/ipython/ipython/issues/10070 # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/406 # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/86 self.flush() sbinfo = CONSOLE_SCREEN_BUFFER_INFO() success = windll.kernel32.GetConsoleScreenBufferInfo( self.hconsole, byref(sbinfo) ) # success = self._winapi(windll.kernel32.GetConsoleScreenBufferInfo, # self.hconsole, byref(sbinfo)) if success: return sbinfo else: raise NoConsoleScreenBufferError def set_title(self, title: str) -> None: """ Set terminal title. """ self._winapi(windll.kernel32.SetConsoleTitleW, title) def clear_title(self) -> None: self._winapi(windll.kernel32.SetConsoleTitleW, "") def erase_screen(self) -> None: start = COORD(0, 0) sbinfo = self.get_win32_screen_buffer_info() length = sbinfo.dwSize.X * sbinfo.dwSize.Y self.cursor_goto(row=0, column=0) self._erase(start, length) def erase_down(self) -> None: sbinfo = self.get_win32_screen_buffer_info() size = sbinfo.dwSize start = sbinfo.dwCursorPosition length = (size.X - size.X) + size.X * (size.Y - sbinfo.dwCursorPosition.Y) self._erase(start, length) def erase_end_of_line(self) -> None: """""" sbinfo = self.get_win32_screen_buffer_info() start = sbinfo.dwCursorPosition length = sbinfo.dwSize.X - sbinfo.dwCursorPosition.X self._erase(start, length) def _erase(self, start: COORD, length: int) -> None: chars_written = c_ulong() self._winapi( windll.kernel32.FillConsoleOutputCharacterA, self.hconsole, c_char(b" "), DWORD(length), _coord_byval(start), byref(chars_written), ) # Reset attributes. sbinfo = self.get_win32_screen_buffer_info() self._winapi( windll.kernel32.FillConsoleOutputAttribute, self.hconsole, sbinfo.wAttributes, length, _coord_byval(start), byref(chars_written), ) def reset_attributes(self) -> None: "Reset the console foreground/background color." self._winapi( windll.kernel32.SetConsoleTextAttribute, self.hconsole, self.default_attrs ) self._hidden = False def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: ( fgcolor, bgcolor, bold, underline, strike, italic, blink, reverse, hidden, dim, ) = attrs self._hidden = bool(hidden) # Start from the default attributes. win_attrs: int = self.default_attrs if color_depth != ColorDepth.DEPTH_1_BIT: # Override the last four bits: foreground color. if fgcolor: win_attrs = win_attrs & ~0xF win_attrs |= self.color_lookup_table.lookup_fg_color(fgcolor) # Override the next four bits: background color. if bgcolor: win_attrs = win_attrs & ~0xF0 win_attrs |= self.color_lookup_table.lookup_bg_color(bgcolor) # Reverse: swap these four bits groups. if reverse: win_attrs = ( (win_attrs & ~0xFF) | ((win_attrs & 0xF) << 4) | ((win_attrs & 0xF0) >> 4) ) self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, win_attrs) def disable_autowrap(self) -> None: # Not supported by Windows. pass def enable_autowrap(self) -> None: # Not supported by Windows. pass def cursor_goto(self, row: int = 0, column: int = 0) -> None: pos = COORD(X=column, Y=row) self._winapi( windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) ) def cursor_up(self, amount: int) -> None: sr = self.get_win32_screen_buffer_info().dwCursorPosition pos = COORD(X=sr.X, Y=sr.Y - amount) self._winapi( windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) ) def cursor_down(self, amount: int) -> None: self.cursor_up(-amount) def cursor_forward(self, amount: int) -> None: sr = self.get_win32_screen_buffer_info().dwCursorPosition # assert sr.X + amount >= 0, 'Negative cursor position: x=%r amount=%r' % (sr.X, amount) pos = COORD(X=max(0, sr.X + amount), Y=sr.Y) self._winapi( windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) ) def cursor_backward(self, amount: int) -> None: self.cursor_forward(-amount) def flush(self) -> None: """ Write to output stream and flush. """ if not self._buffer: # Only flush stdout buffer. (It could be that Python still has # something in its buffer. -- We want to be sure to print that in # the correct color.) self.stdout.flush() return data = "".join(self._buffer) if _DEBUG_RENDER_OUTPUT: self.LOG.write((f"{data!r}").encode() + b"\n") self.LOG.flush() # Print characters one by one. This appears to be the best solution # in order to avoid traces of vertical lines when the completion # menu disappears. for b in data: written = DWORD() retval = windll.kernel32.WriteConsoleW( self.hconsole, b, 1, byref(written), None ) assert retval != 0 self._buffer = [] def get_rows_below_cursor_position(self) -> int: info = self.get_win32_screen_buffer_info() return info.srWindow.Bottom - info.dwCursorPosition.Y + 1 def scroll_buffer_to_prompt(self) -> None: """ To be called before drawing the prompt. This should scroll the console to left, with the cursor at the bottom (if possible). """ # Get current window size info = self.get_win32_screen_buffer_info() sr = info.srWindow cursor_pos = info.dwCursorPosition result = SMALL_RECT() # Scroll to the left. result.Left = 0 result.Right = sr.Right - sr.Left # Scroll vertical win_height = sr.Bottom - sr.Top if 0 < sr.Bottom - cursor_pos.Y < win_height - 1: # no vertical scroll if cursor already on the screen result.Bottom = sr.Bottom else: result.Bottom = max(win_height, cursor_pos.Y) result.Top = result.Bottom - win_height # Scroll API self._winapi( windll.kernel32.SetConsoleWindowInfo, self.hconsole, True, byref(result) ) def enter_alternate_screen(self) -> None: """ Go to alternate screen buffer. """ if not self._in_alternate_screen: GENERIC_READ = 0x80000000 GENERIC_WRITE = 0x40000000 # Create a new console buffer and activate that one. handle = HANDLE( self._winapi( windll.kernel32.CreateConsoleScreenBuffer, GENERIC_READ | GENERIC_WRITE, DWORD(0), None, DWORD(1), None, ) ) self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, handle) self.hconsole = handle self._in_alternate_screen = True def quit_alternate_screen(self) -> None: """ Make stdout again the active buffer. """ if self._in_alternate_screen: stdout = HANDLE( self._winapi(windll.kernel32.GetStdHandle, STD_OUTPUT_HANDLE) ) self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, stdout) self._winapi(windll.kernel32.CloseHandle, self.hconsole) self.hconsole = stdout self._in_alternate_screen = False def enable_mouse_support(self) -> None: ENABLE_MOUSE_INPUT = 0x10 # This `ENABLE_QUICK_EDIT_MODE` flag needs to be cleared for mouse # support to work, but it's possible that it was already cleared # before. ENABLE_QUICK_EDIT_MODE = 0x0040 handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) original_mode = DWORD() self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) self._winapi( windll.kernel32.SetConsoleMode, handle, (original_mode.value | ENABLE_MOUSE_INPUT) & ~ENABLE_QUICK_EDIT_MODE, ) def disable_mouse_support(self) -> None: ENABLE_MOUSE_INPUT = 0x10 handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) original_mode = DWORD() self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) self._winapi( windll.kernel32.SetConsoleMode, handle, original_mode.value & ~ENABLE_MOUSE_INPUT, ) def hide_cursor(self) -> None: pass def show_cursor(self) -> None: pass def set_cursor_shape(self, cursor_shape: CursorShape) -> None: pass def reset_cursor_shape(self) -> None: pass @classmethod def win32_refresh_window(cls) -> None: """ Call win32 API to refresh the whole Window. This is sometimes necessary when the application paints background for completion menus. When the menu disappears, it leaves traces due to a bug in the Windows Console. Sending a repaint request solves it. """ # Get console handle handle = HANDLE(windll.kernel32.GetConsoleWindow()) RDW_INVALIDATE = 0x0001 windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE)) def get_default_color_depth(self) -> ColorDepth: """ Return the default color depth for a windows terminal. Contrary to the Vt100 implementation, this doesn't depend on a $TERM variable. """ if self.default_color_depth is not None: return self.default_color_depth return ColorDepth.DEPTH_4_BIT class FOREGROUND_COLOR: BLACK = 0x0000 BLUE = 0x0001 GREEN = 0x0002 CYAN = 0x0003 RED = 0x0004 MAGENTA = 0x0005 YELLOW = 0x0006 GRAY = 0x0007 INTENSITY = 0x0008 # Foreground color is intensified. class BACKGROUND_COLOR: BLACK = 0x0000 BLUE = 0x0010 GREEN = 0x0020 CYAN = 0x0030 RED = 0x0040 MAGENTA = 0x0050 YELLOW = 0x0060 GRAY = 0x0070 INTENSITY = 0x0080 # Background color is intensified. def _create_ansi_color_dict( color_cls: type[FOREGROUND_COLOR] | type[BACKGROUND_COLOR], ) -> dict[str, int]: "Create a table that maps the 16 named ansi colors to their Windows code." return { "ansidefault": color_cls.BLACK, "ansiblack": color_cls.BLACK, "ansigray": color_cls.GRAY, "ansibrightblack": color_cls.BLACK | color_cls.INTENSITY, "ansiwhite": color_cls.GRAY | color_cls.INTENSITY, # Low intensity. "ansired": color_cls.RED, "ansigreen": color_cls.GREEN, "ansiyellow": color_cls.YELLOW, "ansiblue": color_cls.BLUE, "ansimagenta": color_cls.MAGENTA, "ansicyan": color_cls.CYAN, # High intensity. "ansibrightred": color_cls.RED | color_cls.INTENSITY, "ansibrightgreen": color_cls.GREEN | color_cls.INTENSITY, "ansibrightyellow": color_cls.YELLOW | color_cls.INTENSITY, "ansibrightblue": color_cls.BLUE | color_cls.INTENSITY, "ansibrightmagenta": color_cls.MAGENTA | color_cls.INTENSITY, "ansibrightcyan": color_cls.CYAN | color_cls.INTENSITY, } FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR) BG_ANSI_COLORS = _create_ansi_color_dict(BACKGROUND_COLOR) assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) class ColorLookupTable: """ Inspired by pygments/formatters/terminal256.py """ def __init__(self) -> None: self._win32_colors = self._build_color_table() # Cache (map color string to foreground and background code). self.best_match: dict[str, tuple[int, int]] = {} @staticmethod def _build_color_table() -> list[tuple[int, int, int, int, int]]: """ Build an RGB-to-256 color conversion table """ FG = FOREGROUND_COLOR BG = BACKGROUND_COLOR return [ (0x00, 0x00, 0x00, FG.BLACK, BG.BLACK), (0x00, 0x00, 0xAA, FG.BLUE, BG.BLUE), (0x00, 0xAA, 0x00, FG.GREEN, BG.GREEN), (0x00, 0xAA, 0xAA, FG.CYAN, BG.CYAN), (0xAA, 0x00, 0x00, FG.RED, BG.RED), (0xAA, 0x00, 0xAA, FG.MAGENTA, BG.MAGENTA), (0xAA, 0xAA, 0x00, FG.YELLOW, BG.YELLOW), (0x88, 0x88, 0x88, FG.GRAY, BG.GRAY), (0x44, 0x44, 0xFF, FG.BLUE | FG.INTENSITY, BG.BLUE | BG.INTENSITY), (0x44, 0xFF, 0x44, FG.GREEN | FG.INTENSITY, BG.GREEN | BG.INTENSITY), (0x44, 0xFF, 0xFF, FG.CYAN | FG.INTENSITY, BG.CYAN | BG.INTENSITY), (0xFF, 0x44, 0x44, FG.RED | FG.INTENSITY, BG.RED | BG.INTENSITY), (0xFF, 0x44, 0xFF, FG.MAGENTA | FG.INTENSITY, BG.MAGENTA | BG.INTENSITY), (0xFF, 0xFF, 0x44, FG.YELLOW | FG.INTENSITY, BG.YELLOW | BG.INTENSITY), (0x44, 0x44, 0x44, FG.BLACK | FG.INTENSITY, BG.BLACK | BG.INTENSITY), (0xFF, 0xFF, 0xFF, FG.GRAY | FG.INTENSITY, BG.GRAY | BG.INTENSITY), ] def _closest_color(self, r: int, g: int, b: int) -> tuple[int, int]: distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) fg_match = 0 bg_match = 0 for r_, g_, b_, fg_, bg_ in self._win32_colors: rd = r - r_ gd = g - g_ bd = b - b_ d = rd * rd + gd * gd + bd * bd if d < distance: fg_match = fg_ bg_match = bg_ distance = d return fg_match, bg_match def _color_indexes(self, color: str) -> tuple[int, int]: indexes = self.best_match.get(color, None) if indexes is None: try: rgb = int(str(color), 16) except ValueError: rgb = 0 r = (rgb >> 16) & 0xFF g = (rgb >> 8) & 0xFF b = rgb & 0xFF indexes = self._closest_color(r, g, b) self.best_match[color] = indexes return indexes def lookup_fg_color(self, fg_color: str) -> int: """ Return the color for use in the `windll.kernel32.SetConsoleTextAttribute` API call. :param fg_color: Foreground as text. E.g. 'ffffff' or 'red' """ # Foreground. if fg_color in FG_ANSI_COLORS: return FG_ANSI_COLORS[fg_color] else: return self._color_indexes(fg_color)[0] def lookup_bg_color(self, bg_color: str) -> int: """ Return the color for use in the `windll.kernel32.SetConsoleTextAttribute` API call. :param bg_color: Background as text. E.g. 'ffffff' or 'red' """ # Background. if bg_color in BG_ANSI_COLORS: return BG_ANSI_COLORS[bg_color] else: return self._color_indexes(bg_color)[1] ================================================ FILE: src/prompt_toolkit/output/windows10.py ================================================ from __future__ import annotations import sys assert sys.platform == "win32" from ctypes import byref, windll from ctypes.wintypes import DWORD, HANDLE from typing import Any, TextIO from prompt_toolkit.data_structures import Size from prompt_toolkit.win32_types import STD_OUTPUT_HANDLE from .base import Output from .color_depth import ColorDepth from .vt100 import Vt100_Output from .win32 import Win32Output __all__ = [ "Windows10_Output", ] # See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx ENABLE_PROCESSED_INPUT = 0x0001 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 class Windows10_Output: """ Windows 10 output abstraction. This enables and uses vt100 escape sequences. """ def __init__( self, stdout: TextIO, default_color_depth: ColorDepth | None = None ) -> None: self.default_color_depth = default_color_depth self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth) self.vt100_output = Vt100_Output( stdout, lambda: Size(0, 0), default_color_depth=default_color_depth ) self._hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) def flush(self) -> None: """ Write to output stream and flush. """ original_mode = DWORD(0) # Remember the previous console mode. windll.kernel32.GetConsoleMode(self._hconsole, byref(original_mode)) # Enable processing of vt100 sequences. windll.kernel32.SetConsoleMode( self._hconsole, DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING), ) try: self.vt100_output.flush() finally: # Restore console mode. windll.kernel32.SetConsoleMode(self._hconsole, original_mode) @property def responds_to_cpr(self) -> bool: return False # We don't need this on Windows. def __getattr__(self, name: str) -> Any: # NOTE: Now that we use "virtual terminal input" on # Windows, both input and output are done through # ANSI escape sequences on Windows. This means, we # should enable bracketed paste like on Linux, and # enable mouse support by calling the vt100_output. if name in ( "get_size", "get_rows_below_cursor_position", "scroll_buffer_to_prompt", "get_win32_screen_buffer_info", # "enable_mouse_support", # "disable_mouse_support", # "enable_bracketed_paste", # "disable_bracketed_paste", ): return getattr(self.win32_output, name) else: return getattr(self.vt100_output, name) def get_default_color_depth(self) -> ColorDepth: """ Return the default color depth for a windows terminal. Contrary to the Vt100 implementation, this doesn't depend on a $TERM variable. """ if self.default_color_depth is not None: return self.default_color_depth # Previously, we used `DEPTH_4_BIT`, even on Windows 10. This was # because true color support was added after "Console Virtual Terminal # Sequences" support was added, and there was no good way to detect # what support was given. # 24bit color support was added in 2016, so let's assume it's safe to # take that as a default: # https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/ return ColorDepth.TRUE_COLOR Output.register(Windows10_Output) def is_win_vt100_enabled() -> bool: """ Returns True when we're running Windows and VT100 escape sequences are supported. """ if sys.platform != "win32": return False hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) # Get original console mode. original_mode = DWORD(0) windll.kernel32.GetConsoleMode(hconsole, byref(original_mode)) try: # Try to enable VT100 sequences. result: int = windll.kernel32.SetConsoleMode( hconsole, DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) ) return result == 1 finally: windll.kernel32.SetConsoleMode(hconsole, original_mode) ================================================ FILE: src/prompt_toolkit/patch_stdout.py ================================================ """ patch_stdout ============ This implements a context manager that ensures that print statements within it won't destroy the user interface. The context manager will replace `sys.stdout` by something that draws the output above the current prompt, rather than overwriting the UI. Usage:: with patch_stdout(application): ... application.run() ... Multiple applications can run in the body of the context manager, one after the other. """ from __future__ import annotations import asyncio import queue import sys import threading import time from collections.abc import Generator from contextlib import contextmanager from typing import TextIO, cast from .application import get_app_session, run_in_terminal from .output import Output __all__ = [ "patch_stdout", "StdoutProxy", ] @contextmanager def patch_stdout(raw: bool = False) -> Generator[None, None, None]: """ Replace `sys.stdout` and `sys.stderr` by an :class:`_StdoutProxy` instance. Writing to this proxy will make sure that the text appears above the prompt, and that it doesn't destroy the output from the renderer. If no application is curring, the behavior should be identical to writing to `sys.stdout` directly. Warning: If a new event loop is installed using `asyncio.set_event_loop()`, then make sure that the context manager is applied after the event loop is changed. Printing to stdout will be scheduled in the event loop that's active when the context manager is created. Warning: In order for all text to appear above the prompt `stderr` will also be redirected to the stdout proxy. :param raw: (`bool`) When True, vt100 terminal escape sequences are not removed/escaped. """ with StdoutProxy(raw=raw) as proxy: original_stdout = sys.stdout original_stderr = sys.stderr # Enter. sys.stdout = cast(TextIO, proxy) sys.stderr = cast(TextIO, proxy) try: yield finally: sys.stdout = original_stdout sys.stderr = original_stderr class _Done: "Sentinel value for stopping the stdout proxy." class StdoutProxy: """ File-like object, which prints everything written to it, output above the current application/prompt. This class is compatible with other file objects and can be used as a drop-in replacement for `sys.stdout` or can for instance be passed to `logging.StreamHandler`. The current application, above which we print, is determined by looking what application currently runs in the `AppSession` that is active during the creation of this instance. This class can be used as a context manager. In order to avoid having to repaint the prompt continuously for every little write, a short delay of `sleep_between_writes` seconds will be added between writes in order to bundle many smaller writes in a short timespan. """ def __init__( self, sleep_between_writes: float = 0.2, raw: bool = False, ) -> None: self.sleep_between_writes = sleep_between_writes self.raw = raw self._lock = threading.RLock() self._buffer: list[str] = [] # Keep track of the curret app session. self.app_session = get_app_session() # See what output is active *right now*. We should do it at this point, # before this `StdoutProxy` instance is possibly assigned to `sys.stdout`. # Otherwise, if `patch_stdout` is used, and no `Output` instance has # been created, then the default output creation code will see this # proxy object as `sys.stdout`, and get in a recursive loop trying to # access `StdoutProxy.isatty()` which will again retrieve the output. self._output: Output = self.app_session.output # Flush thread self._flush_queue: queue.Queue[str | _Done] = queue.Queue() self._flush_thread = self._start_write_thread() self.closed = False def __enter__(self) -> StdoutProxy: return self def __exit__(self, *args: object) -> None: self.close() def close(self) -> None: """ Stop `StdoutProxy` proxy. This will terminate the write thread, make sure everything is flushed and wait for the write thread to finish. """ if not self.closed: self._flush_queue.put(_Done()) self._flush_thread.join() self.closed = True def _start_write_thread(self) -> threading.Thread: thread = threading.Thread( target=self._write_thread, name="patch-stdout-flush-thread", daemon=True, ) thread.start() return thread def _write_thread(self) -> None: done = False while not done: item = self._flush_queue.get() if isinstance(item, _Done): break # Don't bother calling when we got an empty string. if not item: continue text = [] text.append(item) # Read the rest of the queue if more data was queued up. while True: try: item = self._flush_queue.get_nowait() except queue.Empty: break else: if isinstance(item, _Done): done = True else: text.append(item) app_loop = self._get_app_loop() self._write_and_flush(app_loop, "".join(text)) # If an application was running that requires repainting, then wait # for a very short time, in order to bundle actual writes and avoid # having to repaint to often. if app_loop is not None: time.sleep(self.sleep_between_writes) def _get_app_loop(self) -> asyncio.AbstractEventLoop | None: """ Return the event loop for the application currently running in our `AppSession`. """ app = self.app_session.app if app is None: return None return app.loop def _write_and_flush( self, loop: asyncio.AbstractEventLoop | None, text: str ) -> None: """ Write the given text to stdout and flush. If an application is running, use `run_in_terminal`. """ def write_and_flush() -> None: # Ensure that autowrap is enabled before calling `write`. # XXX: On Windows, the `Windows10_Output` enables/disables VT # terminal processing for every flush. It turns out that this # causes autowrap to be reset (disabled) after each flush. So, # we have to enable it again before writing text. self._output.enable_autowrap() if self.raw: self._output.write_raw(text) else: self._output.write(text) self._output.flush() def write_and_flush_in_loop() -> None: # If an application is running, use `run_in_terminal`, otherwise # call it directly. run_in_terminal(write_and_flush, in_executor=False) if loop is None: # No loop, write immediately. write_and_flush() else: # Make sure `write_and_flush` is executed *in* the event loop, not # in another thread. loop.call_soon_threadsafe(write_and_flush_in_loop) def _write(self, data: str) -> None: """ Note: print()-statements cause to multiple write calls. (write('line') and write('\n')). Of course we don't want to call `run_in_terminal` for every individual call, because that's too expensive, and as long as the newline hasn't been written, the text itself is again overwritten by the rendering of the input command line. Therefor, we have a little buffer which holds the text until a newline is written to stdout. """ if "\n" in data: # When there is a newline in the data, write everything before the # newline, including the newline itself. before, after = data.rsplit("\n", 1) to_write = self._buffer + [before, "\n"] self._buffer = [after] text = "".join(to_write) self._flush_queue.put(text) else: # Otherwise, cache in buffer. self._buffer.append(data) def _flush(self) -> None: text = "".join(self._buffer) self._buffer = [] self._flush_queue.put(text) def write(self, data: str) -> int: with self._lock: self._write(data) return len(data) # Pretend everything was written. def flush(self) -> None: """ Flush buffered output. """ with self._lock: self._flush() @property def original_stdout(self) -> TextIO | None: return self._output.stdout or sys.__stdout__ # Attributes for compatibility with sys.__stdout__: def fileno(self) -> int: return self._output.fileno() def isatty(self) -> bool: stdout = self._output.stdout if stdout is None: return False return stdout.isatty() @property def encoding(self) -> str: return self._output.encoding() @property def errors(self) -> str: return "strict" ================================================ FILE: src/prompt_toolkit/py.typed ================================================ ================================================ FILE: src/prompt_toolkit/renderer.py ================================================ """ Renders the command line on the console. (Redraws parts of the input line that were changed.) """ from __future__ import annotations from asyncio import FIRST_COMPLETED, Future, ensure_future, sleep, wait from collections import deque from collections.abc import Callable, Hashable from enum import Enum from typing import TYPE_CHECKING, Any from prompt_toolkit.application.current import get_app from prompt_toolkit.cursor_shapes import CursorShape from prompt_toolkit.data_structures import Point, Size from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text from prompt_toolkit.layout.mouse_handlers import MouseHandlers from prompt_toolkit.layout.screen import Char, Screen, WritePosition from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import ( Attrs, BaseStyle, DummyStyleTransformation, StyleTransformation, ) if TYPE_CHECKING: from prompt_toolkit.application import Application from prompt_toolkit.layout.layout import Layout __all__ = [ "Renderer", "print_formatted_text", ] def _output_screen_diff( app: Application[Any], output: Output, screen: Screen, current_pos: Point, color_depth: ColorDepth, previous_screen: Screen | None, last_style: str | None, is_done: bool, # XXX: drop is_done full_screen: bool, attrs_for_style_string: _StyleStringToAttrsCache, style_string_has_style: _StyleStringHasStyleCache, size: Size, previous_width: int, ) -> tuple[Point, str | None]: """ Render the diff between this screen and the previous screen. This takes two `Screen` instances. The one that represents the output like it was during the last rendering and one that represents the current output raster. Looking at these two `Screen` instances, this function will render the difference by calling the appropriate methods of the `Output` object that only paint the changes to the terminal. This is some performance-critical code which is heavily optimized. Don't change things without profiling first. :param current_pos: Current cursor position. :param last_style: The style string, used for drawing the last drawn character. (Color/attributes.) :param attrs_for_style_string: :class:`._StyleStringToAttrsCache` instance. :param width: The width of the terminal. :param previous_width: The width of the terminal during the last rendering. """ width, height = size.columns, size.rows #: Variable for capturing the output. write = output.write write_raw = output.write_raw # Create locals for the most used output methods. # (Save expensive attribute lookups.) _output_set_attributes = output.set_attributes _output_reset_attributes = output.reset_attributes _output_cursor_forward = output.cursor_forward _output_cursor_up = output.cursor_up _output_cursor_backward = output.cursor_backward # Hide cursor before rendering. (Avoid flickering.) output.hide_cursor() def reset_attributes() -> None: "Wrapper around Output.reset_attributes." nonlocal last_style _output_reset_attributes() last_style = None # Forget last char after resetting attributes. def move_cursor(new: Point) -> Point: "Move cursor to this `new` point. Returns the given Point." current_x, current_y = current_pos.x, current_pos.y if new.y > current_y: # Use newlines instead of CURSOR_DOWN, because this might add new lines. # CURSOR_DOWN will never create new lines at the bottom. # Also reset attributes, otherwise the newline could draw a # background color. reset_attributes() write("\r\n" * (new.y - current_y)) current_x = 0 _output_cursor_forward(new.x) return new elif new.y < current_y: _output_cursor_up(current_y - new.y) if current_x >= width - 1: write("\r") _output_cursor_forward(new.x) elif new.x < current_x or current_x >= width - 1: _output_cursor_backward(current_x - new.x) elif new.x > current_x: _output_cursor_forward(new.x - current_x) return new def output_char(char: Char) -> None: """ Write the output of this character. """ nonlocal last_style # If the last printed character has the same style, don't output the # style again. if last_style == char.style: write(char.char) else: # Look up `Attr` for this style string. Only set attributes if different. # (Two style strings can still have the same formatting.) # Note that an empty style string can have formatting that needs to # be applied, because of style transformations. new_attrs = attrs_for_style_string[char.style] if not last_style or new_attrs != attrs_for_style_string[last_style]: _output_set_attributes(new_attrs, color_depth) write(char.char) last_style = char.style def get_max_column_index(row: dict[int, Char]) -> int: """ Return max used column index, ignoring whitespace (without style) at the end of the line. This is important for people that copy/paste terminal output. There are two reasons we are sometimes seeing whitespace at the end: - `BufferControl` adds a trailing space to each line, because it's a possible cursor position, so that the line wrapping won't change if the cursor position moves around. - The `Window` adds a style class to the current line for highlighting (cursor-line). """ numbers = ( index for index, cell in row.items() if cell.char != " " or style_string_has_style[cell.style] ) return max(numbers, default=0) # Render for the first time: reset styling. if not previous_screen: reset_attributes() # Disable autowrap. (When entering a the alternate screen, or anytime when # we have a prompt. - In the case of a REPL, like IPython, people can have # background threads, and it's hard for debugging if their output is not # wrapped.) if not previous_screen or not full_screen: output.disable_autowrap() # When the previous screen has a different size, redraw everything anyway. # Also when we are done. (We might take up less rows, so clearing is important.) if ( is_done or not previous_screen or previous_width != width ): # XXX: also consider height?? current_pos = move_cursor(Point(x=0, y=0)) reset_attributes() output.erase_down() previous_screen = Screen() # Get height of the screen. # (height changes as we loop over data_buffer, so remember the current value.) # (Also make sure to clip the height to the size of the output.) current_height = min(screen.height, height) # Loop over the rows. row_count = min(max(screen.height, previous_screen.height), height) for y in range(row_count): new_row = screen.data_buffer[y] previous_row = previous_screen.data_buffer[y] zero_width_escapes_row = screen.zero_width_escapes[y] new_max_line_len = min(width - 1, get_max_column_index(new_row)) previous_max_line_len = min(width - 1, get_max_column_index(previous_row)) # Loop over the columns. c = 0 # Column counter. while c <= new_max_line_len: new_char = new_row[c] old_char = previous_row[c] char_width = new_char.width or 1 # When the old and new character at this position are different, # draw the output. (Because of the performance, we don't call # `Char.__ne__`, but inline the same expression.) if new_char.char != old_char.char or new_char.style != old_char.style: current_pos = move_cursor(Point(x=c, y=y)) # Send injected escape sequences to output. if c in zero_width_escapes_row: write_raw(zero_width_escapes_row[c]) output_char(new_char) current_pos = Point(x=current_pos.x + char_width, y=current_pos.y) c += char_width # If the new line is shorter, trim it. if previous_screen and new_max_line_len < previous_max_line_len: current_pos = move_cursor(Point(x=new_max_line_len + 1, y=y)) reset_attributes() output.erase_end_of_line() # Correctly reserve vertical space as required by the layout. # When this is a new screen (drawn for the first time), or for some reason # higher than the previous one. Move the cursor once to the bottom of the # output. That way, we're sure that the terminal scrolls up, even when the # lower lines of the canvas just contain whitespace. # The most obvious reason that we actually want this behavior is the avoid # the artifact of the input scrolling when the completion menu is shown. # (If the scrolling is actually wanted, the layout can still be build in a # way to behave that way by setting a dynamic height.) if current_height > previous_screen.height: current_pos = move_cursor(Point(x=0, y=current_height - 1)) # Move cursor: if is_done: current_pos = move_cursor(Point(x=0, y=current_height)) output.erase_down() else: current_pos = move_cursor(screen.get_cursor_position(app.layout.current_window)) if is_done or not full_screen: output.enable_autowrap() # Always reset the color attributes. This is important because a background # thread could print data to stdout and we want that to be displayed in the # default colors. (Also, if a background color has been set, many terminals # give weird artifacts on resize events.) reset_attributes() if screen.show_cursor: output.show_cursor() return current_pos, last_style class HeightIsUnknownError(Exception): "Information unavailable. Did not yet receive the CPR response." class _StyleStringToAttrsCache(dict[str, Attrs]): """ A cache structure that maps style strings to :class:`.Attr`. (This is an important speed up.) """ def __init__( self, get_attrs_for_style_str: Callable[[str], Attrs], style_transformation: StyleTransformation, ) -> None: self.get_attrs_for_style_str = get_attrs_for_style_str self.style_transformation = style_transformation def __missing__(self, style_str: str) -> Attrs: attrs = self.get_attrs_for_style_str(style_str) attrs = self.style_transformation.transform_attrs(attrs) self[style_str] = attrs return attrs class _StyleStringHasStyleCache(dict[str, bool]): """ Cache for remember which style strings don't render the default output style (default fg/bg, no underline and no reverse and no blink). That way we know that we should render these cells, even when they're empty (when they contain a space). Note: we don't consider bold/italic/hidden because they don't change the output if there's no text in the cell. """ def __init__(self, style_string_to_attrs: dict[str, Attrs]) -> None: self.style_string_to_attrs = style_string_to_attrs def __missing__(self, style_str: str) -> bool: attrs = self.style_string_to_attrs[style_str] is_default = bool( attrs.color or attrs.bgcolor or attrs.underline or attrs.strike or attrs.blink or attrs.reverse ) self[style_str] = is_default return is_default class CPR_Support(Enum): "Enum: whether or not CPR is supported." SUPPORTED = "SUPPORTED" NOT_SUPPORTED = "NOT_SUPPORTED" UNKNOWN = "UNKNOWN" class Renderer: """ Typical usage: :: output = Vt100_Output.from_pty(sys.stdout) r = Renderer(style, output) r.render(app, layout=...) """ CPR_TIMEOUT = 2 # Time to wait until we consider CPR to be not supported. def __init__( self, style: BaseStyle, output: Output, full_screen: bool = False, mouse_support: FilterOrBool = False, cpr_not_supported_callback: Callable[[], None] | None = None, ) -> None: self.style = style self.output = output self.full_screen = full_screen self.mouse_support = to_filter(mouse_support) self.cpr_not_supported_callback = cpr_not_supported_callback # TODO: Move following state flags into `Vt100_Output`, similar to # `_cursor_shape_changed` and `_cursor_visible`. But then also # adjust the `Win32Output` to not call win32 APIs if nothing has # to be changed. self._in_alternate_screen = False self._mouse_support_enabled = False self._bracketed_paste_enabled = False self._cursor_key_mode_reset = False # Future set when we are waiting for a CPR flag. self._waiting_for_cpr_futures: deque[Future[None]] = deque() self.cpr_support = CPR_Support.UNKNOWN if not output.responds_to_cpr: self.cpr_support = CPR_Support.NOT_SUPPORTED # Cache for the style. self._attrs_for_style: _StyleStringToAttrsCache | None = None self._style_string_has_style: _StyleStringHasStyleCache | None = None self._last_style_hash: Hashable | None = None self._last_transformation_hash: Hashable | None = None self._last_color_depth: ColorDepth | None = None self.reset(_scroll=True) def reset(self, _scroll: bool = False, leave_alternate_screen: bool = True) -> None: # Reset position self._cursor_pos = Point(x=0, y=0) # Remember the last screen instance between renderers. This way, # we can create a `diff` between two screens and only output the # difference. It's also to remember the last height. (To show for # instance a toolbar at the bottom position.) self._last_screen: Screen | None = None self._last_size: Size | None = None self._last_style: str | None = None self._last_cursor_shape: CursorShape | None = None # Default MouseHandlers. (Just empty.) self.mouse_handlers = MouseHandlers() #: Space from the top of the layout, until the bottom of the terminal. #: We don't know this until a `report_absolute_cursor_row` call. self._min_available_height = 0 # In case of Windows, also make sure to scroll to the current cursor # position. (Only when rendering the first time.) # It does nothing for vt100 terminals. if _scroll: self.output.scroll_buffer_to_prompt() # Quit alternate screen. if self._in_alternate_screen and leave_alternate_screen: self.output.quit_alternate_screen() self._in_alternate_screen = False # Disable mouse support. if self._mouse_support_enabled: self.output.disable_mouse_support() self._mouse_support_enabled = False # Disable bracketed paste. if self._bracketed_paste_enabled: self.output.disable_bracketed_paste() self._bracketed_paste_enabled = False self.output.reset_cursor_shape() self.output.show_cursor() # NOTE: No need to set/reset cursor key mode here. # Flush output. `disable_mouse_support` needs to write to stdout. self.output.flush() @property def last_rendered_screen(self) -> Screen | None: """ The `Screen` class that was generated during the last rendering. This can be `None`. """ return self._last_screen @property def height_is_known(self) -> bool: """ True when the height from the cursor until the bottom of the terminal is known. (It's often nicer to draw bottom toolbars only if the height is known, in order to avoid flickering when the CPR response arrives.) """ if self.full_screen or self._min_available_height > 0: return True try: self._min_available_height = self.output.get_rows_below_cursor_position() return True except NotImplementedError: return False @property def rows_above_layout(self) -> int: """ Return the number of rows visible in the terminal above the layout. """ if self._in_alternate_screen: return 0 elif self._min_available_height > 0: total_rows = self.output.get_size().rows last_screen_height = self._last_screen.height if self._last_screen else 0 return total_rows - max(self._min_available_height, last_screen_height) else: raise HeightIsUnknownError("Rows above layout is unknown.") def request_absolute_cursor_position(self) -> None: """ Get current cursor position. We do this to calculate the minimum available height that we can consume for rendering the prompt. This is the available space below te cursor. For vt100: Do CPR request. (answer will arrive later.) For win32: Do API call. (Answer comes immediately.) """ # Only do this request when the cursor is at the top row. (after a # clear or reset). We will rely on that in `report_absolute_cursor_row`. assert self._cursor_pos.y == 0 # In full-screen mode, always use the total height as min-available-height. if self.full_screen: self._min_available_height = self.output.get_size().rows return # For Win32, we have an API call to get the number of rows below the # cursor. try: self._min_available_height = self.output.get_rows_below_cursor_position() return except NotImplementedError: pass # Use CPR. if self.cpr_support == CPR_Support.NOT_SUPPORTED: return def do_cpr() -> None: # Asks for a cursor position report (CPR). self._waiting_for_cpr_futures.append(Future()) self.output.ask_for_cpr() if self.cpr_support == CPR_Support.SUPPORTED: do_cpr() return # If we don't know whether CPR is supported, only do a request if # none is pending, and test it, using a timer. if self.waiting_for_cpr: return do_cpr() async def timer() -> None: await sleep(self.CPR_TIMEOUT) # Not set in the meantime -> not supported. if self.cpr_support == CPR_Support.UNKNOWN: self.cpr_support = CPR_Support.NOT_SUPPORTED if self.cpr_not_supported_callback: # Make sure to call this callback in the main thread. self.cpr_not_supported_callback() get_app().create_background_task(timer()) def report_absolute_cursor_row(self, row: int) -> None: """ To be called when we know the absolute cursor position. (As an answer of a "Cursor Position Request" response.) """ self.cpr_support = CPR_Support.SUPPORTED # Calculate the amount of rows from the cursor position until the # bottom of the terminal. total_rows = self.output.get_size().rows rows_below_cursor = total_rows - row + 1 # Set the minimum available height. self._min_available_height = rows_below_cursor # Pop and set waiting for CPR future. try: f = self._waiting_for_cpr_futures.popleft() except IndexError: pass # Received CPR response without having a CPR. else: f.set_result(None) @property def waiting_for_cpr(self) -> bool: """ Waiting for CPR flag. True when we send the request, but didn't got a response. """ return bool(self._waiting_for_cpr_futures) async def wait_for_cpr_responses(self, timeout: int = 1) -> None: """ Wait for a CPR response. """ cpr_futures = list(self._waiting_for_cpr_futures) # Make copy. # When there are no CPRs in the queue. Don't do anything. if not cpr_futures or self.cpr_support == CPR_Support.NOT_SUPPORTED: return None async def wait_for_responses() -> None: for response_f in cpr_futures: await response_f async def wait_for_timeout() -> None: await sleep(timeout) # Got timeout, erase queue. for response_f in cpr_futures: response_f.cancel() self._waiting_for_cpr_futures = deque() tasks = { ensure_future(wait_for_responses()), ensure_future(wait_for_timeout()), } _, pending = await wait(tasks, return_when=FIRST_COMPLETED) for task in pending: task.cancel() def render( self, app: Application[Any], layout: Layout, is_done: bool = False ) -> None: """ Render the current interface to the output. :param is_done: When True, put the cursor at the end of the interface. We won't print any changes to this part. """ output = self.output # Enter alternate screen. if self.full_screen and not self._in_alternate_screen: self._in_alternate_screen = True output.enter_alternate_screen() # Enable bracketed paste. if not self._bracketed_paste_enabled: self.output.enable_bracketed_paste() self._bracketed_paste_enabled = True # Reset cursor key mode. if not self._cursor_key_mode_reset: self.output.reset_cursor_key_mode() self._cursor_key_mode_reset = True # Enable/disable mouse support. needs_mouse_support = self.mouse_support() if needs_mouse_support and not self._mouse_support_enabled: output.enable_mouse_support() self._mouse_support_enabled = True elif not needs_mouse_support and self._mouse_support_enabled: output.disable_mouse_support() self._mouse_support_enabled = False # Create screen and write layout to it. size = output.get_size() screen = Screen() screen.show_cursor = False # Hide cursor by default, unless one of the # containers decides to display it. mouse_handlers = MouseHandlers() # Calculate height. if self.full_screen: height = size.rows elif is_done: # When we are done, we don't necessary want to fill up until the bottom. height = layout.container.preferred_height( size.columns, size.rows ).preferred else: last_height = self._last_screen.height if self._last_screen else 0 height = max( self._min_available_height, last_height, layout.container.preferred_height(size.columns, size.rows).preferred, ) height = min(height, size.rows) # When the size changes, don't consider the previous screen. if self._last_size != size: self._last_screen = None # When we render using another style or another color depth, do a full # repaint. (Forget about the previous rendered screen.) # (But note that we still use _last_screen to calculate the height.) if ( self.style.invalidation_hash() != self._last_style_hash or app.style_transformation.invalidation_hash() != self._last_transformation_hash or app.color_depth != self._last_color_depth ): self._last_screen = None self._attrs_for_style = None self._style_string_has_style = None if self._attrs_for_style is None: self._attrs_for_style = _StyleStringToAttrsCache( self.style.get_attrs_for_style_str, app.style_transformation ) if self._style_string_has_style is None: self._style_string_has_style = _StyleStringHasStyleCache( self._attrs_for_style ) self._last_style_hash = self.style.invalidation_hash() self._last_transformation_hash = app.style_transformation.invalidation_hash() self._last_color_depth = app.color_depth layout.container.write_to_screen( screen, mouse_handlers, WritePosition(xpos=0, ypos=0, width=size.columns, height=height), parent_style="", erase_bg=False, z_index=None, ) screen.draw_all_floats() # When grayed. Replace all styles in the new screen. if app.exit_style: screen.append_style_to_content(app.exit_style) # Process diff and write to output. self._cursor_pos, self._last_style = _output_screen_diff( app, output, screen, self._cursor_pos, app.color_depth, self._last_screen, self._last_style, is_done, full_screen=self.full_screen, attrs_for_style_string=self._attrs_for_style, style_string_has_style=self._style_string_has_style, size=size, previous_width=(self._last_size.columns if self._last_size else 0), ) self._last_screen = screen self._last_size = size self.mouse_handlers = mouse_handlers # Handle cursor shapes. new_cursor_shape = app.cursor.get_cursor_shape(app) if ( self._last_cursor_shape is None or self._last_cursor_shape != new_cursor_shape ): output.set_cursor_shape(new_cursor_shape) self._last_cursor_shape = new_cursor_shape # Flush buffered output. output.flush() # Set visible windows in layout. app.layout.visible_windows = screen.visible_windows if is_done: self.reset() def erase(self, leave_alternate_screen: bool = True) -> None: """ Hide all output and put the cursor back at the first line. This is for instance used for running a system command (while hiding the CLI) and later resuming the same CLI.) :param leave_alternate_screen: When True, and when inside an alternate screen buffer, quit the alternate screen. """ output = self.output output.cursor_backward(self._cursor_pos.x) output.cursor_up(self._cursor_pos.y) output.erase_down() output.reset_attributes() output.enable_autowrap() output.flush() self.reset(leave_alternate_screen=leave_alternate_screen) def clear(self) -> None: """ Clear screen and go to 0,0 """ # Erase current output first. self.erase() # Send "Erase Screen" command and go to (0, 0). output = self.output output.erase_screen() output.cursor_goto(0, 0) output.flush() self.request_absolute_cursor_position() def print_formatted_text( output: Output, formatted_text: AnyFormattedText, style: BaseStyle, style_transformation: StyleTransformation | None = None, color_depth: ColorDepth | None = None, ) -> None: """ Print a list of (style_str, text) tuples in the given style to the output. """ fragments = to_formatted_text(formatted_text) style_transformation = style_transformation or DummyStyleTransformation() color_depth = color_depth or output.get_default_color_depth() # Reset first. output.reset_attributes() output.enable_autowrap() last_attrs: Attrs | None = None # Print all (style_str, text) tuples. attrs_for_style_string = _StyleStringToAttrsCache( style.get_attrs_for_style_str, style_transformation ) for style_str, text, *_ in fragments: attrs = attrs_for_style_string[style_str] # Set style attributes if something changed. if attrs != last_attrs: if attrs: output.set_attributes(attrs, color_depth) else: output.reset_attributes() last_attrs = attrs # Print escape sequences as raw output if "[ZeroWidthEscape]" in style_str: output.write_raw(text) else: # Eliminate carriage returns text = text.replace("\r", "") # Insert a carriage return before every newline (important when the # front-end is a telnet client). text = text.replace("\n", "\r\n") output.write(text) # Reset again. output.reset_attributes() output.flush() ================================================ FILE: src/prompt_toolkit/search.py ================================================ """ Search operations. For the key bindings implementation with attached filters, check `prompt_toolkit.key_binding.bindings.search`. (Use these for new key bindings instead of calling these function directly.) """ from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING from .application.current import get_app from .filters import FilterOrBool, is_searching, to_filter from .key_binding.vi_state import InputMode if TYPE_CHECKING: from prompt_toolkit.layout.controls import BufferControl, SearchBufferControl from prompt_toolkit.layout.layout import Layout __all__ = [ "SearchDirection", "start_search", "stop_search", ] class SearchDirection(Enum): FORWARD = "FORWARD" BACKWARD = "BACKWARD" class SearchState: """ A search 'query', associated with a search field (like a SearchToolbar). Every searchable `BufferControl` points to a `search_buffer_control` (another `BufferControls`) which represents the search field. The `SearchState` attached to that search field is used for storing the current search query. It is possible to have one searchfield for multiple `BufferControls`. In that case, they'll share the same `SearchState`. If there are multiple `BufferControls` that display the same `Buffer`, then they can have a different `SearchState` each (if they have a different search control). """ __slots__ = ("text", "direction", "ignore_case") def __init__( self, text: str = "", direction: SearchDirection = SearchDirection.FORWARD, ignore_case: FilterOrBool = False, ) -> None: self.text = text self.direction = direction self.ignore_case = to_filter(ignore_case) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.text!r}, direction={self.direction!r}, ignore_case={self.ignore_case!r})" def __invert__(self) -> SearchState: """ Create a new SearchState where backwards becomes forwards and the other way around. """ if self.direction == SearchDirection.BACKWARD: direction = SearchDirection.FORWARD else: direction = SearchDirection.BACKWARD return SearchState( text=self.text, direction=direction, ignore_case=self.ignore_case ) def start_search( buffer_control: BufferControl | None = None, direction: SearchDirection = SearchDirection.FORWARD, ) -> None: """ Start search through the given `buffer_control` using the `search_buffer_control`. :param buffer_control: Start search for this `BufferControl`. If not given, search through the current control. """ from prompt_toolkit.layout.controls import BufferControl assert buffer_control is None or isinstance(buffer_control, BufferControl) layout = get_app().layout # When no control is given, use the current control if that's a BufferControl. if buffer_control is None: if not isinstance(layout.current_control, BufferControl): return buffer_control = layout.current_control # Only if this control is searchable. search_buffer_control = buffer_control.search_buffer_control if search_buffer_control: buffer_control.search_state.direction = direction # Make sure to focus the search BufferControl layout.focus(search_buffer_control) # Remember search link. layout.search_links[search_buffer_control] = buffer_control # If we're in Vi mode, make sure to go into insert mode. get_app().vi_state.input_mode = InputMode.INSERT def stop_search(buffer_control: BufferControl | None = None) -> None: """ Stop search through the given `buffer_control`. """ layout = get_app().layout if buffer_control is None: buffer_control = layout.search_target_buffer_control if buffer_control is None: # (Should not happen, but possible when `stop_search` is called # when we're not searching.) return search_buffer_control = buffer_control.search_buffer_control else: assert buffer_control in layout.search_links.values() search_buffer_control = _get_reverse_search_links(layout)[buffer_control] # Focus the original buffer again. layout.focus(buffer_control) if search_buffer_control is not None: # Remove the search link. del layout.search_links[search_buffer_control] # Reset content of search control. search_buffer_control.buffer.reset() # If we're in Vi mode, go back to navigation mode. get_app().vi_state.input_mode = InputMode.NAVIGATION def do_incremental_search(direction: SearchDirection, count: int = 1) -> None: """ Apply search, but keep search buffer focused. """ assert is_searching() layout = get_app().layout # Only search if the current control is a `BufferControl`. from prompt_toolkit.layout.controls import BufferControl search_control = layout.current_control if not isinstance(search_control, BufferControl): return prev_control = layout.search_target_buffer_control if prev_control is None: return search_state = prev_control.search_state # Update search_state. direction_changed = search_state.direction != direction search_state.text = search_control.buffer.text search_state.direction = direction # Apply search to current buffer. if not direction_changed: prev_control.buffer.apply_search( search_state, include_current_position=False, count=count ) def accept_search() -> None: """ Accept current search query. Focus original `BufferControl` again. """ layout = get_app().layout search_control = layout.current_control target_buffer_control = layout.search_target_buffer_control from prompt_toolkit.layout.controls import BufferControl if not isinstance(search_control, BufferControl): return if target_buffer_control is None: return search_state = target_buffer_control.search_state # Update search state. if search_control.buffer.text: search_state.text = search_control.buffer.text # Apply search. target_buffer_control.buffer.apply_search( search_state, include_current_position=True ) # Add query to history of search line. search_control.buffer.append_to_history() # Stop search and focus previous control again. stop_search(target_buffer_control) def _get_reverse_search_links( layout: Layout, ) -> dict[BufferControl, SearchBufferControl]: """ Return mapping from BufferControl to SearchBufferControl. """ return { buffer_control: search_buffer_control for search_buffer_control, buffer_control in layout.search_links.items() } ================================================ FILE: src/prompt_toolkit/selection.py ================================================ """ Data structures for the selection. """ from __future__ import annotations from enum import Enum __all__ = [ "SelectionType", "PasteMode", "SelectionState", ] class SelectionType(Enum): """ Type of selection. """ #: Characters. (Visual in Vi.) CHARACTERS = "CHARACTERS" #: Whole lines. (Visual-Line in Vi.) LINES = "LINES" #: A block selection. (Visual-Block in Vi.) BLOCK = "BLOCK" class PasteMode(Enum): EMACS = "EMACS" # Yank like emacs. VI_AFTER = "VI_AFTER" # When pressing 'p' in Vi. VI_BEFORE = "VI_BEFORE" # When pressing 'P' in Vi. class SelectionState: """ State of the current selection. :param original_cursor_position: int :param type: :class:`~.SelectionType` """ def __init__( self, original_cursor_position: int = 0, type: SelectionType = SelectionType.CHARACTERS, ) -> None: self.original_cursor_position = original_cursor_position self.type = type self.shift_mode = False def enter_shift_mode(self) -> None: self.shift_mode = True def __repr__(self) -> str: return f"{self.__class__.__name__}(original_cursor_position={self.original_cursor_position!r}, type={self.type!r})" ================================================ FILE: src/prompt_toolkit/shortcuts/__init__.py ================================================ from __future__ import annotations from .choice_input import choice from .dialogs import ( button_dialog, checkboxlist_dialog, input_dialog, message_dialog, progress_dialog, radiolist_dialog, yes_no_dialog, ) from .progress_bar import ProgressBar, ProgressBarCounter from .prompt import ( CompleteStyle, PromptSession, confirm, create_confirm_session, prompt, ) from .utils import clear, clear_title, print_container, print_formatted_text, set_title __all__ = [ # Dialogs. "input_dialog", "message_dialog", "progress_dialog", "checkboxlist_dialog", "radiolist_dialog", "yes_no_dialog", "button_dialog", # Prompts. "PromptSession", "prompt", "confirm", "create_confirm_session", "CompleteStyle", # Progress bars. "ProgressBar", "ProgressBarCounter", # Choice selection. "choice", # Utils. "clear", "clear_title", "print_container", "print_formatted_text", "set_title", ] ================================================ FILE: src/prompt_toolkit/shortcuts/choice_input.py ================================================ from __future__ import annotations from collections.abc import Sequence from typing import Generic, TypeVar from prompt_toolkit.application import Application from prompt_toolkit.filters import ( Condition, FilterOrBool, is_done, renderer_height_is_known, to_filter, ) from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.key_binding.key_bindings import ( DynamicKeyBindings, KeyBindings, KeyBindingsBase, merge_key_bindings, ) from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.layout import ( AnyContainer, ConditionalContainer, HSplit, Layout, Window, ) from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.dimension import Dimension from prompt_toolkit.styles import BaseStyle, Style from prompt_toolkit.utils import suspend_to_background_supported from prompt_toolkit.widgets import Box, Frame, Label, RadioList __all__ = [ "ChoiceInput", "choice", ] _T = TypeVar("_T") E = KeyPressEvent def create_default_choice_input_style() -> BaseStyle: return Style.from_dict( { "frame.border": "#884444", "selected-option": "bold", } ) class ChoiceInput(Generic[_T]): """ Input selection prompt. Ask the user to choose among a set of options. Example usage:: input_selection = ChoiceInput( message="Please select a dish:", options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], default="pizza", ) result = input_selection.prompt() :param message: Plain text or formatted text to be shown before the options. :param options: Sequence of ``(value, label)`` tuples. The labels can be formatted text. :param default: Default value. If none is given, the first option is considered the default. :param mouse_support: Enable mouse support. :param style: :class:`.Style` instance for the color scheme. :param symbol: Symbol to be displayed in front of the selected choice. :param bottom_toolbar: Formatted text or callable that returns formatted text to be displayed at the bottom of the screen. :param show_frame: `bool` or :class:`~prompt_toolkit.filters.Filter`. When True, surround the input with a frame. :param enable_interrupt: `bool` or :class:`~prompt_toolkit.filters.Filter`. When True, raise the ``interrupt_exception`` (``KeyboardInterrupt`` by default) when control-c has been pressed. :param interrupt_exception: The exception type that will be raised when there is a keyboard interrupt (control-c keypress). """ def __init__( self, *, message: AnyFormattedText, options: Sequence[tuple[_T, AnyFormattedText]], default: _T | None = None, mouse_support: bool = False, style: BaseStyle | None = None, symbol: str = ">", bottom_toolbar: AnyFormattedText = None, show_frame: FilterOrBool = False, enable_suspend: FilterOrBool = False, enable_interrupt: FilterOrBool = True, interrupt_exception: type[BaseException] = KeyboardInterrupt, key_bindings: KeyBindingsBase | None = None, ) -> None: if style is None: style = create_default_choice_input_style() self.message = message self.default = default self.options = options self.mouse_support = mouse_support self.style = style self.symbol = symbol self.show_frame = show_frame self.enable_suspend = enable_suspend self.interrupt_exception = interrupt_exception self.enable_interrupt = enable_interrupt self.bottom_toolbar = bottom_toolbar self.key_bindings = key_bindings def _create_application(self) -> Application[_T]: radio_list = RadioList( values=self.options, default=self.default, select_on_focus=True, open_character="", select_character=self.symbol, close_character="", show_cursor=False, show_numbers=True, container_style="class:input-selection", default_style="class:option", selected_style="", checked_style="class:selected-option", number_style="class:number", show_scrollbar=False, ) container: AnyContainer = HSplit( [ Box( Label(text=self.message, dont_extend_height=True), padding_top=0, padding_left=1, padding_right=1, padding_bottom=0, ), Box( radio_list, padding_top=0, padding_left=3, padding_right=1, padding_bottom=0, ), ] ) @Condition def show_frame_filter() -> bool: return to_filter(self.show_frame)() show_bottom_toolbar = ( Condition(lambda: self.bottom_toolbar is not None) & ~is_done & renderer_height_is_known ) container = ConditionalContainer( Frame(container), alternative_content=container, filter=show_frame_filter, ) bottom_toolbar = ConditionalContainer( Window( FormattedTextControl( lambda: self.bottom_toolbar, style="class:bottom-toolbar.text" ), style="class:bottom-toolbar", dont_extend_height=True, height=Dimension(min=1), ), filter=show_bottom_toolbar, ) layout = Layout( HSplit( [ container, # Add an empty window between the selection input and the # bottom toolbar, if the bottom toolbar is visible, in # order to allow the bottom toolbar to be displayed at the # bottom of the screen. ConditionalContainer(Window(), filter=show_bottom_toolbar), bottom_toolbar, ] ), focused_element=radio_list, ) kb = KeyBindings() @kb.add("enter", eager=True) def _accept_input(event: E) -> None: "Accept input when enter has been pressed." event.app.exit(result=radio_list.current_value, style="class:accepted") @Condition def enable_interrupt() -> bool: return to_filter(self.enable_interrupt)() @kb.add("c-c", filter=enable_interrupt) @kb.add("<sigint>", filter=enable_interrupt) def _keyboard_interrupt(event: E) -> None: "Abort when Control-C has been pressed." event.app.exit(exception=self.interrupt_exception(), style="class:aborting") suspend_supported = Condition(suspend_to_background_supported) @Condition def enable_suspend() -> bool: return to_filter(self.enable_suspend)() @kb.add("c-z", filter=suspend_supported & enable_suspend) def _suspend(event: E) -> None: """ Suspend process to background. """ event.app.suspend_to_background() return Application( layout=layout, full_screen=False, mouse_support=self.mouse_support, key_bindings=merge_key_bindings( [kb, DynamicKeyBindings(lambda: self.key_bindings)] ), style=self.style, ) def prompt(self) -> _T: return self._create_application().run() async def prompt_async(self) -> _T: return await self._create_application().run_async() def choice( message: AnyFormattedText, *, options: Sequence[tuple[_T, AnyFormattedText]], default: _T | None = None, mouse_support: bool = False, style: BaseStyle | None = None, symbol: str = ">", bottom_toolbar: AnyFormattedText = None, show_frame: bool = False, enable_suspend: FilterOrBool = False, enable_interrupt: FilterOrBool = True, interrupt_exception: type[BaseException] = KeyboardInterrupt, key_bindings: KeyBindingsBase | None = None, ) -> _T: """ Choice selection prompt. Ask the user to choose among a set of options. Example usage:: result = choice( message="Please select a dish:", options=[ ("pizza", "Pizza with mushrooms"), ("salad", "Salad with tomatoes"), ("sushi", "Sushi"), ], default="pizza", ) :param message: Plain text or formatted text to be shown before the options. :param options: Sequence of ``(value, label)`` tuples. The labels can be formatted text. :param default: Default value. If none is given, the first option is considered the default. :param mouse_support: Enable mouse support. :param style: :class:`.Style` instance for the color scheme. :param symbol: Symbol to be displayed in front of the selected choice. :param bottom_toolbar: Formatted text or callable that returns formatted text to be displayed at the bottom of the screen. :param show_frame: `bool` or :class:`~prompt_toolkit.filters.Filter`. When True, surround the input with a frame. :param enable_interrupt: `bool` or :class:`~prompt_toolkit.filters.Filter`. When True, raise the ``interrupt_exception`` (``KeyboardInterrupt`` by default) when control-c has been pressed. :param interrupt_exception: The exception type that will be raised when there is a keyboard interrupt (control-c keypress). """ return ChoiceInput[_T]( message=message, options=options, default=default, mouse_support=mouse_support, style=style, symbol=symbol, bottom_toolbar=bottom_toolbar, show_frame=show_frame, enable_suspend=enable_suspend, enable_interrupt=enable_interrupt, interrupt_exception=interrupt_exception, key_bindings=key_bindings, ).prompt() ================================================ FILE: src/prompt_toolkit/shortcuts/dialogs.py ================================================ from __future__ import annotations import functools from collections.abc import Callable, Sequence from typing import Any, TypeVar from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer from prompt_toolkit.completion import Completer from prompt_toolkit.eventloop import run_in_executor_with_context from prompt_toolkit.filters import FilterOrBool from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.key_binding.key_bindings import KeyBindings, merge_key_bindings from prompt_toolkit.layout import Layout from prompt_toolkit.layout.containers import AnyContainer, HSplit from prompt_toolkit.layout.dimension import Dimension as D from prompt_toolkit.styles import BaseStyle from prompt_toolkit.validation import Validator from prompt_toolkit.widgets import ( Box, Button, CheckboxList, Dialog, Label, ProgressBar, RadioList, TextArea, ValidationToolbar, ) __all__ = [ "yes_no_dialog", "button_dialog", "input_dialog", "message_dialog", "radiolist_dialog", "checkboxlist_dialog", "progress_dialog", ] def yes_no_dialog( title: AnyFormattedText = "", text: AnyFormattedText = "", yes_text: str = "Yes", no_text: str = "No", style: BaseStyle | None = None, ) -> Application[bool]: """ Display a Yes/No dialog. Return a boolean. """ def yes_handler() -> None: get_app().exit(result=True) def no_handler() -> None: get_app().exit(result=False) dialog = Dialog( title=title, body=Label(text=text, dont_extend_height=True), buttons=[ Button(text=yes_text, handler=yes_handler), Button(text=no_text, handler=no_handler), ], with_background=True, ) return _create_app(dialog, style) _T = TypeVar("_T") def button_dialog( title: AnyFormattedText = "", text: AnyFormattedText = "", buttons: list[tuple[str, _T]] = [], style: BaseStyle | None = None, ) -> Application[_T]: """ Display a dialog with button choices (given as a list of tuples). Return the value associated with button. """ def button_handler(v: _T) -> None: get_app().exit(result=v) dialog = Dialog( title=title, body=Label(text=text, dont_extend_height=True), buttons=[ Button(text=t, handler=functools.partial(button_handler, v)) for t, v in buttons ], with_background=True, ) return _create_app(dialog, style) def input_dialog( title: AnyFormattedText = "", text: AnyFormattedText = "", ok_text: str = "OK", cancel_text: str = "Cancel", completer: Completer | None = None, validator: Validator | None = None, password: FilterOrBool = False, style: BaseStyle | None = None, default: str = "", ) -> Application[str]: """ Display a text input box. Return the given text, or None when cancelled. """ def accept(buf: Buffer) -> bool: get_app().layout.focus(ok_button) return True # Keep text. def ok_handler() -> None: get_app().exit(result=textfield.text) ok_button = Button(text=ok_text, handler=ok_handler) cancel_button = Button(text=cancel_text, handler=_return_none) textfield = TextArea( text=default, multiline=False, password=password, completer=completer, validator=validator, accept_handler=accept, ) dialog = Dialog( title=title, body=HSplit( [ Label(text=text, dont_extend_height=True), textfield, ValidationToolbar(), ], padding=D(preferred=1, max=1), ), buttons=[ok_button, cancel_button], with_background=True, ) return _create_app(dialog, style) def message_dialog( title: AnyFormattedText = "", text: AnyFormattedText = "", ok_text: str = "Ok", style: BaseStyle | None = None, ) -> Application[None]: """ Display a simple message box and wait until the user presses enter. """ dialog = Dialog( title=title, body=Label(text=text, dont_extend_height=True), buttons=[Button(text=ok_text, handler=_return_none)], with_background=True, ) return _create_app(dialog, style) def radiolist_dialog( title: AnyFormattedText = "", text: AnyFormattedText = "", ok_text: str = "Ok", cancel_text: str = "Cancel", values: Sequence[tuple[_T, AnyFormattedText]] | None = None, default: _T | None = None, style: BaseStyle | None = None, ) -> Application[_T]: """ Display a simple list of element the user can choose amongst. Only one element can be selected at a time using Arrow keys and Enter. The focus can be moved between the list and the Ok/Cancel button with tab. """ if values is None: values = [] def ok_handler() -> None: get_app().exit(result=radio_list.current_value) radio_list = RadioList(values=values, default=default) dialog = Dialog( title=title, body=HSplit( [Label(text=text, dont_extend_height=True), radio_list], padding=1, ), buttons=[ Button(text=ok_text, handler=ok_handler), Button(text=cancel_text, handler=_return_none), ], with_background=True, ) return _create_app(dialog, style) def checkboxlist_dialog( title: AnyFormattedText = "", text: AnyFormattedText = "", ok_text: str = "Ok", cancel_text: str = "Cancel", values: Sequence[tuple[_T, AnyFormattedText]] | None = None, default_values: Sequence[_T] | None = None, style: BaseStyle | None = None, ) -> Application[list[_T]]: """ Display a simple list of element the user can choose multiple values amongst. Several elements can be selected at a time using Arrow keys and Enter. The focus can be moved between the list and the Ok/Cancel button with tab. """ if values is None: values = [] def ok_handler() -> None: get_app().exit(result=cb_list.current_values) cb_list = CheckboxList(values=values, default_values=default_values) dialog = Dialog( title=title, body=HSplit( [Label(text=text, dont_extend_height=True), cb_list], padding=1, ), buttons=[ Button(text=ok_text, handler=ok_handler), Button(text=cancel_text, handler=_return_none), ], with_background=True, ) return _create_app(dialog, style) def progress_dialog( title: AnyFormattedText = "", text: AnyFormattedText = "", run_callback: Callable[[Callable[[int], None], Callable[[str], None]], None] = ( lambda *a: None ), style: BaseStyle | None = None, ) -> Application[None]: """ :param run_callback: A function that receives as input a `set_percentage` function and it does the work. """ progressbar = ProgressBar() text_area = TextArea( focusable=False, # Prefer this text area as big as possible, to avoid having a window # that keeps resizing when we add text to it. height=D(preferred=10**10), ) dialog = Dialog( body=HSplit( [ Box(Label(text=text)), Box(text_area, padding=D.exact(1)), progressbar, ] ), title=title, with_background=True, ) app = _create_app(dialog, style) def set_percentage(value: int) -> None: progressbar.percentage = int(value) app.invalidate() def log_text(text: str) -> None: loop = app.loop if loop is not None: loop.call_soon_threadsafe(text_area.buffer.insert_text, text) app.invalidate() # Run the callback in the executor. When done, set a return value for the # UI, so that it quits. def start() -> None: try: run_callback(set_percentage, log_text) finally: app.exit() def pre_run() -> None: run_in_executor_with_context(start) app.pre_run_callables.append(pre_run) return app def _create_app(dialog: AnyContainer, style: BaseStyle | None) -> Application[Any]: # Key bindings. bindings = KeyBindings() bindings.add("tab")(focus_next) bindings.add("s-tab")(focus_previous) return Application( layout=Layout(dialog), key_bindings=merge_key_bindings([load_key_bindings(), bindings]), mouse_support=True, style=style, full_screen=True, ) def _return_none() -> None: "Button handler that returns None." get_app().exit() ================================================ FILE: src/prompt_toolkit/shortcuts/progress_bar/__init__.py ================================================ from __future__ import annotations from .base import ProgressBar, ProgressBarCounter from .formatters import ( Bar, Formatter, IterationsPerSecond, Label, Percentage, Progress, Rainbow, SpinningWheel, Text, TimeElapsed, TimeLeft, ) __all__ = [ "ProgressBar", "ProgressBarCounter", # Formatters. "Formatter", "Text", "Label", "Percentage", "Bar", "Progress", "TimeElapsed", "TimeLeft", "IterationsPerSecond", "SpinningWheel", "Rainbow", ] ================================================ FILE: src/prompt_toolkit/shortcuts/progress_bar/base.py ================================================ """ Progress bar implementation on top of prompt_toolkit. :: with ProgressBar(...) as pb: for item in pb(data): ... """ from __future__ import annotations import contextvars import datetime import functools import os import signal import threading import traceback from collections.abc import Callable, Iterable, Iterator, Sequence, Sized from typing import ( Generic, TextIO, TypeVar, cast, ) from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app_session from prompt_toolkit.filters import Condition, is_done, renderer_height_is_known from prompt_toolkit.formatted_text import ( AnyFormattedText, StyleAndTextTuples, to_formatted_text, ) from prompt_toolkit.input import Input from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.layout import ( ConditionalContainer, FormattedTextControl, HSplit, Layout, VSplit, Window, ) from prompt_toolkit.layout.controls import UIContent, UIControl from prompt_toolkit.layout.dimension import AnyDimension, D from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import BaseStyle from prompt_toolkit.utils import in_main_thread from .formatters import Formatter, create_default_formatters __all__ = ["ProgressBar"] E = KeyPressEvent _SIGWINCH = getattr(signal, "SIGWINCH", None) def create_key_bindings(cancel_callback: Callable[[], None] | None) -> KeyBindings: """ Key bindings handled by the progress bar. (The main thread is not supposed to handle any key bindings.) """ kb = KeyBindings() @kb.add("c-l") def _clear(event: E) -> None: event.app.renderer.clear() if cancel_callback is not None: @kb.add("c-c") def _interrupt(event: E) -> None: "Kill the 'body' of the progress bar, but only if we run from the main thread." assert cancel_callback is not None cancel_callback() return kb _T = TypeVar("_T") class ProgressBar: """ Progress bar context manager. Usage :: with ProgressBar(...) as pb: for item in pb(data): ... :param title: Text to be displayed above the progress bars. This can be a callable or formatted text as well. :param formatters: List of :class:`.Formatter` instances. :param bottom_toolbar: Text to be displayed in the bottom toolbar. This can be a callable or formatted text. :param style: :class:`prompt_toolkit.styles.BaseStyle` instance. :param key_bindings: :class:`.KeyBindings` instance. :param cancel_callback: Callback function that's called when control-c is pressed by the user. This can be used for instance to start "proper" cancellation if the wrapped code supports it. :param file: The file object used for rendering, by default `sys.stderr` is used. :param color_depth: `prompt_toolkit` `ColorDepth` instance. :param output: :class:`~prompt_toolkit.output.Output` instance. :param input: :class:`~prompt_toolkit.input.Input` instance. """ def __init__( self, title: AnyFormattedText = None, formatters: Sequence[Formatter] | None = None, bottom_toolbar: AnyFormattedText = None, style: BaseStyle | None = None, key_bindings: KeyBindings | None = None, cancel_callback: Callable[[], None] | None = None, file: TextIO | None = None, color_depth: ColorDepth | None = None, output: Output | None = None, input: Input | None = None, ) -> None: self.title = title self.formatters = formatters or create_default_formatters() self.bottom_toolbar = bottom_toolbar self.counters: list[ProgressBarCounter[object]] = [] self.style = style self.key_bindings = key_bindings self.cancel_callback = cancel_callback # If no `cancel_callback` was given, and we're creating the progress # bar from the main thread. Cancel by sending a `KeyboardInterrupt` to # the main thread. if self.cancel_callback is None and in_main_thread(): def keyboard_interrupt_to_main_thread() -> None: os.kill(os.getpid(), signal.SIGINT) self.cancel_callback = keyboard_interrupt_to_main_thread # Note that we use __stderr__ as default error output, because that # works best with `patch_stdout`. self.color_depth = color_depth self.output = output or get_app_session().output self.input = input or get_app_session().input self._thread: threading.Thread | None = None self._has_sigwinch = False self._app_started = threading.Event() def __enter__(self) -> ProgressBar: # Create UI Application. title_toolbar = ConditionalContainer( Window( FormattedTextControl(lambda: self.title), height=1, style="class:progressbar,title", ), filter=Condition(lambda: self.title is not None), ) bottom_toolbar = ConditionalContainer( Window( FormattedTextControl( lambda: self.bottom_toolbar, style="class:bottom-toolbar.text" ), style="class:bottom-toolbar", height=1, ), filter=~is_done & renderer_height_is_known & Condition(lambda: self.bottom_toolbar is not None), ) def width_for_formatter(formatter: Formatter) -> AnyDimension: # Needs to be passed as callable (partial) to the 'width' # parameter, because we want to call it on every resize. return formatter.get_width(progress_bar=self) progress_controls = [ Window( content=_ProgressControl(self, f, self.cancel_callback), width=functools.partial(width_for_formatter, f), ) for f in self.formatters ] self.app: Application[None] = Application( min_redraw_interval=0.05, layout=Layout( HSplit( [ title_toolbar, VSplit( progress_controls, height=lambda: D( preferred=len(self.counters), max=len(self.counters) ), ), Window(), bottom_toolbar, ] ) ), style=self.style, key_bindings=self.key_bindings, refresh_interval=0.3, color_depth=self.color_depth, output=self.output, input=self.input, ) # Run application in different thread. def run() -> None: try: self.app.run(pre_run=self._app_started.set) except BaseException as e: traceback.print_exc() print(e) ctx: contextvars.Context = contextvars.copy_context() self._thread = threading.Thread(target=ctx.run, args=(run,)) self._thread.start() return self def __exit__(self, *a: object) -> None: # Wait for the app to be started. Make sure we don't quit earlier, # otherwise `self.app.exit` won't terminate the app because # `self.app.future` has not yet been set. self._app_started.wait() # Quit UI application. if self.app.is_running and self.app.loop is not None: self.app.loop.call_soon_threadsafe(self.app.exit) if self._thread is not None: self._thread.join() def __call__( self, data: Iterable[_T] | None = None, label: AnyFormattedText = "", remove_when_done: bool = False, total: int | None = None, ) -> ProgressBarCounter[_T]: """ Start a new counter. :param label: Title text or description for this progress. (This can be formatted text as well). :param remove_when_done: When `True`, hide this progress bar. :param total: Specify the maximum value if it can't be calculated by calling ``len``. """ counter = ProgressBarCounter( self, data, label=label, remove_when_done=remove_when_done, total=total ) self.counters.append(counter) return counter def invalidate(self) -> None: self.app.invalidate() class _ProgressControl(UIControl): """ User control for the progress bar. """ def __init__( self, progress_bar: ProgressBar, formatter: Formatter, cancel_callback: Callable[[], None] | None, ) -> None: self.progress_bar = progress_bar self.formatter = formatter self._key_bindings = create_key_bindings(cancel_callback) def create_content(self, width: int, height: int) -> UIContent: items: list[StyleAndTextTuples] = [] for pr in self.progress_bar.counters: try: text = self.formatter.format(self.progress_bar, pr, width) except BaseException: traceback.print_exc() text = "ERROR" items.append(to_formatted_text(text)) def get_line(i: int) -> StyleAndTextTuples: return items[i] return UIContent(get_line=get_line, line_count=len(items), show_cursor=False) def is_focusable(self) -> bool: return True # Make sure that the key bindings work. def get_key_bindings(self) -> KeyBindings: return self._key_bindings _CounterItem = TypeVar("_CounterItem", covariant=True) class ProgressBarCounter(Generic[_CounterItem]): """ An individual counter (A progress bar can have multiple counters). """ def __init__( self, progress_bar: ProgressBar, data: Iterable[_CounterItem] | None = None, label: AnyFormattedText = "", remove_when_done: bool = False, total: int | None = None, ) -> None: self.start_time = datetime.datetime.now() self.stop_time: datetime.datetime | None = None self.progress_bar = progress_bar self.data = data self.items_completed = 0 self.label = label self.remove_when_done = remove_when_done self._done = False self.total: int | None if total is None: try: self.total = len(cast(Sized, data)) except TypeError: self.total = None # We don't know the total length. else: self.total = total def __iter__(self) -> Iterator[_CounterItem]: if self.data is not None: try: for item in self.data: yield item self.item_completed() # Only done if we iterate to the very end. self.done = True finally: # Ensure counter has stopped even if we did not iterate to the # end (e.g. break or exceptions). self.stopped = True else: raise NotImplementedError("No data defined to iterate over.") def item_completed(self) -> None: """ Start handling the next item. (Can be called manually in case we don't have a collection to loop through.) """ self.items_completed += 1 self.progress_bar.invalidate() @property def done(self) -> bool: """Whether a counter has been completed. Done counter have been stopped (see stopped) and removed depending on remove_when_done value. Contrast this with stopped. A stopped counter may be terminated before 100% completion. A done counter has reached its 100% completion. """ return self._done @done.setter def done(self, value: bool) -> None: self._done = value self.stopped = value if value and self.remove_when_done: self.progress_bar.counters.remove(self) @property def stopped(self) -> bool: """Whether a counter has been stopped. Stopped counters no longer have increasing time_elapsed. This distinction is also used to prevent the Bar formatter with unknown totals from continuing to run. A stopped counter (but not done) can be used to signal that a given counter has encountered an error but allows other counters to continue (e.g. download X of Y failed). Given how only done counters are removed (see remove_when_done) this can help aggregate failures from a large number of successes. Contrast this with done. A done counter has reached its 100% completion. A stopped counter may be terminated before 100% completion. """ return self.stop_time is not None @stopped.setter def stopped(self, value: bool) -> None: if value: # This counter has not already been stopped. if not self.stop_time: self.stop_time = datetime.datetime.now() else: # Clearing any previously set stop_time. self.stop_time = None @property def percentage(self) -> float: if self.total is None: return 0 else: return self.items_completed * 100 / max(self.total, 1) @property def time_elapsed(self) -> datetime.timedelta: """ Return how much time has been elapsed since the start. """ if self.stop_time is None: return datetime.datetime.now() - self.start_time else: return self.stop_time - self.start_time @property def time_left(self) -> datetime.timedelta | None: """ Timedelta representing the time left. """ if self.total is None or not self.percentage: return None elif self.done or self.stopped: return datetime.timedelta(0) else: return self.time_elapsed * (100 - self.percentage) / self.percentage ================================================ FILE: src/prompt_toolkit/shortcuts/progress_bar/formatters.py ================================================ """ Formatter classes for the progress bar. Each progress bar consists of a list of these formatters. """ from __future__ import annotations import datetime import time from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING from prompt_toolkit.formatted_text import ( HTML, AnyFormattedText, StyleAndTextTuples, to_formatted_text, ) from prompt_toolkit.formatted_text.utils import fragment_list_width from prompt_toolkit.layout.dimension import AnyDimension, D from prompt_toolkit.layout.utils import explode_text_fragments from prompt_toolkit.utils import get_cwidth if TYPE_CHECKING: from .base import ProgressBar, ProgressBarCounter __all__ = [ "Formatter", "Text", "Label", "Percentage", "Bar", "Progress", "TimeElapsed", "TimeLeft", "IterationsPerSecond", "SpinningWheel", "Rainbow", "create_default_formatters", ] class Formatter(metaclass=ABCMeta): """ Base class for any formatter. """ @abstractmethod def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: pass def get_width(self, progress_bar: ProgressBar) -> AnyDimension: return D() class Text(Formatter): """ Display plain text. """ def __init__(self, text: AnyFormattedText, style: str = "") -> None: self.text = to_formatted_text(text, style=style) def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: return self.text def get_width(self, progress_bar: ProgressBar) -> AnyDimension: return fragment_list_width(self.text) class Label(Formatter): """ Display the name of the current task. :param width: If a `width` is given, use this width. Scroll the text if it doesn't fit in this width. :param suffix: String suffix to be added after the task name, e.g. ': '. If no task name was given, no suffix will be added. """ def __init__(self, width: AnyDimension = None, suffix: str = "") -> None: self.width = width self.suffix = suffix def _add_suffix(self, label: AnyFormattedText) -> StyleAndTextTuples: label = to_formatted_text(label, style="class:label") return label + [("", self.suffix)] def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: label = self._add_suffix(progress.label) cwidth = fragment_list_width(label) if cwidth > width: # It doesn't fit -> scroll task name. label = explode_text_fragments(label) max_scroll = cwidth - width current_scroll = int(time.time() * 3 % max_scroll) label = label[current_scroll:] return label def get_width(self, progress_bar: ProgressBar) -> AnyDimension: if self.width: return self.width all_labels = [self._add_suffix(c.label) for c in progress_bar.counters] if all_labels: max_widths = max(fragment_list_width(l) for l in all_labels) return D(preferred=max_widths, max=max_widths) else: return D() class Percentage(Formatter): """ Display the progress as a percentage. """ template = HTML("<percentage>{percentage:>5}%</percentage>") def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: return self.template.format(percentage=round(progress.percentage, 1)) def get_width(self, progress_bar: ProgressBar) -> AnyDimension: return D.exact(6) class Bar(Formatter): """ Display the progress bar itself. """ template = HTML( "<bar>{start}<bar-a>{bar_a}</bar-a><bar-b>{bar_b}</bar-b><bar-c>{bar_c}</bar-c>{end}</bar>" ) def __init__( self, start: str = "[", end: str = "]", sym_a: str = "=", sym_b: str = ">", sym_c: str = " ", unknown: str = "#", ) -> None: assert len(sym_a) == 1 and get_cwidth(sym_a) == 1 assert len(sym_c) == 1 and get_cwidth(sym_c) == 1 self.start = start self.end = end self.sym_a = sym_a self.sym_b = sym_b self.sym_c = sym_c self.unknown = unknown def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: if progress.done or progress.total or progress.stopped: sym_a, sym_b, sym_c = self.sym_a, self.sym_b, self.sym_c # Compute pb_a based on done, total, or stopped states. if progress.done: # 100% completed irrelevant of how much was actually marked as completed. percent = 1.0 else: # Show percentage completed. percent = progress.percentage / 100 else: # Total is unknown and bar is still running. sym_a, sym_b, sym_c = self.sym_c, self.unknown, self.sym_c # Compute percent based on the time. percent = time.time() * 20 % 100 / 100 # Subtract left, sym_b, and right. width -= get_cwidth(self.start + sym_b + self.end) # Scale percent by width pb_a = int(percent * width) bar_a = sym_a * pb_a bar_b = sym_b bar_c = sym_c * (width - pb_a) return self.template.format( start=self.start, end=self.end, bar_a=bar_a, bar_b=bar_b, bar_c=bar_c ) def get_width(self, progress_bar: ProgressBar) -> AnyDimension: return D(min=9) class Progress(Formatter): """ Display the progress as text. E.g. "8/20" """ template = HTML("<current>{current:>3}</current>/<total>{total:>3}</total>") def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: return self.template.format( current=progress.items_completed, total=progress.total or "?" ) def get_width(self, progress_bar: ProgressBar) -> AnyDimension: all_lengths = [ len("{:>3}".format(c.total or "?")) for c in progress_bar.counters ] all_lengths.append(1) return D.exact(max(all_lengths) * 2 + 1) def _format_timedelta(timedelta: datetime.timedelta) -> str: """ Return hh:mm:ss, or mm:ss if the amount of hours is zero. """ result = f"{timedelta}".split(".")[0] if result.startswith("0:"): result = result[2:] return result class TimeElapsed(Formatter): """ Display the elapsed time. """ template = HTML("<time-elapsed>{time_elapsed}</time-elapsed>") def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: text = _format_timedelta(progress.time_elapsed).rjust(width) return self.template.format(time_elapsed=text) def get_width(self, progress_bar: ProgressBar) -> AnyDimension: all_values = [ len(_format_timedelta(c.time_elapsed)) for c in progress_bar.counters ] if all_values: return max(all_values) return 0 class TimeLeft(Formatter): """ Display the time left. """ template = HTML("<time-left>{time_left}</time-left>") unknown = "?:??:??" def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: time_left = progress.time_left if time_left is not None: formatted_time_left = _format_timedelta(time_left) else: formatted_time_left = self.unknown return self.template.format(time_left=formatted_time_left.rjust(width)) def get_width(self, progress_bar: ProgressBar) -> AnyDimension: all_values = [ len(_format_timedelta(c.time_left)) if c.time_left is not None else 7 for c in progress_bar.counters ] if all_values: return max(all_values) return 0 class IterationsPerSecond(Formatter): """ Display the iterations per second. """ template = HTML( "<iterations-per-second>{iterations_per_second:.2f}</iterations-per-second>" ) def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: value = progress.items_completed / progress.time_elapsed.total_seconds() return self.template.format(iterations_per_second=value) def get_width(self, progress_bar: ProgressBar) -> AnyDimension: all_values = [ len(f"{c.items_completed / c.time_elapsed.total_seconds():.2f}") for c in progress_bar.counters ] if all_values: return max(all_values) return 0 class SpinningWheel(Formatter): """ Display a spinning wheel. """ template = HTML("<spinning-wheel>{0}</spinning-wheel>") characters = r"/-\|" def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: index = int(time.time() * 3) % len(self.characters) return self.template.format(self.characters[index]) def get_width(self, progress_bar: ProgressBar) -> AnyDimension: return D.exact(1) def _hue_to_rgb(hue: float) -> tuple[int, int, int]: """ Take hue between 0 and 1, return (r, g, b). """ i = int(hue * 6.0) f = (hue * 6.0) - i q = int(255 * (1.0 - f)) t = int(255 * (1.0 - (1.0 - f))) i %= 6 return [ (255, t, 0), (q, 255, 0), (0, 255, t), (0, q, 255), (t, 0, 255), (255, 0, q), ][i] class Rainbow(Formatter): """ For the fun. Add rainbow colors to any of the other formatters. """ colors = ["#%.2x%.2x%.2x" % _hue_to_rgb(h / 100.0) for h in range(0, 100)] def __init__(self, formatter: Formatter) -> None: self.formatter = formatter def format( self, progress_bar: ProgressBar, progress: ProgressBarCounter[object], width: int, ) -> AnyFormattedText: # Get formatted text from nested formatter, and explode it in # text/style tuples. result = self.formatter.format(progress_bar, progress, width) result = explode_text_fragments(to_formatted_text(result)) # Insert colors. result2: StyleAndTextTuples = [] shift = int(time.time() * 3) % len(self.colors) for i, (style, text, *_) in enumerate(result): result2.append( (style + " " + self.colors[(i + shift) % len(self.colors)], text) ) return result2 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: return self.formatter.get_width(progress_bar) def create_default_formatters() -> list[Formatter]: """ Return the list of default formatters. """ return [ Label(), Text(" "), Percentage(), Text(" "), Bar(), Text(" "), Progress(), Text(" "), Text("eta [", style="class:time-left"), TimeLeft(), Text("]", style="class:time-left"), Text(" "), ] ================================================ FILE: src/prompt_toolkit/shortcuts/prompt.py ================================================ """ Line editing functionality. --------------------------- This provides a UI for a line input, similar to GNU Readline, libedit and linenoise. Either call the `prompt` function for every line input. Or create an instance of the :class:`.PromptSession` class and call the `prompt` method from that class. In the second case, we'll have a 'session' that keeps all the state like the history in between several calls. There is a lot of overlap between the arguments taken by the `prompt` function and the `PromptSession` (like `completer`, `style`, etcetera). There we have the freedom to decide which settings we want for the whole 'session', and which we want for an individual `prompt`. Example:: # Simple `prompt` call. result = prompt('Say something: ') # Using a 'session'. s = PromptSession() result = s.prompt('Say something: ') """ from __future__ import annotations from asyncio import get_running_loop from collections.abc import Callable, Iterator from contextlib import contextmanager from enum import Enum from functools import partial from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.clipboard import Clipboard, DynamicClipboard, InMemoryClipboard from prompt_toolkit.completion import Completer, DynamicCompleter, ThreadedCompleter from prompt_toolkit.cursor_shapes import ( AnyCursorShapeConfig, CursorShapeConfig, DynamicCursorShapeConfig, ) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode from prompt_toolkit.eventloop import InputHook from prompt_toolkit.filters import ( Condition, FilterOrBool, has_arg, has_focus, is_done, is_true, renderer_height_is_known, to_filter, ) from prompt_toolkit.formatted_text import ( AnyFormattedText, StyleAndTextTuples, fragment_list_to_text, merge_formatted_text, to_formatted_text, ) from prompt_toolkit.history import History, InMemoryHistory from prompt_toolkit.input.base import Input from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings from prompt_toolkit.key_binding.bindings.completion import ( display_completions_like_readline, ) from prompt_toolkit.key_binding.bindings.open_in_editor import ( load_open_in_editor_bindings, ) from prompt_toolkit.key_binding.key_bindings import ( ConditionalKeyBindings, DynamicKeyBindings, KeyBindings, KeyBindingsBase, merge_key_bindings, ) from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from prompt_toolkit.layout import Float, FloatContainer, HSplit, Window from prompt_toolkit.layout.containers import ConditionalContainer, WindowAlign from prompt_toolkit.layout.controls import ( BufferControl, FormattedTextControl, SearchBufferControl, ) from prompt_toolkit.layout.dimension import Dimension from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import ( AfterInput, AppendAutoSuggestion, ConditionalProcessor, DisplayMultipleCursors, DynamicProcessor, HighlightIncrementalSearchProcessor, HighlightSelectionProcessor, PasswordProcessor, Processor, ReverseSearchProcessor, merge_processors, ) from prompt_toolkit.layout.utils import explode_text_fragments from prompt_toolkit.lexers import DynamicLexer, Lexer from prompt_toolkit.output import ColorDepth, DummyOutput, Output from prompt_toolkit.styles import ( BaseStyle, ConditionalStyleTransformation, DynamicStyle, DynamicStyleTransformation, StyleTransformation, SwapLightAndDarkStyleTransformation, merge_style_transformations, ) from prompt_toolkit.utils import ( get_cwidth, is_dumb_terminal, suspend_to_background_supported, to_str, ) from prompt_toolkit.validation import DynamicValidator, Validator from prompt_toolkit.widgets import Frame from prompt_toolkit.widgets.toolbars import ( SearchToolbar, SystemToolbar, ValidationToolbar, ) if TYPE_CHECKING: from prompt_toolkit.formatted_text.base import MagicFormattedText __all__ = [ "PromptSession", "prompt", "confirm", "create_confirm_session", # Used by '_display_completions_like_readline'. "CompleteStyle", ] _StyleAndTextTuplesCallable = Callable[[], StyleAndTextTuples] E = KeyPressEvent def _split_multiline_prompt( get_prompt_text: _StyleAndTextTuplesCallable, ) -> tuple[ Callable[[], bool], _StyleAndTextTuplesCallable, _StyleAndTextTuplesCallable ]: """ Take a `get_prompt_text` function and return three new functions instead. One that tells whether this prompt consists of multiple lines; one that returns the fragments to be shown on the lines above the input; and another one with the fragments to be shown at the first line of the input. """ def has_before_fragments() -> bool: for fragment, char, *_ in get_prompt_text(): if "\n" in char: return True return False def before() -> StyleAndTextTuples: result: StyleAndTextTuples = [] found_nl = False for fragment, char, *_ in reversed(explode_text_fragments(get_prompt_text())): if found_nl: result.insert(0, (fragment, char)) elif char == "\n": found_nl = True return result def first_input_line() -> StyleAndTextTuples: result: StyleAndTextTuples = [] for fragment, char, *_ in reversed(explode_text_fragments(get_prompt_text())): if char == "\n": break else: result.insert(0, (fragment, char)) return result return has_before_fragments, before, first_input_line class _RPrompt(Window): """ The prompt that is displayed on the right side of the Window. """ def __init__(self, text: AnyFormattedText) -> None: super().__init__( FormattedTextControl(text=text), align=WindowAlign.RIGHT, style="class:rprompt", ) class CompleteStyle(str, Enum): """ How to display autocompletions for the prompt. """ value: str COLUMN = "COLUMN" MULTI_COLUMN = "MULTI_COLUMN" READLINE_LIKE = "READLINE_LIKE" # Formatted text for the continuation prompt. It's the same like other # formatted text, except that if it's a callable, it takes three arguments. PromptContinuationText = Union[ str, "MagicFormattedText", StyleAndTextTuples, # (prompt_width, line_number, wrap_count) -> AnyFormattedText. Callable[[int, int, int], AnyFormattedText], ] _T = TypeVar("_T") class PromptSession(Generic[_T]): """ PromptSession for a prompt application, which can be used as a GNU Readline replacement. This is a wrapper around a lot of ``prompt_toolkit`` functionality and can be a replacement for `raw_input`. All parameters that expect "formatted text" can take either just plain text (a unicode object), a list of ``(style_str, text)`` tuples or an HTML object. Example usage:: s = PromptSession(message='>') text = s.prompt() :param message: Plain text or formatted text to be shown before the prompt. This can also be a callable that returns formatted text. :param multiline: `bool` or :class:`~prompt_toolkit.filters.Filter`. When True, prefer a layout that is more adapted for multiline input. Text after newlines is automatically indented, and search/arg input is shown below the input, instead of replacing the prompt. :param wrap_lines: `bool` or :class:`~prompt_toolkit.filters.Filter`. When True (the default), automatically wrap long lines instead of scrolling horizontally. :param is_password: Show asterisks instead of the actual typed characters. :param editing_mode: ``EditingMode.VI`` or ``EditingMode.EMACS``. :param vi_mode: `bool`, if True, Identical to ``editing_mode=EditingMode.VI``. :param complete_while_typing: `bool` or :class:`~prompt_toolkit.filters.Filter`. Enable autocompletion while typing. :param validate_while_typing: `bool` or :class:`~prompt_toolkit.filters.Filter`. Enable input validation while typing. :param enable_history_search: `bool` or :class:`~prompt_toolkit.filters.Filter`. Enable up-arrow parting string matching. :param search_ignore_case: :class:`~prompt_toolkit.filters.Filter`. Search case insensitive. :param lexer: :class:`~prompt_toolkit.lexers.Lexer` to be used for the syntax highlighting. :param validator: :class:`~prompt_toolkit.validation.Validator` instance for input validation. :param completer: :class:`~prompt_toolkit.completion.Completer` instance for input completion. :param complete_in_thread: `bool` or :class:`~prompt_toolkit.filters.Filter`. Run the completer code in a background thread in order to avoid blocking the user interface. For ``CompleteStyle.READLINE_LIKE``, this setting has no effect. There we always run the completions in the main thread. :param reserve_space_for_menu: Space to be reserved for displaying the menu. (0 means that no space needs to be reserved.) :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` instance for input suggestions. :param style: :class:`.Style` instance for the color scheme. :param include_default_pygments_style: `bool` or :class:`~prompt_toolkit.filters.Filter`. Tell whether the default styling for Pygments lexers has to be included. By default, this is true, but it is recommended to be disabled if another Pygments style is passed as the `style` argument, otherwise, two Pygments styles will be merged. :param style_transformation: :class:`~prompt_toolkit.style.StyleTransformation` instance. :param swap_light_and_dark_colors: `bool` or :class:`~prompt_toolkit.filters.Filter`. When enabled, apply :class:`~prompt_toolkit.style.SwapLightAndDarkStyleTransformation`. This is useful for switching between dark and light terminal backgrounds. :param enable_system_prompt: `bool` or :class:`~prompt_toolkit.filters.Filter`. Pressing Meta+'!' will show a system prompt. :param enable_suspend: `bool` or :class:`~prompt_toolkit.filters.Filter`. Enable Control-Z style suspension. :param enable_open_in_editor: `bool` or :class:`~prompt_toolkit.filters.Filter`. Pressing 'v' in Vi mode or C-X C-E in emacs mode will open an external editor. :param history: :class:`~prompt_toolkit.history.History` instance. :param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` instance. (e.g. :class:`~prompt_toolkit.clipboard.InMemoryClipboard`) :param rprompt: Text or formatted text to be displayed on the right side. This can also be a callable that returns (formatted) text. :param bottom_toolbar: Formatted text or callable that returns formatted text to be displayed at the bottom of the screen. :param prompt_continuation: Text that needs to be displayed for a multiline prompt continuation. This can either be formatted text or a callable that takes a `prompt_width`, `line_number` and `wrap_count` as input and returns formatted text. When this is `None` (the default), then `prompt_width` spaces will be used. :param complete_style: ``CompleteStyle.COLUMN``, ``CompleteStyle.MULTI_COLUMN`` or ``CompleteStyle.READLINE_LIKE``. :param mouse_support: `bool` or :class:`~prompt_toolkit.filters.Filter` to enable mouse support. :param placeholder: Text to be displayed when no input has been given yet. Unlike the `default` parameter, this won't be returned as part of the output ever. This can be formatted text or a callable that returns formatted text. :param show_frame: `bool` or :class:`~prompt_toolkit.filters.Filter`. When True, surround the input with a frame. :param refresh_interval: (number; in seconds) When given, refresh the UI every so many seconds. :param input: `Input` object. (Note that the preferred way to change the input/output is by creating an `AppSession`.) :param output: `Output` object. :param interrupt_exception: The exception type that will be raised when there is a keyboard interrupt (control-c keypress). :param eof_exception: The exception type that will be raised when there is an end-of-file/exit event (control-d keypress). """ _fields = ( "message", "lexer", "completer", "complete_in_thread", "is_password", "editing_mode", "key_bindings", "is_password", "bottom_toolbar", "style", "style_transformation", "swap_light_and_dark_colors", "color_depth", "cursor", "include_default_pygments_style", "rprompt", "multiline", "prompt_continuation", "wrap_lines", "enable_history_search", "search_ignore_case", "complete_while_typing", "validate_while_typing", "complete_style", "mouse_support", "auto_suggest", "clipboard", "validator", "refresh_interval", "input_processors", "placeholder", "enable_system_prompt", "enable_suspend", "enable_open_in_editor", "reserve_space_for_menu", "tempfile_suffix", "tempfile", "show_frame", ) def __init__( self, message: AnyFormattedText = "", *, multiline: FilterOrBool = False, wrap_lines: FilterOrBool = True, is_password: FilterOrBool = False, vi_mode: bool = False, editing_mode: EditingMode = EditingMode.EMACS, complete_while_typing: FilterOrBool = True, validate_while_typing: FilterOrBool = True, enable_history_search: FilterOrBool = False, search_ignore_case: FilterOrBool = False, lexer: Lexer | None = None, enable_system_prompt: FilterOrBool = False, enable_suspend: FilterOrBool = False, enable_open_in_editor: FilterOrBool = False, validator: Validator | None = None, completer: Completer | None = None, complete_in_thread: bool = False, reserve_space_for_menu: int = 8, complete_style: CompleteStyle = CompleteStyle.COLUMN, auto_suggest: AutoSuggest | None = None, style: BaseStyle | None = None, style_transformation: StyleTransformation | None = None, swap_light_and_dark_colors: FilterOrBool = False, color_depth: ColorDepth | None = None, cursor: AnyCursorShapeConfig = None, include_default_pygments_style: FilterOrBool = True, history: History | None = None, clipboard: Clipboard | None = None, prompt_continuation: PromptContinuationText | None = None, rprompt: AnyFormattedText = None, bottom_toolbar: AnyFormattedText = None, mouse_support: FilterOrBool = False, input_processors: list[Processor] | None = None, placeholder: AnyFormattedText | None = None, key_bindings: KeyBindingsBase | None = None, erase_when_done: bool = False, tempfile_suffix: str | Callable[[], str] | None = ".txt", tempfile: str | Callable[[], str] | None = None, refresh_interval: float = 0, show_frame: FilterOrBool = False, input: Input | None = None, output: Output | None = None, interrupt_exception: type[BaseException] = KeyboardInterrupt, eof_exception: type[BaseException] = EOFError, ) -> None: history = history or InMemoryHistory() clipboard = clipboard or InMemoryClipboard() # Ensure backwards-compatibility, when `vi_mode` is passed. if vi_mode: editing_mode = EditingMode.VI # Store all settings in this class. self._input = input self._output = output # Store attributes. # (All except 'editing_mode'.) self.message = message self.lexer = lexer self.completer = completer self.complete_in_thread = complete_in_thread self.is_password = is_password self.key_bindings = key_bindings self.bottom_toolbar = bottom_toolbar self.style = style self.style_transformation = style_transformation self.swap_light_and_dark_colors = swap_light_and_dark_colors self.color_depth = color_depth self.cursor = cursor self.include_default_pygments_style = include_default_pygments_style self.rprompt = rprompt self.multiline = multiline self.prompt_continuation = prompt_continuation self.wrap_lines = wrap_lines self.enable_history_search = enable_history_search self.search_ignore_case = search_ignore_case self.complete_while_typing = complete_while_typing self.validate_while_typing = validate_while_typing self.complete_style = complete_style self.mouse_support = mouse_support self.auto_suggest = auto_suggest self.clipboard = clipboard self.validator = validator self.refresh_interval = refresh_interval self.input_processors = input_processors self.placeholder = placeholder self.enable_system_prompt = enable_system_prompt self.enable_suspend = enable_suspend self.enable_open_in_editor = enable_open_in_editor self.reserve_space_for_menu = reserve_space_for_menu self.tempfile_suffix = tempfile_suffix self.tempfile = tempfile self.show_frame = show_frame self.interrupt_exception = interrupt_exception self.eof_exception = eof_exception # Create buffers, layout and Application. self.history = history self.default_buffer = self._create_default_buffer() self.search_buffer = self._create_search_buffer() self.layout = self._create_layout() self.app = self._create_application(editing_mode, erase_when_done) def _dyncond(self, attr_name: str) -> Condition: """ Dynamically take this setting from this 'PromptSession' class. `attr_name` represents an attribute name of this class. Its value can either be a boolean or a `Filter`. This returns something that can be used as either a `Filter` or `Filter`. """ @Condition def dynamic() -> bool: value = cast(FilterOrBool, getattr(self, attr_name)) return to_filter(value)() return dynamic def _create_default_buffer(self) -> Buffer: """ Create and return the default input buffer. """ dyncond = self._dyncond # Create buffers list. def accept(buff: Buffer) -> bool: """Accept the content of the default buffer. This is called when the validation succeeds.""" cast(Application[str], get_app()).exit( result=buff.document.text, style="class:accepted" ) return True # Keep text, we call 'reset' later on. return Buffer( name=DEFAULT_BUFFER, # Make sure that complete_while_typing is disabled when # enable_history_search is enabled. (First convert to Filter, # to avoid doing bitwise operations on bool objects.) complete_while_typing=Condition( lambda: ( is_true(self.complete_while_typing) and not is_true(self.enable_history_search) and not self.complete_style == CompleteStyle.READLINE_LIKE ) ), validate_while_typing=dyncond("validate_while_typing"), enable_history_search=dyncond("enable_history_search"), validator=DynamicValidator(lambda: self.validator), completer=DynamicCompleter( lambda: ( ThreadedCompleter(self.completer) if self.complete_in_thread and self.completer else self.completer ) ), history=self.history, auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), accept_handler=accept, tempfile_suffix=lambda: to_str(self.tempfile_suffix or ""), tempfile=lambda: to_str(self.tempfile or ""), ) def _create_search_buffer(self) -> Buffer: return Buffer(name=SEARCH_BUFFER) def _create_layout(self) -> Layout: """ Create `Layout` for this prompt. """ dyncond = self._dyncond # Create functions that will dynamically split the prompt. (If we have # a multiline prompt.) ( has_before_fragments, get_prompt_text_1, get_prompt_text_2, ) = _split_multiline_prompt(self._get_prompt) default_buffer = self.default_buffer search_buffer = self.search_buffer # Create processors list. @Condition def display_placeholder() -> bool: return self.placeholder is not None and self.default_buffer.text == "" all_input_processors = [ HighlightIncrementalSearchProcessor(), HighlightSelectionProcessor(), ConditionalProcessor( AppendAutoSuggestion(), has_focus(default_buffer) & ~is_done ), ConditionalProcessor(PasswordProcessor(), dyncond("is_password")), DisplayMultipleCursors(), # Users can insert processors here. DynamicProcessor(lambda: merge_processors(self.input_processors or [])), ConditionalProcessor( AfterInput(lambda: self.placeholder), filter=display_placeholder, ), ] # Create bottom toolbars. bottom_toolbar = ConditionalContainer( Window( FormattedTextControl( lambda: self.bottom_toolbar, style="class:bottom-toolbar.text" ), style="class:bottom-toolbar", dont_extend_height=True, height=Dimension(min=1), ), filter=Condition(lambda: self.bottom_toolbar is not None) & ~is_done & renderer_height_is_known, ) search_toolbar = SearchToolbar( search_buffer, ignore_case=dyncond("search_ignore_case") ) search_buffer_control = SearchBufferControl( buffer=search_buffer, input_processors=[ReverseSearchProcessor()], ignore_case=dyncond("search_ignore_case"), ) system_toolbar = SystemToolbar( enable_global_bindings=dyncond("enable_system_prompt") ) def get_search_buffer_control() -> SearchBufferControl: "Return the UIControl to be focused when searching start." if is_true(self.multiline): return search_toolbar.control else: return search_buffer_control default_buffer_control = BufferControl( buffer=default_buffer, search_buffer_control=get_search_buffer_control, input_processors=all_input_processors, include_default_input_processors=False, lexer=DynamicLexer(lambda: self.lexer), preview_search=True, ) default_buffer_window = Window( default_buffer_control, height=self._get_default_buffer_control_height, get_line_prefix=partial( self._get_line_prefix, get_prompt_text_2=get_prompt_text_2 ), wrap_lines=dyncond("wrap_lines"), ) @Condition def multi_column_complete_style() -> bool: return self.complete_style == CompleteStyle.MULTI_COLUMN # Build the layout. # The main input, with completion menus floating on top of it. main_input_container = FloatContainer( HSplit( [ ConditionalContainer( Window( FormattedTextControl(get_prompt_text_1), dont_extend_height=True, ), Condition(has_before_fragments), ), ConditionalContainer( default_buffer_window, Condition( lambda: ( get_app().layout.current_control != search_buffer_control ) ), ), ConditionalContainer( Window(search_buffer_control), Condition( lambda: ( get_app().layout.current_control == search_buffer_control ) ), ), ] ), [ # Completion menus. # NOTE: Especially the multi-column menu needs to be # transparent, because the shape is not always # rectangular due to the meta-text below the menu. Float( xcursor=True, ycursor=True, transparent=True, content=CompletionsMenu( max_height=16, scroll_offset=1, extra_filter=has_focus(default_buffer) & ~multi_column_complete_style, ), ), Float( xcursor=True, ycursor=True, transparent=True, content=MultiColumnCompletionsMenu( show_meta=True, extra_filter=has_focus(default_buffer) & multi_column_complete_style, ), ), # The right prompt. Float( right=0, top=0, hide_when_covering_content=True, content=_RPrompt(lambda: self.rprompt), ), ], ) layout = HSplit( [ # Wrap the main input in a frame, if requested. ConditionalContainer( Frame(main_input_container), filter=dyncond("show_frame"), alternative_content=main_input_container, ), ConditionalContainer(ValidationToolbar(), filter=~is_done), ConditionalContainer( system_toolbar, dyncond("enable_system_prompt") & ~is_done ), # In multiline mode, we use two toolbars for 'arg' and 'search'. ConditionalContainer( Window(FormattedTextControl(self._get_arg_text), height=1), dyncond("multiline") & has_arg, ), ConditionalContainer(search_toolbar, dyncond("multiline") & ~is_done), bottom_toolbar, ] ) return Layout(layout, default_buffer_window) def _create_application( self, editing_mode: EditingMode, erase_when_done: bool ) -> Application[_T]: """ Create the `Application` object. """ dyncond = self._dyncond # Default key bindings. auto_suggest_bindings = load_auto_suggest_bindings() open_in_editor_bindings = load_open_in_editor_bindings() prompt_bindings = self._create_prompt_bindings() # Create application application: Application[_T] = Application( layout=self.layout, style=DynamicStyle(lambda: self.style), style_transformation=merge_style_transformations( [ DynamicStyleTransformation(lambda: self.style_transformation), ConditionalStyleTransformation( SwapLightAndDarkStyleTransformation(), dyncond("swap_light_and_dark_colors"), ), ] ), include_default_pygments_style=dyncond("include_default_pygments_style"), clipboard=DynamicClipboard(lambda: self.clipboard), key_bindings=merge_key_bindings( [ merge_key_bindings( [ auto_suggest_bindings, ConditionalKeyBindings( open_in_editor_bindings, dyncond("enable_open_in_editor") & has_focus(DEFAULT_BUFFER), ), prompt_bindings, ] ), DynamicKeyBindings(lambda: self.key_bindings), ] ), mouse_support=dyncond("mouse_support"), editing_mode=editing_mode, erase_when_done=erase_when_done, reverse_vi_search_direction=True, color_depth=lambda: self.color_depth, cursor=DynamicCursorShapeConfig(lambda: self.cursor), refresh_interval=self.refresh_interval, input=self._input, output=self._output, ) # During render time, make sure that we focus the right search control # (if we are searching). - This could be useful if people make the # 'multiline' property dynamic. """ def on_render(app): multiline = is_true(self.multiline) current_control = app.layout.current_control if multiline: if current_control == search_buffer_control: app.layout.current_control = search_toolbar.control app.invalidate() else: if current_control == search_toolbar.control: app.layout.current_control = search_buffer_control app.invalidate() app.on_render += on_render """ return application def _create_prompt_bindings(self) -> KeyBindings: """ Create the KeyBindings for a prompt application. """ kb = KeyBindings() handle = kb.add default_focused = has_focus(DEFAULT_BUFFER) @Condition def do_accept() -> bool: return not is_true(self.multiline) and self.app.layout.has_focus( DEFAULT_BUFFER ) @handle("enter", filter=do_accept & default_focused) def _accept_input(event: E) -> None: "Accept input when enter has been pressed." self.default_buffer.validate_and_handle() @Condition def readline_complete_style() -> bool: return self.complete_style == CompleteStyle.READLINE_LIKE @handle("tab", filter=readline_complete_style & default_focused) def _complete_like_readline(event: E) -> None: "Display completions (like Readline)." display_completions_like_readline(event) @handle("c-c", filter=default_focused) @handle("<sigint>") def _keyboard_interrupt(event: E) -> None: "Abort when Control-C has been pressed." event.app.exit(exception=self.interrupt_exception(), style="class:aborting") @Condition def ctrl_d_condition() -> bool: """Ctrl-D binding is only active when the default buffer is selected and empty.""" app = get_app() return ( app.current_buffer.name == DEFAULT_BUFFER and not app.current_buffer.text ) @handle("c-d", filter=ctrl_d_condition & default_focused) def _eof(event: E) -> None: "Exit when Control-D has been pressed." event.app.exit(exception=self.eof_exception(), style="class:exiting") suspend_supported = Condition(suspend_to_background_supported) @Condition def enable_suspend() -> bool: return to_filter(self.enable_suspend)() @handle("c-z", filter=suspend_supported & enable_suspend) def _suspend(event: E) -> None: """ Suspend process to background. """ event.app.suspend_to_background() return kb def prompt( self, # When any of these arguments are passed, this value is overwritten # in this PromptSession. message: AnyFormattedText | None = None, # `message` should go first, because people call it as # positional argument. *, editing_mode: EditingMode | None = None, refresh_interval: float | None = None, vi_mode: bool | None = None, lexer: Lexer | None = None, completer: Completer | None = None, complete_in_thread: bool | None = None, is_password: bool | None = None, key_bindings: KeyBindingsBase | None = None, bottom_toolbar: AnyFormattedText | None = None, style: BaseStyle | None = None, color_depth: ColorDepth | None = None, cursor: AnyCursorShapeConfig | None = None, include_default_pygments_style: FilterOrBool | None = None, style_transformation: StyleTransformation | None = None, swap_light_and_dark_colors: FilterOrBool | None = None, rprompt: AnyFormattedText | None = None, multiline: FilterOrBool | None = None, prompt_continuation: PromptContinuationText | None = None, wrap_lines: FilterOrBool | None = None, enable_history_search: FilterOrBool | None = None, search_ignore_case: FilterOrBool | None = None, complete_while_typing: FilterOrBool | None = None, validate_while_typing: FilterOrBool | None = None, complete_style: CompleteStyle | None = None, auto_suggest: AutoSuggest | None = None, validator: Validator | None = None, clipboard: Clipboard | None = None, mouse_support: FilterOrBool | None = None, input_processors: list[Processor] | None = None, placeholder: AnyFormattedText | None = None, reserve_space_for_menu: int | None = None, enable_system_prompt: FilterOrBool | None = None, enable_suspend: FilterOrBool | None = None, enable_open_in_editor: FilterOrBool | None = None, tempfile_suffix: str | Callable[[], str] | None = None, tempfile: str | Callable[[], str] | None = None, show_frame: FilterOrBool | None = None, # Following arguments are specific to the current `prompt()` call. default: str | Document = "", accept_default: bool = False, pre_run: Callable[[], None] | None = None, set_exception_handler: bool = True, handle_sigint: bool = True, in_thread: bool = False, inputhook: InputHook | None = None, ) -> _T: """ Display the prompt. The first set of arguments is a subset of the :class:`~.PromptSession` class itself. For these, passing in ``None`` will keep the current values that are active in the session. Passing in a value will set the attribute for the session, which means that it applies to the current, but also to the next prompts. Note that in order to erase a ``Completer``, ``Validator`` or ``AutoSuggest``, you can't use ``None``. Instead pass in a ``DummyCompleter``, ``DummyValidator`` or ``DummyAutoSuggest`` instance respectively. For a ``Lexer`` you can pass in an empty ``SimpleLexer``. Additional arguments, specific for this prompt: :param default: The default input text to be shown. (This can be edited by the user). :param accept_default: When `True`, automatically accept the default value without allowing the user to edit the input. :param pre_run: Callable, called at the start of `Application.run`. :param in_thread: Run the prompt in a background thread; block the current thread. This avoids interference with an event loop in the current thread. Like `Application.run(in_thread=True)`. This method will raise ``KeyboardInterrupt`` when control-c has been pressed (for abort) and ``EOFError`` when control-d has been pressed (for exit). """ # NOTE: We used to create a backup of the PromptSession attributes and # restore them after exiting the prompt. This code has been # removed, because it was confusing and didn't really serve a use # case. (People were changing `Application.editing_mode` # dynamically and surprised that it was reset after every call.) # NOTE 2: YES, this is a lot of repeation below... # However, it is a very convenient for a user to accept all # these parameters in this `prompt` method as well. We could # use `locals()` and `setattr` to avoid the repetition, but # then we loose the advantage of mypy and pyflakes to be able # to verify the code. if message is not None: self.message = message if editing_mode is not None: self.editing_mode = editing_mode if refresh_interval is not None: self.refresh_interval = refresh_interval if vi_mode: self.editing_mode = EditingMode.VI if lexer is not None: self.lexer = lexer if completer is not None: self.completer = completer if complete_in_thread is not None: self.complete_in_thread = complete_in_thread if is_password is not None: self.is_password = is_password if key_bindings is not None: self.key_bindings = key_bindings if bottom_toolbar is not None: self.bottom_toolbar = bottom_toolbar if style is not None: self.style = style if color_depth is not None: self.color_depth = color_depth if cursor is not None: self.cursor = cursor if include_default_pygments_style is not None: self.include_default_pygments_style = include_default_pygments_style if style_transformation is not None: self.style_transformation = style_transformation if swap_light_and_dark_colors is not None: self.swap_light_and_dark_colors = swap_light_and_dark_colors if rprompt is not None: self.rprompt = rprompt if multiline is not None: self.multiline = multiline if prompt_continuation is not None: self.prompt_continuation = prompt_continuation if wrap_lines is not None: self.wrap_lines = wrap_lines if enable_history_search is not None: self.enable_history_search = enable_history_search if search_ignore_case is not None: self.search_ignore_case = search_ignore_case if complete_while_typing is not None: self.complete_while_typing = complete_while_typing if validate_while_typing is not None: self.validate_while_typing = validate_while_typing if complete_style is not None: self.complete_style = complete_style if auto_suggest is not None: self.auto_suggest = auto_suggest if validator is not None: self.validator = validator if clipboard is not None: self.clipboard = clipboard if mouse_support is not None: self.mouse_support = mouse_support if input_processors is not None: self.input_processors = input_processors if placeholder is not None: self.placeholder = placeholder if reserve_space_for_menu is not None: self.reserve_space_for_menu = reserve_space_for_menu if enable_system_prompt is not None: self.enable_system_prompt = enable_system_prompt if enable_suspend is not None: self.enable_suspend = enable_suspend if enable_open_in_editor is not None: self.enable_open_in_editor = enable_open_in_editor if tempfile_suffix is not None: self.tempfile_suffix = tempfile_suffix if tempfile is not None: self.tempfile = tempfile if show_frame is not None: self.show_frame = show_frame self._add_pre_run_callables(pre_run, accept_default) self.default_buffer.reset( default if isinstance(default, Document) else Document(default) ) self.app.refresh_interval = self.refresh_interval # This is not reactive. # If we are using the default output, and have a dumb terminal. Use the # dumb prompt. if self._output is None and is_dumb_terminal(): with self._dumb_prompt(self.message) as dump_app: return dump_app.run(in_thread=in_thread, handle_sigint=handle_sigint) return self.app.run( set_exception_handler=set_exception_handler, in_thread=in_thread, handle_sigint=handle_sigint, inputhook=inputhook, ) @contextmanager def _dumb_prompt(self, message: AnyFormattedText = "") -> Iterator[Application[_T]]: """ Create prompt `Application` for prompt function for dumb terminals. Dumb terminals have minimum rendering capabilities. We can only print text to the screen. We can't use colors, and we can't do cursor movements. The Emacs inferior shell is an example of a dumb terminal. We will show the prompt, and wait for the input. We still handle arrow keys, and all custom key bindings, but we don't really render the cursor movements. Instead we only print the typed character that's right before the cursor. """ # Send prompt to output. self.output.write(fragment_list_to_text(to_formatted_text(self.message))) self.output.flush() # Key bindings for the dumb prompt: mostly the same as the full prompt. key_bindings: KeyBindingsBase = self._create_prompt_bindings() if self.key_bindings: key_bindings = merge_key_bindings([self.key_bindings, key_bindings]) # Create and run application. application = cast( Application[_T], Application( input=self.input, output=DummyOutput(), layout=self.layout, key_bindings=key_bindings, ), ) def on_text_changed(_: object) -> None: self.output.write(self.default_buffer.document.text_before_cursor[-1:]) self.output.flush() self.default_buffer.on_text_changed += on_text_changed try: yield application finally: # Render line ending. self.output.write("\r\n") self.output.flush() self.default_buffer.on_text_changed -= on_text_changed async def prompt_async( self, # When any of these arguments are passed, this value is overwritten # in this PromptSession. message: AnyFormattedText | None = None, # `message` should go first, because people call it as # positional argument. *, editing_mode: EditingMode | None = None, refresh_interval: float | None = None, vi_mode: bool | None = None, lexer: Lexer | None = None, completer: Completer | None = None, complete_in_thread: bool | None = None, is_password: bool | None = None, key_bindings: KeyBindingsBase | None = None, bottom_toolbar: AnyFormattedText | None = None, style: BaseStyle | None = None, color_depth: ColorDepth | None = None, cursor: CursorShapeConfig | None = None, include_default_pygments_style: FilterOrBool | None = None, style_transformation: StyleTransformation | None = None, swap_light_and_dark_colors: FilterOrBool | None = None, rprompt: AnyFormattedText | None = None, multiline: FilterOrBool | None = None, prompt_continuation: PromptContinuationText | None = None, wrap_lines: FilterOrBool | None = None, enable_history_search: FilterOrBool | None = None, search_ignore_case: FilterOrBool | None = None, complete_while_typing: FilterOrBool | None = None, validate_while_typing: FilterOrBool | None = None, complete_style: CompleteStyle | None = None, auto_suggest: AutoSuggest | None = None, validator: Validator | None = None, clipboard: Clipboard | None = None, mouse_support: FilterOrBool | None = None, input_processors: list[Processor] | None = None, placeholder: AnyFormattedText | None = None, reserve_space_for_menu: int | None = None, enable_system_prompt: FilterOrBool | None = None, enable_suspend: FilterOrBool | None = None, enable_open_in_editor: FilterOrBool | None = None, tempfile_suffix: str | Callable[[], str] | None = None, tempfile: str | Callable[[], str] | None = None, show_frame: FilterOrBool = False, # Following arguments are specific to the current `prompt()` call. default: str | Document = "", accept_default: bool = False, pre_run: Callable[[], None] | None = None, set_exception_handler: bool = True, handle_sigint: bool = True, ) -> _T: if message is not None: self.message = message if editing_mode is not None: self.editing_mode = editing_mode if refresh_interval is not None: self.refresh_interval = refresh_interval if vi_mode: self.editing_mode = EditingMode.VI if lexer is not None: self.lexer = lexer if completer is not None: self.completer = completer if complete_in_thread is not None: self.complete_in_thread = complete_in_thread if is_password is not None: self.is_password = is_password if key_bindings is not None: self.key_bindings = key_bindings if bottom_toolbar is not None: self.bottom_toolbar = bottom_toolbar if style is not None: self.style = style if color_depth is not None: self.color_depth = color_depth if cursor is not None: self.cursor = cursor if include_default_pygments_style is not None: self.include_default_pygments_style = include_default_pygments_style if style_transformation is not None: self.style_transformation = style_transformation if swap_light_and_dark_colors is not None: self.swap_light_and_dark_colors = swap_light_and_dark_colors if rprompt is not None: self.rprompt = rprompt if multiline is not None: self.multiline = multiline if prompt_continuation is not None: self.prompt_continuation = prompt_continuation if wrap_lines is not None: self.wrap_lines = wrap_lines if enable_history_search is not None: self.enable_history_search = enable_history_search if search_ignore_case is not None: self.search_ignore_case = search_ignore_case if complete_while_typing is not None: self.complete_while_typing = complete_while_typing if validate_while_typing is not None: self.validate_while_typing = validate_while_typing if complete_style is not None: self.complete_style = complete_style if auto_suggest is not None: self.auto_suggest = auto_suggest if validator is not None: self.validator = validator if clipboard is not None: self.clipboard = clipboard if mouse_support is not None: self.mouse_support = mouse_support if input_processors is not None: self.input_processors = input_processors if placeholder is not None: self.placeholder = placeholder if reserve_space_for_menu is not None: self.reserve_space_for_menu = reserve_space_for_menu if enable_system_prompt is not None: self.enable_system_prompt = enable_system_prompt if enable_suspend is not None: self.enable_suspend = enable_suspend if enable_open_in_editor is not None: self.enable_open_in_editor = enable_open_in_editor if tempfile_suffix is not None: self.tempfile_suffix = tempfile_suffix if tempfile is not None: self.tempfile = tempfile if show_frame is not None: self.show_frame = show_frame self._add_pre_run_callables(pre_run, accept_default) self.default_buffer.reset( default if isinstance(default, Document) else Document(default) ) self.app.refresh_interval = self.refresh_interval # This is not reactive. # If we are using the default output, and have a dumb terminal. Use the # dumb prompt. if self._output is None and is_dumb_terminal(): with self._dumb_prompt(self.message) as dump_app: return await dump_app.run_async(handle_sigint=handle_sigint) return await self.app.run_async( set_exception_handler=set_exception_handler, handle_sigint=handle_sigint ) def _add_pre_run_callables( self, pre_run: Callable[[], None] | None, accept_default: bool ) -> None: def pre_run2() -> None: if pre_run: pre_run() if accept_default: # Validate and handle input. We use `call_from_executor` in # order to run it "soon" (during the next iteration of the # event loop), instead of right now. Otherwise, it won't # display the default value. get_running_loop().call_soon(self.default_buffer.validate_and_handle) self.app.pre_run_callables.append(pre_run2) @property def editing_mode(self) -> EditingMode: return self.app.editing_mode @editing_mode.setter def editing_mode(self, value: EditingMode) -> None: self.app.editing_mode = value def _get_default_buffer_control_height(self) -> Dimension: # If there is an autocompletion menu to be shown, make sure that our # layout has at least a minimal height in order to display it. if ( self.completer is not None and self.complete_style != CompleteStyle.READLINE_LIKE ): space = self.reserve_space_for_menu else: space = 0 if space and not get_app().is_done: buff = self.default_buffer # Reserve the space, either when there are completions, or when # `complete_while_typing` is true and we expect completions very # soon. if buff.complete_while_typing() or buff.complete_state is not None: return Dimension(min=space) return Dimension() def _get_prompt(self) -> StyleAndTextTuples: return to_formatted_text(self.message, style="class:prompt") def _get_continuation( self, width: int, line_number: int, wrap_count: int ) -> StyleAndTextTuples: """ Insert the prompt continuation. :param width: The width that was used for the prompt. (more or less can be used.) :param line_number: :param wrap_count: Amount of times that the line has been wrapped. """ prompt_continuation = self.prompt_continuation if callable(prompt_continuation): continuation: AnyFormattedText = prompt_continuation( width, line_number, wrap_count ) else: continuation = prompt_continuation # When the continuation prompt is not given, choose the same width as # the actual prompt. if continuation is None and is_true(self.multiline): continuation = " " * width return to_formatted_text(continuation, style="class:prompt-continuation") def _get_line_prefix( self, line_number: int, wrap_count: int, get_prompt_text_2: _StyleAndTextTuplesCallable, ) -> StyleAndTextTuples: """ Return whatever needs to be inserted before every line. (the prompt, or a line continuation.) """ # First line: display the "arg" or the prompt. if line_number == 0 and wrap_count == 0: if not is_true(self.multiline) and get_app().key_processor.arg is not None: return self._inline_arg() else: return get_prompt_text_2() # For the next lines, display the appropriate continuation. prompt_width = get_cwidth(fragment_list_to_text(get_prompt_text_2())) return self._get_continuation(prompt_width, line_number, wrap_count) def _get_arg_text(self) -> StyleAndTextTuples: "'arg' toolbar, for in multiline mode." arg = self.app.key_processor.arg if arg is None: # Should not happen because of the `has_arg` filter in the layout. return [] if arg == "-": arg = "-1" return [("class:arg-toolbar", "Repeat: "), ("class:arg-toolbar.text", arg)] def _inline_arg(self) -> StyleAndTextTuples: "'arg' prefix, for in single line mode." app = get_app() if app.key_processor.arg is None: return [] else: arg = app.key_processor.arg return [ ("class:prompt.arg", "(arg: "), ("class:prompt.arg.text", str(arg)), ("class:prompt.arg", ") "), ] # Expose the Input and Output objects as attributes, mainly for # backward-compatibility. @property def input(self) -> Input: return self.app.input @property def output(self) -> Output: return self.app.output def prompt( message: AnyFormattedText | None = None, *, history: History | None = None, editing_mode: EditingMode | None = None, refresh_interval: float | None = None, vi_mode: bool | None = None, lexer: Lexer | None = None, completer: Completer | None = None, complete_in_thread: bool | None = None, is_password: bool | None = None, key_bindings: KeyBindingsBase | None = None, bottom_toolbar: AnyFormattedText | None = None, style: BaseStyle | None = None, color_depth: ColorDepth | None = None, cursor: AnyCursorShapeConfig = None, include_default_pygments_style: FilterOrBool | None = None, style_transformation: StyleTransformation | None = None, swap_light_and_dark_colors: FilterOrBool | None = None, rprompt: AnyFormattedText | None = None, multiline: FilterOrBool | None = None, prompt_continuation: PromptContinuationText | None = None, wrap_lines: FilterOrBool | None = None, enable_history_search: FilterOrBool | None = None, search_ignore_case: FilterOrBool | None = None, complete_while_typing: FilterOrBool | None = None, validate_while_typing: FilterOrBool | None = None, complete_style: CompleteStyle | None = None, auto_suggest: AutoSuggest | None = None, validator: Validator | None = None, clipboard: Clipboard | None = None, mouse_support: FilterOrBool | None = None, input_processors: list[Processor] | None = None, placeholder: AnyFormattedText | None = None, reserve_space_for_menu: int | None = None, enable_system_prompt: FilterOrBool | None = None, enable_suspend: FilterOrBool | None = None, enable_open_in_editor: FilterOrBool | None = None, tempfile_suffix: str | Callable[[], str] | None = None, tempfile: str | Callable[[], str] | None = None, show_frame: FilterOrBool | None = None, # Following arguments are specific to the current `prompt()` call. default: str = "", accept_default: bool = False, pre_run: Callable[[], None] | None = None, set_exception_handler: bool = True, handle_sigint: bool = True, in_thread: bool = False, inputhook: InputHook | None = None, ) -> str: """ The global `prompt` function. This will create a new `PromptSession` instance for every call. """ # The history is the only attribute that has to be passed to the # `PromptSession`, it can't be passed into the `prompt()` method. session: PromptSession[str] = PromptSession(history=history) return session.prompt( message, editing_mode=editing_mode, refresh_interval=refresh_interval, vi_mode=vi_mode, lexer=lexer, completer=completer, complete_in_thread=complete_in_thread, is_password=is_password, key_bindings=key_bindings, bottom_toolbar=bottom_toolbar, style=style, color_depth=color_depth, cursor=cursor, include_default_pygments_style=include_default_pygments_style, style_transformation=style_transformation, swap_light_and_dark_colors=swap_light_and_dark_colors, rprompt=rprompt, multiline=multiline, prompt_continuation=prompt_continuation, wrap_lines=wrap_lines, enable_history_search=enable_history_search, search_ignore_case=search_ignore_case, complete_while_typing=complete_while_typing, validate_while_typing=validate_while_typing, complete_style=complete_style, auto_suggest=auto_suggest, validator=validator, clipboard=clipboard, mouse_support=mouse_support, input_processors=input_processors, placeholder=placeholder, reserve_space_for_menu=reserve_space_for_menu, enable_system_prompt=enable_system_prompt, enable_suspend=enable_suspend, enable_open_in_editor=enable_open_in_editor, tempfile_suffix=tempfile_suffix, tempfile=tempfile, show_frame=show_frame, default=default, accept_default=accept_default, pre_run=pre_run, set_exception_handler=set_exception_handler, handle_sigint=handle_sigint, in_thread=in_thread, inputhook=inputhook, ) prompt.__doc__ = PromptSession.prompt.__doc__ def create_confirm_session( message: AnyFormattedText, suffix: str = " (y/n) " ) -> PromptSession[bool]: """ Create a `PromptSession` object for the 'confirm' function. """ bindings = KeyBindings() @bindings.add("y") @bindings.add("Y") def yes(event: E) -> None: session.default_buffer.text = "y" event.app.exit(result=True) @bindings.add("n") @bindings.add("N") def no(event: E) -> None: session.default_buffer.text = "n" event.app.exit(result=False) @bindings.add(Keys.Any) def _(event: E) -> None: "Disallow inserting other text." pass complete_message = merge_formatted_text([message, suffix]) session: PromptSession[bool] = PromptSession( complete_message, key_bindings=bindings ) return session def confirm(message: AnyFormattedText = "Confirm?", suffix: str = " (y/n) ") -> bool: """ Display a confirmation prompt that returns True/False. """ session = create_confirm_session(message, suffix) return session.prompt() ================================================ FILE: src/prompt_toolkit/shortcuts/utils.py ================================================ from __future__ import annotations from asyncio.events import AbstractEventLoop from typing import TYPE_CHECKING, Any, TextIO from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app_or_none, get_app_session from prompt_toolkit.application.run_in_terminal import run_in_terminal from prompt_toolkit.formatted_text import ( FormattedText, StyleAndTextTuples, to_formatted_text, ) from prompt_toolkit.input import DummyInput from prompt_toolkit.layout import Layout from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.output.defaults import create_output from prompt_toolkit.renderer import ( print_formatted_text as renderer_print_formatted_text, ) from prompt_toolkit.styles import ( BaseStyle, StyleTransformation, default_pygments_style, default_ui_style, merge_styles, ) if TYPE_CHECKING: from prompt_toolkit.layout.containers import AnyContainer __all__ = [ "print_formatted_text", "print_container", "clear", "set_title", "clear_title", ] def print_formatted_text( *values: Any, sep: str = " ", end: str = "\n", file: TextIO | None = None, flush: bool = False, style: BaseStyle | None = None, output: Output | None = None, color_depth: ColorDepth | None = None, style_transformation: StyleTransformation | None = None, include_default_pygments_style: bool = True, ) -> None: """ :: print_formatted_text(*values, sep=' ', end='\\n', file=None, flush=False, style=None, output=None) Print text to stdout. This is supposed to be compatible with Python's print function, but supports printing of formatted text. You can pass a :class:`~prompt_toolkit.formatted_text.FormattedText`, :class:`~prompt_toolkit.formatted_text.HTML` or :class:`~prompt_toolkit.formatted_text.ANSI` object to print formatted text. * Print HTML as follows:: print_formatted_text(HTML('<i>Some italic text</i> <ansired>This is red!</ansired>')) style = Style.from_dict({ 'hello': '#ff0066', 'world': '#884444 italic', }) print_formatted_text(HTML('<hello>Hello</hello> <world>world</world>!'), style=style) * Print a list of (style_str, text) tuples in the given style to the output. E.g.:: style = Style.from_dict({ 'hello': '#ff0066', 'world': '#884444 italic', }) fragments = FormattedText([ ('class:hello', 'Hello'), ('class:world', 'World'), ]) print_formatted_text(fragments, style=style) If you want to print a list of Pygments tokens, wrap it in :class:`~prompt_toolkit.formatted_text.PygmentsTokens` to do the conversion. If a prompt_toolkit `Application` is currently running, this will always print above the application or prompt (similar to `patch_stdout`). So, `print_formatted_text` will erase the current application, print the text, and render the application again. :param values: Any kind of printable object, or formatted string. :param sep: String inserted between values, default a space. :param end: String appended after the last value, default a newline. :param style: :class:`.Style` instance for the color scheme. :param include_default_pygments_style: `bool`. Include the default Pygments style when set to `True` (the default). """ assert not (output and file) # Create Output object. if output is None: if file: output = create_output(stdout=file) else: output = get_app_session().output assert isinstance(output, Output) # Get color depth. color_depth = color_depth or output.get_default_color_depth() # Merges values. def to_text(val: Any) -> StyleAndTextTuples: # Normal lists which are not instances of `FormattedText` are # considered plain text. if isinstance(val, list) and not isinstance(val, FormattedText): return to_formatted_text(f"{val}") return to_formatted_text(val, auto_convert=True) fragments = [] for i, value in enumerate(values): fragments.extend(to_text(value)) if sep and i != len(values) - 1: fragments.extend(to_text(sep)) fragments.extend(to_text(end)) # Print output. def render() -> None: assert isinstance(output, Output) renderer_print_formatted_text( output, fragments, _create_merged_style( style, include_default_pygments_style=include_default_pygments_style ), color_depth=color_depth, style_transformation=style_transformation, ) # Flush the output stream. if flush: output.flush() # If an application is running, print above the app. This does not require # `patch_stdout`. loop: AbstractEventLoop | None = None app = get_app_or_none() if app is not None: loop = app.loop if loop is not None: loop.call_soon_threadsafe(lambda: run_in_terminal(render)) else: render() def print_container( container: AnyContainer, file: TextIO | None = None, style: BaseStyle | None = None, include_default_pygments_style: bool = True, ) -> None: """ Print any layout to the output in a non-interactive way. Example usage:: from prompt_toolkit.widgets import Frame, TextArea print_container( Frame(TextArea(text='Hello world!'))) """ if file: output = create_output(stdout=file) else: output = get_app_session().output app: Application[None] = Application( layout=Layout(container=container), output=output, # `DummyInput` will cause the application to terminate immediately. input=DummyInput(), style=_create_merged_style( style, include_default_pygments_style=include_default_pygments_style ), ) try: app.run(in_thread=True) except EOFError: pass def _create_merged_style( style: BaseStyle | None, include_default_pygments_style: bool ) -> BaseStyle: """ Merge user defined style with built-in style. """ styles = [default_ui_style()] if include_default_pygments_style: styles.append(default_pygments_style()) if style: styles.append(style) return merge_styles(styles) def clear() -> None: """ Clear the screen. """ output = get_app_session().output output.erase_screen() output.cursor_goto(0, 0) output.flush() def set_title(text: str) -> None: """ Set the terminal title. """ output = get_app_session().output output.set_title(text) def clear_title() -> None: """ Erase the current title. """ set_title("") ================================================ FILE: src/prompt_toolkit/styles/__init__.py ================================================ """ Styling for prompt_toolkit applications. """ from __future__ import annotations from .base import ( ANSI_COLOR_NAMES, DEFAULT_ATTRS, Attrs, BaseStyle, DummyStyle, DynamicStyle, ) from .defaults import default_pygments_style, default_ui_style from .named_colors import NAMED_COLORS from .pygments import ( pygments_token_to_classname, style_from_pygments_cls, style_from_pygments_dict, ) from .style import Priority, Style, merge_styles, parse_color from .style_transformation import ( AdjustBrightnessStyleTransformation, ConditionalStyleTransformation, DummyStyleTransformation, DynamicStyleTransformation, ReverseStyleTransformation, SetDefaultColorStyleTransformation, StyleTransformation, SwapLightAndDarkStyleTransformation, merge_style_transformations, ) __all__ = [ # Base. "Attrs", "DEFAULT_ATTRS", "ANSI_COLOR_NAMES", "BaseStyle", "DummyStyle", "DynamicStyle", # Defaults. "default_ui_style", "default_pygments_style", # Style. "Style", "Priority", "merge_styles", "parse_color", # Style transformation. "StyleTransformation", "SwapLightAndDarkStyleTransformation", "ReverseStyleTransformation", "SetDefaultColorStyleTransformation", "AdjustBrightnessStyleTransformation", "DummyStyleTransformation", "ConditionalStyleTransformation", "DynamicStyleTransformation", "merge_style_transformations", # Pygments. "style_from_pygments_cls", "style_from_pygments_dict", "pygments_token_to_classname", # Named colors. "NAMED_COLORS", ] ================================================ FILE: src/prompt_toolkit/styles/base.py ================================================ """ The base classes for the styling. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable, Hashable from typing import NamedTuple __all__ = [ "Attrs", "DEFAULT_ATTRS", "ANSI_COLOR_NAMES", "ANSI_COLOR_NAMES_ALIASES", "BaseStyle", "DummyStyle", "DynamicStyle", ] #: Style attributes. class Attrs(NamedTuple): color: str | None bgcolor: str | None bold: bool | None underline: bool | None strike: bool | None italic: bool | None blink: bool | None reverse: bool | None hidden: bool | None dim: bool | None """ :param color: Hexadecimal string. E.g. '000000' or Ansi color name: e.g. 'ansiblue' :param bgcolor: Hexadecimal string. E.g. 'ffffff' or Ansi color name: e.g. 'ansired' :param bold: Boolean :param underline: Boolean :param strike: Boolean :param italic: Boolean :param blink: Boolean :param reverse: Boolean :param hidden: Boolean :param dim: Boolean """ #: The default `Attrs`. DEFAULT_ATTRS = Attrs( color="", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) #: ``Attrs.bgcolor/fgcolor`` can be in either 'ffffff' format, or can be any of #: the following in case we want to take colors from the 8/16 color palette. #: Usually, in that case, the terminal application allows to configure the RGB #: values for these names. #: ISO 6429 colors ANSI_COLOR_NAMES = [ "ansidefault", # Low intensity, dark. (One or two components 0x80, the other 0x00.) "ansiblack", "ansired", "ansigreen", "ansiyellow", "ansiblue", "ansimagenta", "ansicyan", "ansigray", # High intensity, bright. (One or two components 0xff, the other 0x00. Not supported everywhere.) "ansibrightblack", "ansibrightred", "ansibrightgreen", "ansibrightyellow", "ansibrightblue", "ansibrightmagenta", "ansibrightcyan", "ansiwhite", ] # People don't use the same ANSI color names everywhere. In prompt_toolkit 1.0 # we used some unconventional names (which were contributed like that to # Pygments). This is fixed now, but we still support the old names. # The table below maps the old aliases to the current names. ANSI_COLOR_NAMES_ALIASES: dict[str, str] = { "ansidarkgray": "ansibrightblack", "ansiteal": "ansicyan", "ansiturquoise": "ansibrightcyan", "ansibrown": "ansiyellow", "ansipurple": "ansimagenta", "ansifuchsia": "ansibrightmagenta", "ansilightgray": "ansigray", "ansidarkred": "ansired", "ansidarkgreen": "ansigreen", "ansidarkblue": "ansiblue", } assert set(ANSI_COLOR_NAMES_ALIASES.values()).issubset(set(ANSI_COLOR_NAMES)) assert not (set(ANSI_COLOR_NAMES_ALIASES.keys()) & set(ANSI_COLOR_NAMES)) class BaseStyle(metaclass=ABCMeta): """ Abstract base class for prompt_toolkit styles. """ @abstractmethod def get_attrs_for_style_str( self, style_str: str, default: Attrs = DEFAULT_ATTRS ) -> Attrs: """ Return :class:`.Attrs` for the given style string. :param style_str: The style string. This can contain inline styling as well as classnames (e.g. "class:title"). :param default: `Attrs` to be used if no styling was defined. """ @property @abstractmethod def style_rules(self) -> list[tuple[str, str]]: """ The list of style rules, used to create this style. (Required for `DynamicStyle` and `_MergedStyle` to work.) """ return [] @abstractmethod def invalidation_hash(self) -> Hashable: """ Invalidation hash for the style. When this changes over time, the renderer knows that something in the style changed, and that everything has to be redrawn. """ class DummyStyle(BaseStyle): """ A style that doesn't style anything. """ def get_attrs_for_style_str( self, style_str: str, default: Attrs = DEFAULT_ATTRS ) -> Attrs: return default def invalidation_hash(self) -> Hashable: return 1 # Always the same value. @property def style_rules(self) -> list[tuple[str, str]]: return [] class DynamicStyle(BaseStyle): """ Style class that can dynamically returns an other Style. :param get_style: Callable that returns a :class:`.Style` instance. """ def __init__(self, get_style: Callable[[], BaseStyle | None]): self.get_style = get_style self._dummy = DummyStyle() def get_attrs_for_style_str( self, style_str: str, default: Attrs = DEFAULT_ATTRS ) -> Attrs: style = self.get_style() or self._dummy return style.get_attrs_for_style_str(style_str, default) def invalidation_hash(self) -> Hashable: return (self.get_style() or self._dummy).invalidation_hash() @property def style_rules(self) -> list[tuple[str, str]]: return (self.get_style() or self._dummy).style_rules ================================================ FILE: src/prompt_toolkit/styles/defaults.py ================================================ """ The default styling. """ from __future__ import annotations from prompt_toolkit.cache import memoized from .base import ANSI_COLOR_NAMES, BaseStyle from .named_colors import NAMED_COLORS from .style import Style, merge_styles __all__ = [ "default_ui_style", "default_pygments_style", ] #: Default styling. Mapping from classnames to their style definition. PROMPT_TOOLKIT_STYLE = [ # Highlighting of search matches in document. ("search", "bg:ansibrightyellow ansiblack"), ("search.current", ""), # Incremental search. ("incsearch", ""), ("incsearch.current", "reverse"), # Highlighting of select text in document. ("selected", "reverse"), ("cursor-column", "bg:#dddddd"), ("cursor-line", "underline"), ("color-column", "bg:#ccaacc"), # Highlighting of matching brackets. ("matching-bracket", ""), ("matching-bracket.other", "#000000 bg:#aacccc"), ("matching-bracket.cursor", "#ff8888 bg:#880000"), # Styling of other cursors, in case of block editing. ("multiple-cursors", "#000000 bg:#ccccaa"), # Line numbers. ("line-number", "#888888"), ("line-number.current", "bold"), ("tilde", "#8888ff"), # Default prompt. ("prompt", ""), ("prompt.arg", "noinherit"), ("prompt.arg.text", ""), ("prompt.search", "noinherit"), ("prompt.search.text", ""), # Search toolbar. ("search-toolbar", "bold"), ("search-toolbar.text", "nobold"), # System toolbar ("system-toolbar", "bold"), ("system-toolbar.text", "nobold"), # "arg" toolbar. ("arg-toolbar", "bold"), ("arg-toolbar.text", "nobold"), # Validation toolbar. ("validation-toolbar", "bg:#550000 #ffffff"), ("window-too-small", "bg:#550000 #ffffff"), # Completions toolbar. ("completion-toolbar", "bg:#bbbbbb #000000"), ("completion-toolbar.arrow", "bg:#bbbbbb #000000 bold"), ("completion-toolbar.completion", "bg:#bbbbbb #000000"), ("completion-toolbar.completion.current", "bg:#444444 #ffffff"), # Completions menu. ("completion-menu", "bg:#bbbbbb #000000"), ("completion-menu.completion", ""), # (Note: for the current completion, we use 'reverse' on top of fg/bg # colors. This is to have proper rendering with NO_COLOR=1). ("completion-menu.completion.current", "fg:#888888 bg:#ffffff reverse"), ("completion-menu.meta.completion", "bg:#999999 #000000"), ("completion-menu.meta.completion.current", "bg:#aaaaaa #000000"), ("completion-menu.multi-column-meta", "bg:#aaaaaa #000000"), # Fuzzy matches in completion menu (for FuzzyCompleter). ("completion-menu.completion fuzzymatch.outside", "fg:#444444"), ("completion-menu.completion fuzzymatch.inside", "bold"), ("completion-menu.completion fuzzymatch.inside.character", "underline"), ("completion-menu.completion.current fuzzymatch.outside", "fg:default"), ("completion-menu.completion.current fuzzymatch.inside", "nobold"), # Styling of readline-like completions. ("readline-like-completions", ""), ("readline-like-completions.completion", ""), ("readline-like-completions.completion fuzzymatch.outside", "#888888"), ("readline-like-completions.completion fuzzymatch.inside", ""), ("readline-like-completions.completion fuzzymatch.inside.character", "underline"), # Scrollbars. ("scrollbar.background", "bg:#aaaaaa"), ("scrollbar.button", "bg:#444444"), ("scrollbar.arrow", "noinherit bold"), # Start/end of scrollbars. Adding 'underline' here provides a nice little # detail to the progress bar, but it doesn't look good on all terminals. # ('scrollbar.start', 'underline #ffffff'), # ('scrollbar.end', 'underline #000000'), # Auto suggestion text. ("auto-suggestion", "#666666"), # Trailing whitespace and tabs. ("trailing-whitespace", "#999999"), ("tab", "#999999"), # When Control-C/D has been pressed. Grayed. ("aborting", "#888888 bg:default noreverse noitalic nounderline noblink"), ("exiting", "#888888 bg:default noreverse noitalic nounderline noblink"), # Entering a Vi digraph. ("digraph", "#4444ff"), # Control characters, like ^C, ^X. ("control-character", "ansiblue"), # Non-breaking space. ("nbsp", "underline ansiyellow"), # Default styling of HTML elements. ("i", "italic"), ("u", "underline"), ("s", "strike"), ("b", "bold"), ("em", "italic"), ("strong", "bold"), ("del", "strike"), ("hidden", "hidden"), # It should be possible to use the style names in HTML. # <reverse>...</reverse> or <noreverse>...</noreverse>. ("italic", "italic"), ("underline", "underline"), ("strike", "strike"), ("bold", "bold"), ("reverse", "reverse"), ("noitalic", "noitalic"), ("nounderline", "nounderline"), ("nostrike", "nostrike"), ("nobold", "nobold"), ("noreverse", "noreverse"), # Prompt bottom toolbar ("bottom-toolbar", "reverse"), ] # Style that will turn for instance the class 'red' into 'red'. COLORS_STYLE = [(name, "fg:" + name) for name in ANSI_COLOR_NAMES] + [ (name.lower(), "fg:" + name) for name in NAMED_COLORS ] WIDGETS_STYLE = [ # Dialog windows. ("dialog", "bg:#4444ff"), ("dialog.body", "bg:#ffffff #000000"), ("dialog.body text-area", "bg:#cccccc"), ("dialog.body text-area last-line", "underline"), ("dialog frame.label", "#ff0000 bold"), # Scrollbars in dialogs. ("dialog.body scrollbar.background", ""), ("dialog.body scrollbar.button", "bg:#000000"), ("dialog.body scrollbar.arrow", ""), ("dialog.body scrollbar.start", "nounderline"), ("dialog.body scrollbar.end", "nounderline"), # Buttons. ("button", ""), ("button.arrow", "bold"), ("button.focused", "bg:#aa0000 #ffffff"), # Menu bars. ("menu-bar", "bg:#aaaaaa #000000"), ("menu-bar.selected-item", "bg:#ffffff #000000"), ("menu", "bg:#888888 #ffffff"), ("menu.border", "#aaaaaa"), ("menu.border shadow", "#444444"), # Shadows. ("dialog shadow", "bg:#000088"), ("dialog.body shadow", "bg:#aaaaaa"), ("progress-bar", "bg:#000088"), ("progress-bar.used", "bg:#ff0000"), ] # The default Pygments style, include this by default in case a Pygments lexer # is used. PYGMENTS_DEFAULT_STYLE = { "pygments.whitespace": "#bbbbbb", "pygments.comment": "italic #408080", "pygments.comment.preproc": "noitalic #bc7a00", "pygments.keyword": "bold #008000", "pygments.keyword.pseudo": "nobold", "pygments.keyword.type": "nobold #b00040", "pygments.operator": "#666666", "pygments.operator.word": "bold #aa22ff", "pygments.name.builtin": "#008000", "pygments.name.function": "#0000ff", "pygments.name.class": "bold #0000ff", "pygments.name.namespace": "bold #0000ff", "pygments.name.exception": "bold #d2413a", "pygments.name.variable": "#19177c", "pygments.name.constant": "#880000", "pygments.name.label": "#a0a000", "pygments.name.entity": "bold #999999", "pygments.name.attribute": "#7d9029", "pygments.name.tag": "bold #008000", "pygments.name.decorator": "#aa22ff", # Note: In Pygments, Token.String is an alias for Token.Literal.String, # and Token.Number as an alias for Token.Literal.Number. "pygments.literal.string": "#ba2121", "pygments.literal.string.doc": "italic", "pygments.literal.string.interpol": "bold #bb6688", "pygments.literal.string.escape": "bold #bb6622", "pygments.literal.string.regex": "#bb6688", "pygments.literal.string.symbol": "#19177c", "pygments.literal.string.other": "#008000", "pygments.literal.number": "#666666", "pygments.generic.heading": "bold #000080", "pygments.generic.subheading": "bold #800080", "pygments.generic.deleted": "#a00000", "pygments.generic.inserted": "#00a000", "pygments.generic.error": "#ff0000", "pygments.generic.emph": "italic", "pygments.generic.strong": "bold", "pygments.generic.prompt": "bold #000080", "pygments.generic.output": "#888", "pygments.generic.traceback": "#04d", "pygments.error": "border:#ff0000", } @memoized() def default_ui_style() -> BaseStyle: """ Create a default `Style` object. """ return merge_styles( [ Style(PROMPT_TOOLKIT_STYLE), Style(COLORS_STYLE), Style(WIDGETS_STYLE), ] ) @memoized() def default_pygments_style() -> Style: """ Create a `Style` object that contains the default Pygments style. """ return Style.from_dict(PYGMENTS_DEFAULT_STYLE) ================================================ FILE: src/prompt_toolkit/styles/named_colors.py ================================================ """ All modern web browsers support these 140 color names. Taken from: https://www.w3schools.com/colors/colors_names.asp """ from __future__ import annotations __all__ = [ "NAMED_COLORS", ] NAMED_COLORS: dict[str, str] = { "AliceBlue": "#f0f8ff", "AntiqueWhite": "#faebd7", "Aqua": "#00ffff", "Aquamarine": "#7fffd4", "Azure": "#f0ffff", "Beige": "#f5f5dc", "Bisque": "#ffe4c4", "Black": "#000000", "BlanchedAlmond": "#ffebcd", "Blue": "#0000ff", "BlueViolet": "#8a2be2", "Brown": "#a52a2a", "BurlyWood": "#deb887", "CadetBlue": "#5f9ea0", "Chartreuse": "#7fff00", "Chocolate": "#d2691e", "Coral": "#ff7f50", "CornflowerBlue": "#6495ed", "Cornsilk": "#fff8dc", "Crimson": "#dc143c", "Cyan": "#00ffff", "DarkBlue": "#00008b", "DarkCyan": "#008b8b", "DarkGoldenRod": "#b8860b", "DarkGray": "#a9a9a9", "DarkGreen": "#006400", "DarkGrey": "#a9a9a9", "DarkKhaki": "#bdb76b", "DarkMagenta": "#8b008b", "DarkOliveGreen": "#556b2f", "DarkOrange": "#ff8c00", "DarkOrchid": "#9932cc", "DarkRed": "#8b0000", "DarkSalmon": "#e9967a", "DarkSeaGreen": "#8fbc8f", "DarkSlateBlue": "#483d8b", "DarkSlateGray": "#2f4f4f", "DarkSlateGrey": "#2f4f4f", "DarkTurquoise": "#00ced1", "DarkViolet": "#9400d3", "DeepPink": "#ff1493", "DeepSkyBlue": "#00bfff", "DimGray": "#696969", "DimGrey": "#696969", "DodgerBlue": "#1e90ff", "FireBrick": "#b22222", "FloralWhite": "#fffaf0", "ForestGreen": "#228b22", "Fuchsia": "#ff00ff", "Gainsboro": "#dcdcdc", "GhostWhite": "#f8f8ff", "Gold": "#ffd700", "GoldenRod": "#daa520", "Gray": "#808080", "Green": "#008000", "GreenYellow": "#adff2f", "Grey": "#808080", "HoneyDew": "#f0fff0", "HotPink": "#ff69b4", "IndianRed": "#cd5c5c", "Indigo": "#4b0082", "Ivory": "#fffff0", "Khaki": "#f0e68c", "Lavender": "#e6e6fa", "LavenderBlush": "#fff0f5", "LawnGreen": "#7cfc00", "LemonChiffon": "#fffacd", "LightBlue": "#add8e6", "LightCoral": "#f08080", "LightCyan": "#e0ffff", "LightGoldenRodYellow": "#fafad2", "LightGray": "#d3d3d3", "LightGreen": "#90ee90", "LightGrey": "#d3d3d3", "LightPink": "#ffb6c1", "LightSalmon": "#ffa07a", "LightSeaGreen": "#20b2aa", "LightSkyBlue": "#87cefa", "LightSlateGray": "#778899", "LightSlateGrey": "#778899", "LightSteelBlue": "#b0c4de", "LightYellow": "#ffffe0", "Lime": "#00ff00", "LimeGreen": "#32cd32", "Linen": "#faf0e6", "Magenta": "#ff00ff", "Maroon": "#800000", "MediumAquaMarine": "#66cdaa", "MediumBlue": "#0000cd", "MediumOrchid": "#ba55d3", "MediumPurple": "#9370db", "MediumSeaGreen": "#3cb371", "MediumSlateBlue": "#7b68ee", "MediumSpringGreen": "#00fa9a", "MediumTurquoise": "#48d1cc", "MediumVioletRed": "#c71585", "MidnightBlue": "#191970", "MintCream": "#f5fffa", "MistyRose": "#ffe4e1", "Moccasin": "#ffe4b5", "NavajoWhite": "#ffdead", "Navy": "#000080", "OldLace": "#fdf5e6", "Olive": "#808000", "OliveDrab": "#6b8e23", "Orange": "#ffa500", "OrangeRed": "#ff4500", "Orchid": "#da70d6", "PaleGoldenRod": "#eee8aa", "PaleGreen": "#98fb98", "PaleTurquoise": "#afeeee", "PaleVioletRed": "#db7093", "PapayaWhip": "#ffefd5", "PeachPuff": "#ffdab9", "Peru": "#cd853f", "Pink": "#ffc0cb", "Plum": "#dda0dd", "PowderBlue": "#b0e0e6", "Purple": "#800080", "RebeccaPurple": "#663399", "Red": "#ff0000", "RosyBrown": "#bc8f8f", "RoyalBlue": "#4169e1", "SaddleBrown": "#8b4513", "Salmon": "#fa8072", "SandyBrown": "#f4a460", "SeaGreen": "#2e8b57", "SeaShell": "#fff5ee", "Sienna": "#a0522d", "Silver": "#c0c0c0", "SkyBlue": "#87ceeb", "SlateBlue": "#6a5acd", "SlateGray": "#708090", "SlateGrey": "#708090", "Snow": "#fffafa", "SpringGreen": "#00ff7f", "SteelBlue": "#4682b4", "Tan": "#d2b48c", "Teal": "#008080", "Thistle": "#d8bfd8", "Tomato": "#ff6347", "Turquoise": "#40e0d0", "Violet": "#ee82ee", "Wheat": "#f5deb3", "White": "#ffffff", "WhiteSmoke": "#f5f5f5", "Yellow": "#ffff00", "YellowGreen": "#9acd32", } ================================================ FILE: src/prompt_toolkit/styles/pygments.py ================================================ """ Adaptor for building prompt_toolkit styles, starting from a Pygments style. Usage:: from pygments.styles.tango import TangoStyle style = style_from_pygments_cls(pygments_style_cls=TangoStyle) """ from __future__ import annotations from typing import TYPE_CHECKING from .style import Style if TYPE_CHECKING: from pygments.style import Style as PygmentsStyle from pygments.token import Token __all__ = [ "style_from_pygments_cls", "style_from_pygments_dict", "pygments_token_to_classname", ] def style_from_pygments_cls(pygments_style_cls: type[PygmentsStyle]) -> Style: """ Shortcut to create a :class:`.Style` instance from a Pygments style class and a style dictionary. Example:: from prompt_toolkit.styles.from_pygments import style_from_pygments_cls from pygments.styles import get_style_by_name style = style_from_pygments_cls(get_style_by_name('monokai')) :param pygments_style_cls: Pygments style class to start from. """ # Import inline. from pygments.style import Style as PygmentsStyle assert issubclass(pygments_style_cls, PygmentsStyle) return style_from_pygments_dict(pygments_style_cls.styles) def style_from_pygments_dict(pygments_dict: dict[Token, str]) -> Style: """ Create a :class:`.Style` instance from a Pygments style dictionary. (One that maps Token objects to style strings.) """ pygments_style = [] for token, style in pygments_dict.items(): pygments_style.append((pygments_token_to_classname(token), style)) return Style(pygments_style) def pygments_token_to_classname(token: Token) -> str: """ Turn e.g. `Token.Name.Exception` into `'pygments.name.exception'`. (Our Pygments lexer will also turn the tokens that pygments produces in a prompt_toolkit list of fragments that match these styling rules.) """ parts = ("pygments",) + token return ".".join(parts).lower() ================================================ FILE: src/prompt_toolkit/styles/style.py ================================================ """ Tool for creating styles from a dictionary. """ from __future__ import annotations import itertools import re from collections.abc import Hashable from enum import Enum from typing import TypeVar from prompt_toolkit.cache import SimpleCache from .base import ( ANSI_COLOR_NAMES, ANSI_COLOR_NAMES_ALIASES, DEFAULT_ATTRS, Attrs, BaseStyle, ) from .named_colors import NAMED_COLORS __all__ = [ "Style", "parse_color", "Priority", "merge_styles", ] _named_colors_lowercase = {k.lower(): v.lstrip("#") for k, v in NAMED_COLORS.items()} def parse_color(text: str) -> str: """ Parse/validate color format. Like in Pygments, but also support the ANSI color names. (These will map to the colors of the 16 color palette.) """ # ANSI color names. if text in ANSI_COLOR_NAMES: return text if text in ANSI_COLOR_NAMES_ALIASES: return ANSI_COLOR_NAMES_ALIASES[text] # 140 named colors. try: # Replace by 'hex' value. return _named_colors_lowercase[text.lower()] except KeyError: pass # Hex codes. if text[0:1] == "#": col = text[1:] # Keep this for backwards-compatibility (Pygments does it). # I don't like the '#' prefix for named colors. if col in ANSI_COLOR_NAMES: return col elif col in ANSI_COLOR_NAMES_ALIASES: return ANSI_COLOR_NAMES_ALIASES[col] # 6 digit hex color. elif len(col) == 6: return col # 3 digit hex color. elif len(col) == 3: return col[0] * 2 + col[1] * 2 + col[2] * 2 # Default. elif text in ("", "default"): return text raise ValueError(f"Wrong color format {text!r}") # Attributes, when they are not filled in by a style. None means that we take # the value from the parent. _EMPTY_ATTRS = Attrs( color=None, bgcolor=None, bold=None, underline=None, strike=None, italic=None, blink=None, reverse=None, hidden=None, dim=None, ) def _expand_classname(classname: str) -> list[str]: """ Split a single class name at the `.` operator, and build a list of classes. E.g. 'a.b.c' becomes ['a', 'a.b', 'a.b.c'] """ result = [] parts = classname.split(".") for i in range(1, len(parts) + 1): result.append(".".join(parts[:i]).lower()) return result def _parse_style_str(style_str: str) -> Attrs: """ Take a style string, e.g. 'bg:red #88ff00 class:title' and return a `Attrs` instance. """ # Start from default Attrs. if "noinherit" in style_str: attrs = DEFAULT_ATTRS else: attrs = _EMPTY_ATTRS # Now update with the given attributes. for part in style_str.split(): if part == "noinherit": pass elif part == "bold": attrs = attrs._replace(bold=True) elif part == "nobold": attrs = attrs._replace(bold=False) elif part == "italic": attrs = attrs._replace(italic=True) elif part == "noitalic": attrs = attrs._replace(italic=False) elif part == "underline": attrs = attrs._replace(underline=True) elif part == "nounderline": attrs = attrs._replace(underline=False) elif part == "strike": attrs = attrs._replace(strike=True) elif part == "nostrike": attrs = attrs._replace(strike=False) # prompt_toolkit extensions. Not in Pygments. elif part == "blink": attrs = attrs._replace(blink=True) elif part == "noblink": attrs = attrs._replace(blink=False) elif part == "reverse": attrs = attrs._replace(reverse=True) elif part == "noreverse": attrs = attrs._replace(reverse=False) elif part == "hidden": attrs = attrs._replace(hidden=True) elif part == "nohidden": attrs = attrs._replace(hidden=False) elif part == "dim": attrs = attrs._replace(dim=True) elif part == "nodim": attrs = attrs._replace(dim=False) # Pygments properties that we ignore. elif part in ("roman", "sans", "mono"): pass elif part.startswith("border:"): pass # Ignore pieces in between square brackets. This is internal stuff. # Like '[transparent]' or '[set-cursor-position]'. elif part.startswith("[") and part.endswith("]"): pass # Colors. elif part.startswith("bg:"): attrs = attrs._replace(bgcolor=parse_color(part[3:])) elif part.startswith("fg:"): # The 'fg:' prefix is optional. attrs = attrs._replace(color=parse_color(part[3:])) else: attrs = attrs._replace(color=parse_color(part)) return attrs CLASS_NAMES_RE = re.compile(r"^[a-z0-9.\s_-]*$") # This one can't contain a comma! class Priority(Enum): """ The priority of the rules, when a style is created from a dictionary. In a `Style`, rules that are defined later will always override previous defined rules, however in a dictionary, the key order was arbitrary before Python 3.6. This means that the style could change at random between rules. We have two options: - `DICT_KEY_ORDER`: This means, iterate through the dictionary, and take the key/value pairs in order as they come. This is a good option if you have Python >3.6. Rules at the end will override rules at the beginning. - `MOST_PRECISE`: keys that are defined with most precision will get higher priority. (More precise means: more elements.) """ DICT_KEY_ORDER = "KEY_ORDER" MOST_PRECISE = "MOST_PRECISE" # We don't support Python versions older than 3.6 anymore, so we can always # depend on dictionary ordering. This is the default. default_priority = Priority.DICT_KEY_ORDER class Style(BaseStyle): """ Create a ``Style`` instance from a list of style rules. The `style_rules` is supposed to be a list of ('classnames', 'style') tuples. The classnames are a whitespace separated string of class names and the style string is just like a Pygments style definition, but with a few additions: it supports 'reverse' and 'blink'. Later rules always override previous rules. Usage:: Style([ ('title', '#ff0000 bold underline'), ('something-else', 'reverse'), ('class1 class2', 'reverse'), ]) The ``from_dict`` classmethod is similar, but takes a dictionary as input. """ def __init__(self, style_rules: list[tuple[str, str]]) -> None: class_names_and_attrs = [] # Loop through the rules in the order they were defined. # Rules that are defined later get priority. for class_names, style_str in style_rules: assert CLASS_NAMES_RE.match(class_names), repr(class_names) # The order of the class names doesn't matter. # (But the order of rules does matter.) class_names_set = frozenset(class_names.lower().split()) attrs = _parse_style_str(style_str) class_names_and_attrs.append((class_names_set, attrs)) self._style_rules = style_rules self.class_names_and_attrs = class_names_and_attrs @property def style_rules(self) -> list[tuple[str, str]]: return self._style_rules @classmethod def from_dict( cls, style_dict: dict[str, str], priority: Priority = default_priority ) -> Style: """ :param style_dict: Style dictionary. :param priority: `Priority` value. """ if priority == Priority.MOST_PRECISE: def key(item: tuple[str, str]) -> int: # Split on '.' and whitespace. Count elements. return sum(len(i.split(".")) for i in item[0].split()) return cls(sorted(style_dict.items(), key=key)) else: return cls(list(style_dict.items())) def get_attrs_for_style_str( self, style_str: str, default: Attrs = DEFAULT_ATTRS ) -> Attrs: """ Get `Attrs` for the given style string. """ list_of_attrs = [default] class_names: set[str] = set() # Apply default styling. for names, attr in self.class_names_and_attrs: if not names: list_of_attrs.append(attr) # Go from left to right through the style string. Things on the right # take precedence. for part in style_str.split(): # This part represents a class. # Do lookup of this class name in the style definition, as well # as all class combinations that we have so far. if part.startswith("class:"): # Expand all class names (comma separated list). new_class_names = [] for p in part[6:].lower().split(","): new_class_names.extend(_expand_classname(p)) for new_name in new_class_names: # Build a set of all possible class combinations to be applied. combos = set() combos.add(frozenset([new_name])) for count in range(1, len(class_names) + 1): for c2 in itertools.combinations(class_names, count): combos.add(frozenset(c2 + (new_name,))) # Apply the styles that match these class names. for names, attr in self.class_names_and_attrs: if names in combos: list_of_attrs.append(attr) class_names.add(new_name) # Process inline style. else: inline_attrs = _parse_style_str(part) list_of_attrs.append(inline_attrs) return _merge_attrs(list_of_attrs) def invalidation_hash(self) -> Hashable: return id(self.class_names_and_attrs) _T = TypeVar("_T") def _merge_attrs(list_of_attrs: list[Attrs]) -> Attrs: """ Take a list of :class:`.Attrs` instances and merge them into one. Every `Attr` in the list can override the styling of the previous one. So, the last one has highest priority. """ def _or(*values: _T) -> _T: "Take first not-None value, starting at the end." for v in values[::-1]: if v is not None: return v raise ValueError # Should not happen, there's always one non-null value. return Attrs( color=_or("", *[a.color for a in list_of_attrs]), bgcolor=_or("", *[a.bgcolor for a in list_of_attrs]), bold=_or(False, *[a.bold for a in list_of_attrs]), underline=_or(False, *[a.underline for a in list_of_attrs]), strike=_or(False, *[a.strike for a in list_of_attrs]), italic=_or(False, *[a.italic for a in list_of_attrs]), blink=_or(False, *[a.blink for a in list_of_attrs]), reverse=_or(False, *[a.reverse for a in list_of_attrs]), hidden=_or(False, *[a.hidden for a in list_of_attrs]), dim=_or(False, *[a.dim for a in list_of_attrs]), ) def merge_styles(styles: list[BaseStyle]) -> _MergedStyle: """ Merge multiple `Style` objects. """ styles = [s for s in styles if s is not None] return _MergedStyle(styles) class _MergedStyle(BaseStyle): """ Merge multiple `Style` objects into one. This is supposed to ensure consistency: if any of the given styles changes, then this style will be updated. """ # NOTE: previously, we used an algorithm where we did not generate the # combined style. Instead this was a proxy that called one style # after the other, passing the outcome of the previous style as the # default for the next one. This did not work, because that way, the # priorities like described in the `Style` class don't work. # 'class:aborted' was for instance never displayed in gray, because # the next style specified a default color for any text. (The # explicit styling of class:aborted should have taken priority, # because it was more precise.) def __init__(self, styles: list[BaseStyle]) -> None: self.styles = styles self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1) @property def _merged_style(self) -> Style: "The `Style` object that has the other styles merged together." def get() -> Style: return Style(self.style_rules) return self._style.get(self.invalidation_hash(), get) @property def style_rules(self) -> list[tuple[str, str]]: style_rules = [] for s in self.styles: style_rules.extend(s.style_rules) return style_rules def get_attrs_for_style_str( self, style_str: str, default: Attrs = DEFAULT_ATTRS ) -> Attrs: return self._merged_style.get_attrs_for_style_str(style_str, default) def invalidation_hash(self) -> Hashable: return tuple(s.invalidation_hash() for s in self.styles) ================================================ FILE: src/prompt_toolkit/styles/style_transformation.py ================================================ """ Collection of style transformations. Think of it as a kind of color post processing after the rendering is done. This could be used for instance to change the contrast/saturation; swap light and dark colors or even change certain colors for other colors. When the UI is rendered, these transformations can be applied right after the style strings are turned into `Attrs` objects that represent the actual formatting. """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable, Hashable, Sequence from colorsys import hls_to_rgb, rgb_to_hls from prompt_toolkit.cache import memoized from prompt_toolkit.filters import FilterOrBool, to_filter from prompt_toolkit.utils import AnyFloat, to_float, to_str from .base import ANSI_COLOR_NAMES, Attrs from .style import parse_color __all__ = [ "StyleTransformation", "SwapLightAndDarkStyleTransformation", "ReverseStyleTransformation", "SetDefaultColorStyleTransformation", "AdjustBrightnessStyleTransformation", "DummyStyleTransformation", "ConditionalStyleTransformation", "DynamicStyleTransformation", "merge_style_transformations", ] class StyleTransformation(metaclass=ABCMeta): """ Base class for any style transformation. """ @abstractmethod def transform_attrs(self, attrs: Attrs) -> Attrs: """ Take an `Attrs` object and return a new `Attrs` object. Remember that the color formats can be either "ansi..." or a 6 digit lowercase hexadecimal color (without '#' prefix). """ def invalidation_hash(self) -> Hashable: """ When this changes, the cache should be invalidated. """ return f"{self.__class__.__name__}-{id(self)}" class SwapLightAndDarkStyleTransformation(StyleTransformation): """ Turn dark colors into light colors and the other way around. This is meant to make color schemes that work on a dark background usable on a light background (and the other way around). Notice that this doesn't swap foreground and background like "reverse" does. It turns light green into dark green and the other way around. Foreground and background colors are considered individually. Also notice that when <reverse> is used somewhere and no colors are given in particular (like what is the default for the bottom toolbar), then this doesn't change anything. This is what makes sense, because when the 'default' color is chosen, it's what works best for the terminal, and reverse works good with that. """ def transform_attrs(self, attrs: Attrs) -> Attrs: """ Return the `Attrs` used when opposite luminosity should be used. """ # Reverse colors. attrs = attrs._replace(color=get_opposite_color(attrs.color)) attrs = attrs._replace(bgcolor=get_opposite_color(attrs.bgcolor)) return attrs class ReverseStyleTransformation(StyleTransformation): """ Swap the 'reverse' attribute. (This is still experimental.) """ def transform_attrs(self, attrs: Attrs) -> Attrs: return attrs._replace(reverse=not attrs.reverse) class SetDefaultColorStyleTransformation(StyleTransformation): """ Set default foreground/background color for output that doesn't specify anything. This is useful for overriding the terminal default colors. :param fg: Color string or callable that returns a color string for the foreground. :param bg: Like `fg`, but for the background. """ def __init__( self, fg: str | Callable[[], str], bg: str | Callable[[], str] ) -> None: self.fg = fg self.bg = bg def transform_attrs(self, attrs: Attrs) -> Attrs: if attrs.bgcolor in ("", "default"): attrs = attrs._replace(bgcolor=parse_color(to_str(self.bg))) if attrs.color in ("", "default"): attrs = attrs._replace(color=parse_color(to_str(self.fg))) return attrs def invalidation_hash(self) -> Hashable: return ( "set-default-color", to_str(self.fg), to_str(self.bg), ) class AdjustBrightnessStyleTransformation(StyleTransformation): """ Adjust the brightness to improve the rendering on either dark or light backgrounds. For dark backgrounds, it's best to increase `min_brightness`. For light backgrounds it's best to decrease `max_brightness`. Usually, only one setting is adjusted. This will only change the brightness for text that has a foreground color defined, but no background color. It works best for 256 or true color output. .. note:: Notice that there is no universal way to detect whether the application is running in a light or dark terminal. As a developer of an command line application, you'll have to make this configurable for the user. :param min_brightness: Float between 0.0 and 1.0 or a callable that returns a float. :param max_brightness: Float between 0.0 and 1.0 or a callable that returns a float. """ def __init__( self, min_brightness: AnyFloat = 0.0, max_brightness: AnyFloat = 1.0 ) -> None: self.min_brightness = min_brightness self.max_brightness = max_brightness def transform_attrs(self, attrs: Attrs) -> Attrs: min_brightness = to_float(self.min_brightness) max_brightness = to_float(self.max_brightness) assert 0 <= min_brightness <= 1 assert 0 <= max_brightness <= 1 # Don't do anything if the whole brightness range is acceptable. # This also avoids turning ansi colors into RGB sequences. if min_brightness == 0.0 and max_brightness == 1.0: return attrs # If a foreground color is given without a background color. no_background = not attrs.bgcolor or attrs.bgcolor == "default" has_fgcolor = attrs.color and attrs.color != "ansidefault" if has_fgcolor and no_background: # Calculate new RGB values. r, g, b = self._color_to_rgb(attrs.color or "") hue, brightness, saturation = rgb_to_hls(r, g, b) brightness = self._interpolate_brightness( brightness, min_brightness, max_brightness ) r, g, b = hls_to_rgb(hue, brightness, saturation) new_color = f"{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}" attrs = attrs._replace(color=new_color) return attrs def _color_to_rgb(self, color: str) -> tuple[float, float, float]: """ Parse `style.Attrs` color into RGB tuple. """ # Do RGB lookup for ANSI colors. try: from prompt_toolkit.output.vt100 import ANSI_COLORS_TO_RGB r, g, b = ANSI_COLORS_TO_RGB[color] return r / 255.0, g / 255.0, b / 255.0 except KeyError: pass # Parse RRGGBB format. return ( int(color[0:2], 16) / 255.0, int(color[2:4], 16) / 255.0, int(color[4:6], 16) / 255.0, ) # NOTE: we don't have to support named colors here. They are already # transformed into RGB values in `style.parse_color`. def _interpolate_brightness( self, value: float, min_brightness: float, max_brightness: float ) -> float: """ Map the brightness to the (min_brightness..max_brightness) range. """ return min_brightness + (max_brightness - min_brightness) * value def invalidation_hash(self) -> Hashable: return ( "adjust-brightness", to_float(self.min_brightness), to_float(self.max_brightness), ) class DummyStyleTransformation(StyleTransformation): """ Don't transform anything at all. """ def transform_attrs(self, attrs: Attrs) -> Attrs: return attrs def invalidation_hash(self) -> Hashable: # Always return the same hash for these dummy instances. return "dummy-style-transformation" class DynamicStyleTransformation(StyleTransformation): """ StyleTransformation class that can dynamically returns any `StyleTransformation`. :param get_style_transformation: Callable that returns a :class:`.StyleTransformation` instance. """ def __init__( self, get_style_transformation: Callable[[], StyleTransformation | None] ) -> None: self.get_style_transformation = get_style_transformation def transform_attrs(self, attrs: Attrs) -> Attrs: style_transformation = ( self.get_style_transformation() or DummyStyleTransformation() ) return style_transformation.transform_attrs(attrs) def invalidation_hash(self) -> Hashable: style_transformation = ( self.get_style_transformation() or DummyStyleTransformation() ) return style_transformation.invalidation_hash() class ConditionalStyleTransformation(StyleTransformation): """ Apply the style transformation depending on a condition. """ def __init__( self, style_transformation: StyleTransformation, filter: FilterOrBool ) -> None: self.style_transformation = style_transformation self.filter = to_filter(filter) def transform_attrs(self, attrs: Attrs) -> Attrs: if self.filter(): return self.style_transformation.transform_attrs(attrs) return attrs def invalidation_hash(self) -> Hashable: return (self.filter(), self.style_transformation.invalidation_hash()) class _MergedStyleTransformation(StyleTransformation): def __init__(self, style_transformations: Sequence[StyleTransformation]) -> None: self.style_transformations = style_transformations def transform_attrs(self, attrs: Attrs) -> Attrs: for transformation in self.style_transformations: attrs = transformation.transform_attrs(attrs) return attrs def invalidation_hash(self) -> Hashable: return tuple(t.invalidation_hash() for t in self.style_transformations) def merge_style_transformations( style_transformations: Sequence[StyleTransformation], ) -> StyleTransformation: """ Merge multiple transformations together. """ return _MergedStyleTransformation(style_transformations) # Dictionary that maps ANSI color names to their opposite. This is useful for # turning color schemes that are optimized for a black background usable for a # white background. OPPOSITE_ANSI_COLOR_NAMES = { "ansidefault": "ansidefault", "ansiblack": "ansiwhite", "ansired": "ansibrightred", "ansigreen": "ansibrightgreen", "ansiyellow": "ansibrightyellow", "ansiblue": "ansibrightblue", "ansimagenta": "ansibrightmagenta", "ansicyan": "ansibrightcyan", "ansigray": "ansibrightblack", "ansiwhite": "ansiblack", "ansibrightred": "ansired", "ansibrightgreen": "ansigreen", "ansibrightyellow": "ansiyellow", "ansibrightblue": "ansiblue", "ansibrightmagenta": "ansimagenta", "ansibrightcyan": "ansicyan", "ansibrightblack": "ansigray", } assert set(OPPOSITE_ANSI_COLOR_NAMES.keys()) == set(ANSI_COLOR_NAMES) assert set(OPPOSITE_ANSI_COLOR_NAMES.values()) == set(ANSI_COLOR_NAMES) @memoized() def get_opposite_color(colorname: str | None) -> str | None: """ Take a color name in either 'ansi...' format or 6 digit RGB, return the color of opposite luminosity (same hue/saturation). This is used for turning color schemes that work on a light background usable on a dark background. """ if colorname is None: # Because color/bgcolor can be None in `Attrs`. return None # Special values. if colorname in ("", "default"): return colorname # Try ANSI color names. try: return OPPOSITE_ANSI_COLOR_NAMES[colorname] except KeyError: # Try 6 digit RGB colors. r = int(colorname[:2], 16) / 255.0 g = int(colorname[2:4], 16) / 255.0 b = int(colorname[4:6], 16) / 255.0 h, l, s = rgb_to_hls(r, g, b) l = 1 - l r, g, b = hls_to_rgb(h, l, s) r = int(r * 255) g = int(g * 255) b = int(b * 255) return f"{r:02x}{g:02x}{b:02x}" ================================================ FILE: src/prompt_toolkit/token.py ================================================ """ """ from __future__ import annotations __all__ = [ "ZeroWidthEscape", ] ZeroWidthEscape = "[ZeroWidthEscape]" ================================================ FILE: src/prompt_toolkit/utils.py ================================================ from __future__ import annotations import os import signal import sys import threading from collections import deque from collections.abc import Callable, Generator from contextlib import AbstractContextManager from typing import ( Generic, TypeVar, ) from wcwidth import wcwidth __all__ = [ "Event", "DummyContext", "get_cwidth", "suspend_to_background_supported", "is_conemu_ansi", "is_windows", "in_main_thread", "get_bell_environment_variable", "get_term_environment_variable", "take_using_weights", "to_str", "to_int", "AnyFloat", "to_float", "is_dumb_terminal", ] # Used to ensure sphinx autodoc does not try to import platform-specific # stuff when documenting win32.py modules. SPHINX_AUTODOC_RUNNING = "sphinx.ext.autodoc" in sys.modules _Sender = TypeVar("_Sender", covariant=True) class Event(Generic[_Sender]): """ Simple event to which event handlers can be attached. For instance:: class Cls: def __init__(self): # Define event. The first parameter is the sender. self.event = Event(self) obj = Cls() def handler(sender): pass # Add event handler by using the += operator. obj.event += handler # Fire event. obj.event() """ def __init__( self, sender: _Sender, handler: Callable[[_Sender], None] | None = None ) -> None: self.sender = sender self._handlers: list[Callable[[_Sender], None]] = [] if handler is not None: self += handler def __call__(self) -> None: "Fire event." for handler in self._handlers: handler(self.sender) def fire(self) -> None: "Alias for just calling the event." self() def add_handler(self, handler: Callable[[_Sender], None]) -> None: """ Add another handler to this callback. (Handler should be a callable that takes exactly one parameter: the sender object.) """ # Add to list of event handlers. self._handlers.append(handler) def remove_handler(self, handler: Callable[[_Sender], None]) -> None: """ Remove a handler from this callback. """ if handler in self._handlers: self._handlers.remove(handler) def __iadd__(self, handler: Callable[[_Sender], None]) -> Event[_Sender]: """ `event += handler` notation for adding a handler. """ self.add_handler(handler) return self def __isub__(self, handler: Callable[[_Sender], None]) -> Event[_Sender]: """ `event -= handler` notation for removing a handler. """ self.remove_handler(handler) return self class DummyContext(AbstractContextManager[None]): """ (contextlib.nested is not available on Py3) """ def __enter__(self) -> None: pass def __exit__(self, *a: object) -> None: pass class _CharSizesCache(dict[str, int]): """ Cache for wcwidth sizes. """ LONG_STRING_MIN_LEN = 64 # Minimum string length for considering it long. MAX_LONG_STRINGS = 16 # Maximum number of long strings to remember. def __init__(self) -> None: super().__init__() # Keep track of the "long" strings in this cache. self._long_strings: deque[str] = deque() def __missing__(self, string: str) -> int: # Note: We use the `max(0, ...` because some non printable control # characters, like e.g. Ctrl-underscore get a -1 wcwidth value. # It can be possible that these characters end up in the input # text. result: int if len(string) == 1: result = max(0, wcwidth(string)) else: result = sum(self[c] for c in string) # Store in cache. self[string] = result # Rotate long strings. # (It's hard to tell what we can consider short...) if len(string) > self.LONG_STRING_MIN_LEN: long_strings = self._long_strings long_strings.append(string) if len(long_strings) > self.MAX_LONG_STRINGS: key_to_remove = long_strings.popleft() if key_to_remove in self: del self[key_to_remove] return result _CHAR_SIZES_CACHE = _CharSizesCache() def get_cwidth(string: str) -> int: """ Return width of a string. Wrapper around ``wcwidth``. """ return _CHAR_SIZES_CACHE[string] def suspend_to_background_supported() -> bool: """ Returns `True` when the Python implementation supports suspend-to-background. This is typically `False' on Windows systems. """ return hasattr(signal, "SIGTSTP") def is_windows() -> bool: """ True when we are using Windows. """ return sys.platform == "win32" # Not 'darwin' or 'linux2' def is_windows_vt100_supported() -> bool: """ True when we are using Windows, but VT100 escape sequences are supported. """ if sys.platform == "win32": # Import needs to be inline. Windows libraries are not always available. from prompt_toolkit.output.windows10 import is_win_vt100_enabled return is_win_vt100_enabled() return False def is_conemu_ansi() -> bool: """ True when the ConEmu Windows console is used. """ return sys.platform == "win32" and os.environ.get("ConEmuANSI", "OFF") == "ON" def in_main_thread() -> bool: """ True when the current thread is the main thread. """ return threading.current_thread().__class__.__name__ == "_MainThread" def get_bell_environment_variable() -> bool: """ True if env variable is set to true (true, TRUE, True, 1). """ value = os.environ.get("PROMPT_TOOLKIT_BELL", "true") return value.lower() in ("1", "true") def get_term_environment_variable() -> str: "Return the $TERM environment variable." return os.environ.get("TERM", "") _T = TypeVar("_T") def take_using_weights( items: list[_T], weights: list[int] ) -> Generator[_T, None, None]: """ Generator that keeps yielding items from the items list, in proportion to their weight. For instance:: # Getting the first 70 items from this generator should have yielded 10 # times A, 20 times B and 40 times C, all distributed equally.. take_using_weights(['A', 'B', 'C'], [5, 10, 20]) :param items: List of items to take from. :param weights: Integers representing the weight. (Numbers have to be integers, not floats.) """ assert len(items) == len(weights) assert len(items) > 0 # Remove items with zero-weight. items2 = [] weights2 = [] for item, w in zip(items, weights): if w > 0: items2.append(item) weights2.append(w) items = items2 weights = weights2 # Make sure that we have some items left. if not items: raise ValueError("Did't got any items with a positive weight.") # already_taken = [0 for i in items] item_count = len(items) max_weight = max(weights) i = 0 while True: # Each iteration of this loop, we fill up until by (total_weight/max_weight). adding = True while adding: adding = False for item_i, item, weight in zip(range(item_count), items, weights): if already_taken[item_i] < i * weight / float(max_weight): yield item already_taken[item_i] += 1 adding = True i += 1 def to_str(value: Callable[[], str] | str) -> str: "Turn callable or string into string." if callable(value): return to_str(value()) else: return str(value) def to_int(value: Callable[[], int] | int) -> int: "Turn callable or int into int." if callable(value): return to_int(value()) else: return int(value) AnyFloat = Callable[[], float] | float def to_float(value: AnyFloat) -> float: "Turn callable or float into float." if callable(value): return to_float(value()) else: return float(value) def is_dumb_terminal(term: str | None = None) -> bool: """ True if this terminal type is considered "dumb". If so, we should fall back to the simplest possible form of line editing, without cursor positioning and color support. """ if term is None: return is_dumb_terminal(os.environ.get("TERM", "")) return term.lower() in ["dumb", "unknown"] ================================================ FILE: src/prompt_toolkit/validation.py ================================================ """ Input validation for a `Buffer`. (Validators will be called before accepting input.) """ from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Callable from prompt_toolkit.eventloop import run_in_executor_with_context from .document import Document from .filters import FilterOrBool, to_filter __all__ = [ "ConditionalValidator", "ValidationError", "Validator", "ThreadedValidator", "DummyValidator", "DynamicValidator", ] class ValidationError(Exception): """ Error raised by :meth:`.Validator.validate`. :param cursor_position: The cursor position where the error occurred. :param message: Text. """ def __init__(self, cursor_position: int = 0, message: str = "") -> None: super().__init__(message) self.cursor_position = cursor_position self.message = message def __repr__(self) -> str: return f"{self.__class__.__name__}(cursor_position={self.cursor_position!r}, message={self.message!r})" class Validator(metaclass=ABCMeta): """ Abstract base class for an input validator. A validator is typically created in one of the following two ways: - Either by overriding this class and implementing the `validate` method. - Or by passing a callable to `Validator.from_callable`. If the validation takes some time and needs to happen in a background thread, this can be wrapped in a :class:`.ThreadedValidator`. """ @abstractmethod def validate(self, document: Document) -> None: """ Validate the input. If invalid, this should raise a :class:`.ValidationError`. :param document: :class:`~prompt_toolkit.document.Document` instance. """ pass async def validate_async(self, document: Document) -> None: """ Return a `Future` which is set when the validation is ready. This function can be overloaded in order to provide an asynchronous implementation. """ try: self.validate(document) except ValidationError: raise @classmethod def from_callable( cls, validate_func: Callable[[str], bool], error_message: str = "Invalid input", move_cursor_to_end: bool = False, ) -> Validator: """ Create a validator from a simple validate callable. E.g.: .. code:: python def is_valid(text): return text in ['hello', 'world'] Validator.from_callable(is_valid, error_message='Invalid input') :param validate_func: Callable that takes the input string, and returns `True` if the input is valid input. :param error_message: Message to be displayed if the input is invalid. :param move_cursor_to_end: Move the cursor to the end of the input, if the input is invalid. """ return _ValidatorFromCallable(validate_func, error_message, move_cursor_to_end) class _ValidatorFromCallable(Validator): """ Validate input from a simple callable. """ def __init__( self, func: Callable[[str], bool], error_message: str, move_cursor_to_end: bool ) -> None: self.func = func self.error_message = error_message self.move_cursor_to_end = move_cursor_to_end def __repr__(self) -> str: return f"Validator.from_callable({self.func!r})" def validate(self, document: Document) -> None: if not self.func(document.text): if self.move_cursor_to_end: index = len(document.text) else: index = 0 raise ValidationError(cursor_position=index, message=self.error_message) class ThreadedValidator(Validator): """ Wrapper that runs input validation in a thread. (Use this to prevent the user interface from becoming unresponsive if the input validation takes too much time.) """ def __init__(self, validator: Validator) -> None: self.validator = validator def validate(self, document: Document) -> None: self.validator.validate(document) async def validate_async(self, document: Document) -> None: """ Run the `validate` function in a thread. """ def run_validation_thread() -> None: return self.validate(document) await run_in_executor_with_context(run_validation_thread) class DummyValidator(Validator): """ Validator class that accepts any input. """ def validate(self, document: Document) -> None: pass # Don't raise any exception. class ConditionalValidator(Validator): """ Validator that can be switched on/off according to a filter. (This wraps around another validator.) """ def __init__(self, validator: Validator, filter: FilterOrBool) -> None: self.validator = validator self.filter = to_filter(filter) def validate(self, document: Document) -> None: # Call the validator only if the filter is active. if self.filter(): self.validator.validate(document) class DynamicValidator(Validator): """ Validator class that can dynamically returns any Validator. :param get_validator: Callable that returns a :class:`.Validator` instance. """ def __init__(self, get_validator: Callable[[], Validator | None]) -> None: self.get_validator = get_validator def validate(self, document: Document) -> None: validator = self.get_validator() or DummyValidator() validator.validate(document) async def validate_async(self, document: Document) -> None: validator = self.get_validator() or DummyValidator() await validator.validate_async(document) ================================================ FILE: src/prompt_toolkit/widgets/__init__.py ================================================ """ Collection of reusable components for building full screen applications. These are higher level abstractions on top of the `prompt_toolkit.layout` module. Most of these widgets implement the ``__pt_container__`` method, which makes it possible to embed these in the layout like any other container. """ from __future__ import annotations from .base import ( Box, Button, Checkbox, CheckboxList, Frame, HorizontalLine, Label, ProgressBar, RadioList, Shadow, TextArea, VerticalLine, ) from .dialogs import Dialog from .menus import MenuContainer, MenuItem from .toolbars import ( ArgToolbar, CompletionsToolbar, FormattedTextToolbar, SearchToolbar, SystemToolbar, ValidationToolbar, ) __all__ = [ # Base. "TextArea", "Label", "Button", "Frame", "Shadow", "Box", "VerticalLine", "HorizontalLine", "CheckboxList", "RadioList", "Checkbox", "ProgressBar", # Toolbars. "ArgToolbar", "CompletionsToolbar", "FormattedTextToolbar", "SearchToolbar", "SystemToolbar", "ValidationToolbar", # Dialogs. "Dialog", # Menus. "MenuContainer", "MenuItem", ] ================================================ FILE: src/prompt_toolkit/widgets/base.py ================================================ """ Collection of reusable components for building full screen applications. All of these widgets implement the ``__pt_container__`` method, which makes them usable in any situation where we are expecting a `prompt_toolkit` container object. .. warning:: At this point, the API for these widgets is considered unstable, and can potentially change between minor releases (we try not too, but no guarantees are made yet). The public API in `prompt_toolkit.shortcuts.dialogs` on the other hand is considered stable. """ from __future__ import annotations from collections.abc import Callable, Sequence from functools import partial from typing import Generic, TypeVar from prompt_toolkit.application.current import get_app from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest from prompt_toolkit.buffer import Buffer, BufferAcceptHandler from prompt_toolkit.completion import Completer, DynamicCompleter from prompt_toolkit.document import Document from prompt_toolkit.filters import ( Condition, FilterOrBool, has_focus, is_done, is_true, to_filter, ) from prompt_toolkit.formatted_text import ( AnyFormattedText, StyleAndTextTuples, Template, to_formatted_text, ) from prompt_toolkit.formatted_text.utils import fragment_list_to_text from prompt_toolkit.history import History from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import ( AnyContainer, ConditionalContainer, Container, DynamicContainer, Float, FloatContainer, HSplit, VSplit, Window, WindowAlign, ) from prompt_toolkit.layout.controls import ( BufferControl, FormattedTextControl, GetLinePrefixCallable, ) from prompt_toolkit.layout.dimension import AnyDimension from prompt_toolkit.layout.dimension import Dimension as D from prompt_toolkit.layout.margins import ( ConditionalMargin, NumberedMargin, ScrollbarMargin, ) from prompt_toolkit.layout.processors import ( AppendAutoSuggestion, BeforeInput, ConditionalProcessor, PasswordProcessor, Processor, ) from prompt_toolkit.lexers import DynamicLexer, Lexer from prompt_toolkit.mouse_events import MouseEvent, MouseEventType from prompt_toolkit.utils import get_cwidth from prompt_toolkit.validation import DynamicValidator, Validator from .toolbars import SearchToolbar __all__ = [ "TextArea", "Label", "Button", "Frame", "Shadow", "Box", "VerticalLine", "HorizontalLine", "RadioList", "CheckboxList", "Checkbox", # backward compatibility "ProgressBar", ] E = KeyPressEvent class Border: "Box drawing characters. (Thin)" HORIZONTAL = "\u2500" VERTICAL = "\u2502" TOP_LEFT = "\u250c" TOP_RIGHT = "\u2510" BOTTOM_LEFT = "\u2514" BOTTOM_RIGHT = "\u2518" class TextArea: """ A simple input field. This is a higher level abstraction on top of several other classes with sane defaults. This widget does have the most common options, but it does not intend to cover every single use case. For more configurations options, you can always build a text area manually, using a :class:`~prompt_toolkit.buffer.Buffer`, :class:`~prompt_toolkit.layout.BufferControl` and :class:`~prompt_toolkit.layout.Window`. Buffer attributes: :param text: The initial text. :param multiline: If True, allow multiline input. :param completer: :class:`~prompt_toolkit.completion.Completer` instance for auto completion. :param complete_while_typing: Boolean. :param accept_handler: Called when `Enter` is pressed (This should be a callable that takes a buffer as input). :param history: :class:`~prompt_toolkit.history.History` instance. :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` instance for input suggestions. BufferControl attributes: :param password: When `True`, display using asterisks. :param focusable: When `True`, allow this widget to receive the focus. :param focus_on_click: When `True`, focus after mouse click. :param input_processors: `None` or a list of :class:`~prompt_toolkit.layout.Processor` objects. :param validator: `None` or a :class:`~prompt_toolkit.validation.Validator` object. Window attributes: :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax highlighting. :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines. :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.) :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.) :param scrollbar: When `True`, display a scroll bar. :param style: A style string. :param dont_extend_width: When `True`, don't take up more width then the preferred width reported by the control. :param dont_extend_height: When `True`, don't take up more width then the preferred height reported by the control. :param get_line_prefix: None or a callable that returns formatted text to be inserted before a line. It takes a line number (int) and a wrap_count and returns formatted text. This can be used for implementation of line continuations, things like Vim "breakindent" and so on. Other attributes: :param search_field: An optional `SearchToolbar` object. """ def __init__( self, text: str = "", multiline: FilterOrBool = True, password: FilterOrBool = False, lexer: Lexer | None = None, auto_suggest: AutoSuggest | None = None, completer: Completer | None = None, complete_while_typing: FilterOrBool = True, validator: Validator | None = None, accept_handler: BufferAcceptHandler | None = None, history: History | None = None, focusable: FilterOrBool = True, focus_on_click: FilterOrBool = False, wrap_lines: FilterOrBool = True, read_only: FilterOrBool = False, width: AnyDimension = None, height: AnyDimension = None, dont_extend_height: FilterOrBool = False, dont_extend_width: FilterOrBool = False, line_numbers: bool = False, get_line_prefix: GetLinePrefixCallable | None = None, scrollbar: bool = False, style: str = "", search_field: SearchToolbar | None = None, preview_search: FilterOrBool = True, prompt: AnyFormattedText = "", input_processors: list[Processor] | None = None, name: str = "", ) -> None: if search_field is None: search_control = None elif isinstance(search_field, SearchToolbar): search_control = search_field.control if input_processors is None: input_processors = [] # Writeable attributes. self.completer = completer self.complete_while_typing = complete_while_typing self.lexer = lexer self.auto_suggest = auto_suggest self.read_only = read_only self.wrap_lines = wrap_lines self.validator = validator self.buffer = Buffer( document=Document(text, 0), multiline=multiline, read_only=Condition(lambda: is_true(self.read_only)), completer=DynamicCompleter(lambda: self.completer), complete_while_typing=Condition( lambda: is_true(self.complete_while_typing) ), validator=DynamicValidator(lambda: self.validator), auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), accept_handler=accept_handler, history=history, name=name, ) self.control = BufferControl( buffer=self.buffer, lexer=DynamicLexer(lambda: self.lexer), input_processors=[ ConditionalProcessor( AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done ), ConditionalProcessor( processor=PasswordProcessor(), filter=to_filter(password) ), BeforeInput(prompt, style="class:text-area.prompt"), ] + input_processors, search_buffer_control=search_control, preview_search=preview_search, focusable=focusable, focus_on_click=focus_on_click, ) if multiline: if scrollbar: right_margins = [ScrollbarMargin(display_arrows=True)] else: right_margins = [] if line_numbers: left_margins = [NumberedMargin()] else: left_margins = [] else: height = D.exact(1) left_margins = [] right_margins = [] style = "class:text-area " + style # If no height was given, guarantee height of at least 1. if height is None: height = D(min=1) self.window = Window( height=height, width=width, dont_extend_height=dont_extend_height, dont_extend_width=dont_extend_width, content=self.control, style=style, wrap_lines=Condition(lambda: is_true(self.wrap_lines)), left_margins=left_margins, right_margins=right_margins, get_line_prefix=get_line_prefix, ) @property def text(self) -> str: """ The `Buffer` text. """ return self.buffer.text @text.setter def text(self, value: str) -> None: self.document = Document(value, 0) @property def document(self) -> Document: """ The `Buffer` document (text + cursor position). """ return self.buffer.document @document.setter def document(self, value: Document) -> None: self.buffer.set_document(value, bypass_readonly=True) @property def accept_handler(self) -> BufferAcceptHandler | None: """ The accept handler. Called when the user accepts the input. """ return self.buffer.accept_handler @accept_handler.setter def accept_handler(self, value: BufferAcceptHandler) -> None: self.buffer.accept_handler = value def __pt_container__(self) -> Container: return self.window class Label: """ Widget that displays the given text. It is not editable or focusable. :param text: Text to display. Can be multiline. All value types accepted by :class:`prompt_toolkit.layout.FormattedTextControl` are allowed, including a callable. :param style: A style string. :param width: When given, use this width, rather than calculating it from the text size. :param dont_extend_width: When `True`, don't take up more width than preferred, i.e. the length of the longest line of the text, or value of `width` parameter, if given. `True` by default :param dont_extend_height: When `True`, don't take up more width than the preferred height, i.e. the number of lines of the text. `False` by default. """ def __init__( self, text: AnyFormattedText, style: str = "", width: AnyDimension = None, dont_extend_height: bool = True, dont_extend_width: bool = False, align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, # There is no cursor navigation in a label, so it makes sense to always # wrap lines by default. wrap_lines: FilterOrBool = True, ) -> None: self.text = text def get_width() -> AnyDimension: if width is None: text_fragments = to_formatted_text(self.text) text = fragment_list_to_text(text_fragments) if text: longest_line = max(get_cwidth(line) for line in text.splitlines()) else: return D(preferred=0) return D(preferred=longest_line) else: return width self.formatted_text_control = FormattedTextControl(text=lambda: self.text) self.window = Window( content=self.formatted_text_control, width=get_width, height=D(min=1), style="class:label " + style, dont_extend_height=dont_extend_height, dont_extend_width=dont_extend_width, align=align, wrap_lines=wrap_lines, ) def __pt_container__(self) -> Container: return self.window class Button: """ Clickable button. :param text: The caption for the button. :param handler: `None` or callable. Called when the button is clicked. No parameters are passed to this callable. Use for instance Python's `functools.partial` to pass parameters to this callable if needed. :param width: Width of the button. """ def __init__( self, text: str, handler: Callable[[], None] | None = None, width: int = 12, left_symbol: str = "<", right_symbol: str = ">", ) -> None: self.text = text self.left_symbol = left_symbol self.right_symbol = right_symbol self.handler = handler self.width = width self.control = FormattedTextControl( self._get_text_fragments, key_bindings=self._get_key_bindings(), focusable=True, ) def get_style() -> str: if get_app().layout.has_focus(self): return "class:button.focused" else: return "class:button" # Note: `dont_extend_width` is False, because we want to allow buttons # to take more space if the parent container provides more space. # Otherwise, we will also truncate the text. # Probably we need a better way here to adjust to width of the # button to the text. self.window = Window( self.control, align=WindowAlign.CENTER, height=1, width=width, style=get_style, dont_extend_width=False, dont_extend_height=True, ) def _get_text_fragments(self) -> StyleAndTextTuples: width = ( self.width - (get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)) + (len(self.text) - get_cwidth(self.text)) ) text = (f"{{:^{max(0, width)}}}").format(self.text) def handler(mouse_event: MouseEvent) -> None: if ( self.handler is not None and mouse_event.event_type == MouseEventType.MOUSE_UP ): self.handler() return [ ("class:button.arrow", self.left_symbol, handler), ("[SetCursorPosition]", ""), ("class:button.text", text, handler), ("class:button.arrow", self.right_symbol, handler), ] def _get_key_bindings(self) -> KeyBindings: "Key bindings for the Button." kb = KeyBindings() @kb.add(" ") @kb.add("enter") def _(event: E) -> None: if self.handler is not None: self.handler() return kb def __pt_container__(self) -> Container: return self.window class Frame: """ Draw a border around any container, optionally with a title text. Changing the title and body of the frame is possible at runtime by assigning to the `body` and `title` attributes of this class. :param body: Another container object. :param title: Text to be displayed in the top of the frame (can be formatted text). :param style: Style string to be applied to this widget. """ def __init__( self, body: AnyContainer, title: AnyFormattedText = "", style: str = "", width: AnyDimension = None, height: AnyDimension = None, key_bindings: KeyBindings | None = None, modal: bool = False, ) -> None: self.title = title self.body = body fill = partial(Window, style="class:frame.border") style = "class:frame " + style top_row_with_title = VSplit( [ fill(width=1, height=1, char=Border.TOP_LEFT), fill(char=Border.HORIZONTAL), fill(width=1, height=1, char="|"), # Notice: we use `Template` here, because `self.title` can be an # `HTML` object for instance. Label( lambda: Template(" {} ").format(self.title), style="class:frame.label", dont_extend_width=True, ), fill(width=1, height=1, char="|"), fill(char=Border.HORIZONTAL), fill(width=1, height=1, char=Border.TOP_RIGHT), ], height=1, ) top_row_without_title = VSplit( [ fill(width=1, height=1, char=Border.TOP_LEFT), fill(char=Border.HORIZONTAL), fill(width=1, height=1, char=Border.TOP_RIGHT), ], height=1, ) @Condition def has_title() -> bool: return bool(self.title) self.container = HSplit( [ ConditionalContainer( content=top_row_with_title, filter=has_title, alternative_content=top_row_without_title, ), VSplit( [ fill(width=1, char=Border.VERTICAL), DynamicContainer(lambda: self.body), fill(width=1, char=Border.VERTICAL), # Padding is required to make sure that if the content is # too small, the right frame border is still aligned. ], padding=0, ), VSplit( [ fill(width=1, height=1, char=Border.BOTTOM_LEFT), fill(char=Border.HORIZONTAL), fill(width=1, height=1, char=Border.BOTTOM_RIGHT), ], # specifying height here will increase the rendering speed. height=1, ), ], width=width, height=height, style=style, key_bindings=key_bindings, modal=modal, ) def __pt_container__(self) -> Container: return self.container class Shadow: """ Draw a shadow underneath/behind this container. (This applies `class:shadow` the the cells under the shadow. The Style should define the colors for the shadow.) :param body: Another container object. """ def __init__(self, body: AnyContainer) -> None: self.container = FloatContainer( content=body, floats=[ Float( bottom=-1, height=1, left=1, right=-1, transparent=True, content=Window(style="class:shadow"), ), Float( bottom=-1, top=1, width=1, right=-1, transparent=True, content=Window(style="class:shadow"), ), ], ) def __pt_container__(self) -> Container: return self.container class Box: """ Add padding around a container. This also makes sure that the parent can provide more space than required by the child. This is very useful when wrapping a small element with a fixed size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit`` try to make sure to adapt respectively the width and height, possibly shrinking other elements. Wrapping something in a ``Box`` makes it flexible. :param body: Another container object. :param padding: The margin to be used around the body. This can be overridden by `padding_left`, padding_right`, `padding_top` and `padding_bottom`. :param style: A style string. :param char: Character to be used for filling the space around the body. (This is supposed to be a character with a terminal width of 1.) """ def __init__( self, body: AnyContainer, padding: AnyDimension = None, padding_left: AnyDimension = None, padding_right: AnyDimension = None, padding_top: AnyDimension = None, padding_bottom: AnyDimension = None, width: AnyDimension = None, height: AnyDimension = None, style: str = "", char: None | str | Callable[[], str] = None, modal: bool = False, key_bindings: KeyBindings | None = None, ) -> None: self.padding = padding self.padding_left = padding_left self.padding_right = padding_right self.padding_top = padding_top self.padding_bottom = padding_bottom self.body = body def left() -> AnyDimension: if self.padding_left is None: return self.padding return self.padding_left def right() -> AnyDimension: if self.padding_right is None: return self.padding return self.padding_right def top() -> AnyDimension: if self.padding_top is None: return self.padding return self.padding_top def bottom() -> AnyDimension: if self.padding_bottom is None: return self.padding return self.padding_bottom self.container = HSplit( [ Window(height=top, char=char), VSplit( [ Window(width=left, char=char), body, Window(width=right, char=char), ] ), Window(height=bottom, char=char), ], width=width, height=height, style=style, modal=modal, key_bindings=None, ) def __pt_container__(self) -> Container: return self.container _T = TypeVar("_T") class _DialogList(Generic[_T]): """ Common code for `RadioList` and `CheckboxList`. """ def __init__( self, values: Sequence[tuple[_T, AnyFormattedText]], default_values: Sequence[_T] | None = None, select_on_focus: bool = False, open_character: str = "", select_character: str = "*", close_character: str = "", container_style: str = "", default_style: str = "", number_style: str = "", selected_style: str = "", checked_style: str = "", multiple_selection: bool = False, show_scrollbar: bool = True, show_cursor: bool = True, show_numbers: bool = False, ) -> None: assert len(values) > 0 default_values = default_values or [] self.values = values self.show_numbers = show_numbers self.open_character = open_character self.select_character = select_character self.close_character = close_character self.container_style = container_style self.default_style = default_style self.number_style = number_style self.selected_style = selected_style self.checked_style = checked_style self.multiple_selection = multiple_selection self.show_scrollbar = show_scrollbar # current_values will be used in multiple_selection, # current_value will be used otherwise. keys: list[_T] = [value for (value, _) in values] self.current_values: list[_T] = [ value for value in default_values if value in keys ] self.current_value: _T = ( default_values[0] if len(default_values) and default_values[0] in keys else values[0][0] ) # Cursor index: take first selected item or first item otherwise. if len(self.current_values) > 0: self._selected_index = keys.index(self.current_values[0]) else: self._selected_index = 0 # Key bindings. kb = KeyBindings() @kb.add("up") @kb.add("k") # Vi-like. def _up(event: E) -> None: self._selected_index = max(0, self._selected_index - 1) if select_on_focus: self._handle_enter() @kb.add("down") @kb.add("j") # Vi-like. def _down(event: E) -> None: self._selected_index = min(len(self.values) - 1, self._selected_index + 1) if select_on_focus: self._handle_enter() @kb.add("pageup") def _pageup(event: E) -> None: w = event.app.layout.current_window if w.render_info: self._selected_index = max( 0, self._selected_index - len(w.render_info.displayed_lines) ) @kb.add("pagedown") def _pagedown(event: E) -> None: w = event.app.layout.current_window if w.render_info: self._selected_index = min( len(self.values) - 1, self._selected_index + len(w.render_info.displayed_lines), ) @kb.add("enter") @kb.add(" ") def _click(event: E) -> None: self._handle_enter() @kb.add(Keys.Any) def _find(event: E) -> None: # We first check values after the selected value, then all values. values = list(self.values) for value in values[self._selected_index + 1 :] + values: text = fragment_list_to_text(to_formatted_text(value[1])).lower() if text.startswith(event.data.lower()): self._selected_index = self.values.index(value) return numbers_visible = Condition(lambda: self.show_numbers) for i in range(1, 10): @kb.add(str(i), filter=numbers_visible) def _select_i(event: E, index: int = i) -> None: self._selected_index = min(len(self.values) - 1, index - 1) if select_on_focus: self._handle_enter() # Control and window. self.control = FormattedTextControl( self._get_text_fragments, key_bindings=kb, focusable=True, show_cursor=show_cursor, ) self.window = Window( content=self.control, style=self.container_style, right_margins=[ ConditionalMargin( margin=ScrollbarMargin(display_arrows=True), filter=Condition(lambda: self.show_scrollbar), ), ], dont_extend_height=True, ) def _handle_enter(self) -> None: if self.multiple_selection: val = self.values[self._selected_index][0] if val in self.current_values: self.current_values.remove(val) else: self.current_values.append(val) else: self.current_value = self.values[self._selected_index][0] def _get_text_fragments(self) -> StyleAndTextTuples: def mouse_handler(mouse_event: MouseEvent) -> None: """ Set `_selected_index` and `current_value` according to the y position of the mouse click event. """ if mouse_event.event_type == MouseEventType.MOUSE_UP: self._selected_index = mouse_event.position.y self._handle_enter() result: StyleAndTextTuples = [] for i, value in enumerate(self.values): if self.multiple_selection: checked = value[0] in self.current_values else: checked = value[0] == self.current_value selected = i == self._selected_index style = "" if checked: style += " " + self.checked_style if selected: style += " " + self.selected_style result.append((style, self.open_character)) if selected: result.append(("[SetCursorPosition]", "")) if checked: result.append((style, self.select_character)) else: result.append((style, " ")) result.append((style, self.close_character)) result.append((f"{style} {self.default_style}", " ")) if self.show_numbers: result.append((f"{style} {self.number_style}", f"{i + 1:2d}. ")) result.extend( to_formatted_text(value[1], style=f"{style} {self.default_style}") ) result.append(("", "\n")) # Add mouse handler to all fragments. for i in range(len(result)): result[i] = (result[i][0], result[i][1], mouse_handler) result.pop() # Remove last newline. return result def __pt_container__(self) -> Container: return self.window class RadioList(_DialogList[_T]): """ List of radio buttons. Only one can be checked at the same time. :param values: List of (value, label) tuples. """ def __init__( self, values: Sequence[tuple[_T, AnyFormattedText]], default: _T | None = None, show_numbers: bool = False, select_on_focus: bool = False, open_character: str = "(", select_character: str = "*", close_character: str = ")", container_style: str = "class:radio-list", default_style: str = "class:radio", selected_style: str = "class:radio-selected", checked_style: str = "class:radio-checked", number_style: str = "class:radio-number", multiple_selection: bool = False, show_cursor: bool = True, show_scrollbar: bool = True, ) -> None: if default is None: default_values = None else: default_values = [default] super().__init__( values, default_values=default_values, select_on_focus=select_on_focus, show_numbers=show_numbers, open_character=open_character, select_character=select_character, close_character=close_character, container_style=container_style, default_style=default_style, selected_style=selected_style, checked_style=checked_style, number_style=number_style, multiple_selection=False, show_cursor=show_cursor, show_scrollbar=show_scrollbar, ) class CheckboxList(_DialogList[_T]): """ List of checkbox buttons. Several can be checked at the same time. :param values: List of (value, label) tuples. """ def __init__( self, values: Sequence[tuple[_T, AnyFormattedText]], default_values: Sequence[_T] | None = None, open_character: str = "[", select_character: str = "*", close_character: str = "]", container_style: str = "class:checkbox-list", default_style: str = "class:checkbox", selected_style: str = "class:checkbox-selected", checked_style: str = "class:checkbox-checked", ) -> None: super().__init__( values, default_values=default_values, open_character=open_character, select_character=select_character, close_character=close_character, container_style=container_style, default_style=default_style, selected_style=selected_style, checked_style=checked_style, multiple_selection=True, ) class Checkbox(CheckboxList[str]): """Backward compatibility util: creates a 1-sized CheckboxList :param text: the text """ show_scrollbar = False def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None: values = [("value", text)] super().__init__(values=values) self.checked = checked @property def checked(self) -> bool: return "value" in self.current_values @checked.setter def checked(self, value: bool) -> None: if value: self.current_values = ["value"] else: self.current_values = [] class VerticalLine: """ A simple vertical line with a width of 1. """ def __init__(self) -> None: self.window = Window( char=Border.VERTICAL, style="class:line,vertical-line", width=1 ) def __pt_container__(self) -> Container: return self.window class HorizontalLine: """ A simple horizontal line with a height of 1. """ def __init__(self) -> None: self.window = Window( char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1 ) def __pt_container__(self) -> Container: return self.window class ProgressBar: def __init__(self) -> None: self._percentage = 60 self.label = Label("60%") self.container = FloatContainer( content=Window(height=1), floats=[ # We first draw the label, then the actual progress bar. Right # now, this is the only way to have the colors of the progress # bar appear on top of the label. The problem is that our label # can't be part of any `Window` below. Float(content=self.label, top=0, bottom=0), Float( left=0, top=0, right=0, bottom=0, content=VSplit( [ Window( style="class:progress-bar.used", width=lambda: D(weight=int(self._percentage)), ), Window( style="class:progress-bar", width=lambda: D(weight=int(100 - self._percentage)), ), ] ), ), ], ) @property def percentage(self) -> int: return self._percentage @percentage.setter def percentage(self, value: int) -> None: self._percentage = value self.label.text = f"{value}%" def __pt_container__(self) -> Container: return self.container ================================================ FILE: src/prompt_toolkit/widgets/dialogs.py ================================================ """ Collection of reusable components for building full screen applications. """ from __future__ import annotations from collections.abc import Sequence from prompt_toolkit.filters import has_completions, has_focus from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.layout.containers import ( AnyContainer, DynamicContainer, HSplit, VSplit, ) from prompt_toolkit.layout.dimension import AnyDimension from prompt_toolkit.layout.dimension import Dimension as D from .base import Box, Button, Frame, Shadow __all__ = [ "Dialog", ] class Dialog: """ Simple dialog window. This is the base for input dialogs, message dialogs and confirmation dialogs. Changing the title and body of the dialog is possible at runtime by assigning to the `body` and `title` attributes of this class. :param body: Child container object. :param title: Text to be displayed in the heading of the dialog. :param buttons: A list of `Button` widgets, displayed at the bottom. """ def __init__( self, body: AnyContainer, title: AnyFormattedText = "", buttons: Sequence[Button] | None = None, modal: bool = True, width: AnyDimension = None, with_background: bool = False, ) -> None: self.body = body self.title = title buttons = buttons or [] # When a button is selected, handle left/right key bindings. buttons_kb = KeyBindings() if len(buttons) > 1: first_selected = has_focus(buttons[0]) last_selected = has_focus(buttons[-1]) buttons_kb.add("left", filter=~first_selected)(focus_previous) buttons_kb.add("right", filter=~last_selected)(focus_next) frame_body: AnyContainer if buttons: frame_body = HSplit( [ # Add optional padding around the body. Box( body=DynamicContainer(lambda: self.body), padding=D(preferred=1, max=1), padding_bottom=0, ), # The buttons. Box( body=VSplit(buttons, padding=1, key_bindings=buttons_kb), height=D(min=1, max=3, preferred=3), ), ] ) else: frame_body = body # Key bindings for whole dialog. kb = KeyBindings() kb.add("tab", filter=~has_completions)(focus_next) kb.add("s-tab", filter=~has_completions)(focus_previous) frame = Shadow( body=Frame( title=lambda: self.title, body=frame_body, style="class:dialog.body", width=(None if with_background is None else width), key_bindings=kb, modal=modal, ) ) self.container: Box | Shadow if with_background: self.container = Box(body=frame, style="class:dialog", width=width) else: self.container = frame def __pt_container__(self) -> AnyContainer: return self.container ================================================ FILE: src/prompt_toolkit/widgets/menus.py ================================================ from __future__ import annotations from collections.abc import Callable, Iterable, Sequence from prompt_toolkit.application.current import get_app from prompt_toolkit.filters import Condition from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple, StyleAndTextTuples from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import ( AnyContainer, ConditionalContainer, Container, Float, FloatContainer, HSplit, Window, ) from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.mouse_events import MouseEvent, MouseEventType from prompt_toolkit.utils import get_cwidth from prompt_toolkit.widgets import Shadow from .base import Border __all__ = [ "MenuContainer", "MenuItem", ] E = KeyPressEvent class MenuContainer: """ :param floats: List of extra Float objects to display. :param menu_items: List of `MenuItem` objects. """ def __init__( self, body: AnyContainer, menu_items: list[MenuItem], floats: list[Float] | None = None, key_bindings: KeyBindingsBase | None = None, ) -> None: self.body = body self.menu_items = menu_items self.selected_menu = [0] # Key bindings. kb = KeyBindings() @Condition def in_main_menu() -> bool: return len(self.selected_menu) == 1 @Condition def in_sub_menu() -> bool: return len(self.selected_menu) > 1 # Navigation through the main menu. @kb.add("left", filter=in_main_menu) def _left(event: E) -> None: self.selected_menu[0] = max(0, self.selected_menu[0] - 1) @kb.add("right", filter=in_main_menu) def _right(event: E) -> None: self.selected_menu[0] = min( len(self.menu_items) - 1, self.selected_menu[0] + 1 ) @kb.add("down", filter=in_main_menu) def _down(event: E) -> None: self.selected_menu.append(0) @kb.add("c-c", filter=in_main_menu) @kb.add("c-g", filter=in_main_menu) def _cancel(event: E) -> None: "Leave menu." event.app.layout.focus_last() # Sub menu navigation. @kb.add("left", filter=in_sub_menu) @kb.add("c-g", filter=in_sub_menu) @kb.add("c-c", filter=in_sub_menu) def _back(event: E) -> None: "Go back to parent menu." if len(self.selected_menu) > 1: self.selected_menu.pop() @kb.add("right", filter=in_sub_menu) def _submenu(event: E) -> None: "go into sub menu." if self._get_menu(len(self.selected_menu) - 1).children: self.selected_menu.append(0) # If This item does not have a sub menu. Go up in the parent menu. elif ( len(self.selected_menu) == 2 and self.selected_menu[0] < len(self.menu_items) - 1 ): self.selected_menu = [ min(len(self.menu_items) - 1, self.selected_menu[0] + 1) ] if self.menu_items[self.selected_menu[0]].children: self.selected_menu.append(0) @kb.add("up", filter=in_sub_menu) def _up_in_submenu(event: E) -> None: "Select previous (enabled) menu item or return to main menu." # Look for previous enabled items in this sub menu. menu = self._get_menu(len(self.selected_menu) - 2) index = self.selected_menu[-1] previous_indexes = [ i for i, item in enumerate(menu.children) if i < index and not item.disabled ] if previous_indexes: self.selected_menu[-1] = previous_indexes[-1] elif len(self.selected_menu) == 2: # Return to main menu. self.selected_menu.pop() @kb.add("down", filter=in_sub_menu) def _down_in_submenu(event: E) -> None: "Select next (enabled) menu item." menu = self._get_menu(len(self.selected_menu) - 2) index = self.selected_menu[-1] next_indexes = [ i for i, item in enumerate(menu.children) if i > index and not item.disabled ] if next_indexes: self.selected_menu[-1] = next_indexes[0] @kb.add("enter") def _click(event: E) -> None: "Click the selected menu item." item = self._get_menu(len(self.selected_menu) - 1) if item.handler: event.app.layout.focus_last() item.handler() # Controls. self.control = FormattedTextControl( self._get_menu_fragments, key_bindings=kb, focusable=True, show_cursor=False ) self.window = Window(height=1, content=self.control, style="class:menu-bar") submenu = self._submenu(0) submenu2 = self._submenu(1) submenu3 = self._submenu(2) @Condition def has_focus() -> bool: return get_app().layout.current_window == self.window self.container = FloatContainer( content=HSplit( [ # The titlebar. self.window, # The 'body', like defined above. body, ] ), floats=[ Float( xcursor=True, ycursor=True, content=ConditionalContainer( content=Shadow(body=submenu), filter=has_focus ), ), Float( attach_to_window=submenu, xcursor=True, ycursor=True, allow_cover_cursor=True, content=ConditionalContainer( content=Shadow(body=submenu2), filter=has_focus & Condition(lambda: len(self.selected_menu) >= 1), ), ), Float( attach_to_window=submenu2, xcursor=True, ycursor=True, allow_cover_cursor=True, content=ConditionalContainer( content=Shadow(body=submenu3), filter=has_focus & Condition(lambda: len(self.selected_menu) >= 2), ), ), # -- ] + (floats or []), key_bindings=key_bindings, ) def _get_menu(self, level: int) -> MenuItem: menu = self.menu_items[self.selected_menu[0]] for i, index in enumerate(self.selected_menu[1:]): if i < level: try: menu = menu.children[index] except IndexError: return MenuItem("debug") return menu def _get_menu_fragments(self) -> StyleAndTextTuples: focused = get_app().layout.has_focus(self.window) # This is called during the rendering. When we discover that this # widget doesn't have the focus anymore. Reset menu state. if not focused: self.selected_menu = [0] # Generate text fragments for the main menu. def one_item(i: int, item: MenuItem) -> Iterable[OneStyleAndTextTuple]: def mouse_handler(mouse_event: MouseEvent) -> None: hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE if ( mouse_event.event_type == MouseEventType.MOUSE_DOWN or hover and focused ): # Toggle focus. app = get_app() if not hover: if app.layout.has_focus(self.window): if self.selected_menu == [i]: app.layout.focus_last() else: app.layout.focus(self.window) self.selected_menu = [i] yield ("class:menu-bar", " ", mouse_handler) if i == self.selected_menu[0] and focused: yield ("[SetMenuPosition]", "", mouse_handler) style = "class:menu-bar.selected-item" else: style = "class:menu-bar" yield style, item.text, mouse_handler result: StyleAndTextTuples = [] for i, item in enumerate(self.menu_items): result.extend(one_item(i, item)) return result def _submenu(self, level: int = 0) -> Window: def get_text_fragments() -> StyleAndTextTuples: result: StyleAndTextTuples = [] if level < len(self.selected_menu): menu = self._get_menu(level) if menu.children: result.append(("class:menu", Border.TOP_LEFT)) result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4))) result.append(("class:menu", Border.TOP_RIGHT)) result.append(("", "\n")) try: selected_item = self.selected_menu[level + 1] except IndexError: selected_item = -1 def one_item( i: int, item: MenuItem ) -> Iterable[OneStyleAndTextTuple]: def mouse_handler(mouse_event: MouseEvent) -> None: if item.disabled: # The arrow keys can't interact with menu items that are disabled. # The mouse shouldn't be able to either. return hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE if ( mouse_event.event_type == MouseEventType.MOUSE_UP or hover ): app = get_app() if not hover and item.handler: app.layout.focus_last() item.handler() else: self.selected_menu = self.selected_menu[ : level + 1 ] + [i] if i == selected_item: yield ("[SetCursorPosition]", "") style = "class:menu-bar.selected-item" else: style = "" yield ("class:menu", Border.VERTICAL) if item.text == "-": yield ( style + "class:menu-border", f"{Border.HORIZONTAL * (menu.width + 3)}", mouse_handler, ) else: yield ( style, f" {item.text}".ljust(menu.width + 3), mouse_handler, ) if item.children: yield (style, ">", mouse_handler) else: yield (style, " ", mouse_handler) if i == selected_item: yield ("[SetMenuPosition]", "") yield ("class:menu", Border.VERTICAL) yield ("", "\n") for i, item in enumerate(menu.children): result.extend(one_item(i, item)) result.append(("class:menu", Border.BOTTOM_LEFT)) result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4))) result.append(("class:menu", Border.BOTTOM_RIGHT)) return result return Window(FormattedTextControl(get_text_fragments), style="class:menu") @property def floats(self) -> list[Float] | None: return self.container.floats def __pt_container__(self) -> Container: return self.container class MenuItem: def __init__( self, text: str = "", handler: Callable[[], None] | None = None, children: list[MenuItem] | None = None, shortcut: Sequence[Keys | str] | None = None, disabled: bool = False, ) -> None: self.text = text self.handler = handler self.children = children or [] self.shortcut = shortcut self.disabled = disabled self.selected_item = 0 @property def width(self) -> int: if self.children: return max(get_cwidth(c.text) for c in self.children) else: return 0 ================================================ FILE: src/prompt_toolkit/widgets/toolbars.py ================================================ from __future__ import annotations from typing import Any from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer from prompt_toolkit.enums import SYSTEM_BUFFER from prompt_toolkit.filters import ( Condition, FilterOrBool, emacs_mode, has_arg, has_completions, has_focus, has_validation_error, to_filter, vi_mode, vi_navigation_mode, ) from prompt_toolkit.formatted_text import ( AnyFormattedText, StyleAndTextTuples, fragment_list_len, to_formatted_text, ) from prompt_toolkit.key_binding.key_bindings import ( ConditionalKeyBindings, KeyBindings, KeyBindingsBase, merge_key_bindings, ) from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window from prompt_toolkit.layout.controls import ( BufferControl, FormattedTextControl, SearchBufferControl, UIContent, UIControl, ) from prompt_toolkit.layout.dimension import Dimension from prompt_toolkit.layout.processors import BeforeInput from prompt_toolkit.lexers import SimpleLexer from prompt_toolkit.search import SearchDirection __all__ = [ "ArgToolbar", "CompletionsToolbar", "FormattedTextToolbar", "SearchToolbar", "SystemToolbar", "ValidationToolbar", ] E = KeyPressEvent class FormattedTextToolbar(Window): def __init__(self, text: AnyFormattedText, style: str = "", **kw: Any) -> None: # Note: The style needs to be applied to the toolbar as a whole, not # just the `FormattedTextControl`. super().__init__( FormattedTextControl(text, **kw), style=style, dont_extend_height=True, height=Dimension(min=1), ) class SystemToolbar: """ Toolbar for a system prompt. :param prompt: Prompt to be displayed to the user. """ def __init__( self, prompt: AnyFormattedText = "Shell command: ", enable_global_bindings: FilterOrBool = True, ) -> None: self.prompt = prompt self.enable_global_bindings = to_filter(enable_global_bindings) self.system_buffer = Buffer(name=SYSTEM_BUFFER) self._bindings = self._build_key_bindings() self.buffer_control = BufferControl( buffer=self.system_buffer, lexer=SimpleLexer(style="class:system-toolbar.text"), input_processors=[ BeforeInput(lambda: self.prompt, style="class:system-toolbar") ], key_bindings=self._bindings, ) self.window = Window( self.buffer_control, height=1, style="class:system-toolbar" ) self.container = ConditionalContainer( content=self.window, filter=has_focus(self.system_buffer) ) def _get_display_before_text(self) -> StyleAndTextTuples: return [ ("class:system-toolbar", "Shell command: "), ("class:system-toolbar.text", self.system_buffer.text), ("", "\n"), ] def _build_key_bindings(self) -> KeyBindingsBase: focused = has_focus(self.system_buffer) # Emacs emacs_bindings = KeyBindings() handle = emacs_bindings.add @handle("escape", filter=focused) @handle("c-g", filter=focused) @handle("c-c", filter=focused) def _cancel(event: E) -> None: "Hide system prompt." self.system_buffer.reset() event.app.layout.focus_last() @handle("enter", filter=focused) async def _accept(event: E) -> None: "Run system command." await event.app.run_system_command( self.system_buffer.text, display_before_text=self._get_display_before_text(), ) self.system_buffer.reset(append_to_history=True) event.app.layout.focus_last() # Vi. vi_bindings = KeyBindings() handle = vi_bindings.add @handle("escape", filter=focused) @handle("c-c", filter=focused) def _cancel_vi(event: E) -> None: "Hide system prompt." event.app.vi_state.input_mode = InputMode.NAVIGATION self.system_buffer.reset() event.app.layout.focus_last() @handle("enter", filter=focused) async def _accept_vi(event: E) -> None: "Run system command." event.app.vi_state.input_mode = InputMode.NAVIGATION await event.app.run_system_command( self.system_buffer.text, display_before_text=self._get_display_before_text(), ) self.system_buffer.reset(append_to_history=True) event.app.layout.focus_last() # Global bindings. (Listen to these bindings, even when this widget is # not focussed.) global_bindings = KeyBindings() handle = global_bindings.add @handle(Keys.Escape, "!", filter=~focused & emacs_mode, is_global=True) def _focus_me(event: E) -> None: "M-'!' will focus this user control." event.app.layout.focus(self.window) @handle("!", filter=~focused & vi_mode & vi_navigation_mode, is_global=True) def _focus_me_vi(event: E) -> None: "Focus." event.app.vi_state.input_mode = InputMode.INSERT event.app.layout.focus(self.window) return merge_key_bindings( [ ConditionalKeyBindings(emacs_bindings, emacs_mode), ConditionalKeyBindings(vi_bindings, vi_mode), ConditionalKeyBindings(global_bindings, self.enable_global_bindings), ] ) def __pt_container__(self) -> Container: return self.container class ArgToolbar: def __init__(self) -> None: def get_formatted_text() -> StyleAndTextTuples: arg = get_app().key_processor.arg or "" if arg == "-": arg = "-1" return [ ("class:arg-toolbar", "Repeat: "), ("class:arg-toolbar.text", arg), ] self.window = Window(FormattedTextControl(get_formatted_text), height=1) self.container = ConditionalContainer(content=self.window, filter=has_arg) def __pt_container__(self) -> Container: return self.container class SearchToolbar: """ :param vi_mode: Display '/' and '?' instead of I-search. :param ignore_case: Search case insensitive. """ def __init__( self, search_buffer: Buffer | None = None, vi_mode: bool = False, text_if_not_searching: AnyFormattedText = "", forward_search_prompt: AnyFormattedText = "I-search: ", backward_search_prompt: AnyFormattedText = "I-search backward: ", ignore_case: FilterOrBool = False, ) -> None: if search_buffer is None: search_buffer = Buffer() @Condition def is_searching() -> bool: return self.control in get_app().layout.search_links def get_before_input() -> AnyFormattedText: if not is_searching(): return text_if_not_searching elif ( self.control.searcher_search_state.direction == SearchDirection.BACKWARD ): return "?" if vi_mode else backward_search_prompt else: return "/" if vi_mode else forward_search_prompt self.search_buffer = search_buffer self.control = SearchBufferControl( buffer=search_buffer, input_processors=[ BeforeInput(get_before_input, style="class:search-toolbar.prompt") ], lexer=SimpleLexer(style="class:search-toolbar.text"), ignore_case=ignore_case, ) self.container = ConditionalContainer( content=Window(self.control, height=1, style="class:search-toolbar"), filter=is_searching, ) def __pt_container__(self) -> Container: return self.container class _CompletionsToolbarControl(UIControl): def create_content(self, width: int, height: int) -> UIContent: all_fragments: StyleAndTextTuples = [] complete_state = get_app().current_buffer.complete_state if complete_state: completions = complete_state.completions index = complete_state.complete_index # Can be None! # Width of the completions without the left/right arrows in the margins. content_width = width - 6 # Booleans indicating whether we stripped from the left/right cut_left = False cut_right = False # Create Menu content. fragments: StyleAndTextTuples = [] for i, c in enumerate(completions): # When there is no more place for the next completion if fragment_list_len(fragments) + len(c.display_text) >= content_width: # If the current one was not yet displayed, page to the next sequence. if i <= (index or 0): fragments = [] cut_left = True # If the current one is visible, stop here. else: cut_right = True break fragments.extend( to_formatted_text( c.display_text, style=( "class:completion-toolbar.completion.current" if i == index else "class:completion-toolbar.completion" ), ) ) fragments.append(("", " ")) # Extend/strip until the content width. fragments.append(("", " " * (content_width - fragment_list_len(fragments)))) fragments = fragments[:content_width] # Return fragments all_fragments.append(("", " ")) all_fragments.append( ("class:completion-toolbar.arrow", "<" if cut_left else " ") ) all_fragments.append(("", " ")) all_fragments.extend(fragments) all_fragments.append(("", " ")) all_fragments.append( ("class:completion-toolbar.arrow", ">" if cut_right else " ") ) all_fragments.append(("", " ")) def get_line(i: int) -> StyleAndTextTuples: return all_fragments return UIContent(get_line=get_line, line_count=1) class CompletionsToolbar: def __init__(self) -> None: self.container = ConditionalContainer( content=Window( _CompletionsToolbarControl(), height=1, style="class:completion-toolbar" ), filter=has_completions, ) def __pt_container__(self) -> Container: return self.container class ValidationToolbar: def __init__(self, show_position: bool = False) -> None: def get_formatted_text() -> StyleAndTextTuples: buff = get_app().current_buffer if buff.validation_error: row, column = buff.document.translate_index_to_position( buff.validation_error.cursor_position ) if show_position: text = f"{buff.validation_error.message} (line={row + 1} column={column + 1})" else: text = buff.validation_error.message return [("class:validation-toolbar", text)] else: return [] self.control = FormattedTextControl(get_formatted_text) self.container = ConditionalContainer( content=Window(self.control, height=1), filter=has_validation_error ) def __pt_container__(self) -> Container: return self.container ================================================ FILE: src/prompt_toolkit/win32_types.py ================================================ from __future__ import annotations from ctypes import Structure, Union, c_char, c_long, c_short, c_ulong from ctypes.wintypes import BOOL, DWORD, LPVOID, WCHAR, WORD from typing import TYPE_CHECKING # Input/Output standard device numbers. Note that these are not handle objects. # It's the `windll.kernel32.GetStdHandle` system call that turns them into a # real handle object. STD_INPUT_HANDLE = c_ulong(-10) STD_OUTPUT_HANDLE = c_ulong(-11) STD_ERROR_HANDLE = c_ulong(-12) class COORD(Structure): """ Struct in wincon.h http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx """ if TYPE_CHECKING: X: int Y: int _fields_ = [ ("X", c_short), # Short ("Y", c_short), # Short ] def __repr__(self) -> str: return "{}(X={!r}, Y={!r}, type_x={!r}, type_y={!r})".format( self.__class__.__name__, self.X, self.Y, type(self.X), type(self.Y), ) class UNICODE_OR_ASCII(Union): if TYPE_CHECKING: AsciiChar: bytes UnicodeChar: str _fields_ = [ ("AsciiChar", c_char), ("UnicodeChar", WCHAR), ] class KEY_EVENT_RECORD(Structure): """ http://msdn.microsoft.com/en-us/library/windows/desktop/ms684166(v=vs.85).aspx """ if TYPE_CHECKING: KeyDown: int RepeatCount: int VirtualKeyCode: int VirtualScanCode: int uChar: UNICODE_OR_ASCII ControlKeyState: int _fields_ = [ ("KeyDown", c_long), # bool ("RepeatCount", c_short), # word ("VirtualKeyCode", c_short), # word ("VirtualScanCode", c_short), # word ("uChar", UNICODE_OR_ASCII), # Unicode or ASCII. ("ControlKeyState", c_long), # double word ] class MOUSE_EVENT_RECORD(Structure): """ http://msdn.microsoft.com/en-us/library/windows/desktop/ms684239(v=vs.85).aspx """ if TYPE_CHECKING: MousePosition: COORD ButtonState: int ControlKeyState: int EventFlags: int _fields_ = [ ("MousePosition", COORD), ("ButtonState", c_long), # dword ("ControlKeyState", c_long), # dword ("EventFlags", c_long), # dword ] class WINDOW_BUFFER_SIZE_RECORD(Structure): """ http://msdn.microsoft.com/en-us/library/windows/desktop/ms687093(v=vs.85).aspx """ if TYPE_CHECKING: Size: COORD _fields_ = [("Size", COORD)] class MENU_EVENT_RECORD(Structure): """ http://msdn.microsoft.com/en-us/library/windows/desktop/ms684213(v=vs.85).aspx """ if TYPE_CHECKING: CommandId: int _fields_ = [("CommandId", c_long)] # uint class FOCUS_EVENT_RECORD(Structure): """ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683149(v=vs.85).aspx """ if TYPE_CHECKING: SetFocus: int _fields_ = [("SetFocus", c_long)] # bool class EVENT_RECORD(Union): if TYPE_CHECKING: KeyEvent: KEY_EVENT_RECORD MouseEvent: MOUSE_EVENT_RECORD WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD MenuEvent: MENU_EVENT_RECORD FocusEvent: FOCUS_EVENT_RECORD _fields_ = [ ("KeyEvent", KEY_EVENT_RECORD), ("MouseEvent", MOUSE_EVENT_RECORD), ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), ("MenuEvent", MENU_EVENT_RECORD), ("FocusEvent", FOCUS_EVENT_RECORD), ] class INPUT_RECORD(Structure): """ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx """ if TYPE_CHECKING: EventType: int Event: EVENT_RECORD _fields_ = [("EventType", c_short), ("Event", EVENT_RECORD)] # word # Union. EventTypes = { 1: "KeyEvent", 2: "MouseEvent", 4: "WindowBufferSizeEvent", 8: "MenuEvent", 16: "FocusEvent", } class SMALL_RECT(Structure): """struct in wincon.h.""" if TYPE_CHECKING: Left: int Top: int Right: int Bottom: int _fields_ = [ ("Left", c_short), ("Top", c_short), ("Right", c_short), ("Bottom", c_short), ] class CONSOLE_SCREEN_BUFFER_INFO(Structure): """struct in wincon.h.""" if TYPE_CHECKING: dwSize: COORD dwCursorPosition: COORD wAttributes: int srWindow: SMALL_RECT dwMaximumWindowSize: COORD _fields_ = [ ("dwSize", COORD), ("dwCursorPosition", COORD), ("wAttributes", WORD), ("srWindow", SMALL_RECT), ("dwMaximumWindowSize", COORD), ] def __repr__(self) -> str: return "CONSOLE_SCREEN_BUFFER_INFO({!r},{!r},{!r},{!r},{!r},{!r},{!r},{!r},{!r},{!r},{!r})".format( self.dwSize.Y, self.dwSize.X, self.dwCursorPosition.Y, self.dwCursorPosition.X, self.wAttributes, self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right, self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X, ) class SECURITY_ATTRIBUTES(Structure): """ http://msdn.microsoft.com/en-us/library/windows/desktop/aa379560(v=vs.85).aspx """ if TYPE_CHECKING: nLength: int lpSecurityDescriptor: int bInheritHandle: int # BOOL comes back as 'int'. _fields_ = [ ("nLength", DWORD), ("lpSecurityDescriptor", LPVOID), ("bInheritHandle", BOOL), ] ================================================ FILE: tests/test_async_generator.py ================================================ from __future__ import annotations from asyncio import run from prompt_toolkit.eventloop import generator_to_async_generator def _sync_generator(): yield 1 yield 10 def test_generator_to_async_generator(): """ Test conversion of sync to async generator. This should run the synchronous parts in a background thread. """ async_gen = generator_to_async_generator(_sync_generator) items = [] async def consume_async_generator(): async for item in async_gen: items.append(item) # Run the event loop until all items are collected. run(consume_async_generator()) assert items == [1, 10] ================================================ FILE: tests/test_buffer.py ================================================ from __future__ import annotations import pytest from prompt_toolkit.buffer import Buffer @pytest.fixture def _buffer(): buff = Buffer() return buff def test_initial(_buffer): assert _buffer.text == "" assert _buffer.cursor_position == 0 def test_insert_text(_buffer): _buffer.insert_text("some_text") assert _buffer.text == "some_text" assert _buffer.cursor_position == len("some_text") def test_cursor_movement(_buffer): _buffer.insert_text("some_text") _buffer.cursor_left() _buffer.cursor_left() _buffer.cursor_left() _buffer.cursor_right() _buffer.insert_text("A") assert _buffer.text == "some_teAxt" assert _buffer.cursor_position == len("some_teA") def test_backspace(_buffer): _buffer.insert_text("some_text") _buffer.cursor_left() _buffer.cursor_left() _buffer.delete_before_cursor() assert _buffer.text == "some_txt" assert _buffer.cursor_position == len("some_t") def test_cursor_up(_buffer): # Cursor up to a line thats longer. _buffer.insert_text("long line1\nline2") _buffer.cursor_up() assert _buffer.document.cursor_position == 5 # Going up when already at the top. _buffer.cursor_up() assert _buffer.document.cursor_position == 5 # Going up to a line that's shorter. _buffer.reset() _buffer.insert_text("line1\nlong line2") _buffer.cursor_up() assert _buffer.document.cursor_position == 5 def test_cursor_down(_buffer): _buffer.insert_text("line1\nline2") _buffer.cursor_position = 3 # Normally going down _buffer.cursor_down() assert _buffer.document.cursor_position == len("line1\nlin") # Going down to a line that's shorter. _buffer.reset() _buffer.insert_text("long line1\na\nb") _buffer.cursor_position = 3 _buffer.cursor_down() assert _buffer.document.cursor_position == len("long line1\na") def test_join_next_line(_buffer): _buffer.insert_text("line1\nline2\nline3") _buffer.cursor_up() _buffer.join_next_line() assert _buffer.text == "line1\nline2 line3" # Test when there is no '\n' in the text _buffer.reset() _buffer.insert_text("line1") _buffer.cursor_position = 0 _buffer.join_next_line() assert _buffer.text == "line1" def test_newline(_buffer): _buffer.insert_text("hello world") _buffer.newline() assert _buffer.text == "hello world\n" def test_swap_characters_before_cursor(_buffer): _buffer.insert_text("hello world") _buffer.cursor_left() _buffer.cursor_left() _buffer.swap_characters_before_cursor() assert _buffer.text == "hello wrold" ================================================ FILE: tests/test_cli.py ================================================ """ These are almost end-to-end tests. They create a Prompt, feed it with some input and check the result. """ from __future__ import annotations from functools import partial import pytest from prompt_toolkit.clipboard import ClipboardData, InMemoryClipboard from prompt_toolkit.enums import EditingMode from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.input.defaults import create_pipe_input from prompt_toolkit.input.vt100_parser import ANSI_SEQUENCES from prompt_toolkit.key_binding.bindings.named_commands import prefix_meta from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.output import DummyOutput from prompt_toolkit.shortcuts import PromptSession def _history(): h = InMemoryHistory() h.append_string("line1 first input") h.append_string("line2 second input") h.append_string("line3 third input") return h def _feed_cli_with_input( text, editing_mode=EditingMode.EMACS, clipboard=None, history=None, multiline=False, check_line_ending=True, key_bindings=None, ): """ Create a Prompt, feed it with the given user input and return the CLI object. This returns a (result, Application) tuple. """ # If the given text doesn't end with a newline, the interface won't finish. if check_line_ending: assert text.endswith("\r") with create_pipe_input() as inp: inp.send_text(text) session = PromptSession( input=inp, output=DummyOutput(), editing_mode=editing_mode, history=history, multiline=multiline, clipboard=clipboard, key_bindings=key_bindings, ) _ = session.prompt() return session.default_buffer.document, session.app def test_simple_text_input(): # Simple text input, followed by enter. result, cli = _feed_cli_with_input("hello\r") assert result.text == "hello" assert cli.current_buffer.text == "hello" def test_emacs_cursor_movements(): """ Test cursor movements with Emacs key bindings. """ # ControlA (beginning-of-line) result, cli = _feed_cli_with_input("hello\x01X\r") assert result.text == "Xhello" # ControlE (end-of-line) result, cli = _feed_cli_with_input("hello\x01X\x05Y\r") assert result.text == "XhelloY" # ControlH or \b result, cli = _feed_cli_with_input("hello\x08X\r") assert result.text == "hellX" # Delete. (Left, left, delete) result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x1b[3~\r") assert result.text == "helo" # Left. result, cli = _feed_cli_with_input("hello\x1b[DX\r") assert result.text == "hellXo" # ControlA, right result, cli = _feed_cli_with_input("hello\x01\x1b[CX\r") assert result.text == "hXello" # ControlB (backward-char) result, cli = _feed_cli_with_input("hello\x02X\r") assert result.text == "hellXo" # ControlF (forward-char) result, cli = _feed_cli_with_input("hello\x01\x06X\r") assert result.text == "hXello" # ControlD: delete after cursor. result, cli = _feed_cli_with_input("hello\x01\x04\r") assert result.text == "ello" # ControlD at the end of the input ssshould not do anything. result, cli = _feed_cli_with_input("hello\x04\r") assert result.text == "hello" # Left, Left, ControlK (kill-line) result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x0b\r") assert result.text == "hel" # Left, Left Esc- ControlK (kill-line, but negative) result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x1b-\x0b\r") assert result.text == "lo" # ControlL: should not influence the result. result, cli = _feed_cli_with_input("hello\x0c\r") assert result.text == "hello" # ControlRight (forward-word) result, cli = _feed_cli_with_input("hello world\x01X\x1b[1;5CY\r") assert result.text == "XhelloY world" # ContrlolLeft (backward-word) result, cli = _feed_cli_with_input("hello world\x1b[1;5DY\r") assert result.text == "hello Yworld" # <esc>-f with argument. (forward-word) result, cli = _feed_cli_with_input("hello world abc def\x01\x1b3\x1bfX\r") assert result.text == "hello world abcX def" # <esc>-f with negative argument. (forward-word) result, cli = _feed_cli_with_input("hello world abc def\x1b-\x1b3\x1bfX\r") assert result.text == "hello Xworld abc def" # <esc>-b with argument. (backward-word) result, cli = _feed_cli_with_input("hello world abc def\x1b3\x1bbX\r") assert result.text == "hello Xworld abc def" # <esc>-b with negative argument. (backward-word) result, cli = _feed_cli_with_input("hello world abc def\x01\x1b-\x1b3\x1bbX\r") assert result.text == "hello world abc Xdef" # ControlW (kill-word / unix-word-rubout) result, cli = _feed_cli_with_input("hello world\x17\r") assert result.text == "hello " assert cli.clipboard.get_data().text == "world" result, cli = _feed_cli_with_input("test hello world\x1b2\x17\r") assert result.text == "test " # Escape Backspace (unix-word-rubout) result, cli = _feed_cli_with_input("hello world\x1b\x7f\r") assert result.text == "hello " assert cli.clipboard.get_data().text == "world" result, cli = _feed_cli_with_input("hello world\x1b\x08\r") assert result.text == "hello " assert cli.clipboard.get_data().text == "world" # Backspace (backward-delete-char) result, cli = _feed_cli_with_input("hello world\x7f\r") assert result.text == "hello worl" assert result.cursor_position == len("hello worl") result, cli = _feed_cli_with_input("hello world\x08\r") assert result.text == "hello worl" assert result.cursor_position == len("hello worl") # Delete (delete-char) result, cli = _feed_cli_with_input("hello world\x01\x1b[3~\r") assert result.text == "ello world" assert result.cursor_position == 0 # Escape-\\ (delete-horizontal-space) result, cli = _feed_cli_with_input("hello world\x1b8\x02\x1b\\\r") assert result.text == "helloworld" assert result.cursor_position == len("hello") def test_emacs_kill_multiple_words_and_paste(): # Using control-w twice should place both words on the clipboard. result, cli = _feed_cli_with_input( "hello world test\x17\x17--\x19\x19\r" # Twice c-w. Twice c-y. ) assert result.text == "hello --world testworld test" assert cli.clipboard.get_data().text == "world test" # Using alt-d twice should place both words on the clipboard. result, cli = _feed_cli_with_input( "hello world test" "\x1bb\x1bb" # Twice left. "\x1bd\x1bd" # Twice kill-word. "abc" "\x19" # Paste. "\r" ) assert result.text == "hello abcworld test" assert cli.clipboard.get_data().text == "world test" def test_interrupts(): # ControlC: raise KeyboardInterrupt. with pytest.raises(KeyboardInterrupt): result, cli = _feed_cli_with_input("hello\x03\r") with pytest.raises(KeyboardInterrupt): result, cli = _feed_cli_with_input("hello\x03\r") # ControlD without any input: raises EOFError. with pytest.raises(EOFError): result, cli = _feed_cli_with_input("\x04\r") def test_emacs_yank(): # ControlY (yank) c = InMemoryClipboard(ClipboardData("XYZ")) result, cli = _feed_cli_with_input("hello\x02\x19\r", clipboard=c) assert result.text == "hellXYZo" assert result.cursor_position == len("hellXYZ") def test_quoted_insert(): # ControlQ - ControlB (quoted-insert) result, cli = _feed_cli_with_input("hello\x11\x02\r") assert result.text == "hello\x02" def test_transformations(): # Meta-c (capitalize-word) result, cli = _feed_cli_with_input("hello world\01\x1bc\r") assert result.text == "Hello world" assert result.cursor_position == len("Hello") # Meta-u (uppercase-word) result, cli = _feed_cli_with_input("hello world\01\x1bu\r") assert result.text == "HELLO world" assert result.cursor_position == len("Hello") # Meta-u (downcase-word) result, cli = _feed_cli_with_input("HELLO WORLD\01\x1bl\r") assert result.text == "hello WORLD" assert result.cursor_position == len("Hello") # ControlT (transpose-chars) result, cli = _feed_cli_with_input("hello\x14\r") assert result.text == "helol" assert result.cursor_position == len("hello") # Left, Left, Control-T (transpose-chars) result, cli = _feed_cli_with_input("abcde\x1b[D\x1b[D\x14\r") assert result.text == "abdce" assert result.cursor_position == len("abcd") def test_emacs_other_bindings(): # Transpose characters. result, cli = _feed_cli_with_input("abcde\x14X\r") # Ctrl-T assert result.text == "abcedX" # Left, Left, Transpose. (This is slightly different.) result, cli = _feed_cli_with_input("abcde\x1b[D\x1b[D\x14X\r") assert result.text == "abdcXe" # Clear before cursor. result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x15X\r") assert result.text == "Xlo" # unix-word-rubout: delete word before the cursor. # (ControlW). result, cli = _feed_cli_with_input("hello world test\x17X\r") assert result.text == "hello world X" result, cli = _feed_cli_with_input("hello world /some/very/long/path\x17X\r") assert result.text == "hello world X" # (with argument.) result, cli = _feed_cli_with_input("hello world test\x1b2\x17X\r") assert result.text == "hello X" result, cli = _feed_cli_with_input("hello world /some/very/long/path\x1b2\x17X\r") assert result.text == "hello X" # backward-kill-word: delete word before the cursor. # (Esc-ControlH). result, cli = _feed_cli_with_input("hello world /some/very/long/path\x1b\x08X\r") assert result.text == "hello world /some/very/long/X" # (with arguments.) result, cli = _feed_cli_with_input( "hello world /some/very/long/path\x1b3\x1b\x08X\r" ) assert result.text == "hello world /some/very/X" def test_controlx_controlx(): # At the end: go to the start of the line. result, cli = _feed_cli_with_input("hello world\x18\x18X\r") assert result.text == "Xhello world" assert result.cursor_position == 1 # At the start: go to the end of the line. result, cli = _feed_cli_with_input("hello world\x01\x18\x18X\r") assert result.text == "hello worldX" # Left, Left Control-X Control-X: go to the end of the line. result, cli = _feed_cli_with_input("hello world\x1b[D\x1b[D\x18\x18X\r") assert result.text == "hello worldX" def test_emacs_history_bindings(): # Adding a new item to the history. history = _history() result, cli = _feed_cli_with_input("new input\r", history=history) assert result.text == "new input" history.get_strings()[-1] == "new input" # Go up in history, and accept the last item. result, cli = _feed_cli_with_input("hello\x1b[A\r", history=history) assert result.text == "new input" # Esc< (beginning-of-history) result, cli = _feed_cli_with_input("hello\x1b<\r", history=history) assert result.text == "line1 first input" # Esc> (end-of-history) result, cli = _feed_cli_with_input( "another item\x1b[A\x1b[a\x1b>\r", history=history ) assert result.text == "another item" # ControlUp (previous-history) result, cli = _feed_cli_with_input("\x1b[1;5A\r", history=history) assert result.text == "another item" # Esc< ControlDown (beginning-of-history, next-history) result, cli = _feed_cli_with_input("\x1b<\x1b[1;5B\r", history=history) assert result.text == "line2 second input" def test_emacs_reverse_search(): history = _history() # ControlR (reverse-search-history) result, cli = _feed_cli_with_input("\x12input\r\r", history=history) assert result.text == "line3 third input" # Hitting ControlR twice. result, cli = _feed_cli_with_input("\x12input\x12\r\r", history=history) assert result.text == "line2 second input" def test_emacs_arguments(): """ Test various combinations of arguments in Emacs mode. """ # esc 4 result, cli = _feed_cli_with_input("\x1b4x\r") assert result.text == "xxxx" # esc 4 4 result, cli = _feed_cli_with_input("\x1b44x\r") assert result.text == "x" * 44 # esc 4 esc 4 result, cli = _feed_cli_with_input("\x1b4\x1b4x\r") assert result.text == "x" * 44 # esc - right (-1 position to the right, equals 1 to the left.) result, cli = _feed_cli_with_input("aaaa\x1b-\x1b[Cbbbb\r") assert result.text == "aaabbbba" # esc - 3 right result, cli = _feed_cli_with_input("aaaa\x1b-3\x1b[Cbbbb\r") assert result.text == "abbbbaaa" # esc - - - 3 right result, cli = _feed_cli_with_input("aaaa\x1b---3\x1b[Cbbbb\r") assert result.text == "abbbbaaa" def test_emacs_arguments_for_all_commands(): """ Test all Emacs commands with Meta-[0-9] arguments (both positive and negative). No one should crash. """ for key in ANSI_SEQUENCES: # Ignore BracketedPaste. This would hang forever, because it waits for # the end sequence. if key != "\x1b[200~": try: # Note: we add an 'X' after the key, because Ctrl-Q (quoted-insert) # expects something to follow. We add an additional \r, because # Ctrl-R and Ctrl-S (reverse-search) expect that. result, cli = _feed_cli_with_input("hello\x1b4" + key + "X\r\r") result, cli = _feed_cli_with_input("hello\x1b-" + key + "X\r\r") except KeyboardInterrupt: # This exception should only be raised for Ctrl-C assert key == "\x03" def test_emacs_kill_ring(): operations = ( # abc ControlA ControlK "abc\x01\x0b" # def ControlA ControlK "def\x01\x0b" # ghi ControlA ControlK "ghi\x01\x0b" # ControlY (yank) "\x19" ) result, cli = _feed_cli_with_input(operations + "\r") assert result.text == "ghi" result, cli = _feed_cli_with_input(operations + "\x1by\r") assert result.text == "def" result, cli = _feed_cli_with_input(operations + "\x1by\x1by\r") assert result.text == "abc" result, cli = _feed_cli_with_input(operations + "\x1by\x1by\x1by\r") assert result.text == "ghi" def test_emacs_selection(): # Copy/paste empty selection should not do anything. operations = ( "hello" # Twice left. "\x1b[D\x1b[D" # Control-Space "\x00" # ControlW (cut) "\x17" # ControlY twice. (paste twice) "\x19\x19\r" ) result, cli = _feed_cli_with_input(operations) assert result.text == "hello" # Copy/paste one character. operations = ( "hello" # Twice left. "\x1b[D\x1b[D" # Control-Space "\x00" # Right. "\x1b[C" # ControlW (cut) "\x17" # ControlA (Home). "\x01" # ControlY (paste) "\x19\r" ) result, cli = _feed_cli_with_input(operations) assert result.text == "lhelo" def test_emacs_insert_comment(): # Test insert-comment (M-#) binding. result, cli = _feed_cli_with_input("hello\x1b#", check_line_ending=False) assert result.text == "#hello" result, cli = _feed_cli_with_input( "hello\rworld\x1b#", check_line_ending=False, multiline=True ) assert result.text == "#hello\n#world" def test_emacs_record_macro(): operations = ( " " "\x18(" # Start recording macro. C-X( "hello" "\x18)" # Stop recording macro. " " "\x18e" # Execute macro. "\x18e" # Execute macro. "\r" ) result, cli = _feed_cli_with_input(operations) assert result.text == " hello hellohello" def test_emacs_nested_macro(): "Test calling the macro within a macro." # Calling a macro within a macro should take the previous recording (if one # exists), not the one that is in progress. operations = ( "\x18(" # Start recording macro. C-X( "hello" "\x18e" # Execute macro. "\x18)" # Stop recording macro. "\x18e" # Execute macro. "\r" ) result, cli = _feed_cli_with_input(operations) assert result.text == "hellohello" operations = ( "\x18(" # Start recording macro. C-X( "hello" "\x18)" # Stop recording macro. "\x18(" # Start recording macro. C-X( "\x18e" # Execute macro. "world" "\x18)" # Stop recording macro. "\x01\x0b" # Delete all (c-a c-k). "\x18e" # Execute macro. "\r" ) result, cli = _feed_cli_with_input(operations) assert result.text == "helloworld" def test_prefix_meta(): # Test the prefix-meta command. b = KeyBindings() b.add("j", "j", filter=ViInsertMode())(prefix_meta) result, cli = _feed_cli_with_input( "hellojjIX\r", key_bindings=b, editing_mode=EditingMode.VI ) assert result.text == "Xhello" def test_bracketed_paste(): result, cli = _feed_cli_with_input("\x1b[200~hello world\x1b[201~\r") assert result.text == "hello world" result, cli = _feed_cli_with_input("\x1b[200~hello\rworld\x1b[201~\x1b\r") assert result.text == "hello\nworld" # With \r\n endings. result, cli = _feed_cli_with_input("\x1b[200~hello\r\nworld\x1b[201~\x1b\r") assert result.text == "hello\nworld" # With \n endings. result, cli = _feed_cli_with_input("\x1b[200~hello\nworld\x1b[201~\x1b\r") assert result.text == "hello\nworld" def test_vi_cursor_movements(): """ Test cursor movements with Vi key bindings. """ feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) result, cli = feed("\x1b\r") assert result.text == "" assert cli.editing_mode == EditingMode.VI # Esc h a X result, cli = feed("hello\x1bhaX\r") assert result.text == "hellXo" # Esc I X result, cli = feed("hello\x1bIX\r") assert result.text == "Xhello" # Esc I X result, cli = feed("hello\x1bIX\r") assert result.text == "Xhello" # Esc 2hiX result, cli = feed("hello\x1b2hiX\r") assert result.text == "heXllo" # Esc 2h2liX result, cli = feed("hello\x1b2h2liX\r") assert result.text == "hellXo" # Esc \b\b result, cli = feed("hello\b\b\r") assert result.text == "hel" # Esc \b\b result, cli = feed("hello\b\b\r") assert result.text == "hel" # Esc 2h D result, cli = feed("hello\x1b2hD\r") assert result.text == "he" # Esc 2h rX \r result, cli = feed("hello\x1b2hrX\r") assert result.text == "heXlo" def test_vi_operators(): feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) # Esc g~0 result, cli = feed("hello\x1bg~0\r") assert result.text == "HELLo" # Esc gU0 result, cli = feed("hello\x1bgU0\r") assert result.text == "HELLo" # Esc d0 result, cli = feed("hello\x1bd0\r") assert result.text == "o" def test_vi_text_objects(): feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) # Esc gUgg result, cli = feed("hello\x1bgUgg\r") assert result.text == "HELLO" # Esc gUU result, cli = feed("hello\x1bgUU\r") assert result.text == "HELLO" # Esc di( result, cli = feed("before(inside)after\x1b8hdi(\r") assert result.text == "before()after" # Esc di[ result, cli = feed("before[inside]after\x1b8hdi[\r") assert result.text == "before[]after" # Esc da( result, cli = feed("before(inside)after\x1b8hda(\r") assert result.text == "beforeafter" def test_vi_digraphs(): feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) # C-K o/ result, cli = feed("hello\x0bo/\r") assert result.text == "helloø" # C-K /o (reversed input.) result, cli = feed("hello\x0b/o\r") assert result.text == "helloø" # C-K e: result, cli = feed("hello\x0be:\r") assert result.text == "helloë" # C-K xxy (Unknown digraph.) result, cli = feed("hello\x0bxxy\r") assert result.text == "helloy" def test_vi_block_editing(): "Test Vi Control-V style block insertion." feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) operations = ( # Six lines of text. "-line1\r-line2\r-line3\r-line4\r-line5\r-line6" # Go to the second character of the second line. "\x1bkkkkkkkj0l" # Enter Visual block mode. "\x16" # Go down two more lines. "jj" # Go 3 characters to the right. "lll" # Go to insert mode. "insert" # (Will be replaced.) # Insert stars. "***" # Escape again. "\x1b\r" ) # Control-I result, cli = feed(operations.replace("insert", "I")) assert result.text == "-line1\n-***line2\n-***line3\n-***line4\n-line5\n-line6" # Control-A result, cli = feed(operations.replace("insert", "A")) assert result.text == "-line1\n-line***2\n-line***3\n-line***4\n-line5\n-line6" def test_vi_block_editing_empty_lines(): "Test block editing on empty lines." feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) operations = ( # Six empty lines. "\r\r\r\r\r" # Go to beginning of the document. "\x1bgg" # Enter Visual block mode. "\x16" # Go down two more lines. "jj" # Go 3 characters to the right. "lll" # Go to insert mode. "insert" # (Will be replaced.) # Insert stars. "***" # Escape again. "\x1b\r" ) # Control-I result, cli = feed(operations.replace("insert", "I")) assert result.text == "***\n***\n***\n\n\n" # Control-A result, cli = feed(operations.replace("insert", "A")) assert result.text == "***\n***\n***\n\n\n" def test_vi_visual_line_copy(): feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) operations = ( # Three lines of text. "-line1\r-line2\r-line3\r-line4\r-line5\r-line6" # Go to the second character of the second line. "\x1bkkkkkkkj0l" # Enter Visual linemode. "V" # Go down one line. "j" # Go 3 characters to the right (should not do much). "lll" # Copy this block. "y" # Go down one line. "j" # Insert block twice. "2p" # Escape again. "\x1b\r" ) result, cli = feed(operations) assert ( result.text == "-line1\n-line2\n-line3\n-line4\n-line2\n-line3\n-line2\n-line3\n-line5\n-line6" ) def test_vi_visual_empty_line(): """ Test edge case with an empty line in Visual-line mode. """ feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) # 1. Delete first two lines. operations = ( # Three lines of text. The middle one is empty. "hello\r\rworld" # Go to the start. "\x1bgg" # Visual line and move down. "Vj" # Delete. "d\r" ) result, cli = feed(operations) assert result.text == "world" # 1. Delete middle line. operations = ( # Three lines of text. The middle one is empty. "hello\r\rworld" # Go to middle line. "\x1bggj" # Delete line "Vd\r" ) result, cli = feed(operations) assert result.text == "hello\nworld" def test_vi_character_delete_after_cursor(): "Test 'x' keypress." feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) # Delete one character. result, cli = feed("abcd\x1bHx\r") assert result.text == "bcd" # Delete multiple character.s result, cli = feed("abcd\x1bH3x\r") assert result.text == "d" # Delete on empty line. result, cli = feed("\x1bo\x1bo\x1bggx\r") assert result.text == "\n\n" # Delete multiple on empty line. result, cli = feed("\x1bo\x1bo\x1bgg10x\r") assert result.text == "\n\n" # Delete multiple on empty line. result, cli = feed("hello\x1bo\x1bo\x1bgg3x\r") assert result.text == "lo\n\n" def test_vi_character_delete_before_cursor(): "Test 'X' keypress." feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) # Delete one character. result, cli = feed("abcd\x1bX\r") assert result.text == "abd" # Delete multiple character. result, cli = feed("hello world\x1b3X\r") assert result.text == "hello wd" # Delete multiple character on multiple lines. result, cli = feed("hello\x1boworld\x1bgg$3X\r") assert result.text == "ho\nworld" result, cli = feed("hello\x1boworld\x1b100X\r") assert result.text == "hello\nd" # Delete on empty line. result, cli = feed("\x1bo\x1bo\x1b10X\r") assert result.text == "\n\n" def test_vi_character_paste(): feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) # Test 'p' character paste. result, cli = feed("abcde\x1bhhxp\r") assert result.text == "abdce" assert result.cursor_position == 3 # Test 'P' character paste. result, cli = feed("abcde\x1bhhxP\r") assert result.text == "abcde" assert result.cursor_position == 2 def test_vi_temp_navigation_mode(): """ Test c-o binding: go for one action into navigation mode. """ feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) result, cli = feed("abcde\x0f3hx\r") # c-o # 3 times to the left. assert result.text == "axbcde" assert result.cursor_position == 2 result, cli = feed("abcde\x0fbx\r") # c-o # One word backwards. assert result.text == "xabcde" assert result.cursor_position == 1 # In replace mode result, cli = feed( "abcdef" "\x1b" # Navigation mode. "0l" # Start of line, one character to the right. "R" # Replace mode "78" "\x0f" # c-o "l" # One character forwards. "9\r" ) assert result.text == "a78d9f" assert result.cursor_position == 5 def test_vi_macros(): feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) # Record and execute macro. result, cli = feed("\x1bqcahello\x1bq@c\r") assert result.text == "hellohello" assert result.cursor_position == 9 # Running unknown macro. result, cli = feed("\x1b@d\r") assert result.text == "" assert result.cursor_position == 0 # When a macro is called within a macro. # It shouldn't result in eternal recursion. result, cli = feed("\x1bqxahello\x1b@xq@x\r") assert result.text == "hellohello" assert result.cursor_position == 9 # Nested macros. result, cli = feed( # Define macro 'x'. "\x1bqxahello\x1bq" # Define macro 'y' which calls 'x'. "qya\x1b@xaworld\x1bq" # Delete line. "2dd" # Execute 'y' "@y\r" ) assert result.text == "helloworld" def test_accept_default(): """ Test `prompt(accept_default=True)`. """ with create_pipe_input() as inp: session = PromptSession(input=inp, output=DummyOutput()) result = session.prompt(default="hello", accept_default=True) assert result == "hello" # Test calling prompt() for a second time. (We had an issue where the # prompt reset between calls happened at the wrong time, breaking this.) result = session.prompt(default="world", accept_default=True) assert result == "world" ================================================ FILE: tests/test_completion.py ================================================ from __future__ import annotations import os import re import shutil import tempfile from contextlib import contextmanager from prompt_toolkit.completion import ( CompleteEvent, FuzzyWordCompleter, NestedCompleter, PathCompleter, WordCompleter, merge_completers, ) from prompt_toolkit.document import Document @contextmanager def chdir(directory): """Context manager for current working directory temporary change.""" orig_dir = os.getcwd() os.chdir(directory) try: yield finally: os.chdir(orig_dir) def write_test_files(test_dir, names=None): """Write test files in test_dir using the names list.""" names = names or range(10) for i in names: with open(os.path.join(test_dir, str(i)), "wb") as out: out.write(b"") def test_pathcompleter_completes_in_current_directory(): completer = PathCompleter() doc_text = "" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) assert len(completions) > 0 def test_pathcompleter_completes_files_in_current_directory(): # setup: create a test dir with 10 files test_dir = tempfile.mkdtemp() write_test_files(test_dir) expected = sorted(str(i) for i in range(10)) if not test_dir.endswith(os.path.sep): test_dir += os.path.sep with chdir(test_dir): completer = PathCompleter() # this should complete on the cwd doc_text = "" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = sorted(c.text for c in completions) assert expected == result # cleanup shutil.rmtree(test_dir) def test_pathcompleter_completes_files_in_absolute_directory(): # setup: create a test dir with 10 files test_dir = tempfile.mkdtemp() write_test_files(test_dir) expected = sorted(str(i) for i in range(10)) test_dir = os.path.abspath(test_dir) if not test_dir.endswith(os.path.sep): test_dir += os.path.sep completer = PathCompleter() # force unicode doc_text = str(test_dir) doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = sorted(c.text for c in completions) assert expected == result # cleanup shutil.rmtree(test_dir) def test_pathcompleter_completes_directories_with_only_directories(): # setup: create a test dir with 10 files test_dir = tempfile.mkdtemp() write_test_files(test_dir) # create a sub directory there os.mkdir(os.path.join(test_dir, "subdir")) if not test_dir.endswith(os.path.sep): test_dir += os.path.sep with chdir(test_dir): completer = PathCompleter(only_directories=True) doc_text = "" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = [c.text for c in completions] assert ["subdir"] == result # check that there is no completion when passing a file with chdir(test_dir): completer = PathCompleter(only_directories=True) doc_text = "1" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) assert [] == completions # cleanup shutil.rmtree(test_dir) def test_pathcompleter_respects_completions_under_min_input_len(): # setup: create a test dir with 10 files test_dir = tempfile.mkdtemp() write_test_files(test_dir) # min len:1 and no text with chdir(test_dir): completer = PathCompleter(min_input_len=1) doc_text = "" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) assert [] == completions # min len:1 and text of len 1 with chdir(test_dir): completer = PathCompleter(min_input_len=1) doc_text = "1" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = [c.text for c in completions] assert [""] == result # min len:0 and text of len 2 with chdir(test_dir): completer = PathCompleter(min_input_len=0) doc_text = "1" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = [c.text for c in completions] assert [""] == result # create 10 files with a 2 char long name for i in range(10): with open(os.path.join(test_dir, str(i) * 2), "wb") as out: out.write(b"") # min len:1 and text of len 1 with chdir(test_dir): completer = PathCompleter(min_input_len=1) doc_text = "2" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = sorted(c.text for c in completions) assert ["", "2"] == result # min len:2 and text of len 1 with chdir(test_dir): completer = PathCompleter(min_input_len=2) doc_text = "2" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) assert [] == completions # cleanup shutil.rmtree(test_dir) def test_pathcompleter_does_not_expanduser_by_default(): completer = PathCompleter() doc_text = "~" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) assert [] == completions def test_pathcompleter_can_expanduser(): completer = PathCompleter(expanduser=True) doc_text = "~" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) assert len(completions) > 0 def test_pathcompleter_can_apply_file_filter(): # setup: create a test dir with 10 files test_dir = tempfile.mkdtemp() write_test_files(test_dir) # add a .csv file with open(os.path.join(test_dir, "my.csv"), "wb") as out: out.write(b"") file_filter = lambda f: f and f.endswith(".csv") with chdir(test_dir): completer = PathCompleter(file_filter=file_filter) doc_text = "" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = [c.text for c in completions] assert ["my.csv"] == result # cleanup shutil.rmtree(test_dir) def test_pathcompleter_get_paths_constrains_path(): # setup: create a test dir with 10 files test_dir = tempfile.mkdtemp() write_test_files(test_dir) # add a subdir with 10 other files with different names subdir = os.path.join(test_dir, "subdir") os.mkdir(subdir) write_test_files(subdir, "abcdefghij") get_paths = lambda: ["subdir"] with chdir(test_dir): completer = PathCompleter(get_paths=get_paths) doc_text = "" doc = Document(doc_text, len(doc_text)) event = CompleteEvent() completions = list(completer.get_completions(doc, event)) result = [c.text for c in completions] expected = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] assert expected == result # cleanup shutil.rmtree(test_dir) def test_word_completer_static_word_list(): completer = WordCompleter(["abc", "def", "aaa"]) # Static list on empty input. completions = completer.get_completions(Document(""), CompleteEvent()) assert [c.text for c in completions] == ["abc", "def", "aaa"] # Static list on non-empty input. completions = completer.get_completions(Document("a"), CompleteEvent()) assert [c.text for c in completions] == ["abc", "aaa"] completions = completer.get_completions(Document("A"), CompleteEvent()) assert [c.text for c in completions] == [] # Multiple words ending with space. (Accept all options) completions = completer.get_completions(Document("test "), CompleteEvent()) assert [c.text for c in completions] == ["abc", "def", "aaa"] # Multiple words. (Check last only.) completions = completer.get_completions(Document("test a"), CompleteEvent()) assert [c.text for c in completions] == ["abc", "aaa"] def test_word_completer_ignore_case(): completer = WordCompleter(["abc", "def", "aaa"], ignore_case=True) completions = completer.get_completions(Document("a"), CompleteEvent()) assert [c.text for c in completions] == ["abc", "aaa"] completions = completer.get_completions(Document("A"), CompleteEvent()) assert [c.text for c in completions] == ["abc", "aaa"] def test_word_completer_match_middle(): completer = WordCompleter(["abc", "def", "abca"], match_middle=True) completions = completer.get_completions(Document("bc"), CompleteEvent()) assert [c.text for c in completions] == ["abc", "abca"] def test_word_completer_sentence(): # With sentence=True completer = WordCompleter( ["hello world", "www", "hello www", "hello there"], sentence=True ) completions = completer.get_completions(Document("hello w"), CompleteEvent()) assert [c.text for c in completions] == ["hello world", "hello www"] # With sentence=False completer = WordCompleter( ["hello world", "www", "hello www", "hello there"], sentence=False ) completions = completer.get_completions(Document("hello w"), CompleteEvent()) assert [c.text for c in completions] == ["www"] def test_word_completer_dynamic_word_list(): called = [0] def get_words(): called[0] += 1 return ["abc", "def", "aaa"] completer = WordCompleter(get_words) # Dynamic list on empty input. completions = completer.get_completions(Document(""), CompleteEvent()) assert [c.text for c in completions] == ["abc", "def", "aaa"] assert called[0] == 1 # Static list on non-empty input. completions = completer.get_completions(Document("a"), CompleteEvent()) assert [c.text for c in completions] == ["abc", "aaa"] assert called[0] == 2 def test_word_completer_pattern(): # With a pattern which support '.' completer = WordCompleter( ["abc", "a.b.c", "a.b", "xyz"], pattern=re.compile(r"^([a-zA-Z0-9_.]+|[^a-zA-Z0-9_.\s]+)"), ) completions = completer.get_completions(Document("a."), CompleteEvent()) assert [c.text for c in completions] == ["a.b.c", "a.b"] # Without pattern completer = WordCompleter(["abc", "a.b.c", "a.b", "xyz"]) completions = completer.get_completions(Document("a."), CompleteEvent()) assert [c.text for c in completions] == [] def test_fuzzy_completer(): collection = [ "migrations.py", "django_migrations.py", "django_admin_log.py", "api_user.doc", "user_group.doc", "users.txt", "accounts.txt", "123.py", "test123test.py", ] completer = FuzzyWordCompleter(collection) completions = completer.get_completions(Document("txt"), CompleteEvent()) assert [c.text for c in completions] == ["users.txt", "accounts.txt"] completions = completer.get_completions(Document("djmi"), CompleteEvent()) assert [c.text for c in completions] == [ "django_migrations.py", "django_admin_log.py", ] completions = completer.get_completions(Document("mi"), CompleteEvent()) assert [c.text for c in completions] == [ "migrations.py", "django_migrations.py", "django_admin_log.py", ] completions = completer.get_completions(Document("user"), CompleteEvent()) assert [c.text for c in completions] == [ "user_group.doc", "users.txt", "api_user.doc", ] completions = completer.get_completions(Document("123"), CompleteEvent()) assert [c.text for c in completions] == ["123.py", "test123test.py"] completions = completer.get_completions(Document("miGr"), CompleteEvent()) assert [c.text for c in completions] == [ "migrations.py", "django_migrations.py", ] # Multiple words ending with space. (Accept all options) completions = completer.get_completions(Document("test "), CompleteEvent()) assert [c.text for c in completions] == collection # Multiple words. (Check last only.) completions = completer.get_completions(Document("test txt"), CompleteEvent()) assert [c.text for c in completions] == ["users.txt", "accounts.txt"] def test_nested_completer(): completer = NestedCompleter.from_nested_dict( { "show": { "version": None, "clock": None, "interfaces": None, "ip": {"interface": {"brief"}}, }, "exit": None, } ) # Empty input. completions = completer.get_completions(Document(""), CompleteEvent()) assert {c.text for c in completions} == {"show", "exit"} # One character. completions = completer.get_completions(Document("s"), CompleteEvent()) assert {c.text for c in completions} == {"show"} # One word. completions = completer.get_completions(Document("show"), CompleteEvent()) assert {c.text for c in completions} == {"show"} # One word + space. completions = completer.get_completions(Document("show "), CompleteEvent()) assert {c.text for c in completions} == {"version", "clock", "interfaces", "ip"} # One word + space + one character. completions = completer.get_completions(Document("show i"), CompleteEvent()) assert {c.text for c in completions} == {"ip", "interfaces"} # One space + one word + space + one character. completions = completer.get_completions(Document(" show i"), CompleteEvent()) assert {c.text for c in completions} == {"ip", "interfaces"} # Test nested set. completions = completer.get_completions( Document("show ip interface br"), CompleteEvent() ) assert {c.text for c in completions} == {"brief"} def test_deduplicate_completer(): def create_completer(deduplicate: bool): return merge_completers( [ WordCompleter(["hello", "world", "abc", "def"]), WordCompleter(["xyz", "xyz", "abc", "def"]), ], deduplicate=deduplicate, ) completions = list( create_completer(deduplicate=False).get_completions( Document(""), CompleteEvent() ) ) assert len(completions) == 8 completions = list( create_completer(deduplicate=True).get_completions( Document(""), CompleteEvent() ) ) assert len(completions) == 5 ================================================ FILE: tests/test_document.py ================================================ from __future__ import annotations import re import pytest from prompt_toolkit.document import Document @pytest.fixture def document(): return Document( "line 1\n" + "line 2\n" + "line 3\n" + "line 4\n", len("line 1\n" + "lin") ) def test_current_char(document): assert document.current_char == "e" assert document.char_before_cursor == "n" def test_text_before_cursor(document): assert document.text_before_cursor == "line 1\nlin" def test_text_after_cursor(document): assert document.text_after_cursor == "e 2\n" + "line 3\n" + "line 4\n" def test_lines(document): assert document.lines == ["line 1", "line 2", "line 3", "line 4", ""] def test_line_count(document): assert document.line_count == 5 def test_current_line_before_cursor(document): assert document.current_line_before_cursor == "lin" def test_current_line_after_cursor(document): assert document.current_line_after_cursor == "e 2" def test_current_line(document): assert document.current_line == "line 2" def test_cursor_position(document): assert document.cursor_position_row == 1 assert document.cursor_position_col == 3 d = Document("", 0) assert d.cursor_position_row == 0 assert d.cursor_position_col == 0 def test_translate_index_to_position(document): pos = document.translate_index_to_position(len("line 1\nline 2\nlin")) assert pos[0] == 2 assert pos[1] == 3 pos = document.translate_index_to_position(0) assert pos == (0, 0) def test_is_cursor_at_the_end(document): assert Document("hello", 5).is_cursor_at_the_end assert not Document("hello", 4).is_cursor_at_the_end def test_get_word_before_cursor_with_whitespace_and_pattern(): text = "foobar " document = Document(text=text, cursor_position=len(text)) assert document.get_word_before_cursor() == "" _FIND_WORD_RE = re.compile(r"([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") assert document.get_word_before_cursor(pattern=_FIND_WORD_RE) == "" ================================================ FILE: tests/test_filter.py ================================================ from __future__ import annotations import pytest from prompt_toolkit.filters import Always, Condition, Filter, Never, to_filter from prompt_toolkit.filters.base import _AndList, _OrList def test_never(): assert not Never()() def test_always(): assert Always()() def test_invert(): assert not (~Always())() assert (~Never())() c = ~Condition(lambda: False) assert c() def test_or(): for a in (True, False): for b in (True, False): c1 = Condition(lambda: a) c2 = Condition(lambda: b) c3 = c1 | c2 assert isinstance(c3, Filter) assert c3() == a or b def test_and(): for a in (True, False): for b in (True, False): c1 = Condition(lambda: a) c2 = Condition(lambda: b) c3 = c1 & c2 assert isinstance(c3, Filter) assert c3() == (a and b) def test_nested_and(): for a in (True, False): for b in (True, False): for c in (True, False): c1 = Condition(lambda: a) c2 = Condition(lambda: b) c3 = Condition(lambda: c) c4 = (c1 & c2) & c3 assert isinstance(c4, Filter) assert c4() == (a and b and c) def test_nested_or(): for a in (True, False): for b in (True, False): for c in (True, False): c1 = Condition(lambda: a) c2 = Condition(lambda: b) c3 = Condition(lambda: c) c4 = (c1 | c2) | c3 assert isinstance(c4, Filter) assert c4() == (a or b or c) def test_to_filter(): f1 = to_filter(True) f2 = to_filter(False) f3 = to_filter(Condition(lambda: True)) f4 = to_filter(Condition(lambda: False)) assert isinstance(f1, Filter) assert isinstance(f2, Filter) assert isinstance(f3, Filter) assert isinstance(f4, Filter) assert f1() assert not f2() assert f3() assert not f4() with pytest.raises(TypeError): to_filter(4) def test_filter_cache_regression_1(): # See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1729 cond = Condition(lambda: True) # The use of a `WeakValueDictionary` caused this following expression to # fail. The problem is that the nested `(a & a)` expression gets garbage # collected between the two statements and is removed from our cache. x = (cond & cond) & cond y = (cond & cond) & cond assert x == y def test_filter_cache_regression_2(): cond1 = Condition(lambda: True) cond2 = Condition(lambda: True) cond3 = Condition(lambda: True) x = (cond1 & cond2) & cond3 y = (cond1 & cond2) & cond3 assert x == y def test_filter_remove_duplicates(): cond1 = Condition(lambda: True) cond2 = Condition(lambda: True) # When a condition is appended to itself using an `&` or `|` operator, it # should not be present twice. Having it twice in the `_AndList` or # `_OrList` will make them more expensive to evaluate. assert isinstance(cond1 & cond1, Condition) assert isinstance(cond1 & cond1 & cond1, Condition) assert isinstance(cond1 & cond1 & cond2, _AndList) assert len((cond1 & cond1 & cond2).filters) == 2 assert isinstance(cond1 | cond1, Condition) assert isinstance(cond1 | cond1 | cond1, Condition) assert isinstance(cond1 | cond1 | cond2, _OrList) assert len((cond1 | cond1 | cond2).filters) == 2 ================================================ FILE: tests/test_formatted_text.py ================================================ from __future__ import annotations from prompt_toolkit.formatted_text import ( ANSI, HTML, FormattedText, PygmentsTokens, Template, merge_formatted_text, to_formatted_text, ) from prompt_toolkit.formatted_text.utils import split_lines def test_basic_html(): html = HTML("<i>hello</i>") assert to_formatted_text(html) == [("class:i", "hello")] html = HTML("<i><b>hello</b></i>") assert to_formatted_text(html) == [("class:i,b", "hello")] html = HTML("<i><b>hello</b>world<strong>test</strong></i>after") assert to_formatted_text(html) == [ ("class:i,b", "hello"), ("class:i", "world"), ("class:i,strong", "test"), ("", "after"), ] # It's important that `to_formatted_text` returns a `FormattedText` # instance. Otherwise, `print_formatted_text` won't recognize it and will # print a list literal instead. assert isinstance(to_formatted_text(html), FormattedText) def test_html_with_fg_bg(): html = HTML('<style bg="ansired">hello</style>') assert to_formatted_text(html) == [ ("bg:ansired", "hello"), ] html = HTML('<style bg="ansired" fg="#ff0000">hello</style>') assert to_formatted_text(html) == [ ("fg:#ff0000 bg:ansired", "hello"), ] html = HTML( '<style bg="ansired" fg="#ff0000">hello <world fg="ansiblue">world</world></style>' ) assert to_formatted_text(html) == [ ("fg:#ff0000 bg:ansired", "hello "), ("class:world fg:ansiblue bg:ansired", "world"), ] def test_ansi_formatting(): value = ANSI("\x1b[32mHe\x1b[45mllo") assert to_formatted_text(value) == [ ("ansigreen", "H"), ("ansigreen", "e"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "o"), ] # Bold and italic. value = ANSI("\x1b[1mhe\x1b[0mllo") assert to_formatted_text(value) == [ ("bold", "h"), ("bold", "e"), ("", "l"), ("", "l"), ("", "o"), ] # Zero width escapes. value = ANSI("ab\001cd\002ef") assert to_formatted_text(value) == [ ("", "a"), ("", "b"), ("[ZeroWidthEscape]", "cd"), ("", "e"), ("", "f"), ] assert isinstance(to_formatted_text(value), FormattedText) def test_ansi_dim(): # Test dim formatting value = ANSI("\x1b[2mhello\x1b[0m") assert to_formatted_text(value) == [ ("dim", "h"), ("dim", "e"), ("dim", "l"), ("dim", "l"), ("dim", "o"), ] # Test dim with other attributes value = ANSI("\x1b[1;2;31mhello\x1b[0m") assert to_formatted_text(value) == [ ("ansired bold dim", "h"), ("ansired bold dim", "e"), ("ansired bold dim", "l"), ("ansired bold dim", "l"), ("ansired bold dim", "o"), ] # Test dim reset with code 22 value = ANSI("\x1b[1;2mhello\x1b[22mworld\x1b[0m") assert to_formatted_text(value) == [ ("bold dim", "h"), ("bold dim", "e"), ("bold dim", "l"), ("bold dim", "l"), ("bold dim", "o"), ("", "w"), ("", "o"), ("", "r"), ("", "l"), ("", "d"), ] def test_ansi_256_color(): assert to_formatted_text(ANSI("\x1b[38;5;124mtest")) == [ ("#af0000", "t"), ("#af0000", "e"), ("#af0000", "s"), ("#af0000", "t"), ] def test_ansi_true_color(): assert to_formatted_text(ANSI("\033[38;2;144;238;144m$\033[0;39;49m ")) == [ ("#90ee90", "$"), ("ansidefault bg:ansidefault", " "), ] def test_ansi_interpolation(): # %-style interpolation. value = ANSI("\x1b[1m%s\x1b[0m") % "hello\x1b" assert to_formatted_text(value) == [ ("bold", "h"), ("bold", "e"), ("bold", "l"), ("bold", "l"), ("bold", "o"), ("bold", "?"), ] value = ANSI("\x1b[1m%s\x1b[0m") % ("\x1bhello",) assert to_formatted_text(value) == [ ("bold", "?"), ("bold", "h"), ("bold", "e"), ("bold", "l"), ("bold", "l"), ("bold", "o"), ] value = ANSI("\x1b[32m%s\x1b[45m%s") % ("He", "\x1bllo") assert to_formatted_text(value) == [ ("ansigreen", "H"), ("ansigreen", "e"), ("ansigreen bg:ansimagenta", "?"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "o"), ] # Format function. value = ANSI("\x1b[32m{0}\x1b[45m{1}").format("He\x1b", "llo") assert to_formatted_text(value) == [ ("ansigreen", "H"), ("ansigreen", "e"), ("ansigreen", "?"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "o"), ] value = ANSI("\x1b[32m{a}\x1b[45m{b}").format(a="\x1bHe", b="llo") assert to_formatted_text(value) == [ ("ansigreen", "?"), ("ansigreen", "H"), ("ansigreen", "e"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "l"), ("ansigreen bg:ansimagenta", "o"), ] value = ANSI("\x1b[32m{:02d}\x1b[45m{:.3f}").format(3, 3.14159) assert to_formatted_text(value) == [ ("ansigreen", "0"), ("ansigreen", "3"), ("ansigreen bg:ansimagenta", "3"), ("ansigreen bg:ansimagenta", "."), ("ansigreen bg:ansimagenta", "1"), ("ansigreen bg:ansimagenta", "4"), ("ansigreen bg:ansimagenta", "2"), ] def test_interpolation(): value = Template(" {} ").format(HTML("<b>hello</b>")) assert to_formatted_text(value) == [ ("", " "), ("class:b", "hello"), ("", " "), ] value = Template("a{}b{}c").format(HTML("<b>hello</b>"), "world") assert to_formatted_text(value) == [ ("", "a"), ("class:b", "hello"), ("", "b"), ("", "world"), ("", "c"), ] def test_html_interpolation(): # %-style interpolation. value = HTML("<b>%s</b>") % "&hello" assert to_formatted_text(value) == [("class:b", "&hello")] value = HTML("<b>%s</b>") % ("<hello>",) assert to_formatted_text(value) == [("class:b", "<hello>")] value = HTML("<b>%s</b><u>%s</u>") % ("<hello>", "</world>") assert to_formatted_text(value) == [("class:b", "<hello>"), ("class:u", "</world>")] # Format function. value = HTML("<b>{0}</b><u>{1}</u>").format("'hello'", '"world"') assert to_formatted_text(value) == [("class:b", "'hello'"), ("class:u", '"world"')] value = HTML("<b>{a}</b><u>{b}</u>").format(a="hello", b="world") assert to_formatted_text(value) == [("class:b", "hello"), ("class:u", "world")] value = HTML("<b>{:02d}</b><u>{:.3f}</u>").format(3, 3.14159) assert to_formatted_text(value) == [("class:b", "03"), ("class:u", "3.142")] def test_merge_formatted_text(): html1 = HTML("<u>hello</u>") html2 = HTML("<b>world</b>") result = merge_formatted_text([html1, html2]) assert to_formatted_text(result) == [ ("class:u", "hello"), ("class:b", "world"), ] def test_pygments_tokens(): text = [ (("A", "B"), "hello"), # Token.A.B (("C", "D", "E"), "hello"), # Token.C.D.E ((), "world"), # Token ] assert to_formatted_text(PygmentsTokens(text)) == [ ("class:pygments.a.b", "hello"), ("class:pygments.c.d.e", "hello"), ("class:pygments", "world"), ] def test_split_lines(): lines = list(split_lines([("class:a", "line1\nline2\nline3")])) assert lines == [ [("class:a", "line1")], [("class:a", "line2")], [("class:a", "line3")], ] def test_split_lines_2(): lines = list( split_lines([("class:a", "line1"), ("class:b", "line2\nline3\nline4")]) ) assert lines == [ [("class:a", "line1"), ("class:b", "line2")], [("class:b", "line3")], [("class:b", "line4")], ] def test_split_lines_3(): "Edge cases: inputs ending with newlines." # -1- lines = list(split_lines([("class:a", "line1\nline2\n")])) assert lines == [ [("class:a", "line1")], [("class:a", "line2")], [("class:a", "")], ] # -2- lines = list(split_lines([("class:a", "\n")])) assert lines == [ [("class:a", "")], [("class:a", "")], ] # -3- lines = list(split_lines([("class:a", "")])) assert lines == [ [("class:a", "")], ] def test_split_lines_4(): "Edge cases: inputs starting and ending with newlines." # -1- lines = list(split_lines([("class:a", "\nline1\n")])) assert lines == [ [("class:a", "")], [("class:a", "line1")], [("class:a", "")], ] ================================================ FILE: tests/test_history.py ================================================ from __future__ import annotations from asyncio import run from prompt_toolkit.history import FileHistory, InMemoryHistory, ThreadedHistory def _call_history_load(history): """ Helper: Call the history "load" method and return the result as a list of strings. """ result = [] async def call_load(): async for item in history.load(): result.append(item) run(call_load()) return result def test_in_memory_history(): history = InMemoryHistory() history.append_string("hello") history.append_string("world") # Newest should yield first. assert _call_history_load(history) == ["world", "hello"] # Test another call. assert _call_history_load(history) == ["world", "hello"] history.append_string("test3") assert _call_history_load(history) == ["test3", "world", "hello"] # Passing history as a parameter. history2 = InMemoryHistory(["abc", "def"]) assert _call_history_load(history2) == ["def", "abc"] def test_file_history(tmpdir): histfile = tmpdir.join("history") history = FileHistory(histfile) history.append_string("hello") history.append_string("world") # Newest should yield first. assert _call_history_load(history) == ["world", "hello"] # Test another call. assert _call_history_load(history) == ["world", "hello"] history.append_string("test3") assert _call_history_load(history) == ["test3", "world", "hello"] # Create another history instance pointing to the same file. history2 = FileHistory(histfile) assert _call_history_load(history2) == ["test3", "world", "hello"] def test_threaded_file_history(tmpdir): histfile = tmpdir.join("history") history = ThreadedHistory(FileHistory(histfile)) history.append_string("hello") history.append_string("world") # Newest should yield first. assert _call_history_load(history) == ["world", "hello"] # Test another call. assert _call_history_load(history) == ["world", "hello"] history.append_string("test3") assert _call_history_load(history) == ["test3", "world", "hello"] # Create another history instance pointing to the same file. history2 = ThreadedHistory(FileHistory(histfile)) assert _call_history_load(history2) == ["test3", "world", "hello"] def test_threaded_in_memory_history(): # Threaded in memory history is not useful. But testing it anyway, just to # see whether everything plays nicely together. history = ThreadedHistory(InMemoryHistory()) history.append_string("hello") history.append_string("world") # Newest should yield first. assert _call_history_load(history) == ["world", "hello"] # Test another call. assert _call_history_load(history) == ["world", "hello"] history.append_string("test3") assert _call_history_load(history) == ["test3", "world", "hello"] # Passing history as a parameter. history2 = ThreadedHistory(InMemoryHistory(["abc", "def"])) assert _call_history_load(history2) == ["def", "abc"] ================================================ FILE: tests/test_inputstream.py ================================================ from __future__ import annotations import pytest from prompt_toolkit.input.vt100_parser import Vt100Parser from prompt_toolkit.keys import Keys class _ProcessorMock: def __init__(self): self.keys = [] def feed_key(self, key_press): self.keys.append(key_press) @pytest.fixture def processor(): return _ProcessorMock() @pytest.fixture def stream(processor): return Vt100Parser(processor.feed_key) def test_control_keys(processor, stream): stream.feed("\x01\x02\x10") assert len(processor.keys) == 3 assert processor.keys[0].key == Keys.ControlA assert processor.keys[1].key == Keys.ControlB assert processor.keys[2].key == Keys.ControlP assert processor.keys[0].data == "\x01" assert processor.keys[1].data == "\x02" assert processor.keys[2].data == "\x10" def test_arrows(processor, stream): stream.feed("\x1b[A\x1b[B\x1b[C\x1b[D") assert len(processor.keys) == 4 assert processor.keys[0].key == Keys.Up assert processor.keys[1].key == Keys.Down assert processor.keys[2].key == Keys.Right assert processor.keys[3].key == Keys.Left assert processor.keys[0].data == "\x1b[A" assert processor.keys[1].data == "\x1b[B" assert processor.keys[2].data == "\x1b[C" assert processor.keys[3].data == "\x1b[D" def test_escape(processor, stream): stream.feed("\x1bhello") assert len(processor.keys) == 1 + len("hello") assert processor.keys[0].key == Keys.Escape assert processor.keys[1].key == "h" assert processor.keys[0].data == "\x1b" assert processor.keys[1].data == "h" def test_special_double_keys(processor, stream): stream.feed("\x1b[1;3D") # Should both send escape and left. assert len(processor.keys) == 2 assert processor.keys[0].key == Keys.Escape assert processor.keys[1].key == Keys.Left assert processor.keys[0].data == "\x1b[1;3D" assert processor.keys[1].data == "" def test_flush_1(processor, stream): # Send left key in two parts without flush. stream.feed("\x1b") stream.feed("[D") assert len(processor.keys) == 1 assert processor.keys[0].key == Keys.Left assert processor.keys[0].data == "\x1b[D" def test_flush_2(processor, stream): # Send left key with a 'Flush' in between. # The flush should make sure that we process everything before as-is, # with makes the first part just an escape character instead. stream.feed("\x1b") stream.flush() stream.feed("[D") assert len(processor.keys) == 3 assert processor.keys[0].key == Keys.Escape assert processor.keys[1].key == "[" assert processor.keys[2].key == "D" assert processor.keys[0].data == "\x1b" assert processor.keys[1].data == "[" assert processor.keys[2].data == "D" def test_meta_arrows(processor, stream): stream.feed("\x1b\x1b[D") assert len(processor.keys) == 2 assert processor.keys[0].key == Keys.Escape assert processor.keys[1].key == Keys.Left def test_control_square_close(processor, stream): stream.feed("\x1dC") assert len(processor.keys) == 2 assert processor.keys[0].key == Keys.ControlSquareClose assert processor.keys[1].key == "C" def test_invalid(processor, stream): # Invalid sequence that has at two characters in common with other # sequences. stream.feed("\x1b[*") assert len(processor.keys) == 3 assert processor.keys[0].key == Keys.Escape assert processor.keys[1].key == "[" assert processor.keys[2].key == "*" def test_cpr_response(processor, stream): stream.feed("a\x1b[40;10Rb") assert len(processor.keys) == 3 assert processor.keys[0].key == "a" assert processor.keys[1].key == Keys.CPRResponse assert processor.keys[2].key == "b" def test_cpr_response_2(processor, stream): # Make sure that the newline is not included in the CPR response. stream.feed("\x1b[40;1R\n") assert len(processor.keys) == 2 assert processor.keys[0].key == Keys.CPRResponse assert processor.keys[1].key == Keys.ControlJ ================================================ FILE: tests/test_key_binding.py ================================================ from __future__ import annotations from contextlib import contextmanager import pytest from prompt_toolkit.application import Application from prompt_toolkit.application.current import set_app from prompt_toolkit.input.defaults import create_pipe_input from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPress, KeyProcessor from prompt_toolkit.keys import Keys from prompt_toolkit.layout import Layout, Window from prompt_toolkit.output import DummyOutput class Handlers: def __init__(self): self.called = [] def __getattr__(self, name): def func(event): self.called.append(name) return func @contextmanager def set_dummy_app(): """ Return a context manager that makes sure that this dummy application is active. This is important, because we need an `Application` with `is_done=False` flag, otherwise no keys will be processed. """ with create_pipe_input() as pipe_input: app = Application( layout=Layout(Window()), output=DummyOutput(), input=pipe_input, ) def create_background_task(coroutine, **kw): coroutine.close() return None # Don't start background tasks for these tests. The `KeyProcessor` # wants to create a background task for flushing keys. We can ignore it # here for these tests. # This patch is not clean. In the future, when we can use Taskgroups, # the `Application` should pass its task group to the constructor of # `KeyProcessor`. That way, it doesn't have to do a lookup using # `get_app()`. app.create_background_task = create_background_task with set_app(app): yield @pytest.fixture def handlers(): return Handlers() @pytest.fixture def bindings(handlers): bindings = KeyBindings() bindings.add(Keys.ControlX, Keys.ControlC)(handlers.controlx_controlc) bindings.add(Keys.ControlX)(handlers.control_x) bindings.add(Keys.ControlD)(handlers.control_d) bindings.add(Keys.ControlSquareClose, Keys.Any)(handlers.control_square_close_any) return bindings @pytest.fixture def processor(bindings): return KeyProcessor(bindings) def test_remove_bindings(handlers): with set_dummy_app(): h = handlers.controlx_controlc h2 = handlers.controld # Test passing a handler to the remove() function. bindings = KeyBindings() bindings.add(Keys.ControlX, Keys.ControlC)(h) bindings.add(Keys.ControlD)(h2) assert len(bindings.bindings) == 2 bindings.remove(h) assert len(bindings.bindings) == 1 # Test passing a key sequence to the remove() function. bindings = KeyBindings() bindings.add(Keys.ControlX, Keys.ControlC)(h) bindings.add(Keys.ControlD)(h2) assert len(bindings.bindings) == 2 bindings.remove(Keys.ControlX, Keys.ControlC) assert len(bindings.bindings) == 1 def test_feed_simple(processor, handlers): with set_dummy_app(): processor.feed(KeyPress(Keys.ControlX, "\x18")) processor.feed(KeyPress(Keys.ControlC, "\x03")) processor.process_keys() assert handlers.called == ["controlx_controlc"] def test_feed_several(processor, handlers): with set_dummy_app(): # First an unknown key first. processor.feed(KeyPress(Keys.ControlQ, "")) processor.process_keys() assert handlers.called == [] # Followed by a know key sequence. processor.feed(KeyPress(Keys.ControlX, "")) processor.feed(KeyPress(Keys.ControlC, "")) processor.process_keys() assert handlers.called == ["controlx_controlc"] # Followed by another unknown sequence. processor.feed(KeyPress(Keys.ControlR, "")) processor.feed(KeyPress(Keys.ControlS, "")) # Followed again by a know key sequence. processor.feed(KeyPress(Keys.ControlD, "")) processor.process_keys() assert handlers.called == ["controlx_controlc", "control_d"] def test_control_square_closed_any(processor, handlers): with set_dummy_app(): processor.feed(KeyPress(Keys.ControlSquareClose, "")) processor.feed(KeyPress("C", "C")) processor.process_keys() assert handlers.called == ["control_square_close_any"] def test_common_prefix(processor, handlers): with set_dummy_app(): # Sending Control_X should not yet do anything, because there is # another sequence starting with that as well. processor.feed(KeyPress(Keys.ControlX, "")) processor.process_keys() assert handlers.called == [] # When another key is pressed, we know that we did not meant the longer # "ControlX ControlC" sequence and the callbacks are called. processor.feed(KeyPress(Keys.ControlD, "")) processor.process_keys() assert handlers.called == ["control_x", "control_d"] def test_previous_key_sequence(processor): """ test whether we receive the correct previous_key_sequence. """ with set_dummy_app(): events = [] def handler(event): events.append(event) # Build registry. registry = KeyBindings() registry.add("a", "a")(handler) registry.add("b", "b")(handler) processor = KeyProcessor(registry) # Create processor and feed keys. processor.feed(KeyPress("a", "a")) processor.feed(KeyPress("a", "a")) processor.feed(KeyPress("b", "b")) processor.feed(KeyPress("b", "b")) processor.process_keys() # Test. assert len(events) == 2 assert len(events[0].key_sequence) == 2 assert events[0].key_sequence[0].key == "a" assert events[0].key_sequence[0].data == "a" assert events[0].key_sequence[1].key == "a" assert events[0].key_sequence[1].data == "a" assert events[0].previous_key_sequence == [] assert len(events[1].key_sequence) == 2 assert events[1].key_sequence[0].key == "b" assert events[1].key_sequence[0].data == "b" assert events[1].key_sequence[1].key == "b" assert events[1].key_sequence[1].data == "b" assert len(events[1].previous_key_sequence) == 2 assert events[1].previous_key_sequence[0].key == "a" assert events[1].previous_key_sequence[0].data == "a" assert events[1].previous_key_sequence[1].key == "a" assert events[1].previous_key_sequence[1].data == "a" ================================================ FILE: tests/test_layout.py ================================================ from __future__ import annotations import pytest from prompt_toolkit.layout import InvalidLayoutError, Layout from prompt_toolkit.layout.containers import HSplit, VSplit, Window from prompt_toolkit.layout.controls import BufferControl def test_layout_class(): c1 = BufferControl() c2 = BufferControl() c3 = BufferControl() win1 = Window(content=c1) win2 = Window(content=c2) win3 = Window(content=c3) layout = Layout(container=VSplit([HSplit([win1, win2]), win3])) # Listing of windows/controls. assert list(layout.find_all_windows()) == [win1, win2, win3] assert list(layout.find_all_controls()) == [c1, c2, c3] # Focusing something. layout.focus(c1) assert layout.has_focus(c1) assert layout.has_focus(win1) assert layout.current_control == c1 assert layout.previous_control == c1 layout.focus(c2) assert layout.has_focus(c2) assert layout.has_focus(win2) assert layout.current_control == c2 assert layout.previous_control == c1 layout.focus(win3) assert layout.has_focus(c3) assert layout.has_focus(win3) assert layout.current_control == c3 assert layout.previous_control == c2 # Pop focus. This should focus the previous control again. layout.focus_last() assert layout.has_focus(c2) assert layout.has_focus(win2) assert layout.current_control == c2 assert layout.previous_control == c1 def test_create_invalid_layout(): with pytest.raises(InvalidLayoutError): Layout(HSplit([])) ================================================ FILE: tests/test_memory_leaks.py ================================================ from __future__ import annotations import gc from prompt_toolkit.shortcuts.prompt import PromptSession def _count_prompt_session_instances() -> int: # Run full GC collection first. gc.collect() # Count number of remaining referenced `PromptSession` instances. objects = gc.get_objects() return len([obj for obj in objects if isinstance(obj, PromptSession)]) # This test used to fail in GitHub CI, probably due to GC differences. def test_prompt_session_memory_leak() -> None: before_count = _count_prompt_session_instances() # Somehow in CI/CD, the before_count is > 0 assert before_count == 0 p = PromptSession() after_count = _count_prompt_session_instances() assert after_count == before_count + 1 del p after_delete_count = _count_prompt_session_instances() assert after_delete_count == before_count ================================================ FILE: tests/test_print_formatted_text.py ================================================ """ Test the `print` function. """ from __future__ import annotations import pytest from prompt_toolkit import print_formatted_text as pt_print from prompt_toolkit.formatted_text import HTML, FormattedText, to_formatted_text from prompt_toolkit.output import ColorDepth from prompt_toolkit.styles import Style from prompt_toolkit.utils import is_windows class _Capture: "Emulate an stdout object." def __init__(self): self._data = [] def write(self, data): self._data.append(data) @property def data(self): return "".join(self._data) def flush(self): pass def isatty(self): return True def fileno(self): # File descriptor is not used for printing formatted text. # (It is only needed for getting the terminal size.) return -1 @pytest.mark.skipif(is_windows(), reason="Doesn't run on Windows yet.") def test_print_formatted_text(): f = _Capture() pt_print([("", "hello"), ("", "world")], file=f) assert "hello" in f.data assert "world" in f.data @pytest.mark.skipif(is_windows(), reason="Doesn't run on Windows yet.") def test_print_formatted_text_backslash_r(): f = _Capture() pt_print("hello\r\n", file=f) assert "hello" in f.data @pytest.mark.skipif(is_windows(), reason="Doesn't run on Windows yet.") def test_formatted_text_with_style(): f = _Capture() style = Style.from_dict( { "hello": "#ff0066", "world": "#44ff44 italic", } ) tokens = FormattedText( [ ("class:hello", "Hello "), ("class:world", "world"), ] ) # NOTE: We pass the default (8bit) color depth, so that the unit tests # don't start failing when environment variables change. pt_print(tokens, style=style, file=f, color_depth=ColorDepth.DEFAULT) assert "\x1b[0;38;5;197mHello" in f.data assert "\x1b[0;38;5;83;3mworld" in f.data @pytest.mark.skipif(is_windows(), reason="Doesn't run on Windows yet.") def test_html_with_style(): """ Text `print_formatted_text` with `HTML` wrapped in `to_formatted_text`. """ f = _Capture() html = HTML("<ansigreen>hello</ansigreen> <b>world</b>") formatted_text = to_formatted_text(html, style="class:myhtml") pt_print(formatted_text, file=f, color_depth=ColorDepth.DEFAULT) assert ( f.data == "\x1b[0m\x1b[?7h\x1b[0;32mhello\x1b[0m \x1b[0;1mworld\x1b[0m\r\n\x1b[0m" ) @pytest.mark.skipif(is_windows(), reason="Doesn't run on Windows yet.") def test_print_formatted_text_with_dim(): """ Test that dim formatting works correctly. """ f = _Capture() style = Style.from_dict( { "dimtext": "dim", } ) tokens = FormattedText([("class:dimtext", "dim text")]) pt_print(tokens, style=style, file=f, color_depth=ColorDepth.DEFAULT) # Check that the ANSI dim escape code (ESC[2m) is in the output assert "\x1b[0;2m" in f.data or "\x1b[2m" in f.data ================================================ FILE: tests/test_regular_languages.py ================================================ from __future__ import annotations from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.contrib.regular_languages import compile from prompt_toolkit.contrib.regular_languages.compiler import Match, Variables from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from prompt_toolkit.document import Document def test_simple_match(): g = compile("hello|world") m = g.match("hello") assert isinstance(m, Match) m = g.match("world") assert isinstance(m, Match) m = g.match("somethingelse") assert m is None def test_variable_varname(): """ Test `Variable` with varname. """ g = compile("((?P<varname>hello|world)|test)") m = g.match("hello") variables = m.variables() assert isinstance(variables, Variables) assert variables.get("varname") == "hello" assert variables["varname"] == "hello" m = g.match("world") variables = m.variables() assert isinstance(variables, Variables) assert variables.get("varname") == "world" assert variables["varname"] == "world" m = g.match("test") variables = m.variables() assert isinstance(variables, Variables) assert variables.get("varname") is None assert variables["varname"] is None def test_prefix(): """ Test `match_prefix`. """ g = compile(r"(hello\ world|something\ else)") m = g.match_prefix("hello world") assert isinstance(m, Match) m = g.match_prefix("he") assert isinstance(m, Match) m = g.match_prefix("") assert isinstance(m, Match) m = g.match_prefix("som") assert isinstance(m, Match) m = g.match_prefix("hello wor") assert isinstance(m, Match) m = g.match_prefix("no-match") assert m.trailing_input().start == 0 assert m.trailing_input().stop == len("no-match") m = g.match_prefix("hellotest") assert m.trailing_input().start == len("hello") assert m.trailing_input().stop == len("hellotest") def test_completer(): class completer1(Completer): def get_completions(self, document, complete_event): yield Completion(f"before-{document.text}-after", -len(document.text)) yield Completion(f"before-{document.text}-after-B", -len(document.text)) class completer2(Completer): def get_completions(self, document, complete_event): yield Completion(f"before2-{document.text}-after2", -len(document.text)) yield Completion(f"before2-{document.text}-after2-B", -len(document.text)) # Create grammar. "var1" + "whitespace" + "var2" g = compile(r"(?P<var1>[a-z]*) \s+ (?P<var2>[a-z]*)") # Test 'get_completions()' completer = GrammarCompleter(g, {"var1": completer1(), "var2": completer2()}) completions = list( completer.get_completions(Document("abc def", len("abc def")), CompleteEvent()) ) assert len(completions) == 2 assert completions[0].text == "before2-def-after2" assert completions[0].start_position == -3 assert completions[1].text == "before2-def-after2-B" assert completions[1].start_position == -3 ================================================ FILE: tests/test_shortcuts.py ================================================ from __future__ import annotations from prompt_toolkit.shortcuts import print_container from prompt_toolkit.shortcuts.prompt import _split_multiline_prompt from prompt_toolkit.widgets import Frame, TextArea def test_split_multiline_prompt(): # Test 1: no newlines: tokens = [("class:testclass", "ab")] has_before_tokens, before, first_input_line = _split_multiline_prompt( lambda: tokens ) assert has_before_tokens() is False assert before() == [] assert first_input_line() == [ ("class:testclass", "a"), ("class:testclass", "b"), ] # Test 1: multiple lines. tokens = [("class:testclass", "ab\ncd\nef")] has_before_tokens, before, first_input_line = _split_multiline_prompt( lambda: tokens ) assert has_before_tokens() is True assert before() == [ ("class:testclass", "a"), ("class:testclass", "b"), ("class:testclass", "\n"), ("class:testclass", "c"), ("class:testclass", "d"), ] assert first_input_line() == [ ("class:testclass", "e"), ("class:testclass", "f"), ] # Edge case 1: starting with a newline. tokens = [("class:testclass", "\nab")] has_before_tokens, before, first_input_line = _split_multiline_prompt( lambda: tokens ) assert has_before_tokens() is True assert before() == [] assert first_input_line() == [("class:testclass", "a"), ("class:testclass", "b")] # Edge case 2: starting with two newlines. tokens = [("class:testclass", "\n\nab")] has_before_tokens, before, first_input_line = _split_multiline_prompt( lambda: tokens ) assert has_before_tokens() is True assert before() == [("class:testclass", "\n")] assert first_input_line() == [("class:testclass", "a"), ("class:testclass", "b")] def test_print_container(tmpdir): # Call `print_container`, render to a dummy file. f = tmpdir.join("output") with open(f, "w") as fd: print_container(Frame(TextArea(text="Hello world!\n"), title="Title"), file=fd) # Verify rendered output. with open(f) as fd: text = fd.read() assert "Hello world" in text assert "Title" in text ================================================ FILE: tests/test_style.py ================================================ from __future__ import annotations from prompt_toolkit.styles import Attrs, Style, SwapLightAndDarkStyleTransformation def test_style_from_dict(): style = Style.from_dict( { "a": "#ff0000 bold underline strike italic", "b": "bg:#00ff00 blink reverse", } ) # Lookup of class:a. expected = Attrs( color="ff0000", bgcolor="", bold=True, underline=True, strike=True, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:a") == expected # Lookup of class:b. expected = Attrs( color="", bgcolor="00ff00", bold=False, underline=False, strike=False, italic=False, blink=True, reverse=True, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:b") == expected # Test inline style. expected = Attrs( color="ff0000", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("#ff0000") == expected # Combine class name and inline style (Whatever is defined later gets priority.) expected = Attrs( color="00ff00", bgcolor="", bold=True, underline=True, strike=True, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:a #00ff00") == expected expected = Attrs( color="ff0000", bgcolor="", bold=True, underline=True, strike=True, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("#00ff00 class:a") == expected def test_class_combinations_1(): # In this case, our style has both class 'a' and 'b'. # Given that the style for 'a b' is defined at the end, that one is used. style = Style( [ ("a", "#0000ff"), ("b", "#00ff00"), ("a b", "#ff0000"), ] ) expected = Attrs( color="ff0000", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:a class:b") == expected assert style.get_attrs_for_style_str("class:a,b") == expected assert style.get_attrs_for_style_str("class:a,b,c") == expected # Changing the order shouldn't matter. assert style.get_attrs_for_style_str("class:b class:a") == expected assert style.get_attrs_for_style_str("class:b,a") == expected def test_class_combinations_2(): # In this case, our style has both class 'a' and 'b'. # The style that is defined the latest get priority. style = Style( [ ("a b", "#ff0000"), ("b", "#00ff00"), ("a", "#0000ff"), ] ) expected = Attrs( color="00ff00", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:a class:b") == expected assert style.get_attrs_for_style_str("class:a,b") == expected assert style.get_attrs_for_style_str("class:a,b,c") == expected # Defining 'a' latest should give priority to 'a'. expected = Attrs( color="0000ff", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:b class:a") == expected assert style.get_attrs_for_style_str("class:b,a") == expected def test_substyles(): style = Style( [ ("a.b", "#ff0000 bold"), ("a", "#0000ff"), ("b", "#00ff00"), ("b.c", "#0000ff italic"), ] ) # Starting with a.* expected = Attrs( color="0000ff", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:a") == expected expected = Attrs( color="ff0000", bgcolor="", bold=True, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:a.b") == expected assert style.get_attrs_for_style_str("class:a.b.c") == expected # Starting with b.* expected = Attrs( color="00ff00", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:b") == expected assert style.get_attrs_for_style_str("class:b.a") == expected expected = Attrs( color="0000ff", bgcolor="", bold=False, underline=False, strike=False, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) assert style.get_attrs_for_style_str("class:b.c") == expected assert style.get_attrs_for_style_str("class:b.c.d") == expected def test_swap_light_and_dark_style_transformation(): transformation = SwapLightAndDarkStyleTransformation() # Test with 6 digit hex colors. before = Attrs( color="440000", bgcolor="888844", bold=True, underline=True, strike=True, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) after = Attrs( color="ffbbbb", bgcolor="bbbb76", bold=True, underline=True, strike=True, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) assert transformation.transform_attrs(before) == after # Test with ANSI colors. before = Attrs( color="ansired", bgcolor="ansiblack", bold=True, underline=True, strike=True, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) after = Attrs( color="ansibrightred", bgcolor="ansiwhite", bold=True, underline=True, strike=True, italic=True, blink=False, reverse=False, hidden=False, dim=False, ) assert transformation.transform_attrs(before) == after ================================================ FILE: tests/test_style_transformation.py ================================================ from __future__ import annotations import pytest from prompt_toolkit.styles import AdjustBrightnessStyleTransformation, Attrs @pytest.fixture def default_attrs(): return Attrs( color="", bgcolor="", bold=False, underline=False, strike=False, italic=False, blink=False, reverse=False, hidden=False, dim=False, ) def test_adjust_brightness_style_transformation(default_attrs): tr = AdjustBrightnessStyleTransformation(0.5, 1.0) attrs = tr.transform_attrs(default_attrs._replace(color="ff0000")) assert attrs.color == "ff7f7f" attrs = tr.transform_attrs(default_attrs._replace(color="00ffaa")) assert attrs.color == "7fffd4" # When a background color is given, nothing should change. attrs = tr.transform_attrs(default_attrs._replace(color="00ffaa", bgcolor="white")) assert attrs.color == "00ffaa" # Test ansi colors. attrs = tr.transform_attrs(default_attrs._replace(color="ansiblue")) assert attrs.color == "6666ff" # Test 'ansidefault'. This shouldn't change. attrs = tr.transform_attrs(default_attrs._replace(color="ansidefault")) assert attrs.color == "ansidefault" # When 0 and 1 are given, don't do any style transformation. tr2 = AdjustBrightnessStyleTransformation(0, 1) attrs = tr2.transform_attrs(default_attrs._replace(color="ansiblue")) assert attrs.color == "ansiblue" attrs = tr2.transform_attrs(default_attrs._replace(color="00ffaa")) assert attrs.color == "00ffaa" ================================================ FILE: tests/test_utils.py ================================================ from __future__ import annotations import itertools import pytest from prompt_toolkit.utils import take_using_weights def test_using_weights(): def take(generator, count): return list(itertools.islice(generator, 0, count)) # Check distribution. data = take(take_using_weights(["A", "B", "C"], [5, 10, 20]), 35) assert data.count("A") == 5 assert data.count("B") == 10 assert data.count("C") == 20 assert data == [ "A", "B", "C", "C", "B", "C", "C", "A", "B", "C", "C", "B", "C", "C", "A", "B", "C", "C", "B", "C", "C", "A", "B", "C", "C", "B", "C", "C", "A", "B", "C", "C", "B", "C", "C", ] # Another order. data = take(take_using_weights(["A", "B", "C"], [20, 10, 5]), 35) assert data.count("A") == 20 assert data.count("B") == 10 assert data.count("C") == 5 # Bigger numbers. data = take(take_using_weights(["A", "B", "C"], [20, 10, 5]), 70) assert data.count("A") == 40 assert data.count("B") == 20 assert data.count("C") == 10 # Negative numbers. data = take(take_using_weights(["A", "B", "C"], [-20, 10, 0]), 70) assert data.count("A") == 0 assert data.count("B") == 70 assert data.count("C") == 0 # All zero-weight items. with pytest.raises(ValueError): take(take_using_weights(["A", "B", "C"], [0, 0, 0]), 70) ================================================ FILE: tests/test_vt100_output.py ================================================ from __future__ import annotations from prompt_toolkit.output.vt100 import _256_colors, _get_closest_ansi_color def test_get_closest_ansi_color(): # White assert _get_closest_ansi_color(255, 255, 255) == "ansiwhite" assert _get_closest_ansi_color(250, 250, 250) == "ansiwhite" # Black assert _get_closest_ansi_color(0, 0, 0) == "ansiblack" assert _get_closest_ansi_color(5, 5, 5) == "ansiblack" # Green assert _get_closest_ansi_color(0, 255, 0) == "ansibrightgreen" assert _get_closest_ansi_color(10, 255, 0) == "ansibrightgreen" assert _get_closest_ansi_color(0, 255, 10) == "ansibrightgreen" assert _get_closest_ansi_color(220, 220, 100) == "ansiyellow" def test_256_colors(): # 6x6x6 cube assert _256_colors[(0, 0, 0)] == 16 # First color in cube assert _256_colors[(255, 255, 255)] == 231 # Last color in cube assert _256_colors[(95, 95, 95)] == 59 # Verifies a color between the boundaries # Grayscale assert _256_colors[(8, 8, 8)] == 232 # First grayscale level assert _256_colors[(238, 238, 238)] == 255 # Last grayscale level ================================================ FILE: tests/test_widgets.py ================================================ from __future__ import annotations from prompt_toolkit.formatted_text import fragment_list_to_text from prompt_toolkit.layout import to_window from prompt_toolkit.widgets import Button def _to_text(button: Button) -> str: control = to_window(button).content return fragment_list_to_text(control.text()) def test_default_button(): button = Button("Exit") assert _to_text(button) == "< Exit >" def test_custom_button(): button = Button("Exit", left_symbol="[", right_symbol="]") assert _to_text(button) == "[ Exit ]" ================================================ FILE: tests/test_yank_nth_arg.py ================================================ from __future__ import annotations import pytest from prompt_toolkit.buffer import Buffer from prompt_toolkit.history import InMemoryHistory @pytest.fixture def _history(): "Prefilled history." history = InMemoryHistory() history.append_string("alpha beta gamma delta") history.append_string("one two three four") return history # Test yank_last_arg. def test_empty_history(): buf = Buffer() buf.yank_last_arg() assert buf.document.current_line == "" def test_simple_search(_history): buff = Buffer(history=_history) buff.yank_last_arg() assert buff.document.current_line == "four" def test_simple_search_with_quotes(_history): _history.append_string("""one two "three 'x' four"\n""") buff = Buffer(history=_history) buff.yank_last_arg() assert buff.document.current_line == '''"three 'x' four"''' def test_simple_search_with_arg(_history): buff = Buffer(history=_history) buff.yank_last_arg(n=2) assert buff.document.current_line == "three" def test_simple_search_with_arg_out_of_bounds(_history): buff = Buffer(history=_history) buff.yank_last_arg(n=8) assert buff.document.current_line == "" def test_repeated_search(_history): buff = Buffer(history=_history) buff.yank_last_arg() buff.yank_last_arg() assert buff.document.current_line == "delta" def test_repeated_search_with_wraparound(_history): buff = Buffer(history=_history) buff.yank_last_arg() buff.yank_last_arg() buff.yank_last_arg() assert buff.document.current_line == "four" # Test yank_last_arg. def test_yank_nth_arg(_history): buff = Buffer(history=_history) buff.yank_nth_arg() assert buff.document.current_line == "two" def test_repeated_yank_nth_arg(_history): buff = Buffer(history=_history) buff.yank_nth_arg() buff.yank_nth_arg() assert buff.document.current_line == "beta" def test_yank_nth_arg_with_arg(_history): buff = Buffer(history=_history) buff.yank_nth_arg(n=2) assert buff.document.current_line == "three" ================================================ FILE: tools/debug_input_cross_platform.py ================================================ #!/usr/bin/env python """ Read input and print keys. For testing terminal input. Works on both Windows and Posix. """ import asyncio from prompt_toolkit.input import create_input from prompt_toolkit.keys import Keys async def main() -> None: done = asyncio.Event() input = create_input() def keys_ready() -> None: for key_press in input.read_keys(): print(key_press) if key_press.key == Keys.ControlC: done.set() with input.raw_mode(): with input.attach(keys_ready): await done.wait() if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: tools/debug_vt100_input.py ================================================ #!/usr/bin/env python """ Parse vt100 input and print keys. For testing terminal input. (This does not use the `Input` implementation, but only the `Vt100Parser`.) """ import sys from prompt_toolkit.input.vt100 import raw_mode from prompt_toolkit.input.vt100_parser import Vt100Parser from prompt_toolkit.key_binding import KeyPress from prompt_toolkit.keys import Keys def callback(key_press: KeyPress) -> None: print(key_press) if key_press.key == Keys.ControlC: sys.exit(0) def main() -> None: stream = Vt100Parser(callback) with raw_mode(sys.stdin.fileno()): while True: c = sys.stdin.read(1) stream.feed(c) if __name__ == "__main__": main() ================================================ FILE: tox.ini ================================================ # Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py{37, 38, 39, 310, 311, 312, py3} [testenv] commands = pytest [] deps= pytest