summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codecov.yml1
-rw-r--r--.github/workflows/test.yaml54
-rw-r--r--.gitignore52
-rw-r--r--.readthedocs.yml19
-rw-r--r--AUTHORS.rst11
-rw-r--r--CHANGELOG2171
-rw-r--r--LICENSE27
-rw-r--r--MANIFEST.in4
-rw-r--r--PROJECTS.rst68
-rw-r--r--README.rst146
-rw-r--r--appveyor.yml45
-rw-r--r--docs/Makefile177
-rw-r--r--docs/conf.py313
-rw-r--r--docs/images/auto-suggestion.pngbin0 -> 72983 bytes
-rw-r--r--docs/images/bottom-toolbar.pngbin0 -> 77372 bytes
-rw-r--r--docs/images/colored-prompt.pngbin0 -> 94994 bytes
-rw-r--r--docs/images/colorful-completions.pngbin0 -> 95099 bytes
-rw-r--r--docs/images/dialogs/button.pngbin0 -> 134890 bytes
-rw-r--r--docs/images/dialogs/confirm.pngbin0 -> 135405 bytes
-rw-r--r--docs/images/dialogs/inputbox.pngbin0 -> 140141 bytes
-rw-r--r--docs/images/dialogs/messagebox.pngbin0 -> 139732 bytes
-rw-r--r--docs/images/dialogs/styled.pngbin0 -> 144850 bytes
-rw-r--r--docs/images/hello-world-prompt.pngbin0 -> 105790 bytes
-rw-r--r--docs/images/html-completion.pngbin0 -> 92781 bytes
-rw-r--r--docs/images/html-input.pngbin0 -> 116804 bytes
-rwxr-xr-xdocs/images/logo_400px.pngbin0 -> 56254 bytes
-rw-r--r--docs/images/multiline-input.pngbin0 -> 85296 bytes
-rw-r--r--docs/images/number-validator.pngbin0 -> 81683 bytes
-rwxr-xr-xdocs/images/progress-bars/apt-get.pngbin0 -> 5411 bytes
-rwxr-xr-xdocs/images/progress-bars/colored-title-and-label.pngbin0 -> 8575 bytes
-rwxr-xr-xdocs/images/progress-bars/custom-key-bindings.pngbin0 -> 8191 bytes
-rwxr-xr-xdocs/images/progress-bars/simple-progress-bar.pngbin0 -> 13218 bytes
-rwxr-xr-xdocs/images/progress-bars/two-tasks.pngbin0 -> 9723 bytes
-rw-r--r--docs/images/ptpython-2.pngbin0 -> 133464 bytes
-rw-r--r--docs/images/ptpython-history-help.pngbin0 -> 103337 bytes
-rw-r--r--docs/images/ptpython-menu.pngbin0 -> 60225 bytes
-rw-r--r--docs/images/ptpython.pngbin0 -> 28700 bytes
-rw-r--r--docs/images/pymux.pngbin0 -> 205809 bytes
-rw-r--r--docs/images/pyvim.pngbin0 -> 150054 bytes
-rw-r--r--docs/images/repl/sqlite-1.pngbin0 -> 125880 bytes
-rw-r--r--docs/images/repl/sqlite-2.pngbin0 -> 171192 bytes
-rw-r--r--docs/images/repl/sqlite-3.pngbin0 -> 126517 bytes
-rw-r--r--docs/images/repl/sqlite-4.pngbin0 -> 190998 bytes
-rw-r--r--docs/images/repl/sqlite-5.pngbin0 -> 182631 bytes
-rw-r--r--docs/images/repl/sqlite-6.pngbin0 -> 200352 bytes
-rw-r--r--docs/images/rprompt.pngbin0 -> 79787 bytes
-rw-r--r--docs/index.rst89
-rw-r--r--docs/make.bat242
-rw-r--r--docs/pages/advanced_topics/architecture.rst97
-rw-r--r--docs/pages/advanced_topics/asyncio.rst30
-rw-r--r--docs/pages/advanced_topics/filters.rst169
-rw-r--r--docs/pages/advanced_topics/index.rst18
-rw-r--r--docs/pages/advanced_topics/input_hooks.rst41
-rw-r--r--docs/pages/advanced_topics/key_bindings.rst388
-rw-r--r--docs/pages/advanced_topics/rendering_flow.rst86
-rw-r--r--docs/pages/advanced_topics/rendering_pipeline.rst157
-rw-r--r--docs/pages/advanced_topics/styling.rst320
-rw-r--r--docs/pages/advanced_topics/unit_testing.rst125
-rw-r--r--docs/pages/asking_for_input.rst1034
-rw-r--r--docs/pages/dialogs.rst270
-rw-r--r--docs/pages/full_screen_apps.rst422
-rw-r--r--docs/pages/gallery.rst32
-rw-r--r--docs/pages/getting_started.rst84
-rw-r--r--docs/pages/printing_text.rst274
-rw-r--r--docs/pages/progress_bars.rst248
-rw-r--r--docs/pages/reference.rst393
-rw-r--r--docs/pages/related_projects.rst11
-rw-r--r--docs/pages/tutorials/index.rst10
-rw-r--r--docs/pages/tutorials/repl.rst341
-rw-r--r--docs/pages/upgrading/2.0.rst221
-rw-r--r--docs/pages/upgrading/3.0.rst118
-rw-r--r--docs/pages/upgrading/index.rst11
-rw-r--r--docs/requirements.txt5
-rwxr-xr-xexamples/dialogs/button_dialog.py19
-rwxr-xr-xexamples/dialogs/checkbox_dialog.py36
-rwxr-xr-xexamples/dialogs/input_dialog.py17
-rwxr-xr-xexamples/dialogs/messagebox.py16
-rwxr-xr-xexamples/dialogs/password_dialog.py19
-rwxr-xr-xexamples/dialogs/progress_dialog.py47
-rwxr-xr-xexamples/dialogs/radio_dialog.py39
-rwxr-xr-xexamples/dialogs/styled_messagebox.py37
-rwxr-xr-xexamples/dialogs/yes_no_dialog.py17
-rwxr-xr-xexamples/full-screen/ansi-art-and-textarea.py82
-rwxr-xr-xexamples/full-screen/buttons.py91
-rwxr-xr-xexamples/full-screen/calculator.py95
-rwxr-xr-xexamples/full-screen/dummy-app.py8
-rwxr-xr-xexamples/full-screen/full-screen-demo.py225
-rwxr-xr-xexamples/full-screen/hello-world.py43
-rw-r--r--examples/full-screen/no-layout.py7
-rwxr-xr-xexamples/full-screen/pager.py111
-rw-r--r--examples/full-screen/scrollable-panes/simple-example.py45
-rw-r--r--examples/full-screen/scrollable-panes/with-completion-menu.py120
-rwxr-xr-xexamples/full-screen/simple-demos/alignment.py60
-rwxr-xr-xexamples/full-screen/simple-demos/autocompletion.py100
-rwxr-xr-xexamples/full-screen/simple-demos/colorcolumn.py63
-rwxr-xr-xexamples/full-screen/simple-demos/cursorcolumn-cursorline.py59
-rwxr-xr-xexamples/full-screen/simple-demos/float-transparency.py87
-rwxr-xr-xexamples/full-screen/simple-demos/floats.py116
-rwxr-xr-xexamples/full-screen/simple-demos/focus.py98
-rwxr-xr-xexamples/full-screen/simple-demos/horizontal-align.py208
-rwxr-xr-xexamples/full-screen/simple-demos/horizontal-split.py44
-rwxr-xr-xexamples/full-screen/simple-demos/line-prefixes.py110
-rwxr-xr-xexamples/full-screen/simple-demos/margins.py71
-rwxr-xr-xexamples/full-screen/simple-demos/vertical-align.py167
-rwxr-xr-xexamples/full-screen/simple-demos/vertical-split.py44
-rwxr-xr-xexamples/full-screen/split-screen.py156
-rwxr-xr-xexamples/full-screen/text-editor.py383
-rwxr-xr-xexamples/gevent-get-input.py24
-rwxr-xr-xexamples/print-text/ansi-colors.py100
-rwxr-xr-xexamples/print-text/ansi.py50
-rwxr-xr-xexamples/print-text/html.py53
-rwxr-xr-xexamples/print-text/named-colors.py29
-rwxr-xr-xexamples/print-text/print-formatted-text.py46
-rwxr-xr-xexamples/print-text/print-frame.py14
-rwxr-xr-xexamples/print-text/prompt-toolkit-logo-ansi-art.py40
-rwxr-xr-xexamples/print-text/pygments-tokens.py44
-rwxr-xr-xexamples/print-text/true-color-demo.py35
-rwxr-xr-xexamples/progress-bar/a-lot-of-parallel-tasks.py65
-rwxr-xr-xexamples/progress-bar/colored-title-and-label.py22
-rwxr-xr-xexamples/progress-bar/custom-key-bindings.py51
-rwxr-xr-xexamples/progress-bar/many-parallel-tasks.py46
-rwxr-xr-xexamples/progress-bar/nested-progress-bars.py22
-rwxr-xr-xexamples/progress-bar/scrolling-task-name.py23
-rwxr-xr-xexamples/progress-bar/simple-progress-bar.py18
-rwxr-xr-xexamples/progress-bar/styled-1.py36
-rwxr-xr-xexamples/progress-bar/styled-2.py49
-rwxr-xr-xexamples/progress-bar/styled-apt-get-install.py38
-rwxr-xr-xexamples/progress-bar/styled-rainbow.py35
-rwxr-xr-xexamples/progress-bar/styled-tqdm-1.py40
-rwxr-xr-xexamples/progress-bar/styled-tqdm-2.py38
-rwxr-xr-xexamples/progress-bar/two-tasks.py39
-rwxr-xr-xexamples/progress-bar/unknown-length.py26
-rw-r--r--examples/prompts/accept-default.py16
-rwxr-xr-xexamples/prompts/asyncio-prompt.py63
-rwxr-xr-xexamples/prompts/auto-completion/autocomplete-with-control-space.py75
-rwxr-xr-xexamples/prompts/auto-completion/autocompletion-like-readline.py58
-rwxr-xr-xexamples/prompts/auto-completion/autocompletion.py60
-rwxr-xr-xexamples/prompts/auto-completion/colored-completions-with-formatted-text.py137
-rwxr-xr-xexamples/prompts/auto-completion/colored-completions.py78
-rwxr-xr-xexamples/prompts/auto-completion/combine-multiple-completers.py76
-rwxr-xr-xexamples/prompts/auto-completion/fuzzy-custom-completer.py56
-rwxr-xr-xexamples/prompts/auto-completion/fuzzy-word-completer.py59
-rwxr-xr-xexamples/prompts/auto-completion/multi-column-autocompletion-with-meta.py50
-rwxr-xr-xexamples/prompts/auto-completion/multi-column-autocompletion.py57
-rwxr-xr-xexamples/prompts/auto-completion/nested-autocompletion.py22
-rwxr-xr-xexamples/prompts/auto-completion/slow-completions.py103
-rwxr-xr-xexamples/prompts/auto-suggestion.py48
-rwxr-xr-xexamples/prompts/autocorrection.py44
-rwxr-xr-xexamples/prompts/bottom-toolbar.py80
-rwxr-xr-xexamples/prompts/clock-input.py25
-rwxr-xr-xexamples/prompts/colored-prompt.py81
-rwxr-xr-xexamples/prompts/confirmation-prompt.py9
-rwxr-xr-xexamples/prompts/cursor-shapes.py19
-rwxr-xr-xexamples/prompts/custom-key-binding.py77
-rwxr-xr-xexamples/prompts/custom-lexer.py29
-rwxr-xr-xexamples/prompts/custom-vi-operator-and-text-object.py70
-rwxr-xr-xexamples/prompts/enforce-tty-input-output.py13
-rwxr-xr-xexamples/prompts/fancy-zsh-prompt.py79
-rwxr-xr-xexamples/prompts/finalterm-shell-integration.py43
-rwxr-xr-xexamples/prompts/get-input-vi-mode.py7
-rwxr-xr-xexamples/prompts/get-input-with-default.py12
-rwxr-xr-xexamples/prompts/get-input.py9
-rwxr-xr-xexamples/prompts/get-multiline-input.py29
-rwxr-xr-xexamples/prompts/get-password-with-toggle-display-shortcut.py28
-rwxr-xr-xexamples/prompts/get-password.py6
-rwxr-xr-xexamples/prompts/history/persistent-history.py25
-rwxr-xr-xexamples/prompts/history/slow-history.py48
-rwxr-xr-xexamples/prompts/html-input.py18
-rwxr-xr-xexamples/prompts/input-validation.py35
-rwxr-xr-xexamples/prompts/inputhook.py83
-rwxr-xr-xexamples/prompts/mouse-support.py10
-rwxr-xr-xexamples/prompts/multiline-prompt.py11
-rwxr-xr-xexamples/prompts/no-wrapping.py6
-rwxr-xr-xexamples/prompts/operate-and-get-next.py18
-rwxr-xr-xexamples/prompts/patch-stdout.py41
-rwxr-xr-xexamples/prompts/placeholder-text.py13
-rwxr-xr-xexamples/prompts/regular-language.py108
-rwxr-xr-xexamples/prompts/rprompt.py53
-rwxr-xr-xexamples/prompts/swap-light-and-dark-colors.py78
-rwxr-xr-xexamples/prompts/switch-between-vi-emacs.py36
-rwxr-xr-xexamples/prompts/system-clipboard-integration.py17
-rwxr-xr-xexamples/prompts/system-prompt.py20
-rwxr-xr-xexamples/prompts/terminal-title.py10
-rwxr-xr-xexamples/prompts/up-arrow-partial-string-matching.py41
-rwxr-xr-xexamples/ssh/asyncssh-server.py120
-rwxr-xr-xexamples/telnet/chat-app.py103
-rwxr-xr-xexamples/telnet/dialog.py34
-rwxr-xr-xexamples/telnet/hello-world.py39
-rwxr-xr-xexamples/telnet/toolbar.py44
-rwxr-xr-xexamples/tutorial/README.md1
-rwxr-xr-xexamples/tutorial/sqlite-cli.py184
-rw-r--r--mypy.ini18
-rw-r--r--pyproject.toml66
-rw-r--r--setup.cfg40
-rwxr-xr-xsetup.py53
-rw-r--r--src/prompt_toolkit/__init__.py51
-rw-r--r--src/prompt_toolkit/application/__init__.py32
-rw-r--r--src/prompt_toolkit/application/application.py1625
-rw-r--r--src/prompt_toolkit/application/current.py189
-rw-r--r--src/prompt_toolkit/application/dummy.py55
-rw-r--r--src/prompt_toolkit/application/run_in_terminal.py113
-rw-r--r--src/prompt_toolkit/auto_suggest.py176
-rw-r--r--src/prompt_toolkit/buffer.py2026
-rw-r--r--src/prompt_toolkit/cache.py127
-rw-r--r--src/prompt_toolkit/clipboard/__init__.py17
-rw-r--r--src/prompt_toolkit/clipboard/base.py108
-rw-r--r--src/prompt_toolkit/clipboard/in_memory.py44
-rw-r--r--src/prompt_toolkit/clipboard/pyperclip.py42
-rw-r--r--src/prompt_toolkit/completion/__init__.py43
-rw-r--r--src/prompt_toolkit/completion/base.py451
-rw-r--r--src/prompt_toolkit/completion/deduplicate.py45
-rw-r--r--src/prompt_toolkit/completion/filesystem.py118
-rw-r--r--src/prompt_toolkit/completion/fuzzy_completer.py213
-rw-r--r--src/prompt_toolkit/completion/nested.py108
-rw-r--r--src/prompt_toolkit/completion/word_completer.py94
-rw-r--r--src/prompt_toolkit/contrib/__init__.py0
-rw-r--r--src/prompt_toolkit/contrib/completers/__init__.py5
-rw-r--r--src/prompt_toolkit/contrib/completers/system.py64
-rw-r--r--src/prompt_toolkit/contrib/regular_languages/__init__.py79
-rw-r--r--src/prompt_toolkit/contrib/regular_languages/compiler.py571
-rw-r--r--src/prompt_toolkit/contrib/regular_languages/completion.py94
-rw-r--r--src/prompt_toolkit/contrib/regular_languages/lexer.py93
-rw-r--r--src/prompt_toolkit/contrib/regular_languages/regex_parser.py282
-rw-r--r--src/prompt_toolkit/contrib/regular_languages/validation.py59
-rw-r--r--src/prompt_toolkit/contrib/ssh/__init__.py8
-rw-r--r--src/prompt_toolkit/contrib/ssh/server.py177
-rw-r--r--src/prompt_toolkit/contrib/telnet/__init__.py7
-rw-r--r--src/prompt_toolkit/contrib/telnet/log.py12
-rw-r--r--src/prompt_toolkit/contrib/telnet/protocol.py208
-rw-r--r--src/prompt_toolkit/contrib/telnet/server.py427
-rw-r--r--src/prompt_toolkit/cursor_shapes.py104
-rw-r--r--src/prompt_toolkit/data_structures.py18
-rw-r--r--src/prompt_toolkit/document.py1181
-rw-r--r--src/prompt_toolkit/enums.py19
-rw-r--r--src/prompt_toolkit/eventloop/__init__.py31
-rw-r--r--src/prompt_toolkit/eventloop/async_generator.py124
-rw-r--r--src/prompt_toolkit/eventloop/inputhook.py190
-rw-r--r--src/prompt_toolkit/eventloop/utils.py101
-rw-r--r--src/prompt_toolkit/eventloop/win32.py72
-rw-r--r--src/prompt_toolkit/filters/__init__.py70
-rw-r--r--src/prompt_toolkit/filters/app.py418
-rw-r--r--src/prompt_toolkit/filters/base.py255
-rw-r--r--src/prompt_toolkit/filters/cli.py64
-rw-r--r--src/prompt_toolkit/filters/utils.py41
-rw-r--r--src/prompt_toolkit/formatted_text/__init__.py58
-rw-r--r--src/prompt_toolkit/formatted_text/ansi.py299
-rw-r--r--src/prompt_toolkit/formatted_text/base.py180
-rw-r--r--src/prompt_toolkit/formatted_text/html.py145
-rw-r--r--src/prompt_toolkit/formatted_text/pygments.py32
-rw-r--r--src/prompt_toolkit/formatted_text/utils.py102
-rw-r--r--src/prompt_toolkit/history.py302
-rw-r--r--src/prompt_toolkit/input/__init__.py14
-rw-r--r--src/prompt_toolkit/input/ansi_escape_sequences.py343
-rw-r--r--src/prompt_toolkit/input/base.py152
-rw-r--r--src/prompt_toolkit/input/defaults.py79
-rw-r--r--src/prompt_toolkit/input/posix_pipe.py118
-rw-r--r--src/prompt_toolkit/input/posix_utils.py97
-rw-r--r--src/prompt_toolkit/input/typeahead.py77
-rw-r--r--src/prompt_toolkit/input/vt100.py309
-rw-r--r--src/prompt_toolkit/input/vt100_parser.py249
-rw-r--r--src/prompt_toolkit/input/win32.py749
-rw-r--r--src/prompt_toolkit/input/win32_pipe.py156
-rw-r--r--src/prompt_toolkit/key_binding/__init__.py22
-rw-r--r--src/prompt_toolkit/key_binding/bindings/__init__.py0
-rw-r--r--src/prompt_toolkit/key_binding/bindings/auto_suggest.py65
-rw-r--r--src/prompt_toolkit/key_binding/bindings/basic.py255
-rw-r--r--src/prompt_toolkit/key_binding/bindings/completion.py205
-rw-r--r--src/prompt_toolkit/key_binding/bindings/cpr.py30
-rw-r--r--src/prompt_toolkit/key_binding/bindings/emacs.py557
-rw-r--r--src/prompt_toolkit/key_binding/bindings/focus.py26
-rw-r--r--src/prompt_toolkit/key_binding/bindings/mouse.py348
-rw-r--r--src/prompt_toolkit/key_binding/bindings/named_commands.py690
-rw-r--r--src/prompt_toolkit/key_binding/bindings/open_in_editor.py51
-rw-r--r--src/prompt_toolkit/key_binding/bindings/page_navigation.py84
-rw-r--r--src/prompt_toolkit/key_binding/bindings/scroll.py189
-rw-r--r--src/prompt_toolkit/key_binding/bindings/search.py95
-rw-r--r--src/prompt_toolkit/key_binding/bindings/vi.py2224
-rw-r--r--src/prompt_toolkit/key_binding/defaults.py62
-rw-r--r--src/prompt_toolkit/key_binding/digraphs.py1377
-rw-r--r--src/prompt_toolkit/key_binding/emacs_state.py36
-rw-r--r--src/prompt_toolkit/key_binding/key_bindings.py671
-rw-r--r--src/prompt_toolkit/key_binding/key_processor.py529
-rw-r--r--src/prompt_toolkit/key_binding/vi_state.py107
-rw-r--r--src/prompt_toolkit/keys.py222
-rw-r--r--src/prompt_toolkit/layout/__init__.py146
-rw-r--r--src/prompt_toolkit/layout/containers.py2743
-rw-r--r--src/prompt_toolkit/layout/controls.py944
-rw-r--r--src/prompt_toolkit/layout/dimension.py219
-rw-r--r--src/prompt_toolkit/layout/dummy.py39
-rw-r--r--src/prompt_toolkit/layout/layout.py411
-rw-r--r--src/prompt_toolkit/layout/margins.py303
-rw-r--r--src/prompt_toolkit/layout/menus.py751
-rw-r--r--src/prompt_toolkit/layout/mouse_handlers.py56
-rw-r--r--src/prompt_toolkit/layout/processors.py1013
-rw-r--r--src/prompt_toolkit/layout/screen.py329
-rw-r--r--src/prompt_toolkit/layout/scrollable_pane.py494
-rw-r--r--src/prompt_toolkit/layout/utils.py82
-rw-r--r--src/prompt_toolkit/lexers/__init__.py20
-rw-r--r--src/prompt_toolkit/lexers/base.py84
-rw-r--r--src/prompt_toolkit/lexers/pygments.py327
-rw-r--r--src/prompt_toolkit/log.py12
-rw-r--r--src/prompt_toolkit/mouse_events.py89
-rw-r--r--src/prompt_toolkit/output/__init__.py15
-rw-r--r--src/prompt_toolkit/output/base.py331
-rw-r--r--src/prompt_toolkit/output/color_depth.py64
-rw-r--r--src/prompt_toolkit/output/conemu.py65
-rw-r--r--src/prompt_toolkit/output/defaults.py102
-rw-r--r--src/prompt_toolkit/output/flush_stdout.py87
-rw-r--r--src/prompt_toolkit/output/plain_text.py143
-rw-r--r--src/prompt_toolkit/output/vt100.py747
-rw-r--r--src/prompt_toolkit/output/win32.py683
-rw-r--r--src/prompt_toolkit/output/windows10.py128
-rw-r--r--src/prompt_toolkit/patch_stdout.py296
-rw-r--r--src/prompt_toolkit/py.typed0
-rw-r--r--src/prompt_toolkit/renderer.py813
-rw-r--r--src/prompt_toolkit/search.py230
-rw-r--r--src/prompt_toolkit/selection.py61
-rw-r--r--src/prompt_toolkit/shortcuts/__init__.py46
-rw-r--r--src/prompt_toolkit/shortcuts/dialogs.py330
-rw-r--r--src/prompt_toolkit/shortcuts/progress_bar/__init__.py33
-rw-r--r--src/prompt_toolkit/shortcuts/progress_bar/base.py448
-rw-r--r--src/prompt_toolkit/shortcuts/progress_bar/formatters.py429
-rw-r--r--src/prompt_toolkit/shortcuts/prompt.py1504
-rw-r--r--src/prompt_toolkit/shortcuts/utils.py239
-rw-r--r--src/prompt_toolkit/styles/__init__.py66
-rw-r--r--src/prompt_toolkit/styles/base.py183
-rw-r--r--src/prompt_toolkit/styles/defaults.py235
-rw-r--r--src/prompt_toolkit/styles/named_colors.py161
-rw-r--r--src/prompt_toolkit/styles/pygments.py69
-rw-r--r--src/prompt_toolkit/styles/style.py400
-rw-r--r--src/prompt_toolkit/styles/style_transformation.py373
-rw-r--r--src/prompt_toolkit/token.py10
-rw-r--r--src/prompt_toolkit/utils.py327
-rw-r--r--src/prompt_toolkit/validation.py195
-rw-r--r--src/prompt_toolkit/widgets/__init__.py62
-rw-r--r--src/prompt_toolkit/widgets/base.py981
-rw-r--r--src/prompt_toolkit/widgets/dialogs.py107
-rw-r--r--src/prompt_toolkit/widgets/menus.py374
-rw-r--r--src/prompt_toolkit/widgets/toolbars.py374
-rw-r--r--src/prompt_toolkit/win32_types.py229
-rw-r--r--tests/test_async_generator.py28
-rw-r--r--tests/test_buffer.py112
-rw-r--r--tests/test_cli.py941
-rw-r--r--tests/test_completion.py469
-rw-r--r--tests/test_document.py69
-rw-r--r--tests/test_filter.py131
-rw-r--r--tests/test_formatted_text.py286
-rw-r--r--tests/test_history.py103
-rw-r--r--tests/test_inputstream.py141
-rw-r--r--tests/test_key_binding.py200
-rw-r--r--tests/test_layout.py53
-rw-r--r--tests/test_memory_leaks.py35
-rw-r--r--tests/test_print_formatted_text.py92
-rw-r--r--tests/test_regular_languages.py102
-rw-r--r--tests/test_shortcuts.py68
-rw-r--r--tests/test_style.py276
-rw-r--r--tests/test_style_transformation.py51
-rw-r--r--tests/test_utils.py78
-rw-r--r--tests/test_vt100_output.py20
-rw-r--r--tests/test_widgets.py20
-rw-r--r--tests/test_yank_nth_arg.py86
-rwxr-xr-xtools/debug_input_cross_platform.py31
-rwxr-xr-xtools/debug_vt100_input.py32
-rw-r--r--tox.ini13
364 files changed, 59334 insertions, 0 deletions
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..db24720
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1 @@
+comment: off
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..eedbdd8
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,54 @@
+name: test
+
+on:
+ push: # any branch
+ pull_request:
+ branches: [master]
+
+env:
+ FORCE_COLOR: 1
+
+jobs:
+ test-ubuntu:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
+ - name: Install Dependencies
+ run: |
+ sudo apt remove python3-pip
+ python -m pip install --upgrade pip
+ python -m pip install . ruff typos coverage codecov mypy pytest readme_renderer types-contextvars asyncssh
+ pip list
+ - name: Ruff
+ run: |
+ ruff .
+ ruff format --check .
+ typos .
+ - name: Tests
+ run: |
+ coverage run -m pytest
+ - if: "matrix.python-version != '3.7'"
+ name: Mypy
+ # Check whether the imports were sorted correctly.
+ # When this fails, please run ./tools/sort-imports.sh
+ run: |
+ mypy --strict src/prompt_toolkit --platform win32
+ mypy --strict src/prompt_toolkit --platform linux
+ mypy --strict src/prompt_toolkit --platform darwin
+ - name: Validate README.md
+ # Ensure that the README renders correctly (required for uploading to PyPI).
+ run: |
+ python -m readme_renderer README.rst > /dev/null
+ - name: Run codecov
+ run: |
+ codecov
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..337ddba
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,52 @@
+*.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
+
+# Translations
+*.mo
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# Generated documentation
+docs/_build
+
+# pycharm metadata
+.idea
+
+# vscode metadata
+.vscode
+
+# virtualenvs
+.venv*
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..cd40bf2
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,19 @@
+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: .
diff --git a/AUTHORS.rst b/AUTHORS.rst
new file mode 100644
index 0000000..f7c8f60
--- /dev/null
+++ b/AUTHORS.rst
@@ -0,0 +1,11 @@
+Authors
+=======
+
+Creator
+-------
+Jonathan Slenders <jonathan AT slenders.be>
+
+Contributors
+------------
+
+- Amjith Ramanujam <amjith.r AT gmail.com>
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..e34916b
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,2171 @@
+CHANGELOG
+=========
+
+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 <esc> 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 <num>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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e1720e0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..20c6caa
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include *rst LICENSE CHANGELOG MANIFEST.in
+recursive-include examples *.py
+recursive-include tests *.py
+prune examples/sample?/build
diff --git a/PROJECTS.rst b/PROJECTS.rst
new file mode 100644
index 0000000..863c727
--- /dev/null
+++ b/PROJECTS.rst
@@ -0,0 +1,68 @@
+Projects using `prompt_toolkit`
+===============================
+
+Shells:
+
+- `ptpython <http://github.com/prompt-toolkit/ptpython/>`_: Python REPL
+- `ptpdb <http://github.com/jonathanslenders/ptpdb/>`_: Python debugger (pdb replacement)
+- `pgcli <https://www.pgcli.com/>`_: Postgres client.
+- `mycli <https://www.mycli.net/>`_: MySql client.
+- `litecli <https://litecli.com/>`_: SQLite client.
+- `wharfee <http://wharfee.com/>`_: A Docker command line.
+- `xonsh <http://xon.sh/>`_: A Python-ish, BASHwards-compatible shell.
+- `saws <https://github.com/donnemartin/saws>`_: A Supercharged AWS Command Line Interface.
+- `cycli <https://github.com/nicolewhite/cycli>`_: A Command Line Interface for Cypher.
+- `crash <https://github.com/crate/crash>`_: Crate command line client.
+- `vcli <https://github.com/dbcli/vcli>`_: Vertica client.
+- `aws-shell <https://github.com/awslabs/aws-shell>`_: An integrated shell for working with the AWS CLI.
+- `softlayer-python <https://github.com/softlayer/softlayer-python>`_: A command-line interface to manage various SoftLayer products and services.
+- `ipython <http://github.com/ipython/ipython/>`_: The IPython REPL
+- `click-repl <https://github.com/click-contrib/click-repl>`_: Subcommand REPL for click apps.
+- `haxor-news <https://github.com/donnemartin/haxor-news>`_: A Hacker News CLI.
+- `gitsome <https://github.com/donnemartin/gitsome>`_: A Git/Shell Autocompleter with GitHub Integration.
+- `http-prompt <https://github.com/eliangcs/http-prompt>`_: An interactive command-line HTTP client.
+- `coconut <http://coconut-lang.org/>`_: Functional programming in Python.
+- `Ergonomica <https://github.com/ergonomica/ergonomica>`_: A Bash alternative written in Python.
+- `Kube-shell <https://github.com/cloudnativelabs/kube-shell>`_: Kubernetes shell: An integrated shell for working with the Kubernetes CLI
+- `mssql-cli <https://github.com/dbcli/mssql-cli>`_: A command-line client for Microsoft SQL Server.
+- `robotframework-debuglibrary <https://github.com/xyb/robotframework-debuglibrary>`_: A debug library and REPL for RobotFramework.
+- `ptrepl <https://github.com/imomaliev/ptrepl>`_: Run any command as REPL
+- `clipwdmgr <https://github.com/samisalkosuo/clipasswordmgr>`_: Command Line Password Manager.
+- `slacker <https://github.com/netromdk/slacker>`_: Easy access to the Slack API and admin of workspaces via REPL.
+- `EdgeDB <https://edgedb.com/>`_: The next generation object-relational database.
+- `pywit <https://github.com/wit-ai/pywit>`_: Python library for Wit.ai.
+- `objection <https://github.com/sensepost/objection>`_: Runtime Mobile Exploration.
+- `habu <https://github.com/portantier/habu>`_: Python Network Hacking Toolkit.
+- `nawano <https://github.com/rbw/nawano>`_: Nano cryptocurrency wallet
+- `athenacli <https://github.com/dbcli/athenacli>`_: A CLI for AWS Athena.
+- `vulcano <https://github.com/dgarana/vulcano>`_: A framework for creating command-line applications that also runs in REPL mode.
+- `kafka-shell <https://github.com/devshawn/kafka-shell>`_: A supercharged shell for Apache Kafka.
+- `starterTree <https://github.com/thomas10-10/starterTree>`_: A command launcher organized in a tree structure with fuzzy autocompletion
+- `git-delete-merged-branches <https://github.com/hartwork/git-delete-merged-branches>`_: Command-line tool to delete merged Git branches
+
+Full screen applications:
+
+- `pymux <http://github.com/prompt-toolkit/pymux/>`_: A terminal multiplexer (like tmux) in pure Python.
+- `pyvim <http://github.com/prompt-toolkit/pyvim/>`_: A Vim clone in pure Python.
+- `freud <http://github.com/stloma/freud/>`_: REST client backed by SQLite for storing servers
+- `pypager <https://github.com/prompt-toolkit/pypager>`_: A $PAGER in pure Python (like "less").
+- `kubeterminal <https://github.com/samisalkosuo/kubeterminal>`_: Kubectl helper tool.
+- `pydoro <https://github.com/JaDogg/pydoro>`_: Pomodoro timer.
+- `sanctuary-zero <https://github.com/t0xic0der/sanctuary-zero>`_: A secure chatroom with zero logging and total transience.
+- `Hummingbot <https://github.com/CoinAlpha/hummingbot>`_: A Cryptocurrency Algorithmic Trading Platform
+- `git-bbb <https://github.com/MrMino/git-bbb>`_: A `git blame` browser.
+
+Libraries:
+
+- `ptterm <https://github.com/prompt-toolkit/ptterm>`_: A terminal emulator widget for prompt_toolkit.
+- `PyInquirer <https://github.com/CITGuru/PyInquirer/>`_: A Python library that wants to make it easy for existing Inquirer.js users to write immersive command line applications in Python.
+- `clintermission <https://github.com/sebageek/clintermission>`_: Non-fullscreen command-line selection menu
+
+Other libraries and implementations in other languages
+******************************************************
+
+- `go-prompt <https://github.com/c-bata/go-prompt>`_: building a powerful
+ interactive prompt in Go, inspired by python-prompt-toolkit.
+- `urwid <http://urwid.org/>`_: Console user interface library for Python.
+
+(Want your own project to be listed here? Please create a GitHub issue.)
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..3cd6687
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,146 @@
+Python Prompt Toolkit
+=====================
+
+|AppVeyor| |PyPI| |RTD| |License| |Codecov|
+
+.. image :: https://github.com/prompt-toolkit/python-prompt-toolkit/raw/master/docs/images/logo_400px.png
+
+``prompt_toolkit`` *is a library for building powerful interactive command line applications in Python.*
+
+Read the `documentation on readthedocs
+<http://python-prompt-toolkit.readthedocs.io/en/stable/>`_.
+
+
+Gallery
+*******
+
+`ptpython <http://github.com/prompt-toolkit/ptpython/>`_ is an interactive
+Python Shell, build on top of ``prompt_toolkit``.
+
+.. image :: https://github.com/prompt-toolkit/python-prompt-toolkit/raw/master/docs/images/ptpython.png
+
+`More examples <https://python-prompt-toolkit.readthedocs.io/en/stable/pages/gallery.html>`_
+
+
+prompt_toolkit features
+***********************
+
+``prompt_toolkit`` could be a replacement for `GNU readline
+<https://tiswww.case.edu/php/chet/readline/rltop.html>`_, 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 <https://cirw.in/blog/bracketed-paste>`_.
+- Mouse support for cursor positioning and scrolling.
+- Auto suggestions. (Like `fish shell <http://fishshell.com/>`_.)
+- 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.
+
+For Windows, it's recommended to use either `cmder
+<http://cmder.net/>`_ or `conemu <https://conemu.github.io/>`_.
+
+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 <PROJECTS.rst>`_
+***********************************************
+
+Special thanks to
+*****************
+
+- `Pygments <http://pygments.org/>`_: Syntax highlighter.
+- `wcwidth <https://github.com/jquast/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/
+
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..892b186
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,45 @@
+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
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d5ddb2d
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> 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 " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of 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)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/prompt_toolkit.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/prompt_toolkit.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/prompt_toolkit"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/prompt_toolkit"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..abf6db3
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,313 @@
+#
+# 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"
+
+# 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-2023, 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.
+#
+# The short X.Y version.
+version = "3.0.43"
+# The full version, including alpha/beta/rc tags.
+release = "3.0.43"
+
+# 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"
+pygments_dark_style = "dracula"
+
+# 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"
+
+try:
+ import sphinx_nefertiti
+
+ html_theme = "sphinx_nefertiti"
+ html_theme_path = [sphinx_nefertiti.get_html_theme_path()]
+ html_theme_options = {
+ # "style" can take the following values: "blue", "indigo", "purple",
+ # "pink", "red", "orange", "yellow", "green", "tail", and "default".
+ "style": "default",
+ # Fonts are customizable (and are not retrieved online).
+ # https://sphinx-nefertiti.readthedocs.io/en/latest/users-guide/customization/fonts.html
+ # "documentation_font": "Open Sans",
+ # "monospace_font": "Ubuntu Mono",
+ # "monospace_font_size": "1.1rem",
+ "logo": "logo_400px.png",
+ "logo_alt": "python-prompt-toolkit",
+ "logo_width": "36",
+ "logo_height": "36",
+ "repository_url": "https://github.com/prompt-toolkit/python-prompt-toolkit",
+ "repository_name": "python-prompt-toolkit",
+ "footer_links": ",".join(
+ [
+ "Documentation|https://python-prompt-toolkit.readthedocs.io/",
+ "Package|https://pypi.org/project/prompt-toolkit/",
+ "Repository|https://github.com/prompt-toolkit/python-prompt-toolkit",
+ "Issues|https://github.com/prompt-toolkit/python-prompt-toolkit/issues",
+ ]
+ ),
+ }
+
+except ImportError:
+ html_theme = "pyramid"
+
+
+# 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
+# "<project> v<release> 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 <link> 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
diff --git a/docs/images/auto-suggestion.png b/docs/images/auto-suggestion.png
new file mode 100644
index 0000000..7671156
--- /dev/null
+++ b/docs/images/auto-suggestion.png
Binary files differ
diff --git a/docs/images/bottom-toolbar.png b/docs/images/bottom-toolbar.png
new file mode 100644
index 0000000..8d5f17c
--- /dev/null
+++ b/docs/images/bottom-toolbar.png
Binary files differ
diff --git a/docs/images/colored-prompt.png b/docs/images/colored-prompt.png
new file mode 100644
index 0000000..bd85975
--- /dev/null
+++ b/docs/images/colored-prompt.png
Binary files differ
diff --git a/docs/images/colorful-completions.png b/docs/images/colorful-completions.png
new file mode 100644
index 0000000..c397815
--- /dev/null
+++ b/docs/images/colorful-completions.png
Binary files differ
diff --git a/docs/images/dialogs/button.png b/docs/images/dialogs/button.png
new file mode 100644
index 0000000..fb4b3bb
--- /dev/null
+++ b/docs/images/dialogs/button.png
Binary files differ
diff --git a/docs/images/dialogs/confirm.png b/docs/images/dialogs/confirm.png
new file mode 100644
index 0000000..1b7d9ec
--- /dev/null
+++ b/docs/images/dialogs/confirm.png
Binary files differ
diff --git a/docs/images/dialogs/inputbox.png b/docs/images/dialogs/inputbox.png
new file mode 100644
index 0000000..73ae79f
--- /dev/null
+++ b/docs/images/dialogs/inputbox.png
Binary files differ
diff --git a/docs/images/dialogs/messagebox.png b/docs/images/dialogs/messagebox.png
new file mode 100644
index 0000000..3ac08c0
--- /dev/null
+++ b/docs/images/dialogs/messagebox.png
Binary files differ
diff --git a/docs/images/dialogs/styled.png b/docs/images/dialogs/styled.png
new file mode 100644
index 0000000..a359a7a
--- /dev/null
+++ b/docs/images/dialogs/styled.png
Binary files differ
diff --git a/docs/images/hello-world-prompt.png b/docs/images/hello-world-prompt.png
new file mode 100644
index 0000000..17e7e1f
--- /dev/null
+++ b/docs/images/hello-world-prompt.png
Binary files differ
diff --git a/docs/images/html-completion.png b/docs/images/html-completion.png
new file mode 100644
index 0000000..8fd73ad
--- /dev/null
+++ b/docs/images/html-completion.png
Binary files differ
diff --git a/docs/images/html-input.png b/docs/images/html-input.png
new file mode 100644
index 0000000..1f4b11b
--- /dev/null
+++ b/docs/images/html-input.png
Binary files differ
diff --git a/docs/images/logo_400px.png b/docs/images/logo_400px.png
new file mode 100755
index 0000000..5d90d44
--- /dev/null
+++ b/docs/images/logo_400px.png
Binary files differ
diff --git a/docs/images/multiline-input.png b/docs/images/multiline-input.png
new file mode 100644
index 0000000..7d72844
--- /dev/null
+++ b/docs/images/multiline-input.png
Binary files differ
diff --git a/docs/images/number-validator.png b/docs/images/number-validator.png
new file mode 100644
index 0000000..5a12c89
--- /dev/null
+++ b/docs/images/number-validator.png
Binary files differ
diff --git a/docs/images/progress-bars/apt-get.png b/docs/images/progress-bars/apt-get.png
new file mode 100755
index 0000000..ce62464
--- /dev/null
+++ b/docs/images/progress-bars/apt-get.png
Binary files differ
diff --git a/docs/images/progress-bars/colored-title-and-label.png b/docs/images/progress-bars/colored-title-and-label.png
new file mode 100755
index 0000000..ace4393
--- /dev/null
+++ b/docs/images/progress-bars/colored-title-and-label.png
Binary files differ
diff --git a/docs/images/progress-bars/custom-key-bindings.png b/docs/images/progress-bars/custom-key-bindings.png
new file mode 100755
index 0000000..5f3610c
--- /dev/null
+++ b/docs/images/progress-bars/custom-key-bindings.png
Binary files differ
diff --git a/docs/images/progress-bars/simple-progress-bar.png b/docs/images/progress-bars/simple-progress-bar.png
new file mode 100755
index 0000000..6ea3eac
--- /dev/null
+++ b/docs/images/progress-bars/simple-progress-bar.png
Binary files differ
diff --git a/docs/images/progress-bars/two-tasks.png b/docs/images/progress-bars/two-tasks.png
new file mode 100755
index 0000000..0bb3f75
--- /dev/null
+++ b/docs/images/progress-bars/two-tasks.png
Binary files differ
diff --git a/docs/images/ptpython-2.png b/docs/images/ptpython-2.png
new file mode 100644
index 0000000..0fce32c
--- /dev/null
+++ b/docs/images/ptpython-2.png
Binary files differ
diff --git a/docs/images/ptpython-history-help.png b/docs/images/ptpython-history-help.png
new file mode 100644
index 0000000..a52b5c2
--- /dev/null
+++ b/docs/images/ptpython-history-help.png
Binary files differ
diff --git a/docs/images/ptpython-menu.png b/docs/images/ptpython-menu.png
new file mode 100644
index 0000000..923ca12
--- /dev/null
+++ b/docs/images/ptpython-menu.png
Binary files differ
diff --git a/docs/images/ptpython.png b/docs/images/ptpython.png
new file mode 100644
index 0000000..c46b55a
--- /dev/null
+++ b/docs/images/ptpython.png
Binary files differ
diff --git a/docs/images/pymux.png b/docs/images/pymux.png
new file mode 100644
index 0000000..7e8d73a
--- /dev/null
+++ b/docs/images/pymux.png
Binary files differ
diff --git a/docs/images/pyvim.png b/docs/images/pyvim.png
new file mode 100644
index 0000000..f78000f
--- /dev/null
+++ b/docs/images/pyvim.png
Binary files differ
diff --git a/docs/images/repl/sqlite-1.png b/docs/images/repl/sqlite-1.png
new file mode 100644
index 0000000..0511daa
--- /dev/null
+++ b/docs/images/repl/sqlite-1.png
Binary files differ
diff --git a/docs/images/repl/sqlite-2.png b/docs/images/repl/sqlite-2.png
new file mode 100644
index 0000000..47b0238
--- /dev/null
+++ b/docs/images/repl/sqlite-2.png
Binary files differ
diff --git a/docs/images/repl/sqlite-3.png b/docs/images/repl/sqlite-3.png
new file mode 100644
index 0000000..cdee9d2
--- /dev/null
+++ b/docs/images/repl/sqlite-3.png
Binary files differ
diff --git a/docs/images/repl/sqlite-4.png b/docs/images/repl/sqlite-4.png
new file mode 100644
index 0000000..c6ee929
--- /dev/null
+++ b/docs/images/repl/sqlite-4.png
Binary files differ
diff --git a/docs/images/repl/sqlite-5.png b/docs/images/repl/sqlite-5.png
new file mode 100644
index 0000000..d1964e0
--- /dev/null
+++ b/docs/images/repl/sqlite-5.png
Binary files differ
diff --git a/docs/images/repl/sqlite-6.png b/docs/images/repl/sqlite-6.png
new file mode 100644
index 0000000..054beb0
--- /dev/null
+++ b/docs/images/repl/sqlite-6.png
Binary files differ
diff --git a/docs/images/rprompt.png b/docs/images/rprompt.png
new file mode 100644
index 0000000..a44f0e9
--- /dev/null
+++ b/docs/images/rprompt.png
Binary files differ
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..569e14c
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,89 @@
+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
+<http://cnswww.cns.cwru.edu/php/chet/readline/rltop.html>`_, 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 <http://fishshell.com/>`_.)
+- 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
+ <https://docs.python.org/3/library/asyncio.html>`_ process.
+
+Have a look at :ref:`the gallery <gallery>` to get an idea of what is possible.
+
+Getting started
+---------------
+
+Go to :ref:`getting started <getting_started>` and build your first prompt.
+Issues are tracked `on the Github project
+<https://github.com/prompt-toolkit/python-prompt-toolkit>`_.
+
+
+Thanks to:
+----------
+
+A special thanks to `all the contributors
+<https://github.com/prompt-toolkit/python-prompt-toolkit/graphs/contributors>`_
+for making prompt_toolkit possible.
+
+Also, a special thanks to the `Pygments <http://pygments.org/>`_ and `wcwidth
+<https://github.com/jquast/wcwidth>`_ libraries.
+
+
+Table of contents
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ pages/gallery
+ pages/getting_started
+ pages/upgrading/index
+ pages/printing_text
+ pages/asking_for_input
+ 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
+<http://github.com/prompt-toolkit/>`_.
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..18ae022
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,242 @@
+@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 ^<target^>` where ^<target^> 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
diff --git a/docs/pages/advanced_topics/architecture.rst b/docs/pages/advanced_topics/architecture.rst
new file mode 100644
index 0000000..c690104
--- /dev/null
+++ b/docs/pages/advanced_topics/architecture.rst
@@ -0,0 +1,97 @@
+.. _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. |
+ +---------------------------------------------------------------+
diff --git a/docs/pages/advanced_topics/asyncio.rst b/docs/pages/advanced_topics/asyncio.rst
new file mode 100644
index 0000000..a692630
--- /dev/null
+++ b/docs/pages/advanced_topics/asyncio.rst
@@ -0,0 +1,30 @@
+.. _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())
diff --git a/docs/pages/advanced_topics/filters.rst b/docs/pages/advanced_topics/filters.rst
new file mode 100644
index 0000000..4788769
--- /dev/null
+++ b/docs/pages/advanced_topics/filters.rst
@@ -0,0 +1,169 @@
+.. _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)
diff --git a/docs/pages/advanced_topics/index.rst b/docs/pages/advanced_topics/index.rst
new file mode 100644
index 0000000..4c4fcc9
--- /dev/null
+++ b/docs/pages/advanced_topics/index.rst
@@ -0,0 +1,18 @@
+.. _advanced_topics:
+
+Advanced topics
+===============
+
+.. toctree::
+ :caption: Contents:
+ :maxdepth: 1
+
+ key_bindings
+ styling
+ filters
+ rendering_flow
+ asyncio
+ unit_testing
+ input_hooks
+ architecture
+ rendering_pipeline
diff --git a/docs/pages/advanced_topics/input_hooks.rst b/docs/pages/advanced_topics/input_hooks.rst
new file mode 100644
index 0000000..16c1bf5
--- /dev/null
+++ b/docs/pages/advanced_topics/input_hooks.rst
@@ -0,0 +1,41 @@
+.. _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 <https://ipython.org/>`_, 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.
diff --git a/docs/pages/advanced_topics/key_bindings.rst b/docs/pages/advanced_topics/key_bindings.rst
new file mode 100644
index 0000000..8b334fc
--- /dev/null
+++ b/docs/pages/advanced_topics/key_bindings.rst
@@ -0,0 +1,388 @@
+.. _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 '<any>' key:
+
+.. code:: python
+
+ @bindings.add('a', '<any>')
+ 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 <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 ``<sigint>``. For instance:
+
+.. code:: python
+
+ @bindings.add('<sigint>')
+ 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 ``<sigint>`` 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.
diff --git a/docs/pages/advanced_topics/rendering_flow.rst b/docs/pages/advanced_topics/rendering_flow.rst
new file mode 100644
index 0000000..0cd12c7
--- /dev/null
+++ b/docs/pages/advanced_topics/rendering_flow.rst
@@ -0,0 +1,86 @@
+.. _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`.
diff --git a/docs/pages/advanced_topics/rendering_pipeline.rst b/docs/pages/advanced_topics/rendering_pipeline.rst
new file mode 100644
index 0000000..6b38ba5
--- /dev/null
+++ b/docs/pages/advanced_topics/rendering_pipeline.rst
@@ -0,0 +1,157 @@
+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 ...<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.
diff --git a/docs/pages/advanced_topics/styling.rst b/docs/pages/advanced_topics/styling.rst
new file mode 100644
index 0000000..55cf6ee
--- /dev/null
+++ b/docs/pages/advanced_topics/styling.rst
@@ -0,0 +1,320 @@
+.. _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 <http://pygments.org/>`_
+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 <http://pygments.org/>`_ 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 <http://pygments.org/>`_ 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 <http://pygments.org/>`_ 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,
+ )
+ # ...
+ )
diff --git a/docs/pages/advanced_topics/unit_testing.rst b/docs/pages/advanced_topics/unit_testing.rst
new file mode 100644
index 0000000..2224bfc
--- /dev/null
+++ b/docs/pages/advanced_topics/unit_testing.rst
@@ -0,0 +1,125 @@
+.. _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
+
+
+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.
diff --git a/docs/pages/asking_for_input.rst b/docs/pages/asking_for_input.rst
new file mode 100644
index 0000000..20619ac
--- /dev/null
+++ b/docs/pages/asking_for_input.rst
@@ -0,0 +1,1034 @@
+.. _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 <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('You said: %s' % 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
+<http://pygments.org/>`_ 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('You said: %s' % 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('You said: %s' % 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 <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 <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
+<formatted_text>`. 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(['<html>', '<body>', '<head>', '<title>'])
+ text = prompt('Enter HTML: ', completer=html_completer)
+ print('You said: %s' % 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('You said: %s' % 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('You said: %i' % 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('You said: %i' % 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('You said: %s' % 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('You said: %s' % 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('You said: %s' % 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('You said: %s' % 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='%s' % 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())
+
+
+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('You said: %s' % 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.
diff --git a/docs/pages/dialogs.rst b/docs/pages/dialogs.rst
new file mode 100644
index 0000000..e171995
--- /dev/null
+++ b/docs/pages/dialogs.rst
@@ -0,0 +1,270 @@
+.. _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()
diff --git a/docs/pages/full_screen_apps.rst b/docs/pages/full_screen_apps.rst
new file mode 100644
index 0000000..805c8c7
--- /dev/null
+++ b/docs/pages/full_screen_apps.rst
@@ -0,0 +1,422 @@
+.. _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 leafs 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 build-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.
diff --git a/docs/pages/gallery.rst b/docs/pages/gallery.rst
new file mode 100644
index 0000000..40b6917
--- /dev/null
+++ b/docs/pages/gallery.rst
@@ -0,0 +1,32 @@
+.. _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
diff --git a/docs/pages/getting_started.rst b/docs/pages/getting_started.rst
new file mode 100644
index 0000000..06287a0
--- /dev/null
+++ b/docs/pages/getting_started.rst
@@ -0,0 +1,84 @@
+.. _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('You said: %s' % 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>`.
diff --git a/docs/pages/printing_text.rst b/docs/pages/printing_text.rst
new file mode 100644
index 0000000..8359d5f
--- /dev/null
+++ b/docs/pages/printing_text.rst
@@ -0,0 +1,274 @@
+.. _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)
diff --git a/docs/pages/progress_bars.rst b/docs/pages/progress_bars.rst
new file mode 100644
index 0000000..54a8ee1
--- /dev/null
+++ b/docs/pages/progress_bars.rst
@@ -0,0 +1,248 @@
+.. _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 mean 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>`
diff --git a/docs/pages/reference.rst b/docs/pages/reference.rst
new file mode 100644
index 0000000..d8a705e
--- /dev/null
+++ b/docs/pages/reference.rst
@@ -0,0 +1,393 @@
+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
+
+.. 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:
+
+.. autoclass:: prompt_toolkit.filters.Condition
+ :members:
+
+.. 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, get_event_loop
+
+.. 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
diff --git a/docs/pages/related_projects.rst b/docs/pages/related_projects.rst
new file mode 100644
index 0000000..ad0a8af
--- /dev/null
+++ b/docs/pages/related_projects.rst
@@ -0,0 +1,11 @@
+.. _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/>`_
diff --git a/docs/pages/tutorials/index.rst b/docs/pages/tutorials/index.rst
new file mode 100644
index 0000000..827b511
--- /dev/null
+++ b/docs/pages/tutorials/index.rst
@@ -0,0 +1,10 @@
+.. _tutorials:
+
+Tutorials
+=========
+
+.. toctree::
+ :caption: Contents:
+ :maxdepth: 1
+
+ repl
diff --git a/docs/pages/tutorials/repl.rst b/docs/pages/tutorials/repl.rst
new file mode 100644
index 0000000..946786f
--- /dev/null
+++ b/docs/pages/tutorials/repl.rst
@@ -0,0 +1,341 @@
+.. _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/
diff --git a/docs/pages/upgrading/2.0.rst b/docs/pages/upgrading/2.0.rst
new file mode 100644
index 0000000..6067057
--- /dev/null
+++ b/docs/pages/upgrading/2.0.rst
@@ -0,0 +1,221 @@
+.. _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
+
diff --git a/docs/pages/upgrading/3.0.rst b/docs/pages/upgrading/3.0.rst
new file mode 100644
index 0000000..7a867c5
--- /dev/null
+++ b/docs/pages/upgrading/3.0.rst
@@ -0,0 +1,118 @@
+.. _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)
diff --git a/docs/pages/upgrading/index.rst b/docs/pages/upgrading/index.rst
new file mode 100644
index 0000000..b790a64
--- /dev/null
+++ b/docs/pages/upgrading/index.rst
@@ -0,0 +1,11 @@
+.. _upgrading:
+
+Upgrading
+=========
+
+.. toctree::
+ :caption: Contents:
+ :maxdepth: 1
+
+ 2.0
+ 3.0
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..beb1c31
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,5 @@
+Sphinx<7
+wcwidth<1
+pyperclip<2
+sphinx_copybutton>=0.5.0,<1.0.0
+sphinx-nefertiti>=0.2.1
diff --git a/examples/dialogs/button_dialog.py b/examples/dialogs/button_dialog.py
new file mode 100755
index 0000000..7a99b9a
--- /dev/null
+++ b/examples/dialogs/button_dialog.py
@@ -0,0 +1,19 @@
+#!/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()
diff --git a/examples/dialogs/checkbox_dialog.py b/examples/dialogs/checkbox_dialog.py
new file mode 100755
index 0000000..90be263
--- /dev/null
+++ b/examples/dialogs/checkbox_dialog.py
@@ -0,0 +1,36 @@
+#!/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: %s\nGreat choice sir !" % ",".join(results),
+ ).run()
+else:
+ message_dialog("*starves*").run()
diff --git a/examples/dialogs/input_dialog.py b/examples/dialogs/input_dialog.py
new file mode 100755
index 0000000..6235265
--- /dev/null
+++ b/examples/dialogs/input_dialog.py
@@ -0,0 +1,17 @@
+#!/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()
diff --git a/examples/dialogs/messagebox.py b/examples/dialogs/messagebox.py
new file mode 100755
index 0000000..4642b84
--- /dev/null
+++ b/examples/dialogs/messagebox.py
@@ -0,0 +1,16 @@
+#!/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()
diff --git a/examples/dialogs/password_dialog.py b/examples/dialogs/password_dialog.py
new file mode 100755
index 0000000..39d7b9c
--- /dev/null
+++ b/examples/dialogs/password_dialog.py
@@ -0,0 +1,19 @@
+#!/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()
diff --git a/examples/dialogs/progress_dialog.py b/examples/dialogs/progress_dialog.py
new file mode 100755
index 0000000..1fd3ffb
--- /dev/null
+++ b/examples/dialogs/progress_dialog.py
@@ -0,0 +1,47 @@
+#!/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()
diff --git a/examples/dialogs/radio_dialog.py b/examples/dialogs/radio_dialog.py
new file mode 100755
index 0000000..94d80e2
--- /dev/null
+++ b/examples/dialogs/radio_dialog.py
@@ -0,0 +1,39 @@
+#!/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()
diff --git a/examples/dialogs/styled_messagebox.py b/examples/dialogs/styled_messagebox.py
new file mode 100755
index 0000000..3f6fc53
--- /dev/null
+++ b/examples/dialogs/styled_messagebox.py
@@ -0,0 +1,37 @@
+#!/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()
diff --git a/examples/dialogs/yes_no_dialog.py b/examples/dialogs/yes_no_dialog.py
new file mode 100755
index 0000000..4b08dd6
--- /dev/null
+++ b/examples/dialogs/yes_no_dialog.py
@@ -0,0 +1,17 @@
+#!/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()
diff --git a/examples/full-screen/ansi-art-and-textarea.py b/examples/full-screen/ansi-art-and-textarea.py
new file mode 100755
index 0000000..c0a59fd
--- /dev/null
+++ b/examples/full-screen/ansi-art-and-textarea.py
@@ -0,0 +1,82 @@
+#!/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()
diff --git a/examples/full-screen/buttons.py b/examples/full-screen/buttons.py
new file mode 100755
index 0000000..540194d
--- /dev/null
+++ b/examples/full-screen/buttons.py
@@ -0,0 +1,91 @@
+#!/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()
diff --git a/examples/full-screen/calculator.py b/examples/full-screen/calculator.py
new file mode 100755
index 0000000..1cb513f
--- /dev/null
+++ b/examples/full-screen/calculator.py
@@ -0,0 +1,95 @@
+#!/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()
diff --git a/examples/full-screen/dummy-app.py b/examples/full-screen/dummy-app.py
new file mode 100755
index 0000000..7ea7506
--- /dev/null
+++ b/examples/full-screen/dummy-app.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+"""
+This is the most simple example possible.
+"""
+from prompt_toolkit import Application
+
+app = Application(full_screen=False)
+app.run()
diff --git a/examples/full-screen/full-screen-demo.py b/examples/full-screen/full-screen-demo.py
new file mode 100755
index 0000000..de7379a
--- /dev/null
+++ b/examples/full-screen/full-screen-demo.py
@@ -0,0 +1,225 @@
+#!/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("You said: %r" % result)
+
+
+if __name__ == "__main__":
+ run()
diff --git a/examples/full-screen/hello-world.py b/examples/full-screen/hello-world.py
new file mode 100755
index 0000000..b818018
--- /dev/null
+++ b/examples/full-screen/hello-world.py
@@ -0,0 +1,43 @@
+#!/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()
diff --git a/examples/full-screen/no-layout.py b/examples/full-screen/no-layout.py
new file mode 100644
index 0000000..be5c6f8
--- /dev/null
+++ b/examples/full-screen/no-layout.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python
+"""
+An empty full screen application without layout.
+"""
+from prompt_toolkit import Application
+
+Application(full_screen=True).run()
diff --git a/examples/full-screen/pager.py b/examples/full-screen/pager.py
new file mode 100755
index 0000000..799c834
--- /dev/null
+++ b/examples/full-screen/pager.py
@@ -0,0 +1,111 @@
+#!/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",
+ "{}:{}".format(
+ 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()
diff --git a/examples/full-screen/scrollable-panes/simple-example.py b/examples/full-screen/scrollable-panes/simple-example.py
new file mode 100644
index 0000000..a94274f
--- /dev/null
+++ b/examples/full-screen/scrollable-panes/simple-example.py
@@ -0,0 +1,45 @@
+#!/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()
diff --git a/examples/full-screen/scrollable-panes/with-completion-menu.py b/examples/full-screen/scrollable-panes/with-completion-menu.py
new file mode 100644
index 0000000..fba8d17
--- /dev/null
+++ b/examples/full-screen/scrollable-panes/with-completion-menu.py
@@ -0,0 +1,120 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/alignment.py b/examples/full-screen/simple-demos/alignment.py
new file mode 100755
index 0000000..b20b43d
--- /dev/null
+++ b/examples/full-screen/simple-demos/alignment.py
@@ -0,0 +1,60 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/autocompletion.py b/examples/full-screen/simple-demos/autocompletion.py
new file mode 100755
index 0000000..bcbb594
--- /dev/null
+++ b/examples/full-screen/simple-demos/autocompletion.py
@@ -0,0 +1,100 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/colorcolumn.py b/examples/full-screen/simple-demos/colorcolumn.py
new file mode 100755
index 0000000..054aa44
--- /dev/null
+++ b/examples/full-screen/simple-demos/colorcolumn.py
@@ -0,0 +1,63 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/cursorcolumn-cursorline.py b/examples/full-screen/simple-demos/cursorcolumn-cursorline.py
new file mode 100755
index 0000000..505b3ee
--- /dev/null
+++ b/examples/full-screen/simple-demos/cursorcolumn-cursorline.py
@@ -0,0 +1,59 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/float-transparency.py b/examples/full-screen/simple-demos/float-transparency.py
new file mode 100755
index 0000000..4dc38fc
--- /dev/null
+++ b/examples/full-screen/simple-demos/float-transparency.py
@@ -0,0 +1,87 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/floats.py b/examples/full-screen/simple-demos/floats.py
new file mode 100755
index 0000000..0d45be9
--- /dev/null
+++ b/examples/full-screen/simple-demos/floats.py
@@ -0,0 +1,116 @@
+#!/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 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()
diff --git a/examples/full-screen/simple-demos/focus.py b/examples/full-screen/simple-demos/focus.py
new file mode 100755
index 0000000..9fe9b8f
--- /dev/null
+++ b/examples/full-screen/simple-demos/focus.py
@@ -0,0 +1,98 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/horizontal-align.py b/examples/full-screen/simple-demos/horizontal-align.py
new file mode 100755
index 0000000..bb0de12
--- /dev/null
+++ b/examples/full-screen/simple-demos/horizontal-align.py
@@ -0,0 +1,208 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/horizontal-split.py b/examples/full-screen/simple-demos/horizontal-split.py
new file mode 100755
index 0000000..0427e67
--- /dev/null
+++ b/examples/full-screen/simple-demos/horizontal-split.py
@@ -0,0 +1,44 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/line-prefixes.py b/examples/full-screen/simple-demos/line-prefixes.py
new file mode 100755
index 0000000..b52cb48
--- /dev/null
+++ b/examples/full-screen/simple-demos/line-prefixes.py
@@ -0,0 +1,110 @@
+#!/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">--&gt;</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()
diff --git a/examples/full-screen/simple-demos/margins.py b/examples/full-screen/simple-demos/margins.py
new file mode 100755
index 0000000..467492d
--- /dev/null
+++ b/examples/full-screen/simple-demos/margins.py
@@ -0,0 +1,71 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/vertical-align.py b/examples/full-screen/simple-demos/vertical-align.py
new file mode 100755
index 0000000..1475d71
--- /dev/null
+++ b/examples/full-screen/simple-demos/vertical-align.py
@@ -0,0 +1,167 @@
+#!/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()
diff --git a/examples/full-screen/simple-demos/vertical-split.py b/examples/full-screen/simple-demos/vertical-split.py
new file mode 100755
index 0000000..b48d106
--- /dev/null
+++ b/examples/full-screen/simple-demos/vertical-split.py
@@ -0,0 +1,44 @@
+#!/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()
diff --git a/examples/full-screen/split-screen.py b/examples/full-screen/split-screen.py
new file mode 100755
index 0000000..af5403e
--- /dev/null
+++ b/examples/full-screen/split-screen.py
@@ -0,0 +1,156 @@
+#!/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()
diff --git a/examples/full-screen/text-editor.py b/examples/full-screen/text-editor.py
new file mode 100755
index 0000000..9c0a414
--- /dev/null
+++ b/examples/full-screen/text-editor.py
@@ -0,0 +1,383 @@
+#!/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 " {}:{} ".format(
+ 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()
diff --git a/examples/gevent-get-input.py b/examples/gevent-get-input.py
new file mode 100755
index 0000000..ecb89b4
--- /dev/null
+++ b/examples/gevent-get-input.py
@@ -0,0 +1,24 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/print-text/ansi-colors.py b/examples/print-text/ansi-colors.py
new file mode 100755
index 0000000..7bd3831
--- /dev/null
+++ b/examples/print-text/ansi-colors.py
@@ -0,0 +1,100 @@
+#!/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()
diff --git a/examples/print-text/ansi.py b/examples/print-text/ansi.py
new file mode 100755
index 0000000..618775c
--- /dev/null
+++ b/examples/print-text/ansi.py
@@ -0,0 +1,50 @@
+#!/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()
diff --git a/examples/print-text/html.py b/examples/print-text/html.py
new file mode 100755
index 0000000..5276fe3
--- /dev/null
+++ b/examples/print-text/html.py
@@ -0,0 +1,53 @@
+#!/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()
diff --git a/examples/print-text/named-colors.py b/examples/print-text/named-colors.py
new file mode 100755
index 0000000..ea3f0ba
--- /dev/null
+++ b/examples/print-text/named-colors.py
@@ -0,0 +1,29 @@
+#!/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()
diff --git a/examples/print-text/print-formatted-text.py b/examples/print-text/print-formatted-text.py
new file mode 100755
index 0000000..4b09ae2
--- /dev/null
+++ b/examples/print-text/print-formatted-text.py
@@ -0,0 +1,46 @@
+#!/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()
diff --git a/examples/print-text/print-frame.py b/examples/print-text/print-frame.py
new file mode 100755
index 0000000..fb703c5
--- /dev/null
+++ b/examples/print-text/print-frame.py
@@ -0,0 +1,14 @@
+#!/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",
+ )
+)
diff --git a/examples/print-text/prompt-toolkit-logo-ansi-art.py b/examples/print-text/prompt-toolkit-logo-ansi-art.py
new file mode 100755
index 0000000..617a506
--- /dev/null
+++ b/examples/print-text/prompt-toolkit-logo-ansi-art.py
@@ -0,0 +1,40 @@
+#!/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
+"""
+ )
+)
diff --git a/examples/print-text/pygments-tokens.py b/examples/print-text/pygments-tokens.py
new file mode 100755
index 0000000..3470e8e
--- /dev/null
+++ b/examples/print-text/pygments-tokens.py
@@ -0,0 +1,44 @@
+#!/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()
diff --git a/examples/print-text/true-color-demo.py b/examples/print-text/true-color-demo.py
new file mode 100755
index 0000000..a241006
--- /dev/null
+++ b/examples/print-text/true-color-demo.py
@@ -0,0 +1,35 @@
+#!/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()
diff --git a/examples/progress-bar/a-lot-of-parallel-tasks.py b/examples/progress-bar/a-lot-of-parallel-tasks.py
new file mode 100755
index 0000000..31110ac
--- /dev/null
+++ b/examples/progress-bar/a-lot-of-parallel-tasks.py
@@ -0,0 +1,65 @@
+#!/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 = "Task %i" % 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()
diff --git a/examples/progress-bar/colored-title-and-label.py b/examples/progress-bar/colored-title-and-label.py
new file mode 100755
index 0000000..0b5e73a
--- /dev/null
+++ b/examples/progress-bar/colored-title-and-label.py
@@ -0,0 +1,22 @@
+#!/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()
diff --git a/examples/progress-bar/custom-key-bindings.py b/examples/progress-bar/custom-key-bindings.py
new file mode 100755
index 0000000..f700811
--- /dev/null
+++ b/examples/progress-bar/custom-key-bindings.py
@@ -0,0 +1,51 @@
+#!/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()
diff --git a/examples/progress-bar/many-parallel-tasks.py b/examples/progress-bar/many-parallel-tasks.py
new file mode 100755
index 0000000..dc34ef2
--- /dev/null
+++ b/examples/progress-bar/many-parallel-tasks.py
@@ -0,0 +1,46 @@
+#!/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()
diff --git a/examples/progress-bar/nested-progress-bars.py b/examples/progress-bar/nested-progress-bars.py
new file mode 100755
index 0000000..1a1e706
--- /dev/null
+++ b/examples/progress-bar/nested-progress-bars.py
@@ -0,0 +1,22 @@
+#!/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()
diff --git a/examples/progress-bar/scrolling-task-name.py b/examples/progress-bar/scrolling-task-name.py
new file mode 100755
index 0000000..bce155f
--- /dev/null
+++ b/examples/progress-bar/scrolling-task-name.py
@@ -0,0 +1,23 @@
+#!/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()
diff --git a/examples/progress-bar/simple-progress-bar.py b/examples/progress-bar/simple-progress-bar.py
new file mode 100755
index 0000000..c8776e5
--- /dev/null
+++ b/examples/progress-bar/simple-progress-bar.py
@@ -0,0 +1,18 @@
+#!/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()
diff --git a/examples/progress-bar/styled-1.py b/examples/progress-bar/styled-1.py
new file mode 100755
index 0000000..d972e55
--- /dev/null
+++ b/examples/progress-bar/styled-1.py
@@ -0,0 +1,36 @@
+#!/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()
diff --git a/examples/progress-bar/styled-2.py b/examples/progress-bar/styled-2.py
new file mode 100755
index 0000000..15c57d4
--- /dev/null
+++ b/examples/progress-bar/styled-2.py
@@ -0,0 +1,49 @@
+#!/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()
diff --git a/examples/progress-bar/styled-apt-get-install.py b/examples/progress-bar/styled-apt-get-install.py
new file mode 100755
index 0000000..bafe70b
--- /dev/null
+++ b/examples/progress-bar/styled-apt-get-install.py
@@ -0,0 +1,38 @@
+#!/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()
diff --git a/examples/progress-bar/styled-rainbow.py b/examples/progress-bar/styled-rainbow.py
new file mode 100755
index 0000000..b46e949
--- /dev/null
+++ b/examples/progress-bar/styled-rainbow.py
@@ -0,0 +1,35 @@
+#!/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()
diff --git a/examples/progress-bar/styled-tqdm-1.py b/examples/progress-bar/styled-tqdm-1.py
new file mode 100755
index 0000000..9484ac0
--- /dev/null
+++ b/examples/progress-bar/styled-tqdm-1.py
@@ -0,0 +1,40 @@
+#!/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()
diff --git a/examples/progress-bar/styled-tqdm-2.py b/examples/progress-bar/styled-tqdm-2.py
new file mode 100755
index 0000000..0e66e90
--- /dev/null
+++ b/examples/progress-bar/styled-tqdm-2.py
@@ -0,0 +1,38 @@
+#!/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()
diff --git a/examples/progress-bar/two-tasks.py b/examples/progress-bar/two-tasks.py
new file mode 100755
index 0000000..c78604e
--- /dev/null
+++ b/examples/progress-bar/two-tasks.py
@@ -0,0 +1,39 @@
+#!/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()
diff --git a/examples/progress-bar/unknown-length.py b/examples/progress-bar/unknown-length.py
new file mode 100755
index 0000000..e39ac39
--- /dev/null
+++ b/examples/progress-bar/unknown-length.py
@@ -0,0 +1,26 @@
+#!/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()
diff --git a/examples/prompts/accept-default.py b/examples/prompts/accept-default.py
new file mode 100644
index 0000000..311ef46
--- /dev/null
+++ b/examples/prompts/accept-default.py
@@ -0,0 +1,16 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/asyncio-prompt.py b/examples/prompts/asyncio-prompt.py
new file mode 100755
index 0000000..32a1481
--- /dev/null
+++ b/examples/prompts/asyncio-prompt.py
@@ -0,0 +1,63 @@
+#!/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("Counter: %i" % 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())
diff --git a/examples/prompts/auto-completion/autocomplete-with-control-space.py b/examples/prompts/auto-completion/autocomplete-with-control-space.py
new file mode 100755
index 0000000..61160a3
--- /dev/null
+++ b/examples/prompts/auto-completion/autocomplete-with-control-space.py
@@ -0,0 +1,75 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/autocompletion-like-readline.py b/examples/prompts/auto-completion/autocompletion-like-readline.py
new file mode 100755
index 0000000..613d3e7
--- /dev/null
+++ b/examples/prompts/auto-completion/autocompletion-like-readline.py
@@ -0,0 +1,58 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/autocompletion.py b/examples/prompts/auto-completion/autocompletion.py
new file mode 100755
index 0000000..fc9dda0
--- /dev/null
+++ b/examples/prompts/auto-completion/autocompletion.py
@@ -0,0 +1,60 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/colored-completions-with-formatted-text.py b/examples/prompts/auto-completion/colored-completions-with-formatted-text.py
new file mode 100755
index 0000000..8a89c7a
--- /dev/null
+++ b/examples/prompts/auto-completion/colored-completions-with-formatted-text.py
@@ -0,0 +1,137 @@
+#!/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()
diff --git a/examples/prompts/auto-completion/colored-completions.py b/examples/prompts/auto-completion/colored-completions.py
new file mode 100755
index 0000000..9c5cba3
--- /dev/null
+++ b/examples/prompts/auto-completion/colored-completions.py
@@ -0,0 +1,78 @@
+#!/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()
diff --git a/examples/prompts/auto-completion/combine-multiple-completers.py b/examples/prompts/auto-completion/combine-multiple-completers.py
new file mode 100755
index 0000000..511988b
--- /dev/null
+++ b/examples/prompts/auto-completion/combine-multiple-completers.py
@@ -0,0 +1,76 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/fuzzy-custom-completer.py b/examples/prompts/auto-completion/fuzzy-custom-completer.py
new file mode 100755
index 0000000..fd9a7d7
--- /dev/null
+++ b/examples/prompts/auto-completion/fuzzy-custom-completer.py
@@ -0,0 +1,56 @@
+#!/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()
diff --git a/examples/prompts/auto-completion/fuzzy-word-completer.py b/examples/prompts/auto-completion/fuzzy-word-completer.py
new file mode 100755
index 0000000..329c0c1
--- /dev/null
+++ b/examples/prompts/auto-completion/fuzzy-word-completer.py
@@ -0,0 +1,59 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/multi-column-autocompletion-with-meta.py b/examples/prompts/auto-completion/multi-column-autocompletion-with-meta.py
new file mode 100755
index 0000000..5ba3ab5
--- /dev/null
+++ b/examples/prompts/auto-completion/multi-column-autocompletion-with-meta.py
@@ -0,0 +1,50 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/multi-column-autocompletion.py b/examples/prompts/auto-completion/multi-column-autocompletion.py
new file mode 100755
index 0000000..7fcfc52
--- /dev/null
+++ b/examples/prompts/auto-completion/multi-column-autocompletion.py
@@ -0,0 +1,57 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/nested-autocompletion.py b/examples/prompts/auto-completion/nested-autocompletion.py
new file mode 100755
index 0000000..cd85b8c
--- /dev/null
+++ b/examples/prompts/auto-completion/nested-autocompletion.py
@@ -0,0 +1,22 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-completion/slow-completions.py b/examples/prompts/auto-completion/slow-completions.py
new file mode 100755
index 0000000..cce9d59
--- /dev/null
+++ b/examples/prompts/auto-completion/slow-completions.py
@@ -0,0 +1,103 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/auto-suggestion.py b/examples/prompts/auto-suggestion.py
new file mode 100755
index 0000000..6660777
--- /dev/null
+++ b/examples/prompts/auto-suggestion.py
@@ -0,0 +1,48 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/autocorrection.py b/examples/prompts/autocorrection.py
new file mode 100755
index 0000000..6378132
--- /dev/null
+++ b/examples/prompts/autocorrection.py
@@ -0,0 +1,44 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/bottom-toolbar.py b/examples/prompts/bottom-toolbar.py
new file mode 100755
index 0000000..4980e5b
--- /dev/null
+++ b/examples/prompts/bottom-toolbar.py
@@ -0,0 +1,80 @@
+#!/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("You said: %s" % text)
+
+ # Example 2: fixed text from a callable:
+ def get_toolbar():
+ return "Bottom toolbar: time=%r" % time.time()
+
+ text = prompt("Say something: ", bottom_toolbar=get_toolbar, refresh_interval=0.5)
+ print("You said: %s" % 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("You said: %s" % 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("You said: %s" % 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("You said: %s" % 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("You said: %s" % text)
+
+ # Example 7: multiline fixed text.
+ text = prompt("Say something: ", bottom_toolbar="This is\na multiline toolbar")
+ print("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/clock-input.py b/examples/prompts/clock-input.py
new file mode 100755
index 0000000..e43abd8
--- /dev/null
+++ b/examples/prompts/clock-input.py
@@ -0,0 +1,25 @@
+#!/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("You said: %s" % result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/colored-prompt.py b/examples/prompts/colored-prompt.py
new file mode 100755
index 0000000..1e63e29
--- /dev/null
+++ b/examples/prompts/colored-prompt.py
@@ -0,0 +1,81 @@
+#!/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("You said: %s" % 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("You said: %s" % 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("You said: %s" % answer)
+
+
+if __name__ == "__main__":
+ example_1()
+ example_2()
+ example_3()
diff --git a/examples/prompts/confirmation-prompt.py b/examples/prompts/confirmation-prompt.py
new file mode 100755
index 0000000..bd52b9e
--- /dev/null
+++ b/examples/prompts/confirmation-prompt.py
@@ -0,0 +1,9 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/cursor-shapes.py b/examples/prompts/cursor-shapes.py
new file mode 100755
index 0000000..e668243
--- /dev/null
+++ b/examples/prompts/cursor-shapes.py
@@ -0,0 +1,19 @@
+#!/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,
+)
diff --git a/examples/prompts/custom-key-binding.py b/examples/prompts/custom-key-binding.py
new file mode 100755
index 0000000..32d889e
--- /dev/null
+++ b/examples/prompts/custom-key-binding.py
@@ -0,0 +1,77 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/custom-lexer.py b/examples/prompts/custom-lexer.py
new file mode 100755
index 0000000..c4c9fbe
--- /dev/null
+++ b/examples/prompts/custom-lexer.py
@@ -0,0 +1,29 @@
+#!/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("You said: %s" % answer)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/custom-vi-operator-and-text-object.py b/examples/prompts/custom-vi-operator-and-text-object.py
new file mode 100755
index 0000000..7478afc
--- /dev/null
+++ b/examples/prompts/custom-vi-operator-and-text-object.py
@@ -0,0 +1,70 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/enforce-tty-input-output.py b/examples/prompts/enforce-tty-input-output.py
new file mode 100755
index 0000000..93b43ee
--- /dev/null
+++ b/examples/prompts/enforce-tty-input-output.py
@@ -0,0 +1,13 @@
+#!/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(">")
diff --git a/examples/prompts/fancy-zsh-prompt.py b/examples/prompts/fancy-zsh-prompt.py
new file mode 100755
index 0000000..4761c08
--- /dev/null
+++ b/examples/prompts/fancy-zsh-prompt.py
@@ -0,0 +1,79 @@
+#!/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("You said: %s" % answer)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/finalterm-shell-integration.py b/examples/prompts/finalterm-shell-integration.py
new file mode 100755
index 0000000..30c7a7f
--- /dev/null
+++ b/examples/prompts/finalterm-shell-integration.py
@@ -0,0 +1,43 @@
+#!/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("You said: %s" % answer)
+ sys.stdout.write(AFTER_OUTPUT.format(command_status=0))
diff --git a/examples/prompts/get-input-vi-mode.py b/examples/prompts/get-input-vi-mode.py
new file mode 100755
index 0000000..566ffd5
--- /dev/null
+++ b/examples/prompts/get-input-vi-mode.py
@@ -0,0 +1,7 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/get-input-with-default.py b/examples/prompts/get-input-with-default.py
new file mode 100755
index 0000000..67446d5
--- /dev/null
+++ b/examples/prompts/get-input-with-default.py
@@ -0,0 +1,12 @@
+#!/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="%s" % getpass.getuser())
+ print("You said: %s" % answer)
diff --git a/examples/prompts/get-input.py b/examples/prompts/get-input.py
new file mode 100755
index 0000000..5529bbb
--- /dev/null
+++ b/examples/prompts/get-input.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+"""
+The most simple prompt example.
+"""
+from prompt_toolkit import prompt
+
+if __name__ == "__main__":
+ answer = prompt("Give me some input: ")
+ print("You said: %s" % answer)
diff --git a/examples/prompts/get-multiline-input.py b/examples/prompts/get-multiline-input.py
new file mode 100755
index 0000000..eda35be
--- /dev/null
+++ b/examples/prompts/get-multiline-input.py
@@ -0,0 +1,29 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/get-password-with-toggle-display-shortcut.py b/examples/prompts/get-password-with-toggle-display-shortcut.py
new file mode 100755
index 0000000..b89cb41
--- /dev/null
+++ b/examples/prompts/get-password-with-toggle-display-shortcut.py
@@ -0,0 +1,28 @@
+#!/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("You said: %s" % password)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/get-password.py b/examples/prompts/get-password.py
new file mode 100755
index 0000000..1c9517c
--- /dev/null
+++ b/examples/prompts/get-password.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+from prompt_toolkit import prompt
+
+if __name__ == "__main__":
+ password = prompt("Password: ", is_password=True)
+ print("You said: %s" % password)
diff --git a/examples/prompts/history/persistent-history.py b/examples/prompts/history/persistent-history.py
new file mode 100755
index 0000000..2bdb758
--- /dev/null
+++ b/examples/prompts/history/persistent-history.py
@@ -0,0 +1,25 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/history/slow-history.py b/examples/prompts/history/slow-history.py
new file mode 100755
index 0000000..5b6a7a2
--- /dev/null
+++ b/examples/prompts/history/slow-history.py
@@ -0,0 +1,48 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/html-input.py b/examples/prompts/html-input.py
new file mode 100755
index 0000000..4c51737
--- /dev/null
+++ b/examples/prompts/html-input.py
@@ -0,0 +1,18 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/input-validation.py b/examples/prompts/input-validation.py
new file mode 100755
index 0000000..d8bd3ee
--- /dev/null
+++ b/examples/prompts/input-validation.py
@@ -0,0 +1,35 @@
+#!/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("You said: %s" % text)
+
+ # While typing
+ text = prompt(
+ "Enter e-mail address: ", validator=validator, validate_while_typing=True
+ )
+ print("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/inputhook.py b/examples/prompts/inputhook.py
new file mode 100755
index 0000000..7cbfe18
--- /dev/null
+++ b/examples/prompts/inputhook.py
@@ -0,0 +1,83 @@
+#!/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("You said: %s" % result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/mouse-support.py b/examples/prompts/mouse-support.py
new file mode 100755
index 0000000..1e4ee76
--- /dev/null
+++ b/examples/prompts/mouse-support.py
@@ -0,0 +1,10 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/multiline-prompt.py b/examples/prompts/multiline-prompt.py
new file mode 100755
index 0000000..d6a7698
--- /dev/null
+++ b/examples/prompts/multiline-prompt.py
@@ -0,0 +1,11 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/no-wrapping.py b/examples/prompts/no-wrapping.py
new file mode 100755
index 0000000..371486e
--- /dev/null
+++ b/examples/prompts/no-wrapping.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+from prompt_toolkit import prompt
+
+if __name__ == "__main__":
+ answer = prompt("Give me some input: ", wrap_lines=False, multiline=True)
+ print("You said: %s" % answer)
diff --git a/examples/prompts/operate-and-get-next.py b/examples/prompts/operate-and-get-next.py
new file mode 100755
index 0000000..6ea4d79
--- /dev/null
+++ b/examples/prompts/operate-and-get-next.py
@@ -0,0 +1,18 @@
+#!/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()
diff --git a/examples/prompts/patch-stdout.py b/examples/prompts/patch-stdout.py
new file mode 100755
index 0000000..1c83524
--- /dev/null
+++ b/examples/prompts/patch-stdout.py
@@ -0,0 +1,41 @@
+#!/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("i=%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("You said: %s" % result)
+
+ # Stop thread.
+ running = False
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/placeholder-text.py b/examples/prompts/placeholder-text.py
new file mode 100755
index 0000000..35e1c6c
--- /dev/null
+++ b/examples/prompts/placeholder-text.py
@@ -0,0 +1,13 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/regular-language.py b/examples/prompts/regular-language.py
new file mode 100755
index 0000000..cbe7256
--- /dev/null
+++ b/examples/prompts/regular-language.py
@@ -0,0 +1,108 @@
+#!/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("Result: %s\n" % (operator(var1, var2)))
+
+ elif vars.get("operator2"):
+ print("Operator 2")
+
+ except EOFError:
+ pass
diff --git a/examples/prompts/rprompt.py b/examples/prompts/rprompt.py
new file mode 100755
index 0000000..f7656b7
--- /dev/null
+++ b/examples/prompts/rprompt.py
@@ -0,0 +1,53 @@
+#!/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("You said: %s" % answer)
+
+ # Option 2: pass HTML:
+ answer = prompt("> ", rprompt=HTML(" <u>&lt;rprompt&gt;</u> "), style=example_style)
+ print("You said: %s" % answer)
+
+ # Option 3: pass ANSI:
+ answer = prompt(
+ "> ", rprompt=ANSI(" \x1b[4m<rprompt>\x1b[0m "), style=example_style
+ )
+ print("You said: %s" % 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("You said: %s" % answer)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/swap-light-and-dark-colors.py b/examples/prompts/swap-light-and-dark-colors.py
new file mode 100755
index 0000000..e602449
--- /dev/null
+++ b/examples/prompts/swap-light-and-dark-colors.py
@@ -0,0 +1,78 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/prompts/switch-between-vi-emacs.py b/examples/prompts/switch-between-vi-emacs.py
new file mode 100755
index 0000000..249c7ef
--- /dev/null
+++ b/examples/prompts/switch-between-vi-emacs.py
@@ -0,0 +1,36 @@
+#!/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()
diff --git a/examples/prompts/system-clipboard-integration.py b/examples/prompts/system-clipboard-integration.py
new file mode 100755
index 0000000..f605921
--- /dev/null
+++ b/examples/prompts/system-clipboard-integration.py
@@ -0,0 +1,17 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/system-prompt.py b/examples/prompts/system-prompt.py
new file mode 100755
index 0000000..47aa2a5
--- /dev/null
+++ b/examples/prompts/system-prompt.py
@@ -0,0 +1,20 @@
+#!/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("You said: %s" % 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("You said: %s" % 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("You said: %s" % answer)
diff --git a/examples/prompts/terminal-title.py b/examples/prompts/terminal-title.py
new file mode 100755
index 0000000..14f9459
--- /dev/null
+++ b/examples/prompts/terminal-title.py
@@ -0,0 +1,10 @@
+#!/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("You said: %s" % answer)
diff --git a/examples/prompts/up-arrow-partial-string-matching.py b/examples/prompts/up-arrow-partial-string-matching.py
new file mode 100755
index 0000000..742a12e
--- /dev/null
+++ b/examples/prompts/up-arrow-partial-string-matching.py
@@ -0,0 +1,41 @@
+#!/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("You said: %s" % text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/ssh/asyncssh-server.py b/examples/ssh/asyncssh-server.py
new file mode 100755
index 0000000..27d0dd2
--- /dev/null
+++ b/examples/ssh/asyncssh-server.py
@@ -0,0 +1,120 @@
+#!/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())
diff --git a/examples/telnet/chat-app.py b/examples/telnet/chat-app.py
new file mode 100755
index 0000000..2e3508d
--- /dev/null
+++ b/examples/telnet/chat-app.py
@@ -0,0 +1,103 @@
+#!/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> &gt; ').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, "[%s]" % name),
+ ("", " "),
+ ("fg:" + color, "%s\n" % message),
+ ]
+ )
+
+
+async def main():
+ server = TelnetServer(interact=interact, port=2323)
+ server.start()
+
+ # Run forever.
+ await Future()
+
+
+if __name__ == "__main__":
+ run(main())
diff --git a/examples/telnet/dialog.py b/examples/telnet/dialog.py
new file mode 100755
index 0000000..c674a9d
--- /dev/null
+++ b/examples/telnet/dialog.py
@@ -0,0 +1,34 @@
+#!/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())
diff --git a/examples/telnet/hello-world.py b/examples/telnet/hello-world.py
new file mode 100755
index 0000000..c19c60c
--- /dev/null
+++ b/examples/telnet/hello-world.py
@@ -0,0 +1,39 @@
+#!/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())
diff --git a/examples/telnet/toolbar.py b/examples/telnet/toolbar.py
new file mode 100755
index 0000000..d6ae886
--- /dev/null
+++ b/examples/telnet/toolbar.py
@@ -0,0 +1,44 @@
+#!/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())
diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md
new file mode 100755
index 0000000..3aa5f70
--- /dev/null
+++ b/examples/tutorial/README.md
@@ -0,0 +1 @@
+See http://python-prompt-toolkit.readthedocs.io/en/stable/pages/tutorials/repl.html
diff --git a/examples/tutorial/sqlite-cli.py b/examples/tutorial/sqlite-cli.py
new file mode 100755
index 0000000..ea3e2c8
--- /dev/null
+++ b/examples/tutorial/sqlite-cli.py
@@ -0,0 +1,184 @@
+#!/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)
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..be28fce
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,18 @@
+[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
+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
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..7b5a835
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,66 @@
+[tool.ruff]
+target-version = "py37"
+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
+]
+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.per-file-ignores]
+"examples/*" = ["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.
+
+
+[tool.ruff.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",
+ # Deliberate spelling mistakes in autocorrection.py
+ "wolrd",
+ "impotr",
+ # Lorem ipsum.
+ "Nam",
+ "varius",
+]
+
+locale = 'en-us' # US English.
+
+[tool.typos.files]
+extend-exclude = [
+ "tests/test_cli.py",
+ "tests/test_regular_languages.py",
+]
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..0b63c97
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,40 @@
+[flake8]
+exclude=__init__.py
+max_line_length=150
+ignore=
+ E114,
+ E116,
+ E117,
+ E121,
+ E122,
+ E123,
+ E125,
+ E126,
+ E127,
+ E128,
+ E131,
+ E171,
+ E203,
+ E211,
+ E221,
+ E227,
+ E231,
+ E241,
+ E251,
+ E301,
+ E402,
+ E501,
+ E701,
+ E702,
+ E704,
+ E731,
+ E741,
+ F401,
+ F403,
+ F405,
+ F811,
+ W503,
+ W504
+
+[pytest:tool]
+testpaths=tests
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..ca2170c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+import os
+import re
+
+from setuptools import find_packages, setup
+
+with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f:
+ long_description = f.read()
+
+
+def get_version(package):
+ """
+ Return package version as listed in `__version__` in `__init__.py`.
+ """
+ path = os.path.join(os.path.dirname(__file__), "src", package, "__init__.py")
+ with open(path, "rb") as f:
+ init_py = f.read().decode("utf-8")
+ return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
+
+
+setup(
+ name="prompt_toolkit",
+ author="Jonathan Slenders",
+ version=get_version("prompt_toolkit"),
+ url="https://github.com/prompt-toolkit/python-prompt-toolkit",
+ description="Library for building powerful interactive command lines in Python",
+ long_description=long_description,
+ long_description_content_type="text/x-rst",
+ packages=find_packages(where="src"),
+ package_dir={"": "src"},
+ package_data={"prompt_toolkit": ["py.typed"]},
+ install_requires=["wcwidth"],
+ # We require Python 3.7, because we need:
+ # - Context variables - PEP 567
+ # - `asyncio.run()`
+ python_requires=">=3.7.0",
+ 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.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python",
+ "Topic :: Software Development",
+ ],
+)
diff --git a/src/prompt_toolkit/__init__.py b/src/prompt_toolkit/__init__.py
new file mode 100644
index 0000000..82324cb
--- /dev/null
+++ b/src/prompt_toolkit/__init__.py
@@ -0,0 +1,51 @@
+"""
+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
+
+import re
+
+# note: this is a bit more lax than the actual pep 440 to allow for a/b/rc/dev without a number
+pep440 = re.compile(
+ r"^([1-9]\d*!)?(0|[1-9]\d*)(\.(0|[1-9]\d*))*((a|b|rc)(0|[1-9]\d*)?)?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*)?)?$",
+ re.UNICODE,
+)
+from .application import Application
+from .formatted_text import ANSI, HTML
+from .shortcuts import PromptSession, print_formatted_text, prompt
+
+# Don't forget to update in `docs/conf.py`!
+__version__ = "3.0.43"
+
+assert pep440.match(__version__)
+
+# Version tuple.
+VERSION = tuple(int(v.rstrip("abrc")) for v in __version__.split(".")[:3])
+
+
+__all__ = [
+ # Application.
+ "Application",
+ # Shortcuts.
+ "prompt",
+ "PromptSession",
+ "print_formatted_text",
+ # Formatted text.
+ "HTML",
+ "ANSI",
+ # Version info.
+ "__version__",
+ "VERSION",
+]
diff --git a/src/prompt_toolkit/application/__init__.py b/src/prompt_toolkit/application/__init__.py
new file mode 100644
index 0000000..569d8c0
--- /dev/null
+++ b/src/prompt_toolkit/application/__init__.py
@@ -0,0 +1,32 @@
+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",
+]
diff --git a/src/prompt_toolkit/application/application.py b/src/prompt_toolkit/application/application.py
new file mode 100644
index 0000000..d463781
--- /dev/null
+++ b/src/prompt_toolkit/application/application.py
@@ -0,0 +1,1625 @@
+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 contextlib import ExitStack, contextmanager
+from subprocess import Popen
+from traceback import format_tb
+from typing import (
+ Any,
+ Callable,
+ Coroutine,
+ Generator,
+ Generic,
+ Hashable,
+ Iterable,
+ Iterator,
+ 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_appp().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:
+ # Any of the above imports don't exist? Don't do anything here.
+ yield
+ return
+
+ # 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)
+ sigint_os = pythonapi.PyOS_getsig(signal.SIGINT)
+
+ try:
+ yield
+ finally:
+ signal.signal(signal.SIGINT, sigint)
+ pythonapi.PyOS_setsig(signal.SIGINT, sigint_os)
diff --git a/src/prompt_toolkit/application/current.py b/src/prompt_toolkit/application/current.py
new file mode 100644
index 0000000..908141a
--- /dev/null
+++ b/src/prompt_toolkit/application/current.py
@@ -0,0 +1,189 @@
+from __future__ import annotations
+
+from contextlib import contextmanager
+from contextvars import ContextVar
+from typing import TYPE_CHECKING, Any, Generator
+
+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 an Telnet/SSH server.
+ """
+ # If no input/output is specified, fall back to the current input/output,
+ # whatever that is.
+ 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
diff --git a/src/prompt_toolkit/application/dummy.py b/src/prompt_toolkit/application/dummy.py
new file mode 100644
index 0000000..43819e1
--- /dev/null
+++ b/src/prompt_toolkit/application/dummy.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from typing 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
diff --git a/src/prompt_toolkit/application/run_in_terminal.py b/src/prompt_toolkit/application/run_in_terminal.py
new file mode 100644
index 0000000..1e4da2d
--- /dev/null
+++ b/src/prompt_toolkit/application/run_in_terminal.py
@@ -0,0 +1,113 @@
+"""
+Tools for running functions on the terminal above the current application or prompt.
+"""
+from __future__ import annotations
+
+from asyncio import Future, ensure_future
+from contextlib import asynccontextmanager
+from typing import AsyncGenerator, Awaitable, Callable, 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:
+ new_run_in_terminal_f.set_result(None)
diff --git a/src/prompt_toolkit/auto_suggest.py b/src/prompt_toolkit/auto_suggest.py
new file mode 100644
index 0000000..98cb4dd
--- /dev/null
+++ b/src/prompt_toolkit/auto_suggest.py
@@ -0,0 +1,176 @@
+"""
+`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 typing import TYPE_CHECKING, Callable
+
+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 "Suggestion(%s)" % 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)
diff --git a/src/prompt_toolkit/buffer.py b/src/prompt_toolkit/buffer.py
new file mode 100644
index 0000000..100ca78
--- /dev/null
+++ b/src/prompt_toolkit/buffer.py
@@ -0,0 +1,2026 @@
+"""
+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 enum import Enum
+from functools import wraps
+from typing import Any, Callable, Coroutine, Iterable, 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 "{}({!r}, <{!r}> completions, index={!r})".format(
+ self.__class__.__name__,
+ self.original_document,
+ len(self.completions),
+ self.complete_index,
+ )
+
+ 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 "{}(history_position={!r}, n={!r}, previous_inserted_word={!r})".format(
+ self.__class__.__name__,
+ self.history_position,
+ self.n,
+ self.previous_inserted_word,
+ )
+
+
+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.
+
+ 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,
+ 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,
+ ):
+ # 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
+
+ # 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
+ 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)),
+ )
diff --git a/src/prompt_toolkit/cache.py b/src/prompt_toolkit/cache.py
new file mode 100644
index 0000000..01dd1f7
--- /dev/null
+++ b/src/prompt_toolkit/cache.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+from collections import deque
+from functools import wraps
+from typing import Any, Callable, Dict, Generic, Hashable, Tuple, 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
diff --git a/src/prompt_toolkit/clipboard/__init__.py b/src/prompt_toolkit/clipboard/__init__.py
new file mode 100644
index 0000000..e72f30e
--- /dev/null
+++ b/src/prompt_toolkit/clipboard/__init__.py
@@ -0,0 +1,17 @@
+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",
+]
diff --git a/src/prompt_toolkit/clipboard/base.py b/src/prompt_toolkit/clipboard/base.py
new file mode 100644
index 0000000..b05275b
--- /dev/null
+++ b/src/prompt_toolkit/clipboard/base.py
@@ -0,0 +1,108 @@
+"""
+Clipboard for command line interface.
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from typing 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()
diff --git a/src/prompt_toolkit/clipboard/in_memory.py b/src/prompt_toolkit/clipboard/in_memory.py
new file mode 100644
index 0000000..d9ae081
--- /dev/null
+++ b/src/prompt_toolkit/clipboard/in_memory.py
@@ -0,0 +1,44 @@
+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())
diff --git a/src/prompt_toolkit/clipboard/pyperclip.py b/src/prompt_toolkit/clipboard/pyperclip.py
new file mode 100644
index 0000000..66eb711
--- /dev/null
+++ b/src/prompt_toolkit/clipboard/pyperclip.py
@@ -0,0 +1,42 @@
+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,
+ )
diff --git a/src/prompt_toolkit/completion/__init__.py b/src/prompt_toolkit/completion/__init__.py
new file mode 100644
index 0000000..f65a94e
--- /dev/null
+++ b/src/prompt_toolkit/completion/__init__.py
@@ -0,0 +1,43 @@
+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",
+]
diff --git a/src/prompt_toolkit/completion/base.py b/src/prompt_toolkit/completion/base.py
new file mode 100644
index 0000000..04a712d
--- /dev/null
+++ b/src/prompt_toolkit/completion/base.py
@@ -0,0 +1,451 @@
+"""
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from typing 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 "{}(text={!r}, start_position={!r})".format(
+ self.__class__.__name__,
+ self.text,
+ self.start_position,
+ )
+ else:
+ return "{}(text={!r}, start_position={!r}, display={!r})".format(
+ self.__class__.__name__,
+ self.text,
+ self.start_position,
+ self.display,
+ )
+
+ 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 "{}(text_inserted={!r}, completion_requested={!r})".format(
+ self.__class__.__name__,
+ self.text_inserted,
+ self.completion_requested,
+ )
+
+
+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
diff --git a/src/prompt_toolkit/completion/deduplicate.py b/src/prompt_toolkit/completion/deduplicate.py
new file mode 100644
index 0000000..c3d5256
--- /dev/null
+++ b/src/prompt_toolkit/completion/deduplicate.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from typing 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
diff --git a/src/prompt_toolkit/completion/filesystem.py b/src/prompt_toolkit/completion/filesystem.py
new file mode 100644
index 0000000..8e7f87e
--- /dev/null
+++ b/src/prompt_toolkit/completion/filesystem.py
@@ -0,0 +1,118 @@
+from __future__ import annotations
+
+import os
+from typing 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,
+ )
diff --git a/src/prompt_toolkit/completion/fuzzy_completer.py b/src/prompt_toolkit/completion/fuzzy_completer.py
new file mode 100644
index 0000000..25ea892
--- /dev/null
+++ b/src/prompt_toolkit/completion/fuzzy_completer.py
@@ -0,0 +1,213 @@
+from __future__ import annotations
+
+import re
+from typing import Callable, Iterable, 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: list[str] | Callable[[], list[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
diff --git a/src/prompt_toolkit/completion/nested.py b/src/prompt_toolkit/completion/nested.py
new file mode 100644
index 0000000..a1d211a
--- /dev/null
+++ b/src/prompt_toolkit/completion/nested.py
@@ -0,0 +1,108 @@
+"""
+Nestedcompleter for completion of hierarchical data structures.
+"""
+from __future__ import annotations
+
+from typing import Any, Iterable, Mapping, Set, Union
+
+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, Union[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({item: None for item in 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)
diff --git a/src/prompt_toolkit/completion/word_completer.py b/src/prompt_toolkit/completion/word_completer.py
new file mode 100644
index 0000000..6ef4031
--- /dev/null
+++ b/src/prompt_toolkit/completion/word_completer.py
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+from typing import Callable, Iterable, Mapping, 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: list[str] | Callable[[], list[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,
+ )
diff --git a/src/prompt_toolkit/contrib/__init__.py b/src/prompt_toolkit/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/prompt_toolkit/contrib/__init__.py
diff --git a/src/prompt_toolkit/contrib/completers/__init__.py b/src/prompt_toolkit/contrib/completers/__init__.py
new file mode 100644
index 0000000..172fe6f
--- /dev/null
+++ b/src/prompt_toolkit/contrib/completers/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from .system import SystemCompleter
+
+__all__ = ["SystemCompleter"]
diff --git a/src/prompt_toolkit/contrib/completers/system.py b/src/prompt_toolkit/contrib/completers/system.py
new file mode 100644
index 0000000..5d990e5
--- /dev/null
+++ b/src/prompt_toolkit/contrib/completers/system.py
@@ -0,0 +1,64 @@
+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
+ ),
+ },
+ )
diff --git a/src/prompt_toolkit/contrib/regular_languages/__init__.py b/src/prompt_toolkit/contrib/regular_languages/__init__.py
new file mode 100644
index 0000000..c947fd5
--- /dev/null
+++ b/src/prompt_toolkit/contrib/regular_languages/__init__.py
@@ -0,0 +1,79 @@
+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"]
diff --git a/src/prompt_toolkit/contrib/regular_languages/compiler.py b/src/prompt_toolkit/contrib/regular_languages/compiler.py
new file mode 100644
index 0000000..474f6cf
--- /dev/null
+++ b/src/prompt_toolkit/contrib/regular_languages/compiler.py
@@ -0,0 +1,571 @@
+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 typing import Callable, Dict, Iterable, Iterator, Pattern
+from typing import Match as RegexMatch
+
+from .regex_parser import (
+ AnyNode,
+ Lookahead,
+ Node,
+ NodeSequence,
+ Regex,
+ Repeat,
+ Variable,
+ parse_regex,
+ tokenize_regex,
+)
+
+__all__ = [
+ "compile",
+]
+
+
+# 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 = "n%s" % counter[0]
+ self._group_names_to_nodes[name] = node.varname
+ counter[0] += 1
+ return name
+
+ # Compile regex strings.
+ self._re_pattern = "^%s$" % 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 "(?:%s)" % "|".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 "(?:%s)?" % node.regex
+
+ elif isinstance(node, Lookahead):
+ if node.negative:
+ yield "(?!%s)" % 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("Got %r" % node)
+
+ for r in transform(root_node):
+ yield "^(?:%s)$" % 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]))
+
+
+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),
+ )
+
+ def get(self, key: str, default: str | None = None) -> str | 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
+ )
diff --git a/src/prompt_toolkit/contrib/regular_languages/completion.py b/src/prompt_toolkit/contrib/regular_languages/completion.py
new file mode 100644
index 0000000..2e353e8
--- /dev/null
+++ b/src/prompt_toolkit/contrib/regular_languages/completion.py
@@ -0,0 +1,94 @@
+"""
+Completer for a regular grammar.
+"""
+from __future__ import annotations
+
+from typing 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:
+ completions = self._remove_duplicates(
+ self._get_completions_for_match(m, complete_event)
+ )
+
+ yield from completions
+
+ 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]) -> list[Completion]:
+ """
+ Remove duplicates, while keeping the order.
+ (Sometimes we have duplicates, because the there several matches of the
+ same grammar, each yielding similar completions.)
+ """
+ result: list[Completion] = []
+ for i in items:
+ if i not in result:
+ result.append(i)
+ return result
diff --git a/src/prompt_toolkit/contrib/regular_languages/lexer.py b/src/prompt_toolkit/contrib/regular_languages/lexer.py
new file mode 100644
index 0000000..b0a4deb
--- /dev/null
+++ b/src/prompt_toolkit/contrib/regular_languages/lexer.py
@@ -0,0 +1,93 @@
+"""
+`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 typing 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
diff --git a/src/prompt_toolkit/contrib/regular_languages/regex_parser.py b/src/prompt_toolkit/contrib/regular_languages/regex_parser.py
new file mode 100644
index 0000000..a365ba8
--- /dev/null
+++ b/src/prompt_toolkit/contrib/regular_languages/regex_parser.py
@@ -0,0 +1,282 @@
+"""
+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 "{}(childnode={!r}, varname={!r})".format(
+ self.__class__.__name__,
+ self.childnode,
+ self.varname,
+ )
+
+
+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("%r not supported" % t)
+
+ 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
diff --git a/src/prompt_toolkit/contrib/regular_languages/validation.py b/src/prompt_toolkit/contrib/regular_languages/validation.py
new file mode 100644
index 0000000..8e56e05
--- /dev/null
+++ b/src/prompt_toolkit/contrib/regular_languages/validation.py
@@ -0,0 +1,59 @@
+"""
+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"
+ )
diff --git a/src/prompt_toolkit/contrib/ssh/__init__.py b/src/prompt_toolkit/contrib/ssh/__init__.py
new file mode 100644
index 0000000..bbc1c21
--- /dev/null
+++ b/src/prompt_toolkit/contrib/ssh/__init__.py
@@ -0,0 +1,8 @@
+from __future__ import annotations
+
+from .server import PromptToolkitSSHServer, PromptToolkitSSHSession
+
+__all__ = [
+ "PromptToolkitSSHSession",
+ "PromptToolkitSSHServer",
+]
diff --git a/src/prompt_toolkit/contrib/ssh/server.py b/src/prompt_toolkit/contrib/ssh/server.py
new file mode 100644
index 0000000..9a5d402
--- /dev/null
+++ b/src/prompt_toolkit/contrib/ssh/server.py
@@ -0,0 +1,177 @@
+"""
+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 typing import Any, Callable, Coroutine, 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)
diff --git a/src/prompt_toolkit/contrib/telnet/__init__.py b/src/prompt_toolkit/contrib/telnet/__init__.py
new file mode 100644
index 0000000..de902b4
--- /dev/null
+++ b/src/prompt_toolkit/contrib/telnet/__init__.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+from .server import TelnetServer
+
+__all__ = [
+ "TelnetServer",
+]
diff --git a/src/prompt_toolkit/contrib/telnet/log.py b/src/prompt_toolkit/contrib/telnet/log.py
new file mode 100644
index 0000000..0fe8433
--- /dev/null
+++ b/src/prompt_toolkit/contrib/telnet/log.py
@@ -0,0 +1,12 @@
+"""
+Python logger for the telnet server.
+"""
+from __future__ import annotations
+
+import logging
+
+logger = logging.getLogger(__package__)
+
+__all__ = [
+ "logger",
+]
diff --git a/src/prompt_toolkit/contrib/telnet/protocol.py b/src/prompt_toolkit/contrib/telnet/protocol.py
new file mode 100644
index 0000000..4b90e98
--- /dev/null
+++ b/src/prompt_toolkit/contrib/telnet/protocol.py
@@ -0,0 +1,208 @@
+"""
+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 typing 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))
diff --git a/src/prompt_toolkit/contrib/telnet/server.py b/src/prompt_toolkit/contrib/telnet/server.py
new file mode 100644
index 0000000..9ebe66c
--- /dev/null
+++ b/src/prompt_toolkit/contrib/telnet/server.py
@@ -0,0 +1,427 @@
+"""
+Telnet server.
+"""
+from __future__ import annotations
+
+import asyncio
+import contextvars
+import socket
+from asyncio import get_running_loop
+from typing import Any, Callable, Coroutine, 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("Couldn't send data over socket: %s" % 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) # type: ignore
+ 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("Got %s" % 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)
diff --git a/src/prompt_toolkit/cursor_shapes.py b/src/prompt_toolkit/cursor_shapes.py
new file mode 100644
index 0000000..453b72c
--- /dev/null
+++ b/src/prompt_toolkit/cursor_shapes.py
@@ -0,0 +1,104 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from enum import Enum
+from typing import TYPE_CHECKING, Any, Callable, Union
+
+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 = Union[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 == InputMode.INSERT:
+ return CursorShape.BEAM
+ if application.vi_state.input_mode == InputMode.REPLACE:
+ return CursorShape.UNDERLINE
+
+ # 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
diff --git a/src/prompt_toolkit/data_structures.py b/src/prompt_toolkit/data_structures.py
new file mode 100644
index 0000000..27dd458
--- /dev/null
+++ b/src/prompt_toolkit/data_structures.py
@@ -0,0 +1,18 @@
+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
diff --git a/src/prompt_toolkit/document.py b/src/prompt_toolkit/document.py
new file mode 100644
index 0000000..74f4c13
--- /dev/null
+++ b/src/prompt_toolkit/document.py
@@ -0,0 +1,1181 @@
+"""
+The `Document` that implements all the text operations/querying.
+"""
+from __future__ import annotations
+
+import bisect
+import re
+import string
+import weakref
+from typing import Callable, Dict, Iterable, List, NoReturn, Pattern, 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 # type: ignore
+ append = _error
+ clear = _error
+ extend = _error
+ insert = _error
+ pop = _error
+ remove = _error
+ reverse = _error
+ sort = _error # type: ignore
+
+
+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 pattern:
+ return self.find_start_of_previous_word(WORD=WORD, pattern=pattern) is None
+ else:
+ return (
+ self.text_before_cursor == "" or self.text_before_cursor[-1:].isspace()
+ )
+
+ 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,
+ )
diff --git a/src/prompt_toolkit/enums.py b/src/prompt_toolkit/enums.py
new file mode 100644
index 0000000..da03633
--- /dev/null
+++ b/src/prompt_toolkit/enums.py
@@ -0,0 +1,19 @@
+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"
diff --git a/src/prompt_toolkit/eventloop/__init__.py b/src/prompt_toolkit/eventloop/__init__.py
new file mode 100644
index 0000000..5df623b
--- /dev/null
+++ b/src/prompt_toolkit/eventloop/__init__.py
@@ -0,0 +1,31 @@
+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",
+]
diff --git a/src/prompt_toolkit/eventloop/async_generator.py b/src/prompt_toolkit/eventloop/async_generator.py
new file mode 100644
index 0000000..5aee50a
--- /dev/null
+++ b/src/prompt_toolkit/eventloop/async_generator.py
@@ -0,0 +1,124 @@
+"""
+Implementation for async generators.
+"""
+from __future__ import annotations
+
+from asyncio import get_running_loop
+from contextlib import asynccontextmanager
+from queue import Empty, Full, Queue
+from typing import Any, AsyncGenerator, Callable, Iterable, 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
diff --git a/src/prompt_toolkit/eventloop/inputhook.py b/src/prompt_toolkit/eventloop/inputhook.py
new file mode 100644
index 0000000..a4c0eee
--- /dev/null
+++ b/src/prompt_toolkit/eventloop/inputhook.py
@@ -0,0 +1,190 @@
+"""
+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 selectors import BaseSelector, SelectorKey
+from typing import TYPE_CHECKING, Any, Callable, Mapping
+
+__all__ = [
+ "new_eventloop_with_inputhook",
+ "set_eventloop_with_inputhook",
+ "InputHookSelector",
+ "InputHookContext",
+ "InputHook",
+]
+
+if TYPE_CHECKING:
+ from _typeshed import FileDescriptorLike
+ from typing_extensions import TypeAlias
+
+ _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()
diff --git a/src/prompt_toolkit/eventloop/utils.py b/src/prompt_toolkit/eventloop/utils.py
new file mode 100644
index 0000000..3138361
--- /dev/null
+++ b/src/prompt_toolkit/eventloop/utils.py
@@ -0,0 +1,101 @@
+from __future__ import annotations
+
+import asyncio
+import contextvars
+import sys
+import time
+from asyncio import get_running_loop
+from types import TracebackType
+from typing import Any, Awaitable, Callable, 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
diff --git a/src/prompt_toolkit/eventloop/win32.py b/src/prompt_toolkit/eventloop/win32.py
new file mode 100644
index 0000000..56a0c7d
--- /dev/null
+++ b/src/prompt_toolkit/eventloop/win32.py
@@ -0,0 +1,72 @@
+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.
+ )
+ )
diff --git a/src/prompt_toolkit/filters/__init__.py b/src/prompt_toolkit/filters/__init__.py
new file mode 100644
index 0000000..277f428
--- /dev/null
+++ b/src/prompt_toolkit/filters/__init__.py
@@ -0,0 +1,70 @@
+"""
+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)
diff --git a/src/prompt_toolkit/filters/app.py b/src/prompt_toolkit/filters/app.py
new file mode 100644
index 0000000..aacb228
--- /dev/null
+++ b/src/prompt_toolkit/filters/app.py
@@ -0,0 +1,418 @@
+"""
+Filters that accept a `Application` as argument.
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+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 Container, 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(cast(Container, 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()
diff --git a/src/prompt_toolkit/filters/base.py b/src/prompt_toolkit/filters/base.py
new file mode 100644
index 0000000..afce6dc
--- /dev/null
+++ b/src/prompt_toolkit/filters/base.py
@@ -0,0 +1,255 @@
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from typing import Callable, Iterable, Union
+
+__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), "Expecting filter, got %r" % other
+
+ 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), "Expecting filter, got %r" % other
+
+ 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 "~%r" % self.filter
+
+
+class Always(Filter):
+ """
+ Always enable feature.
+ """
+
+ def __call__(self) -> bool:
+ return True
+
+ def __or__(self, other: Filter) -> Filter:
+ return self
+
+ 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 __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 "Condition(%r)" % self.func
+
+
+# Often used as type annotation.
+FilterOrBool = Union[Filter, bool]
diff --git a/src/prompt_toolkit/filters/cli.py b/src/prompt_toolkit/filters/cli.py
new file mode 100644
index 0000000..c95080a
--- /dev/null
+++ b/src/prompt_toolkit/filters/cli.py
@@ -0,0 +1,64 @@
+"""
+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
diff --git a/src/prompt_toolkit/filters/utils.py b/src/prompt_toolkit/filters/utils.py
new file mode 100644
index 0000000..bac85ba
--- /dev/null
+++ b/src/prompt_toolkit/filters/utils.py
@@ -0,0 +1,41 @@
+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("Expecting a bool or a Filter instance. Got %r" % bool_or_filter)
+
+
+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)()
diff --git a/src/prompt_toolkit/formatted_text/__init__.py b/src/prompt_toolkit/formatted_text/__init__.py
new file mode 100644
index 0000000..db44ab9
--- /dev/null
+++ b/src/prompt_toolkit/formatted_text/__init__.py
@@ -0,0 +1,58 @@
+"""
+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",
+]
diff --git a/src/prompt_toolkit/formatted_text/ansi.py b/src/prompt_toolkit/formatted_text/ansi.py
new file mode 100644
index 0000000..08ec0b3
--- /dev/null
+++ b/src/prompt_toolkit/formatted_text/ansi.py
@@ -0,0 +1,299 @@
+from __future__ import annotations
+
+from string import Formatter
+from typing import Generator
+
+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._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._faint = 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
+ 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._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 = "#{:02x}{:02x}{:02x}".format(
+ attrs.pop(),
+ attrs.pop(),
+ attrs.pop(),
+ )
+ 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._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()
diff --git a/src/prompt_toolkit/formatted_text/base.py b/src/prompt_toolkit/formatted_text/base.py
new file mode 100644
index 0000000..92de353
--- /dev/null
+++ b/src/prompt_toolkit/formatted_text/base.py
@@ -0,0 +1,180 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Tuple, 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 = Union[
+ Tuple[str, str], Tuple[str, str, Callable[[MouseEvent], "NotImplementedOrNone"]]
+]
+
+# List of (style, text) tuples.
+StyleAndTextTuples = List[OneStyleAndTextTuple]
+
+
+if TYPE_CHECKING:
+ from typing_extensions 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'] # Recursive definition not supported by mypy.
+ Callable[[], Any],
+ 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(
+ "No formatted text. Expecting a unicode object, "
+ f"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 "FormattedText(%s)" % 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
diff --git a/src/prompt_toolkit/formatted_text/html.py b/src/prompt_toolkit/formatted_text/html.py
new file mode 100644
index 0000000..a940ac8
--- /dev/null
+++ b/src/prompt_toolkit/formatted_text/html.py
@@ -0,0 +1,145 @@
+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("&", "&amp;")
+ .replace("<", "&lt;")
+ .replace(">", "&gt;")
+ .replace('"', "&quot;")
+ )
+
+
+FORMATTER = HTMLFormatter()
diff --git a/src/prompt_toolkit/formatted_text/pygments.py b/src/prompt_toolkit/formatted_text/pygments.py
new file mode 100644
index 0000000..d4ef3ad
--- /dev/null
+++ b/src/prompt_toolkit/formatted_text/pygments.py
@@ -0,0 +1,32 @@
+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
diff --git a/src/prompt_toolkit/formatted_text/utils.py b/src/prompt_toolkit/formatted_text/utils.py
new file mode 100644
index 0000000..c8c37e0
--- /dev/null
+++ b/src/prompt_toolkit/formatted_text/utils.py
@@ -0,0 +1,102 @@
+"""
+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 typing import Iterable, 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]:
+ if part:
+ 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
diff --git a/src/prompt_toolkit/history.py b/src/prompt_toolkit/history.py
new file mode 100644
index 0000000..553918e
--- /dev/null
+++ b/src/prompt_toolkit/history.py
@@ -0,0 +1,302 @@
+"""
+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 typing import AsyncGenerator, Iterable, Sequence
+
+__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
+
+
+class FileHistory(History):
+ """
+ :class:`.History` class that stores all strings in a file.
+ """
+
+ def __init__(self, filename: str) -> 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("\n# %s\n" % datetime.datetime.now())
+ for line in string.split("\n"):
+ write("+%s\n" % line)
diff --git a/src/prompt_toolkit/input/__init__.py b/src/prompt_toolkit/input/__init__.py
new file mode 100644
index 0000000..ed8631b
--- /dev/null
+++ b/src/prompt_toolkit/input/__init__.py
@@ -0,0 +1,14 @@
+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",
+]
diff --git a/src/prompt_toolkit/input/ansi_escape_sequences.py b/src/prompt_toolkit/input/ansi_escape_sequences.py
new file mode 100644
index 0000000..5648c66
--- /dev/null
+++ b/src/prompt_toolkit/input/ansi_escape_sequences.py
@@ -0,0 +1,343 @@
+"""
+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()
diff --git a/src/prompt_toolkit/input/base.py b/src/prompt_toolkit/input/base.py
new file mode 100644
index 0000000..fd1429d
--- /dev/null
+++ b/src/prompt_toolkit/input/base.py
@@ -0,0 +1,152 @@
+"""
+Abstraction of CLI Input.
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod, abstractproperty
+from contextlib import contextmanager
+from typing import Callable, ContextManager, Generator
+
+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
+
+ @abstractproperty
+ def closed(self) -> bool:
+ "Should be true when the input stream is closed."
+ return False
+
+ @abstractmethod
+ def raw_mode(self) -> ContextManager[None]:
+ """
+ Context manager that turns the input into raw mode.
+ """
+
+ @abstractmethod
+ def cooked_mode(self) -> ContextManager[None]:
+ """
+ Context manager that turns the input into cooked mode.
+ """
+
+ @abstractmethod
+ def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
+ """
+ Return a context manager that makes this input active in the current
+ event loop.
+ """
+
+ @abstractmethod
+ def detach(self) -> ContextManager[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 "dummy-%s" % 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) -> ContextManager[None]:
+ return _dummy_context_manager()
+
+ def cooked_mode(self) -> ContextManager[None]:
+ return _dummy_context_manager()
+
+ def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[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) -> ContextManager[None]:
+ return _dummy_context_manager()
+
+
+@contextmanager
+def _dummy_context_manager() -> Generator[None, None, None]:
+ yield
diff --git a/src/prompt_toolkit/input/defaults.py b/src/prompt_toolkit/input/defaults.py
new file mode 100644
index 0000000..483eeb2
--- /dev/null
+++ b/src/prompt_toolkit/input/defaults.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+import io
+import sys
+from typing import ContextManager, 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() -> ContextManager[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()
diff --git a/src/prompt_toolkit/input/posix_pipe.py b/src/prompt_toolkit/input/posix_pipe.py
new file mode 100644
index 0000000..c131fb8
--- /dev/null
+++ b/src/prompt_toolkit/input/posix_pipe.py
@@ -0,0 +1,118 @@
+from __future__ import annotations
+
+import sys
+
+assert sys.platform != "win32"
+
+import os
+from contextlib import contextmanager
+from typing import ContextManager, Iterator, 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) -> ContextManager[None]:
+ return DummyContext()
+
+ def cooked_mode(self) -> ContextManager[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}"
diff --git a/src/prompt_toolkit/input/posix_utils.py b/src/prompt_toolkit/input/posix_utils.py
new file mode 100644
index 0000000..4a78dc4
--- /dev/null
+++ b/src/prompt_toolkit/input/posix_utils.py
@@ -0,0 +1,97 @@
+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)
diff --git a/src/prompt_toolkit/input/typeahead.py b/src/prompt_toolkit/input/typeahead.py
new file mode 100644
index 0000000..a45e7cf
--- /dev/null
+++ b/src/prompt_toolkit/input/typeahead.py
@@ -0,0 +1,77 @@
+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] = []
diff --git a/src/prompt_toolkit/input/vt100.py b/src/prompt_toolkit/input/vt100.py
new file mode 100644
index 0000000..c1660de
--- /dev/null
+++ b/src/prompt_toolkit/input/vt100.py
@@ -0,0 +1,309 @@
+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 typing import Callable, ContextManager, Generator, 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]) -> ContextManager[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) -> ContextManager[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) -> ContextManager[None]:
+ return raw_mode(self.stdin.fileno())
+
+ def cooked_mode(self) -> ContextManager[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
diff --git a/src/prompt_toolkit/input/vt100_parser.py b/src/prompt_toolkit/input/vt100_parser.py
new file mode 100644
index 0000000..99e2d99
--- /dev/null
+++ b/src/prompt_toolkit/input/vt100_parser.py
@@ -0,0 +1,249 @@
+"""
+Parser for VT100 input stream.
+"""
+from __future__ import annotations
+
+import re
+from typing import Callable, Dict, 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()
diff --git a/src/prompt_toolkit/input/win32.py b/src/prompt_toolkit/input/win32.py
new file mode 100644
index 0000000..35e8948
--- /dev/null
+++ b/src/prompt_toolkit/input/win32.py
@@ -0,0 +1,749 @@
+from __future__ import annotations
+
+import os
+import sys
+from abc import abstractmethod
+from asyncio import get_running_loop
+from contextlib import 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 ctypes import Array, pointer
+from ctypes.wintypes import DWORD, HANDLE
+from typing import Callable, ContextManager, Iterable, Iterator, 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
+
+__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
+
+
+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.console_input_reader = ConsoleInputReader()
+
+ def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[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) -> ContextManager[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(self) -> None:
+ pass
+
+ @property
+ def closed(self) -> bool:
+ return False
+
+ def raw_mode(self) -> ContextManager[None]:
+ return raw_mode()
+
+ def cooked_mode(self) -> ContextManager[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 _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 _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) -> None:
+ self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
+
+ 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
+
+ windll.kernel32.SetConsoleMode(
+ self.handle,
+ self.original_mode.value
+ & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT),
+ )
+
+ 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),
+ )
diff --git a/src/prompt_toolkit/input/win32_pipe.py b/src/prompt_toolkit/input/win32_pipe.py
new file mode 100644
index 0000000..0bafa49
--- /dev/null
+++ b/src/prompt_toolkit/input/win32_pipe.py
@@ -0,0 +1,156 @@
+from __future__ import annotations
+
+import sys
+
+assert sys.platform == "win32"
+
+from contextlib import contextmanager
+from ctypes import windll
+from ctypes.wintypes import HANDLE
+from typing import Callable, ContextManager, Iterator
+
+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]) -> ContextManager[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) -> ContextManager[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) -> ContextManager[None]:
+ return DummyContext()
+
+ def cooked_mode(self) -> ContextManager[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}"
diff --git a/src/prompt_toolkit/key_binding/__init__.py b/src/prompt_toolkit/key_binding/__init__.py
new file mode 100644
index 0000000..c31746a
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/__init__.py
@@ -0,0 +1,22 @@
+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",
+]
diff --git a/src/prompt_toolkit/key_binding/bindings/__init__.py b/src/prompt_toolkit/key_binding/bindings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/__init__.py
diff --git a/src/prompt_toolkit/key_binding/bindings/auto_suggest.py b/src/prompt_toolkit/key_binding/bindings/auto_suggest.py
new file mode 100644
index 0000000..3d8a843
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/auto_suggest.py
@@ -0,0 +1,65 @@
+"""
+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
diff --git a/src/prompt_toolkit/key_binding/bindings/basic.py b/src/prompt_toolkit/key_binding/bindings/basic.py
new file mode 100644
index 0000000..084548d
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/basic.py
@@ -0,0 +1,255 @@
+# 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
+
+
+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.
+
+ @Condition
+ def has_text_before_cursor() -> bool:
+ return bool(get_app().current_buffer.text)
+
+ 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)
+
+ @Condition
+ def in_quoted_insert() -> bool:
+ return get_app().quoted_insert
+
+ @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
diff --git a/src/prompt_toolkit/key_binding/bindings/completion.py b/src/prompt_toolkit/key_binding/bindings/completion.py
new file mode 100644
index 0000000..016821f
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/completion.py
@@ -0,0 +1,205 @@
+"""
+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)
diff --git a/src/prompt_toolkit/key_binding/bindings/cpr.py b/src/prompt_toolkit/key_binding/bindings/cpr.py
new file mode 100644
index 0000000..cd9df0a
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/cpr.py
@@ -0,0 +1,30 @@
+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
diff --git a/src/prompt_toolkit/key_binding/bindings/emacs.py b/src/prompt_toolkit/key_binding/bindings/emacs.py
new file mode 100644
index 0000000..80a66fd
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/emacs.py
@@ -0,0 +1,557 @@
+# 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
+
+
+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=Condition(lambda: get_app().key_processor.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 = "-"
+
+ @Condition
+ def is_returnable() -> bool:
+ return get_app().current_buffer.is_returnable
+
+ # 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)
diff --git a/src/prompt_toolkit/key_binding/bindings/focus.py b/src/prompt_toolkit/key_binding/bindings/focus.py
new file mode 100644
index 0000000..24aa3ce
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/focus.py
@@ -0,0 +1,26 @@
+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()
diff --git a/src/prompt_toolkit/key_binding/bindings/mouse.py b/src/prompt_toolkit/key_binding/bindings/mouse.py
new file mode 100644
index 0000000..cb426ce
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/mouse.py
@@ -0,0 +1,348 @@
+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
diff --git a/src/prompt_toolkit/key_binding/bindings/named_commands.py b/src/prompt_toolkit/key_binding/bindings/named_commands.py
new file mode 100644
index 0000000..d836116
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/named_commands.py
@@ -0,0 +1,690 @@
+"""
+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 typing import Callable, TypeVar, Union, 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 = Union[_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("Unknown Readline command: %r" % name) 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)
diff --git a/src/prompt_toolkit/key_binding/bindings/open_in_editor.py b/src/prompt_toolkit/key_binding/bindings/open_in_editor.py
new file mode 100644
index 0000000..d156424
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/open_in_editor.py
@@ -0,0 +1,51 @@
+"""
+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
diff --git a/src/prompt_toolkit/key_binding/bindings/page_navigation.py b/src/prompt_toolkit/key_binding/bindings/page_navigation.py
new file mode 100644
index 0000000..3918e14
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/page_navigation.py
@@ -0,0 +1,84 @@
+"""
+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)
diff --git a/src/prompt_toolkit/key_binding/bindings/scroll.py b/src/prompt_toolkit/key_binding/bindings/scroll.py
new file mode 100644
index 0000000..83a4be1
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/scroll.py
@@ -0,0 +1,189 @@
+"""
+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
diff --git a/src/prompt_toolkit/key_binding/bindings/search.py b/src/prompt_toolkit/key_binding/bindings/search.py
new file mode 100644
index 0000000..ba5e117
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/search.py
@@ -0,0 +1,95 @@
+"""
+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()
diff --git a/src/prompt_toolkit/key_binding/bindings/vi.py b/src/prompt_toolkit/key_binding/bindings/vi.py
new file mode 100644
index 0000000..5cc74b4
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/bindings/vi.py
@@ -0,0 +1,2224 @@
+# pylint: disable=function-redefined
+from __future__ import annotations
+
+import codecs
+import string
+from enum import Enum
+from itertools import accumulate
+from typing import Callable, Iterable, Tuple, 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
+
+
+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()),
+ (
+ ("~",),
+ Condition(lambda: get_app().vi_state.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()
+
+ @Condition
+ def is_returnable() -> bool:
+ return get_app().current_buffer.is_returnable
+
+ # 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
+ )
+ )
+
+ @Condition
+ def in_block_selection() -> bool:
+ buff = get_app().current_buffer
+ return bool(
+ buff.selection_state and buff.selection_state.type == SelectionType.BLOCK
+ )
+
+ @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
+
+ @Condition
+ def digraph_symbol_1_given() -> bool:
+ return get_app().vi_state.digraph_symbol1 is not None
+
+ @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
+
+ @Condition
+ def search_buffer_is_empty() -> bool:
+ "Returns True when the search buffer is empty."
+ return get_app().current_buffer.text == ""
+
+ # 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)
diff --git a/src/prompt_toolkit/key_binding/defaults.py b/src/prompt_toolkit/key_binding/defaults.py
new file mode 100644
index 0000000..166da8d
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/defaults.py
@@ -0,0 +1,62 @@
+"""
+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(),
+ ]
+ )
diff --git a/src/prompt_toolkit/key_binding/digraphs.py b/src/prompt_toolkit/key_binding/digraphs.py
new file mode 100644
index 0000000..1e8a432
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/digraphs.py
@@ -0,0 +1,1377 @@
+"""
+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,
+}
diff --git a/src/prompt_toolkit/key_binding/emacs_state.py b/src/prompt_toolkit/key_binding/emacs_state.py
new file mode 100644
index 0000000..6a2ebf4
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/emacs_state.py
@@ -0,0 +1,36 @@
+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
diff --git a/src/prompt_toolkit/key_binding/key_bindings.py b/src/prompt_toolkit/key_binding/key_bindings.py
new file mode 100644
index 0000000..62530f2
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/key_bindings.py
@@ -0,0 +1,671 @@
+"""
+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, abstractproperty
+from inspect import isawaitable
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Coroutine,
+ Hashable,
+ Sequence,
+ Tuple,
+ 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 "{}(keys={!r}, handler={!r})".format(
+ self.__class__.__name__,
+ self.keys,
+ self.handler,
+ )
+
+
+# Sequence of keys presses.
+KeysTuple = Tuple[Union[Keys, str], ...]
+
+
+class KeyBindingsBase(metaclass=ABCMeta):
+ """
+ Interface for a KeyBindings.
+ """
+
+ @abstractproperty
+ 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 []
+
+ @abstractproperty
+ 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=Union[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[Union[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
diff --git a/src/prompt_toolkit/key_binding/key_processor.py b/src/prompt_toolkit/key_binding/key_processor.py
new file mode 100644
index 0000000..4c4f0d1
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/key_processor.py
@@ -0,0 +1,529 @@
+"""
+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 typing import TYPE_CHECKING, Any, Generator
+
+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 "KeyPressEvent(arg={!r}, key_sequence={!r}, is_repeat={!r})".format(
+ self.arg,
+ self.key_sequence,
+ self.is_repeat,
+ )
+
+ @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
diff --git a/src/prompt_toolkit/key_binding/vi_state.py b/src/prompt_toolkit/key_binding/vi_state.py
new file mode 100644
index 0000000..7ec552f
--- /dev/null
+++ b/src/prompt_toolkit/key_binding/vi_state.py
@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+from enum import Enum
+from typing import TYPE_CHECKING, Callable
+
+from prompt_toolkit.clipboard import ClipboardData
+
+if TYPE_CHECKING:
+ from .key_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 = ""
diff --git a/src/prompt_toolkit/keys.py b/src/prompt_toolkit/keys.py
new file mode 100644
index 0000000..ee52aee
--- /dev/null
+++ b/src/prompt_toolkit/keys.py
@@ -0,0 +1,222 @@
+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",
+}
diff --git a/src/prompt_toolkit/layout/__init__.py b/src/prompt_toolkit/layout/__init__.py
new file mode 100644
index 0000000..c5fce46
--- /dev/null
+++ b/src/prompt_toolkit/layout/__init__.py
@@ -0,0 +1,146 @@
+"""
+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",
+]
diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py
new file mode 100644
index 0000000..100d4aa
--- /dev/null
+++ b/src/prompt_toolkit/layout/containers.py
@@ -0,0 +1,2743 @@
+"""
+Container for the layout.
+(Containers can contain other containers or user interface controls.)
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from enum import Enum
+from functools import partial
+from typing import TYPE_CHECKING, Callable, Sequence, 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_extensions 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 "Float(content=%r)" % self.content
+
+
+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 "ScrollOffsets(top={!r}, bottom={!r}, left={!r}, right={!r})".format(
+ self._top,
+ self._bottom,
+ self._left,
+ self._right,
+ )
+
+
+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 "Window(content=%r)" % self.content
+
+ 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) -> None:
+ self.content = to_container(content)
+ 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)
+ 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)
+ 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
+ )
+
+ def get_children(self) -> list[Container]:
+ return [self.content]
+
+
+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
diff --git a/src/prompt_toolkit/layout/controls.py b/src/prompt_toolkit/layout/controls.py
new file mode 100644
index 0000000..c30c0ef
--- /dev/null
+++ b/src/prompt_toolkit/layout/controls.py
@@ -0,0 +1,944 @@
+"""
+User interface Controls for the layout.
+"""
+from __future__ import annotations
+
+import time
+from abc import ABCMeta, abstractmethod
+from typing import TYPE_CHECKING, Callable, Hashable, Iterable, 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) -> _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
+ )
+ )
+
+ 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))
+ 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)
diff --git a/src/prompt_toolkit/layout/dimension.py b/src/prompt_toolkit/layout/dimension.py
new file mode 100644
index 0000000..c1f05f9
--- /dev/null
+++ b/src/prompt_toolkit/layout/dimension.py
@@ -0,0 +1,219 @@
+"""
+Layout dimensions are used to give the minimum, maximum and preferred
+dimensions for containers and controls.
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Callable, Union
+
+__all__ = [
+ "Dimension",
+ "D",
+ "sum_layout_dimensions",
+ "max_layout_dimensions",
+ "AnyDimension",
+ "to_dimension",
+ "is_dimension",
+]
+
+if TYPE_CHECKING:
+ from typing_extensions 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 is_zero(self) -> bool:
+ "True if this `Dimension` represents a zero size."
+ return self.preferred == 0 or self.max == 0
+
+ def __repr__(self) -> str:
+ fields = []
+ if self.min_specified:
+ fields.append("min=%r" % self.min)
+ if self.max_specified:
+ fields.append("max=%r" % self.max)
+ if self.preferred_specified:
+ fields.append("preferred=%r" % self.preferred)
+ if self.weight_specified:
+ fields.append("weight=%r" % self.weight)
+
+ return "Dimension(%s)" % ", ".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.is_zero() for d in dimensions):
+ return dimensions[0]
+
+ # Ignore empty dimensions. (They should not reduce the size of others.)
+ dimensions = [d for d in dimensions if not d.is_zero()]
+
+ 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 = Union[
+ None, # None is a valid dimension that will fit anything.
+ int,
+ Dimension,
+ # Callable[[], 'AnyDimension'] # Recursive definition not supported by mypy.
+ Callable[[], Any],
+]
+
+
+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
diff --git a/src/prompt_toolkit/layout/dummy.py b/src/prompt_toolkit/layout/dummy.py
new file mode 100644
index 0000000..139f311
--- /dev/null
+++ b/src/prompt_toolkit/layout/dummy.py
@@ -0,0 +1,39 @@
+"""
+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)
diff --git a/src/prompt_toolkit/layout/layout.py b/src/prompt_toolkit/layout/layout.py
new file mode 100644
index 0000000..a5e7a80
--- /dev/null
+++ b/src/prompt_toolkit/layout/layout.py
@@ -0,0 +1,411 @@
+"""
+Wrapper for the layout.
+"""
+from __future__ import annotations
+
+from typing import Generator, Iterable, Union
+
+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 = Union[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)
diff --git a/src/prompt_toolkit/layout/margins.py b/src/prompt_toolkit/layout/margins.py
new file mode 100644
index 0000000..cc9dd96
--- /dev/null
+++ b/src/prompt_toolkit/layout/margins.py
@@ -0,0 +1,303 @@
+"""
+Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`.
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from typing import TYPE_CHECKING, Callable
+
+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("%s" % 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
diff --git a/src/prompt_toolkit/layout/menus.py b/src/prompt_toolkit/layout/menus.py
new file mode 100644
index 0000000..2c2ccb6
--- /dev/null
+++ b/src/prompt_toolkit/layout/menus.py
@@ -0,0 +1,751 @@
+from __future__ import annotations
+
+import math
+from itertools import zip_longest
+from typing import TYPE_CHECKING, Callable, Iterable, Sequence, 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 = "class:completion-menu.completion.current {} {}".format(
+ 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 []
diff --git a/src/prompt_toolkit/layout/mouse_handlers.py b/src/prompt_toolkit/layout/mouse_handlers.py
new file mode 100644
index 0000000..56a4edd
--- /dev/null
+++ b/src/prompt_toolkit/layout/mouse_handlers.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+from collections import defaultdict
+from typing import TYPE_CHECKING, Callable
+
+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
diff --git a/src/prompt_toolkit/layout/processors.py b/src/prompt_toolkit/layout/processors.py
new file mode 100644
index 0000000..b737611
--- /dev/null
+++ b/src/prompt_toolkit/layout/processors.py
@@ -0,0 +1,1013 @@
+"""
+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 typing import TYPE_CHECKING, Callable, Hashable, 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.)
+ """
+
+ def __init__(
+ self,
+ buffer_control: BufferControl,
+ document: Document,
+ lineno: int,
+ source_to_display: SourceToDisplay,
+ fragments: StyleAndTextTuples,
+ width: int,
+ height: int,
+ ) -> 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
+
+ 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 "{}(processor={!r}, filter={!r})".format(
+ self.__class__.__name__,
+ self.processor,
+ self.filter,
+ )
+
+
+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,
+ )
+ )
+ 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)
diff --git a/src/prompt_toolkit/layout/screen.py b/src/prompt_toolkit/layout/screen.py
new file mode 100644
index 0000000..49aebbd
--- /dev/null
+++ b/src/prompt_toolkit/layout/screen.py
@@ -0,0 +1,329 @@
+from __future__ import annotations
+
+from collections import defaultdict
+from typing import TYPE_CHECKING, Callable
+
+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(lambda: "")
+ )
+
+ #: 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 "{}(x={!r}, y={!r}, width={!r}, height={!r})".format(
+ self.__class__.__name__,
+ self.xpos,
+ self.ypos,
+ self.width,
+ self.height,
+ )
diff --git a/src/prompt_toolkit/layout/scrollable_pane.py b/src/prompt_toolkit/layout/scrollable_pane.py
new file mode 100644
index 0000000..e38fd76
--- /dev/null
+++ b/src/prompt_toolkit/layout/scrollable_pane.py
@@ -0,0 +1,494 @@
+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"
+ )
diff --git a/src/prompt_toolkit/layout/utils.py b/src/prompt_toolkit/layout/utils.py
new file mode 100644
index 0000000..0f78f37
--- /dev/null
+++ b/src/prompt_toolkit/layout/utils.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterable, List, 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)
diff --git a/src/prompt_toolkit/lexers/__init__.py b/src/prompt_toolkit/lexers/__init__.py
new file mode 100644
index 0000000..9bdc599
--- /dev/null
+++ b/src/prompt_toolkit/lexers/__init__.py
@@ -0,0 +1,20 @@
+"""
+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",
+]
diff --git a/src/prompt_toolkit/lexers/base.py b/src/prompt_toolkit/lexers/base.py
new file mode 100644
index 0000000..3f65f8e
--- /dev/null
+++ b/src/prompt_toolkit/lexers/base.py
@@ -0,0 +1,84 @@
+"""
+Base classes for prompt_toolkit lexers.
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from typing 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)
diff --git a/src/prompt_toolkit/lexers/pygments.py b/src/prompt_toolkit/lexers/pygments.py
new file mode 100644
index 0000000..4721d73
--- /dev/null
+++ b/src/prompt_toolkit/lexers/pygments.py
@@ -0,0 +1,327 @@
+"""
+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 typing import TYPE_CHECKING, Callable, Dict, Generator, Iterable, Tuple
+
+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: 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
diff --git a/src/prompt_toolkit/log.py b/src/prompt_toolkit/log.py
new file mode 100644
index 0000000..adb5172
--- /dev/null
+++ b/src/prompt_toolkit/log.py
@@ -0,0 +1,12 @@
+"""
+Logging configuration.
+"""
+from __future__ import annotations
+
+import logging
+
+__all__ = [
+ "logger",
+]
+
+logger = logging.getLogger(__package__)
diff --git a/src/prompt_toolkit/mouse_events.py b/src/prompt_toolkit/mouse_events.py
new file mode 100644
index 0000000..743773b
--- /dev/null
+++ b/src/prompt_toolkit/mouse_events.py
@@ -0,0 +1,89 @@
+"""
+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 "MouseEvent({!r},{!r},{!r},{!r})".format(
+ self.position,
+ self.event_type,
+ self.button,
+ self.modifiers,
+ )
diff --git a/src/prompt_toolkit/output/__init__.py b/src/prompt_toolkit/output/__init__.py
new file mode 100644
index 0000000..6b4c5f3
--- /dev/null
+++ b/src/prompt_toolkit/output/__init__.py
@@ -0,0 +1,15 @@
+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",
+]
diff --git a/src/prompt_toolkit/output/base.py b/src/prompt_toolkit/output/base.py
new file mode 100644
index 0000000..3c38cec
--- /dev/null
+++ b/src/prompt_toolkit/output/base.py
@@ -0,0 +1,331 @@
+"""
+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
diff --git a/src/prompt_toolkit/output/color_depth.py b/src/prompt_toolkit/output/color_depth.py
new file mode 100644
index 0000000..f66d2be
--- /dev/null
+++ b/src/prompt_toolkit/output/color_depth.py
@@ -0,0 +1,64 @@
+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()
diff --git a/src/prompt_toolkit/output/conemu.py b/src/prompt_toolkit/output/conemu.py
new file mode 100644
index 0000000..6369944
--- /dev/null
+++ b/src/prompt_toolkit/output/conemu.py
@@ -0,0 +1,65 @@
+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)
diff --git a/src/prompt_toolkit/output/defaults.py b/src/prompt_toolkit/output/defaults.py
new file mode 100644
index 0000000..ed114e3
--- /dev/null
+++ b/src/prompt_toolkit/output/defaults.py
@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+import sys
+from typing import 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
+
+__all__ = [
+ "create_output",
+]
+
+
+def create_output(
+ stdout: TextIO | 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 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 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 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,
+ )
diff --git a/src/prompt_toolkit/output/flush_stdout.py b/src/prompt_toolkit/output/flush_stdout.py
new file mode 100644
index 0000000..daf58ef
--- /dev/null
+++ b/src/prompt_toolkit/output/flush_stdout.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+import errno
+import os
+import sys
+from contextlib import contextmanager
+from typing import IO, Iterator, 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)
diff --git a/src/prompt_toolkit/output/plain_text.py b/src/prompt_toolkit/output/plain_text.py
new file mode 100644
index 0000000..4b24ad9
--- /dev/null
+++ b/src/prompt_toolkit/output/plain_text.py
@@ -0,0 +1,143 @@
+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
diff --git a/src/prompt_toolkit/output/vt100.py b/src/prompt_toolkit/output/vt100.py
new file mode 100644
index 0000000..142deab
--- /dev/null
+++ b/src/prompt_toolkit/output/vt100.py
@@ -0,0 +1,747 @@
+"""
+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 typing import Callable, Dict, Hashable, Iterable, Sequence, TextIO, Tuple
+
+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..232: the 6x6x6 color cube
+ valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF)
+
+ for i in range(217):
+ r = valuerange[(i // 36) % 6]
+ g = valuerange[(i // 6) % 6]
+ b = valuerange[i % 6]
+ colors.append((r, g, b))
+
+ # colors 233..253: grayscale
+ for i in range(1, 22):
+ 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, reverse) 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,
+ ) = attrs
+ parts: list[str] = []
+
+ parts.extend(self._colors_to_code(fgcolor or "", bgcolor or ""))
+
+ if bold:
+ parts.append("1")
+ 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
+
+ @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;%s\x07" % 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:
+ self.write_raw("\x1b[?25l")
+
+ def show_cursor(self) -> None:
+ 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
diff --git a/src/prompt_toolkit/output/win32.py b/src/prompt_toolkit/output/win32.py
new file mode 100644
index 0000000..edeca09
--- /dev/null
+++ b/src/prompt_toolkit/output/win32.py
@@ -0,0 +1,683 @@
+from __future__ import annotations
+
+import sys
+
+assert sys.platform == "win32"
+
+import os
+from ctypes import ArgumentError, byref, c_char, c_long, c_uint, c_ulong, pointer
+from ctypes.wintypes import DWORD, HANDLE
+from typing import Callable, 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 %s, 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." % 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(("%r" % func.__name__).encode("utf-8") + b"\n")
+ self.LOG.write(
+ b" " + ", ".join(["%r" % i for i in a]).encode("utf-8") + b"\n"
+ )
+ self.LOG.write(
+ b" "
+ + ", ".join(["%r" % type(i) 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,
+ ) = 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(("%r" % data).encode("utf-8") + 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]
diff --git a/src/prompt_toolkit/output/windows10.py b/src/prompt_toolkit/output/windows10.py
new file mode 100644
index 0000000..c39f3ec
--- /dev/null
+++ b/src/prompt_toolkit/output/windows10.py
@@ -0,0 +1,128 @@
+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:
+ 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)
+
+ 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)
diff --git a/src/prompt_toolkit/patch_stdout.py b/src/prompt_toolkit/patch_stdout.py
new file mode 100644
index 0000000..528bec7
--- /dev/null
+++ b/src/prompt_toolkit/patch_stdout.py
@@ -0,0 +1,296 @@
+"""
+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 contextlib import contextmanager
+from typing import Generator, 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` 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.
+
+ :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:
+ 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"
diff --git a/src/prompt_toolkit/py.typed b/src/prompt_toolkit/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/prompt_toolkit/py.typed
diff --git a/src/prompt_toolkit/renderer.py b/src/prompt_toolkit/renderer.py
new file mode 100644
index 0000000..5ad1dd6
--- /dev/null
+++ b/src/prompt_toolkit/renderer.py
@@ -0,0 +1,813 @@
+"""
+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 enum import Enum
+from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable
+
+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 or is_done:
+ 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
+
+ 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()
+
+ # 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()
diff --git a/src/prompt_toolkit/search.py b/src/prompt_toolkit/search.py
new file mode 100644
index 0000000..fd90a04
--- /dev/null
+++ b/src/prompt_toolkit/search.py
@@ -0,0 +1,230 @@
+"""
+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 "{}({!r}, direction={!r}, ignore_case={!r})".format(
+ self.__class__.__name__,
+ self.text,
+ self.direction,
+ self.ignore_case,
+ )
+
+ 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()
+ }
diff --git a/src/prompt_toolkit/selection.py b/src/prompt_toolkit/selection.py
new file mode 100644
index 0000000..2158fa9
--- /dev/null
+++ b/src/prompt_toolkit/selection.py
@@ -0,0 +1,61 @@
+"""
+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 "{}(original_cursor_position={!r}, type={!r})".format(
+ self.__class__.__name__,
+ self.original_cursor_position,
+ self.type,
+ )
diff --git a/src/prompt_toolkit/shortcuts/__init__.py b/src/prompt_toolkit/shortcuts/__init__.py
new file mode 100644
index 0000000..49e5ac4
--- /dev/null
+++ b/src/prompt_toolkit/shortcuts/__init__.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+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",
+ # Utils.
+ "clear",
+ "clear_title",
+ "print_container",
+ "print_formatted_text",
+ "set_title",
+]
diff --git a/src/prompt_toolkit/shortcuts/dialogs.py b/src/prompt_toolkit/shortcuts/dialogs.py
new file mode 100644
index 0000000..d78e7db
--- /dev/null
+++ b/src/prompt_toolkit/shortcuts/dialogs.py
@@ -0,0 +1,330 @@
+from __future__ import annotations
+
+import functools
+from asyncio import get_running_loop
+from typing import Any, Callable, Sequence, 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.
+ """
+ loop = get_running_loop()
+ 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.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()
diff --git a/src/prompt_toolkit/shortcuts/progress_bar/__init__.py b/src/prompt_toolkit/shortcuts/progress_bar/__init__.py
new file mode 100644
index 0000000..2261a5b
--- /dev/null
+++ b/src/prompt_toolkit/shortcuts/progress_bar/__init__.py
@@ -0,0 +1,33 @@
+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",
+]
diff --git a/src/prompt_toolkit/shortcuts/progress_bar/base.py b/src/prompt_toolkit/shortcuts/progress_bar/base.py
new file mode 100644
index 0000000..21aa1be
--- /dev/null
+++ b/src/prompt_toolkit/shortcuts/progress_bar/base.py
@@ -0,0 +1,448 @@
+"""
+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 typing import (
+ Callable,
+ Generic,
+ Iterable,
+ Iterator,
+ Sequence,
+ Sized,
+ 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
diff --git a/src/prompt_toolkit/shortcuts/progress_bar/formatters.py b/src/prompt_toolkit/shortcuts/progress_bar/formatters.py
new file mode 100644
index 0000000..dd0339c
--- /dev/null
+++ b/src/prompt_toolkit/shortcuts/progress_bar/formatters.py
@@ -0,0 +1,429 @@
+"""
+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 = "<percentage>{percentage:>5}%</percentage>"
+
+ def format(
+ self,
+ progress_bar: ProgressBar,
+ progress: ProgressBarCounter[object],
+ width: int,
+ ) -> AnyFormattedText:
+ return HTML(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 = "<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 HTML(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 = "<current>{current:>3}</current>/<total>{total:>3}</total>"
+
+ def format(
+ self,
+ progress_bar: ProgressBar,
+ progress: ProgressBarCounter[object],
+ width: int,
+ ) -> AnyFormattedText:
+ return HTML(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.
+ """
+
+ def format(
+ self,
+ progress_bar: ProgressBar,
+ progress: ProgressBarCounter[object],
+ width: int,
+ ) -> AnyFormattedText:
+ text = _format_timedelta(progress.time_elapsed).rjust(width)
+ return HTML("<time-elapsed>{time_elapsed}</time-elapsed>").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 = "<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 HTML(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 = (
+ "<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 HTML(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.
+ """
+
+ characters = r"/-\|"
+
+ def format(
+ self,
+ progress_bar: ProgressBar,
+ progress: ProgressBarCounter[object],
+ width: int,
+ ) -> AnyFormattedText:
+ index = int(time.time() * 3) % len(self.characters)
+ return HTML("<spinning-wheel>{0}</spinning-wheel>").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(" "),
+ ]
diff --git a/src/prompt_toolkit/shortcuts/prompt.py b/src/prompt_toolkit/shortcuts/prompt.py
new file mode 100644
index 0000000..7274b5f
--- /dev/null
+++ b/src/prompt_toolkit/shortcuts/prompt.py
@@ -0,0 +1,1504 @@
+"""
+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 contextlib import contextmanager
+from enum import Enum
+from functools import partial
+from typing import TYPE_CHECKING, Callable, Generic, Iterator, 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.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 which is supposed to
+ return formatted text.
+ :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 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.
+ """
+
+ _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",
+ )
+
+ 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,
+ input: Input | None = None,
+ output: Output | None = None,
+ ) -> 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
+
+ # 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)
+ 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.
+ layout = HSplit(
+ [
+ # The main input, with completion menus floating on top of it.
+ 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),
+ ),
+ ],
+ ),
+ 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=KeyboardInterrupt, 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=EOFError, 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,
+ # 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
+
+ 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,
+ # 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
+
+ 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,
+ # 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,
+ 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: str, 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: str = "Confirm?", suffix: str = " (y/n) ") -> bool:
+ """
+ Display a confirmation prompt that returns True/False.
+ """
+ session = create_confirm_session(message, suffix)
+ return session.prompt()
diff --git a/src/prompt_toolkit/shortcuts/utils.py b/src/prompt_toolkit/shortcuts/utils.py
new file mode 100644
index 0000000..abf4fd2
--- /dev/null
+++ b/src/prompt_toolkit/shortcuts/utils.py
@@ -0,0 +1,239 @@
+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("")
diff --git a/src/prompt_toolkit/styles/__init__.py b/src/prompt_toolkit/styles/__init__.py
new file mode 100644
index 0000000..23f61bb
--- /dev/null
+++ b/src/prompt_toolkit/styles/__init__.py
@@ -0,0 +1,66 @@
+"""
+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",
+]
diff --git a/src/prompt_toolkit/styles/base.py b/src/prompt_toolkit/styles/base.py
new file mode 100644
index 0000000..b50f3b0
--- /dev/null
+++ b/src/prompt_toolkit/styles/base.py
@@ -0,0 +1,183 @@
+"""
+The base classes for the styling.
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod, abstractproperty
+from typing import Callable, Hashable, 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
+
+
+"""
+: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
+"""
+
+#: The default `Attrs`.
+DEFAULT_ATTRS = Attrs(
+ color="",
+ bgcolor="",
+ bold=False,
+ underline=False,
+ strike=False,
+ italic=False,
+ blink=False,
+ reverse=False,
+ hidden=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.
+ """
+
+ @abstractproperty
+ 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
diff --git a/src/prompt_toolkit/styles/defaults.py b/src/prompt_toolkit/styles/defaults.py
new file mode 100644
index 0000000..75b8dd2
--- /dev/null
+++ b/src/prompt_toolkit/styles/defaults.py
@@ -0,0 +1,235 @@
+"""
+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)
diff --git a/src/prompt_toolkit/styles/named_colors.py b/src/prompt_toolkit/styles/named_colors.py
new file mode 100644
index 0000000..0395c8b
--- /dev/null
+++ b/src/prompt_toolkit/styles/named_colors.py
@@ -0,0 +1,161 @@
+"""
+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",
+}
diff --git a/src/prompt_toolkit/styles/pygments.py b/src/prompt_toolkit/styles/pygments.py
new file mode 100644
index 0000000..3e101f1
--- /dev/null
+++ b/src/prompt_toolkit/styles/pygments.py
@@ -0,0 +1,69 @@
+"""
+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()
diff --git a/src/prompt_toolkit/styles/style.py b/src/prompt_toolkit/styles/style.py
new file mode 100644
index 0000000..1abee0f
--- /dev/null
+++ b/src/prompt_toolkit/styles/style.py
@@ -0,0 +1,400 @@
+"""
+Tool for creating styles from a dictionary.
+"""
+from __future__ import annotations
+
+import itertools
+import re
+from enum import Enum
+from typing import Hashable, 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("Wrong color format %r" % text)
+
+
+# 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,
+)
+
+
+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)
+
+ # 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]),
+ )
+
+
+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)
diff --git a/src/prompt_toolkit/styles/style_transformation.py b/src/prompt_toolkit/styles/style_transformation.py
new file mode 100644
index 0000000..fbb5a63
--- /dev/null
+++ b/src/prompt_toolkit/styles/style_transformation.py
@@ -0,0 +1,373 @@
+"""
+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 colorsys import hls_to_rgb, rgb_to_hls
+from typing import Callable, Hashable, Sequence
+
+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}"
diff --git a/src/prompt_toolkit/token.py b/src/prompt_toolkit/token.py
new file mode 100644
index 0000000..a2c80e5
--- /dev/null
+++ b/src/prompt_toolkit/token.py
@@ -0,0 +1,10 @@
+"""
+"""
+
+from __future__ import annotations
+
+__all__ = [
+ "ZeroWidthEscape",
+]
+
+ZeroWidthEscape = "[ZeroWidthEscape]"
diff --git a/src/prompt_toolkit/utils.py b/src/prompt_toolkit/utils.py
new file mode 100644
index 0000000..1a99a28
--- /dev/null
+++ b/src/prompt_toolkit/utils.py
@@ -0,0 +1,327 @@
+from __future__ import annotations
+
+import os
+import signal
+import sys
+import threading
+from collections import deque
+from typing import (
+ Callable,
+ ContextManager,
+ Dict,
+ Generator,
+ Generic,
+ TypeVar,
+ Union,
+)
+
+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(ContextManager[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 = Union[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"]
diff --git a/src/prompt_toolkit/validation.py b/src/prompt_toolkit/validation.py
new file mode 100644
index 0000000..127445e
--- /dev/null
+++ b/src/prompt_toolkit/validation.py
@@ -0,0 +1,195 @@
+"""
+Input validation for a `Buffer`.
+(Validators will be called before accepting input.)
+"""
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from typing 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 "{}(cursor_position={!r}, message={!r})".format(
+ self.__class__.__name__,
+ self.cursor_position,
+ self.message,
+ )
+
+
+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)
diff --git a/src/prompt_toolkit/widgets/__init__.py b/src/prompt_toolkit/widgets/__init__.py
new file mode 100644
index 0000000..9d1d4e3
--- /dev/null
+++ b/src/prompt_toolkit/widgets/__init__.py
@@ -0,0 +1,62 @@
+"""
+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",
+]
diff --git a/src/prompt_toolkit/widgets/base.py b/src/prompt_toolkit/widgets/base.py
new file mode 100644
index 0000000..f36a545
--- /dev/null
+++ b/src/prompt_toolkit/widgets/base.py
@@ -0,0 +1,981 @@
+"""
+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 functools import partial
+from typing import Callable, Generic, Sequence, 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, to_dimension
+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)
+ )
+ text = (f"{{:^{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),
+ ConditionalContainer(content=top_row_without_title, filter=~has_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:
+ if padding is None:
+ padding = D(preferred=0)
+
+ def get(value: AnyDimension) -> D:
+ if value is None:
+ value = padding
+ return to_dimension(value)
+
+ self.padding_left = get(padding_left)
+ self.padding_right = get(padding_right)
+ self.padding_top = get(padding_top)
+ self.padding_bottom = get(padding_bottom)
+ self.body = body
+
+ self.container = HSplit(
+ [
+ Window(height=self.padding_top, char=char),
+ VSplit(
+ [
+ Window(width=self.padding_left, char=char),
+ body,
+ Window(width=self.padding_right, char=char),
+ ]
+ ),
+ Window(height=self.padding_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`.
+ """
+
+ open_character: str = ""
+ close_character: str = ""
+ container_style: str = ""
+ default_style: str = ""
+ selected_style: str = ""
+ checked_style: str = ""
+ multiple_selection: bool = False
+ show_scrollbar: bool = True
+
+ def __init__(
+ self,
+ values: Sequence[tuple[_T, AnyFormattedText]],
+ default_values: Sequence[_T] | None = None,
+ ) -> None:
+ assert len(values) > 0
+ default_values = default_values or []
+
+ self.values = values
+ # 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")
+ def _up(event: E) -> None:
+ self._selected_index = max(0, self._selected_index - 1)
+
+ @kb.add("down")
+ def _down(event: E) -> None:
+ self._selected_index = min(len(self.values) - 1, self._selected_index + 1)
+
+ @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
+
+ # Control and window.
+ self.control = FormattedTextControl(
+ self._get_text_fragments, key_bindings=kb, focusable=True
+ )
+
+ 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, "*"))
+ else:
+ result.append((style, " "))
+
+ result.append((style, self.close_character))
+ result.append((self.default_style, " "))
+ result.extend(to_formatted_text(value[1], 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.
+ """
+
+ open_character = "("
+ close_character = ")"
+ container_style = "class:radio-list"
+ default_style = "class:radio"
+ selected_style = "class:radio-selected"
+ checked_style = "class:radio-checked"
+ multiple_selection = False
+
+ def __init__(
+ self,
+ values: Sequence[tuple[_T, AnyFormattedText]],
+ default: _T | None = None,
+ ) -> None:
+ if default is None:
+ default_values = None
+ else:
+ default_values = [default]
+
+ super().__init__(values, default_values=default_values)
+
+
+class CheckboxList(_DialogList[_T]):
+ """
+ List of checkbox buttons. Several can be checked at the same time.
+
+ :param values: List of (value, label) tuples.
+ """
+
+ open_character = "["
+ close_character = "]"
+ container_style = "class:checkbox-list"
+ default_style = "class:checkbox"
+ selected_style = "class:checkbox-selected"
+ checked_style = "class:checkbox-checked"
+ 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
diff --git a/src/prompt_toolkit/widgets/dialogs.py b/src/prompt_toolkit/widgets/dialogs.py
new file mode 100644
index 0000000..c47c15b
--- /dev/null
+++ b/src/prompt_toolkit/widgets/dialogs.py
@@ -0,0 +1,107 @@
+"""
+Collection of reusable components for building full screen applications.
+"""
+from __future__ import annotations
+
+from typing 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
diff --git a/src/prompt_toolkit/widgets/menus.py b/src/prompt_toolkit/widgets/menus.py
new file mode 100644
index 0000000..c574c06
--- /dev/null
+++ b/src/prompt_toolkit/widgets/menus.py
@@ -0,0 +1,374 @@
+from __future__ import annotations
+
+from typing 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
diff --git a/src/prompt_toolkit/widgets/toolbars.py b/src/prompt_toolkit/widgets/toolbars.py
new file mode 100644
index 0000000..deddf15
--- /dev/null
+++ b/src/prompt_toolkit/widgets/toolbars.py
@@ -0,0 +1,374 @@
+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 = "{} (line={} column={})".format(
+ buff.validation_error.message,
+ row + 1,
+ 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
diff --git a/src/prompt_toolkit/win32_types.py b/src/prompt_toolkit/win32_types.py
new file mode 100644
index 0000000..79283b8
--- /dev/null
+++ b/src/prompt_toolkit/win32_types.py
@@ -0,0 +1,229 @@
+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),
+ ]
diff --git a/tests/test_async_generator.py b/tests/test_async_generator.py
new file mode 100644
index 0000000..8c95f8c
--- /dev/null
+++ b/tests/test_async_generator.py
@@ -0,0 +1,28 @@
+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]
diff --git a/tests/test_buffer.py b/tests/test_buffer.py
new file mode 100644
index 0000000..e636137
--- /dev/null
+++ b/tests/test_buffer.py
@@ -0,0 +1,112 @@
+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"
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..3a16e9f
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,941 @@
+"""
+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" "\x0f" "3h" "x\r") # c-o # 3 times to the left.
+ assert result.text == "axbcde"
+ assert result.cursor_position == 2
+
+ result, cli = feed("abcde" "\x0f" "b" "x\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"
diff --git a/tests/test_completion.py b/tests/test_completion.py
new file mode 100644
index 0000000..8b3541a
--- /dev/null
+++ b/tests/test_completion.py
@@ -0,0 +1,469 @@
+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
diff --git a/tests/test_document.py b/tests/test_document.py
new file mode 100644
index 0000000..d052d53
--- /dev/null
+++ b/tests/test_document.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+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
diff --git a/tests/test_filter.py b/tests/test_filter.py
new file mode 100644
index 0000000..f7184c2
--- /dev/null
+++ b/tests/test_filter.py
@@ -0,0 +1,131 @@
+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
diff --git a/tests/test_formatted_text.py b/tests/test_formatted_text.py
new file mode 100644
index 0000000..843aac1
--- /dev/null
+++ b/tests/test_formatted_text.py
@@ -0,0 +1,286 @@
+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_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", "")],
+ ]
+
+ # -3-
+ lines = list(split_lines([("class:a", "")]))
+
+ assert lines == [
+ [("class:a", "")],
+ ]
diff --git a/tests/test_history.py b/tests/test_history.py
new file mode 100644
index 0000000..500b7f1
--- /dev/null
+++ b/tests/test_history.py
@@ -0,0 +1,103 @@
+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"]
diff --git a/tests/test_inputstream.py b/tests/test_inputstream.py
new file mode 100644
index 0000000..ab1b036
--- /dev/null
+++ b/tests/test_inputstream.py
@@ -0,0 +1,141 @@
+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
diff --git a/tests/test_key_binding.py b/tests/test_key_binding.py
new file mode 100644
index 0000000..1c60880
--- /dev/null
+++ b/tests/test_key_binding.py
@@ -0,0 +1,200 @@
+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,
+ )
+
+ # 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 = lambda *_, **kw: None
+
+ 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"
diff --git a/tests/test_layout.py b/tests/test_layout.py
new file mode 100644
index 0000000..cbbbcd0
--- /dev/null
+++ b/tests/test_layout.py
@@ -0,0 +1,53 @@
+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([]))
diff --git a/tests/test_memory_leaks.py b/tests/test_memory_leaks.py
new file mode 100644
index 0000000..31ea7c8
--- /dev/null
+++ b/tests/test_memory_leaks.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+import gc
+
+import pytest
+
+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)])
+
+
+# Fails in GitHub CI, probably due to GC differences.
+@pytest.mark.xfail(reason="Memory leak testing fails in GitHub CI.")
+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
diff --git a/tests/test_print_formatted_text.py b/tests/test_print_formatted_text.py
new file mode 100644
index 0000000..26c7265
--- /dev/null
+++ b/tests/test_print_formatted_text.py
@@ -0,0 +1,92 @@
+"""
+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"
+ )
diff --git a/tests/test_regular_languages.py b/tests/test_regular_languages.py
new file mode 100644
index 0000000..deef6b8
--- /dev/null
+++ b/tests/test_regular_languages.py
@@ -0,0 +1,102 @@
+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("before-%s-after" % document.text, -len(document.text))
+ yield Completion("before-%s-after-B" % document.text, -len(document.text))
+
+ class completer2(Completer):
+ def get_completions(self, document, complete_event):
+ yield Completion("before2-%s-after2" % document.text, -len(document.text))
+ yield Completion("before2-%s-after2-B" % document.text, -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
diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py
new file mode 100644
index 0000000..287c6d3
--- /dev/null
+++ b/tests/test_shortcuts.py
@@ -0,0 +1,68 @@
+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
diff --git a/tests/test_style.py b/tests/test_style.py
new file mode 100644
index 0000000..d0a4790
--- /dev/null
+++ b/tests/test_style.py
@@ -0,0 +1,276 @@
+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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ 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,
+ )
+ after = Attrs(
+ color="ffbbbb",
+ bgcolor="bbbb76",
+ bold=True,
+ underline=True,
+ strike=True,
+ italic=True,
+ blink=False,
+ reverse=False,
+ hidden=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,
+ )
+ after = Attrs(
+ color="ansibrightred",
+ bgcolor="ansiwhite",
+ bold=True,
+ underline=True,
+ strike=True,
+ italic=True,
+ blink=False,
+ reverse=False,
+ hidden=False,
+ )
+
+ assert transformation.transform_attrs(before) == after
diff --git a/tests/test_style_transformation.py b/tests/test_style_transformation.py
new file mode 100644
index 0000000..e4eee7c
--- /dev/null
+++ b/tests/test_style_transformation.py
@@ -0,0 +1,51 @@
+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,
+ )
+
+
+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"
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..9d4c808
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,78 @@
+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)
diff --git a/tests/test_vt100_output.py b/tests/test_vt100_output.py
new file mode 100644
index 0000000..65c8377
--- /dev/null
+++ b/tests/test_vt100_output.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+from prompt_toolkit.output.vt100 import _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"
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
new file mode 100644
index 0000000..ee7745a
--- /dev/null
+++ b/tests/test_widgets.py
@@ -0,0 +1,20 @@
+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 ]"
diff --git a/tests/test_yank_nth_arg.py b/tests/test_yank_nth_arg.py
new file mode 100644
index 0000000..7167a26
--- /dev/null
+++ b/tests/test_yank_nth_arg.py
@@ -0,0 +1,86 @@
+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"
diff --git a/tools/debug_input_cross_platform.py b/tools/debug_input_cross_platform.py
new file mode 100755
index 0000000..55f6190
--- /dev/null
+++ b/tools/debug_input_cross_platform.py
@@ -0,0 +1,31 @@
+#!/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():
+ 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())
diff --git a/tools/debug_vt100_input.py b/tools/debug_vt100_input.py
new file mode 100755
index 0000000..d3660b9
--- /dev/null
+++ b/tools/debug_vt100_input.py
@@ -0,0 +1,32 @@
+#!/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.keys import Keys
+
+
+def callback(key_press):
+ print(key_press)
+
+ if key_press.key == Keys.ControlC:
+ sys.exit(0)
+
+
+def main():
+ stream = Vt100Parser(callback)
+
+ with raw_mode(sys.stdin.fileno()):
+ while True:
+ c = sys.stdin.read(1)
+ stream.feed(c)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..310f076
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,13 @@
+# 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