From a7c8e630faca6cdef1a50125ee9c847e8a4391bc Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sun, 17 Sep 2023 20:26:11 -0500 Subject: [PATCH] Complete rewrite and removed legacy bash version --- old-bash-version/LICENSE | 340 --- old-bash-version/README.md | 30 - old-bash-version/images/pic1.png | Bin 217739 -> 0 bytes old-bash-version/images/pic2.png | Bin 161857 -> 0 bytes old-bash-version/install.sh | 20 - old-bash-version/shellMen | 197 -- src/__builtins__.py | 37 +- src/__init__.py | 23 +- src/__main__.py | 35 +- src/app.py | 48 + src/core/__init__.py | 3 + src/core/controller.py | 35 + src/core/controller_data.py | 51 + src/core/mixins/__init__.py | 3 + src/core/mixins/processor_mixin.py | 35 + src/core/widgets/__init__.py | 3 + src/core/widgets/desktop_files.py | 146 ++ src/core/widgets/menu.py | 134 ++ src/core/window.py | 51 + src/libs/PyInquirer/__init__.py | 29 + src/libs/PyInquirer/color_print.py | 41 + src/libs/PyInquirer/prompt.py | 98 + src/libs/PyInquirer/prompts/__init__.py | 0 src/libs/PyInquirer/prompts/checkbox.py | 233 ++ src/libs/PyInquirer/prompts/common.py | 92 + src/libs/PyInquirer/prompts/confirm.py | 82 + src/libs/PyInquirer/prompts/editor.py | 197 ++ src/libs/PyInquirer/prompts/expand.py | 195 ++ src/libs/PyInquirer/prompts/input.py | 51 + src/libs/PyInquirer/prompts/list.py | 184 ++ src/libs/PyInquirer/prompts/password.py | 14 + src/libs/PyInquirer/prompts/rawlist.py | 166 ++ src/libs/PyInquirer/separator.py | 15 + src/libs/PyInquirer/utils.py | 35 + src/libs/prompt_toolkit/__init__.py | 22 + src/libs/prompt_toolkit/application.py | 192 ++ src/libs/prompt_toolkit/auto_suggest.py | 88 + src/libs/prompt_toolkit/buffer.py | 1415 ++++++++++++ src/libs/prompt_toolkit/buffer_mapping.py | 92 + src/libs/prompt_toolkit/cache.py | 111 + src/libs/prompt_toolkit/clipboard/__init__.py | 8 + src/libs/prompt_toolkit/clipboard/base.py | 62 + .../prompt_toolkit/clipboard/in_memory.py | 42 + .../prompt_toolkit/clipboard/pyperclip.py | 39 + src/libs/prompt_toolkit/completion.py | 170 ++ src/libs/prompt_toolkit/contrib/__init__.py | 0 .../contrib/completers/__init__.py | 5 + .../prompt_toolkit/contrib/completers/base.py | 61 + .../contrib/completers/filesystem.py | 105 + .../contrib/completers/system.py | 56 + .../contrib/regular_languages/__init__.py | 76 + .../contrib/regular_languages/compiler.py | 408 ++++ .../contrib/regular_languages/completion.py | 84 + .../contrib/regular_languages/lexer.py | 90 + .../contrib/regular_languages/regex_parser.py | 262 +++ .../contrib/regular_languages/validation.py | 57 + .../prompt_toolkit/contrib/telnet/__init__.py | 2 + .../contrib/telnet/application.py | 32 + src/libs/prompt_toolkit/contrib/telnet/log.py | 11 + .../prompt_toolkit/contrib/telnet/protocol.py | 181 ++ .../prompt_toolkit/contrib/telnet/server.py | 407 ++++ .../contrib/validators/__init__.py | 0 .../prompt_toolkit/contrib/validators/base.py | 34 + src/libs/prompt_toolkit/document.py | 1001 +++++++++ src/libs/prompt_toolkit/enums.py | 29 + src/libs/prompt_toolkit/eventloop/__init__.py | 0 .../prompt_toolkit/eventloop/asyncio_base.py | 46 + .../prompt_toolkit/eventloop/asyncio_posix.py | 113 + .../prompt_toolkit/eventloop/asyncio_win32.py | 83 + src/libs/prompt_toolkit/eventloop/base.py | 85 + .../prompt_toolkit/eventloop/callbacks.py | 29 + .../prompt_toolkit/eventloop/inputhook.py | 107 + src/libs/prompt_toolkit/eventloop/posix.py | 311 +++ .../prompt_toolkit/eventloop/posix_utils.py | 82 + src/libs/prompt_toolkit/eventloop/select.py | 216 ++ src/libs/prompt_toolkit/eventloop/utils.py | 23 + src/libs/prompt_toolkit/eventloop/win32.py | 187 ++ src/libs/prompt_toolkit/filters/__init__.py | 36 + src/libs/prompt_toolkit/filters/base.py | 234 ++ src/libs/prompt_toolkit/filters/cli.py | 395 ++++ src/libs/prompt_toolkit/filters/types.py | 55 + src/libs/prompt_toolkit/filters/utils.py | 39 + src/libs/prompt_toolkit/history.py | 120 ++ src/libs/prompt_toolkit/input.py | 135 ++ src/libs/prompt_toolkit/interface.py | 1185 ++++++++++ .../prompt_toolkit/key_binding/__init__.py | 1 + .../key_binding/bindings/__init__.py | 0 .../key_binding/bindings/basic.py | 407 ++++ .../key_binding/bindings/completion.py | 161 ++ .../key_binding/bindings/emacs.py | 451 ++++ .../key_binding/bindings/named_commands.py | 578 +++++ .../key_binding/bindings/scroll.py | 185 ++ .../key_binding/bindings/utils.py | 25 + .../prompt_toolkit/key_binding/bindings/vi.py | 1903 +++++++++++++++++ .../prompt_toolkit/key_binding/defaults.py | 119 ++ .../prompt_toolkit/key_binding/digraphs.py | 1378 ++++++++++++ .../key_binding/input_processor.py | 372 ++++ .../prompt_toolkit/key_binding/manager.py | 96 + .../prompt_toolkit/key_binding/registry.py | 350 +++ .../prompt_toolkit/key_binding/vi_state.py | 61 + src/libs/prompt_toolkit/keys.py | 129 ++ src/libs/prompt_toolkit/layout/__init__.py | 51 + src/libs/prompt_toolkit/layout/containers.py | 1665 ++++++++++++++ src/libs/prompt_toolkit/layout/controls.py | 730 +++++++ src/libs/prompt_toolkit/layout/dimension.py | 92 + src/libs/prompt_toolkit/layout/lexers.py | 320 +++ src/libs/prompt_toolkit/layout/margins.py | 253 +++ src/libs/prompt_toolkit/layout/menus.py | 496 +++++ .../prompt_toolkit/layout/mouse_handlers.py | 29 + src/libs/prompt_toolkit/layout/processors.py | 605 ++++++ src/libs/prompt_toolkit/layout/prompt.py | 111 + src/libs/prompt_toolkit/layout/screen.py | 151 ++ src/libs/prompt_toolkit/layout/toolbars.py | 209 ++ src/libs/prompt_toolkit/layout/utils.py | 181 ++ src/libs/prompt_toolkit/mouse_events.py | 48 + src/libs/prompt_toolkit/output.py | 192 ++ src/libs/prompt_toolkit/reactive.py | 56 + src/libs/prompt_toolkit/renderer.py | 526 +++++ src/libs/prompt_toolkit/search_state.py | 36 + src/libs/prompt_toolkit/selection.py | 47 + src/libs/prompt_toolkit/shortcuts.py | 717 +++++++ src/libs/prompt_toolkit/styles/__init__.py | 21 + src/libs/prompt_toolkit/styles/base.py | 86 + src/libs/prompt_toolkit/styles/defaults.py | 95 + src/libs/prompt_toolkit/styles/from_dict.py | 148 ++ .../prompt_toolkit/styles/from_pygments.py | 77 + src/libs/prompt_toolkit/styles/utils.py | 45 + src/libs/prompt_toolkit/terminal/__init__.py | 0 .../prompt_toolkit/terminal/conemu_output.py | 42 + .../prompt_toolkit/terminal/vt100_input.py | 520 +++++ .../prompt_toolkit/terminal/vt100_output.py | 632 ++++++ .../prompt_toolkit/terminal/win32_input.py | 364 ++++ .../prompt_toolkit/terminal/win32_output.py | 556 +++++ src/libs/prompt_toolkit/token.py | 47 + src/libs/prompt_toolkit/utils.py | 240 +++ src/libs/prompt_toolkit/validation.py | 64 + src/libs/prompt_toolkit/win32_types.py | 155 ++ src/shellmen | 13 - src/signal_classes/Controller.py | 185 -- src/signal_classes/Controller_Data.py | 32 - src/signal_classes/Menu.py | 88 - src/signal_classes/__init__.py | 7 - src/signal_classes/mixins/ProcessorMixin.py | 36 - src/signal_classes/mixins/__init__.py | 5 - src/utils/Logger.py | 56 - src/utils/Settings.py | 51 - src/utils/__init__.py | 3 - src/utils/debugging.py | 52 + src/utils/event_system.py | 54 + src/utils/ipc_server.py | 114 + src/utils/logger.py | 61 + src/utils/settings_manager/__init__.py | 4 + src/utils/settings_manager/manager.py | 74 + .../settings_manager/options/__init__.py | 8 + src/utils/settings_manager/options/config.py | 31 + .../settings_manager/options/debugging.py | 12 + .../settings_manager/options/favorites.py | 11 + src/utils/settings_manager/options/filters.py | 36 + .../settings_manager/options/settings.py | 33 + src/utils/settings_manager/options/theming.py | 13 + .../settings_manager/start_check_mixin.py | 51 + src/utils/singleton.py | 24 + .../mixins/StylesMixin.py => utils/styles.py} | 10 +- 163 files changed, 27028 insertions(+), 1105 deletions(-) delete mode 100644 old-bash-version/LICENSE delete mode 100644 old-bash-version/README.md delete mode 100644 old-bash-version/images/pic1.png delete mode 100644 old-bash-version/images/pic2.png delete mode 100755 old-bash-version/install.sh delete mode 100755 old-bash-version/shellMen create mode 100644 src/app.py create mode 100644 src/core/__init__.py create mode 100644 src/core/controller.py create mode 100644 src/core/controller_data.py create mode 100644 src/core/mixins/__init__.py create mode 100644 src/core/mixins/processor_mixin.py create mode 100644 src/core/widgets/__init__.py create mode 100644 src/core/widgets/desktop_files.py create mode 100644 src/core/widgets/menu.py create mode 100644 src/core/window.py create mode 100644 src/libs/PyInquirer/__init__.py create mode 100644 src/libs/PyInquirer/color_print.py create mode 100644 src/libs/PyInquirer/prompt.py create mode 100644 src/libs/PyInquirer/prompts/__init__.py create mode 100644 src/libs/PyInquirer/prompts/checkbox.py create mode 100644 src/libs/PyInquirer/prompts/common.py create mode 100644 src/libs/PyInquirer/prompts/confirm.py create mode 100644 src/libs/PyInquirer/prompts/editor.py create mode 100644 src/libs/PyInquirer/prompts/expand.py create mode 100644 src/libs/PyInquirer/prompts/input.py create mode 100644 src/libs/PyInquirer/prompts/list.py create mode 100644 src/libs/PyInquirer/prompts/password.py create mode 100644 src/libs/PyInquirer/prompts/rawlist.py create mode 100644 src/libs/PyInquirer/separator.py create mode 100644 src/libs/PyInquirer/utils.py create mode 100644 src/libs/prompt_toolkit/__init__.py create mode 100644 src/libs/prompt_toolkit/application.py create mode 100644 src/libs/prompt_toolkit/auto_suggest.py create mode 100644 src/libs/prompt_toolkit/buffer.py create mode 100644 src/libs/prompt_toolkit/buffer_mapping.py create mode 100644 src/libs/prompt_toolkit/cache.py create mode 100644 src/libs/prompt_toolkit/clipboard/__init__.py create mode 100644 src/libs/prompt_toolkit/clipboard/base.py create mode 100644 src/libs/prompt_toolkit/clipboard/in_memory.py create mode 100644 src/libs/prompt_toolkit/clipboard/pyperclip.py create mode 100644 src/libs/prompt_toolkit/completion.py create mode 100644 src/libs/prompt_toolkit/contrib/__init__.py create mode 100644 src/libs/prompt_toolkit/contrib/completers/__init__.py create mode 100644 src/libs/prompt_toolkit/contrib/completers/base.py create mode 100644 src/libs/prompt_toolkit/contrib/completers/filesystem.py create mode 100644 src/libs/prompt_toolkit/contrib/completers/system.py create mode 100644 src/libs/prompt_toolkit/contrib/regular_languages/__init__.py create mode 100644 src/libs/prompt_toolkit/contrib/regular_languages/compiler.py create mode 100644 src/libs/prompt_toolkit/contrib/regular_languages/completion.py create mode 100644 src/libs/prompt_toolkit/contrib/regular_languages/lexer.py create mode 100644 src/libs/prompt_toolkit/contrib/regular_languages/regex_parser.py create mode 100644 src/libs/prompt_toolkit/contrib/regular_languages/validation.py create mode 100644 src/libs/prompt_toolkit/contrib/telnet/__init__.py create mode 100644 src/libs/prompt_toolkit/contrib/telnet/application.py create mode 100644 src/libs/prompt_toolkit/contrib/telnet/log.py create mode 100644 src/libs/prompt_toolkit/contrib/telnet/protocol.py create mode 100644 src/libs/prompt_toolkit/contrib/telnet/server.py create mode 100644 src/libs/prompt_toolkit/contrib/validators/__init__.py create mode 100644 src/libs/prompt_toolkit/contrib/validators/base.py create mode 100644 src/libs/prompt_toolkit/document.py create mode 100644 src/libs/prompt_toolkit/enums.py create mode 100644 src/libs/prompt_toolkit/eventloop/__init__.py create mode 100644 src/libs/prompt_toolkit/eventloop/asyncio_base.py create mode 100644 src/libs/prompt_toolkit/eventloop/asyncio_posix.py create mode 100644 src/libs/prompt_toolkit/eventloop/asyncio_win32.py create mode 100644 src/libs/prompt_toolkit/eventloop/base.py create mode 100644 src/libs/prompt_toolkit/eventloop/callbacks.py create mode 100644 src/libs/prompt_toolkit/eventloop/inputhook.py create mode 100644 src/libs/prompt_toolkit/eventloop/posix.py create mode 100644 src/libs/prompt_toolkit/eventloop/posix_utils.py create mode 100644 src/libs/prompt_toolkit/eventloop/select.py create mode 100644 src/libs/prompt_toolkit/eventloop/utils.py create mode 100644 src/libs/prompt_toolkit/eventloop/win32.py create mode 100644 src/libs/prompt_toolkit/filters/__init__.py create mode 100644 src/libs/prompt_toolkit/filters/base.py create mode 100644 src/libs/prompt_toolkit/filters/cli.py create mode 100644 src/libs/prompt_toolkit/filters/types.py create mode 100644 src/libs/prompt_toolkit/filters/utils.py create mode 100644 src/libs/prompt_toolkit/history.py create mode 100644 src/libs/prompt_toolkit/input.py create mode 100644 src/libs/prompt_toolkit/interface.py create mode 100644 src/libs/prompt_toolkit/key_binding/__init__.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/__init__.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/basic.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/completion.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/emacs.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/named_commands.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/scroll.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/utils.py create mode 100644 src/libs/prompt_toolkit/key_binding/bindings/vi.py create mode 100644 src/libs/prompt_toolkit/key_binding/defaults.py create mode 100644 src/libs/prompt_toolkit/key_binding/digraphs.py create mode 100644 src/libs/prompt_toolkit/key_binding/input_processor.py create mode 100644 src/libs/prompt_toolkit/key_binding/manager.py create mode 100644 src/libs/prompt_toolkit/key_binding/registry.py create mode 100644 src/libs/prompt_toolkit/key_binding/vi_state.py create mode 100644 src/libs/prompt_toolkit/keys.py create mode 100644 src/libs/prompt_toolkit/layout/__init__.py create mode 100644 src/libs/prompt_toolkit/layout/containers.py create mode 100644 src/libs/prompt_toolkit/layout/controls.py create mode 100644 src/libs/prompt_toolkit/layout/dimension.py create mode 100644 src/libs/prompt_toolkit/layout/lexers.py create mode 100644 src/libs/prompt_toolkit/layout/margins.py create mode 100644 src/libs/prompt_toolkit/layout/menus.py create mode 100644 src/libs/prompt_toolkit/layout/mouse_handlers.py create mode 100644 src/libs/prompt_toolkit/layout/processors.py create mode 100644 src/libs/prompt_toolkit/layout/prompt.py create mode 100644 src/libs/prompt_toolkit/layout/screen.py create mode 100644 src/libs/prompt_toolkit/layout/toolbars.py create mode 100644 src/libs/prompt_toolkit/layout/utils.py create mode 100644 src/libs/prompt_toolkit/mouse_events.py create mode 100644 src/libs/prompt_toolkit/output.py create mode 100644 src/libs/prompt_toolkit/reactive.py create mode 100644 src/libs/prompt_toolkit/renderer.py create mode 100644 src/libs/prompt_toolkit/search_state.py create mode 100644 src/libs/prompt_toolkit/selection.py create mode 100644 src/libs/prompt_toolkit/shortcuts.py create mode 100644 src/libs/prompt_toolkit/styles/__init__.py create mode 100644 src/libs/prompt_toolkit/styles/base.py create mode 100644 src/libs/prompt_toolkit/styles/defaults.py create mode 100644 src/libs/prompt_toolkit/styles/from_dict.py create mode 100644 src/libs/prompt_toolkit/styles/from_pygments.py create mode 100644 src/libs/prompt_toolkit/styles/utils.py create mode 100644 src/libs/prompt_toolkit/terminal/__init__.py create mode 100644 src/libs/prompt_toolkit/terminal/conemu_output.py create mode 100644 src/libs/prompt_toolkit/terminal/vt100_input.py create mode 100644 src/libs/prompt_toolkit/terminal/vt100_output.py create mode 100644 src/libs/prompt_toolkit/terminal/win32_input.py create mode 100644 src/libs/prompt_toolkit/terminal/win32_output.py create mode 100644 src/libs/prompt_toolkit/token.py create mode 100644 src/libs/prompt_toolkit/utils.py create mode 100644 src/libs/prompt_toolkit/validation.py create mode 100644 src/libs/prompt_toolkit/win32_types.py delete mode 100755 src/shellmen delete mode 100644 src/signal_classes/Controller.py delete mode 100644 src/signal_classes/Controller_Data.py delete mode 100644 src/signal_classes/Menu.py delete mode 100644 src/signal_classes/__init__.py delete mode 100644 src/signal_classes/mixins/ProcessorMixin.py delete mode 100644 src/signal_classes/mixins/__init__.py delete mode 100644 src/utils/Logger.py delete mode 100644 src/utils/Settings.py create mode 100644 src/utils/debugging.py create mode 100644 src/utils/event_system.py create mode 100644 src/utils/ipc_server.py create mode 100644 src/utils/logger.py create mode 100644 src/utils/settings_manager/__init__.py create mode 100644 src/utils/settings_manager/manager.py create mode 100644 src/utils/settings_manager/options/__init__.py create mode 100644 src/utils/settings_manager/options/config.py create mode 100644 src/utils/settings_manager/options/debugging.py create mode 100644 src/utils/settings_manager/options/favorites.py create mode 100644 src/utils/settings_manager/options/filters.py create mode 100644 src/utils/settings_manager/options/settings.py create mode 100644 src/utils/settings_manager/options/theming.py create mode 100644 src/utils/settings_manager/start_check_mixin.py create mode 100644 src/utils/singleton.py rename src/{signal_classes/mixins/StylesMixin.py => utils/styles.py} (90%) diff --git a/old-bash-version/LICENSE b/old-bash-version/LICENSE deleted file mode 100644 index 8cdb845..0000000 --- a/old-bash-version/LICENSE +++ /dev/null @@ -1,340 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. - diff --git a/old-bash-version/README.md b/old-bash-version/README.md deleted file mode 100644 index 1044e71..0000000 --- a/old-bash-version/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Shellmen -Shellmen is short for ShellMenu and is intended to be a functional menu for terminals. Rather than needing a full GUI menu for your programs you can view and launch your programs through Shellmen. This is great for systems that don't have panel menus or for systems that have a poorly organized right-click menu. - -# NOTE -This is the old and depricated bash version. It never really functioned to uts full potential. Please use the Python 3 version up a directory... - -# To Install -To install automatically please run the install.sh file and select option 2. -You will need to make it exacutable. -
-To Install manually: -
-sudo cp shellMen.sh /bin/
-sudo chown root:root /bin/shellMen
-sudo chmod 744 /bin/shellMen
-
-# To Uninstall -To uninstall automatically please run the install.sh file and select option 2. -
-To Uninstall manually: -
-sudo rm /bin/shellMen
-
-# License -You should have received a copy of the GNU General Public License along with this program. -
If not, see . - -# Images -![1 Root Menu View](images/pic1.png) -![2 Sub Menu View](images/pic2.png) diff --git a/old-bash-version/images/pic1.png b/old-bash-version/images/pic1.png deleted file mode 100644 index 0e56611a97c0c62d714593620f01c2ab0f07f357..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217739 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 )+4}g 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π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
&%pO(XGzvCbw$d3Go){$ExwPe;jxS97=Ab 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 : " INPUT - if [[ "$INPUT" == 1 ]]; then - sudo cp ./shellMen /bin/ - sudo chown root:root /bin/shellMen - sudo chmod +x /bin/shellMen - elif [[ "$INPUT" == 2 ]]; then - sudo rm /bin/shellMen - elif [[ "$INPUT" != 1 || "$INPUT" != 2 ]] ; then - echo "Please type 1 or 2." - sleep 2 - main - fi -} -main diff --git a/old-bash-version/shellMen b/old-bash-version/shellMen deleted file mode 100755 index b579d21..0000000 --- a/old-bash-version/shellMen +++ /dev/null @@ -1,197 +0,0 @@ -#!/bin/bash -# -# By Maxim F. Stewart -# Contact: [1itdominator@gmail.com] -# -# Copyright 2013 Maxim F. Stewart -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -#---------------------------------------------------------------------------------------# - -declare -a menu=("Accessories" "Utility" "Multimedia" "Video" "Audio" - "Development" "Game" "Internet" "Network" "Graphics" - "Office" "System" "Settings" "Wine") - -mainMENU() { -exec 3>&1; # Custom stream to set variable from dialog - -INPUT=$(dialog --clear --backtitle "Shellmen" \ ---title "[ M A I N - M E N U ]" \ ---menu "Please Select An Option" 16 50 15 \ -Accessories "General Programs" \ -System "Main System Programs" \ -Settings "Main System Settings" \ -Multimedia "Audio & Video Programs" \ -Graphics "Image Programs" \ -Games "Gaming Programs" \ -Office "Wordprocess & Documents Programs" \ -Development "Programing Programs" \ -Internet "Various Internet Related Programs" \ -Wine "Windows Exe & Program Support" 2>&1 1>&3 ) - -case $INPUT in - Accessories) bash /tmp/sysMENU/Accessories.sh ;; - System) bash /tmp/sysMENU/System.sh ;; - Settings) bash /tmp/sysMENU/Settings.sh ;; - Multimedia) bash /tmp/sysMENU/Multimedia.sh ;; - Graphics) bash /tmp/sysMENU/Graphics.sh ;; - Games) bash /tmp/sysMENU/Game.sh ;; - Office) bash /tmp/sysMENU/Office.sh ;; - Development) bash /tmp/sysMENU/Development.sh ;; - Internet) bash /tmp/sysMENU/Internet.sh ;; - Wine) bash /tmp/sysMENU/Wine.sh ;; - Exit) echo "Bye!"; break ;; -esac -} - -commandInsert() { -x=$(cat /tmp/sysMENU/menu.list | wc -l) >> /dev/null ; -i="1" - -while [ $i -le $x ]; do - line1=$(sed -n "${i}p" /tmp/sysMENU/menu.list); - filename="${line1%.*}" - execMethod=$(grep -A 0 "Exec=" /usr/share/applications/"$line1") - catagory=$(grep -A 0 "Categories=" /usr/share/applications/"$line1") - preComment=$(grep -A 0 "Comment=" /usr/share/applications/"$line1") - execCMD=$(echo "${filename}) exec ${filename} ;;") - - writeToPath "$catagory" "$execCMD" - -i=$[$i++1]; -done - -for opt in "${menu[@]}"; do - if [[ $opt == "${menu[8]}" ]]; then - opt=${menu[7]} - fi - - if [[ $opt == "${menu[3]}" || $opt == "${menu[4]}" ]]; then - opt=${menu[2]} - fi - echo "esac" >> /tmp/sysMENU/"${opt}".sh -done - -chmod +x /tmp/sysMENU/*.sh -mainMENU; -} - -menuHeaderInsert() { -x=$(cat /tmp/sysMENU/menu.list | wc -l) >> /dev/null ; # Variable set to number of lines filled in list.txt -i="1" - -while [ $i -le $x ]; do - line1=$(sed -n "${i}p" /tmp/sysMENU/menu.list); # Reads the number of lines in list.txt then sets as a variable counting up to variable x - filename="${line1%.*}" - execMethod=$(grep -A 0 "Exec=" /usr/share/applications/"$line1") - catagory=$(grep -A 0 "Categories=" /usr/share/applications/"$line1") - preComment=$(grep -A 0 "Comment=" /usr/share/applications/"$line1") - comment=$(sed s/"Comment="//g <<< ${preComment}) - inputer=$(echo "$filename "\"$comment"\" \\") - - writeToPath "$catagory" "$inputer" - -i=$[$i++1]; -done - -endMenuInsert=$(echo "2>"\"'${INPUT}'"\"") -menuitmVar=$(echo 'menuitem=$(<"${INPUT}")') -preCMD=$(echo "case \$menuitem in") -menuCall=$(echo "Main_Menu) bash /bin/shellMen ;;") - - -for opt in "${menu[@]}"; do - if [[ $opt == "${menu[8]}" ]]; then - opt=${menu[7]} - fi - - if [[ $opt == "${menu[3]}" || $opt == "${menu[4]}" ]]; then - opt=${menu[2]} - fi - - echo "$endMenuInsert" >> /tmp/sysMENU/"${opt}".sh - echo "$menuitmVar" >> /tmp/sysMENU/"${opt}".sh - echo "$preCMD" >> /tmp/sysMENU/"${opt}".sh - echo "$menuCall" >> /tmp/sysMENU/"${opt}".sh -done - -commandInsert; -} - -function writeToPath() { - catagory=$1 - inputer=$2 - - - if [[ "$catagory" == *"${menu[0]}"* ]] || [[ "$catagory" == *"${menu[1]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[0]}".sh - elif [[ "$catagory" == *"${menu[2]}*" ]] \ - || [[ "$catagory" == *"${menu[3]}"* ]] \ - || [[ "$catagory" == *"${menu[4]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[2]}".sh - elif [[ "$catagory" == *"${menu[5]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[5]}".sh - elif [[ "$catagory" == *"${menu[6]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[6]}".sh - elif [[ "$catagory" == *"${menu[7]}"* ]] || [[ "$catagory" == *"${menu[8]}"* ]] ; then - echo "$inputer" >> /tmp/sysMENU/"${menu[7]}".sh - elif [[ "$catagory" == *"${menu[9]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[9]}".sh - elif [[ "$catagory" == *"${menu[10]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[10]}".sh - elif [[ "$catagory" == *"${menu[11]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[11]}".sh - elif [[ "$catagory" == *"${menu[12]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[12]}".sh - elif [[ "$catagory" == *"${menu[13]}"* ]]; then - echo "$inputer" >> /tmp/sysMENU/"${menu[13]}".sh - fi -} - - -startScan() { -clear -mkdir /tmp/sysMENU -touch /tmp/sysMENU/menu.list ; -sed -i "d" /tmp/sysMENU/menu.list ; -ls -p /usr/share/applications/ | grep -v / >> /tmp/sysMENU/menu.list ; - -header='''#!/bin/bash -INPUT=/tmp/menu.txt -dialog --clear --backtitle "Shellmen" \ ---title "[ S U B - M E N U ]" \ ---menu "Please Select An Option" 15 50 10 \ -Main_Menu "Goes To Main Menu" \''' - -for opt in "${menu[@]}"; do - if [[ $opt == "${menu[8]}" ]]; then - opt=${menu[7]} - fi - - if [[ $opt == "${menu[3]}" || $opt == "${menu[4]}" ]]; then - opt=${menu[2]} - fi - echo "$header" > /tmp/sysMENU/"${opt}".sh -done - -menuHeaderInsert; -} - -pre() { - if [ -d /tmp/sysMENU/ ]; then - mainMENU; - else - startScan; - fi -} -pre; diff --git a/src/__builtins__.py b/src/__builtins__.py index 913686d..d2efe07 100644 --- a/src/__builtins__.py +++ b/src/__builtins__.py @@ -1,19 +1,46 @@ # Python imports import builtins +import threading # Lib imports # Application imports +from utils.event_system import EventSystem +from utils.logger import Logger +from utils.settings_manager.manager import SettingsManager -class Builtins: - def dummy(self): - pass +class BuiltinsException(Exception): + ... + + +# NOTE: Threads WILL NOT die with parent's destruction. +def threaded_wrapper(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start() + return wrapper + +# NOTE: Threads WILL die with parent's destruction. +def daemon_threaded_wrapper(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() + return wrapper # NOTE: Just reminding myself we can add to builtins two different ways... # __builtins__.update({"event_system": Builtins()}) builtins.app_name = "Shellmen" -builtins.debug = False -builtins.trace_debug = False +builtins.event_system = EventSystem() +builtins.settings_manager = SettingsManager() + +settings_manager.load_settings() + +builtins.settings = settings_manager.settings +builtins.logger = Logger(settings_manager.get_home_config_path(), \ + _ch_log_lvl=settings.debugging.ch_log_lvl, \ + _fh_log_lvl=settings.debugging.fh_log_lvl).get_logger() + +builtins.threaded = threaded_wrapper +builtins.daemon_threaded = daemon_threaded_wrapper +builtins.event_sleep_time = 0.05 diff --git a/src/__init__.py b/src/__init__.py index 209ef30..90dc8da 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,20 +1,3 @@ -# Python imports -import os, inspect, time - -# Lib imports - -# Application imports -from utils import Settings -from signal_classes import Controller -from __builtins__ import Builtins - - - - -class Main(Builtins): - def __init__(self, args, unknownargs): - settings = Settings() - controller = Controller(settings, args, unknownargs) - - if not controller: - raise Exception("Controller exited and doesn't exist...") +""" + Start of package. +""" diff --git a/src/__main__.py b/src/__main__.py index b2e005f..ace645c 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,36 +1,43 @@ #!/usr/bin/python3 - # Python imports -import argparse, faulthandler, traceback +import argparse +import faulthandler +import traceback from setproctitle import setproctitle -import tracemalloc -tracemalloc.start() - - # Lib imports - # Application imports -from __init__ import Main +from __builtins__ import * +from app import Application + + if __name__ == "__main__": - try: - # import web_pdb - # web_pdb.set_trace() + ''' Set process title, get arguments, and create GTK main thread. ''' - setproctitle('Shellmen') + try: + setproctitle(f'{app_name}') faulthandler.enable() # For better debug info + parser = argparse.ArgumentParser() # Add long and short arguments - parser.add_argument("--theme", "-t", default="default", help="The theme to use for the menu. (default, orange, red, purple, green)") + parser.add_argument("--theme", "-t", default="default", help="Set the theme. Options [orange, red, purple, green].") + parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.") + parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.") # Read arguments (If any...) args, unknownargs = parser.parse_known_args() - Main(args, unknownargs) + if args.debug == "true": + settings_manager.set_debug(True) + + if args.trace_debug == "true": + settings_manager.set_trace_debug(True) + + Application(args, unknownargs) except Exception as e: traceback.print_exc() quit() diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..9325654 --- /dev/null +++ b/src/app.py @@ -0,0 +1,48 @@ +# Python imports +import signal +import os + +# Lib imports + +# Application imports +from utils.debugging import debug_signal_handler +from utils.ipc_server import IPCServer +from core.window import Window + + + +class AppLaunchException(Exception): + ... + + +class Application(IPCServer): + """ docstring for Application. """ + + def __init__(self, args, unknownargs): + super(Application, self).__init__() + + if not settings_manager.is_trace_debug(): + try: + self.create_ipc_listener() + except Exception: + ... + + if not self.is_ipc_alive: + for arg in unknownargs + [args.new_tab,]: + if os.path.isfile(arg): + message = f"FILE|{arg}" + self.send_ipc_message(message) + + raise AppLaunchException(f"{app_name} IPC Server Exists: Will send path(s) to it and close...") + + try: + # kill -SIGUSR2 from Linux/Unix or SIGBREAK signal from Windows + signal.signal( + vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"), + debug_signal_handler + ) + except ValueError: + # Typically: ValueError: signal only works in main thread + ... + + Window(args, unknownargs) diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..90cfadc --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1,3 @@ +""" + Gtk Bound Signal Module +""" diff --git a/src/core/controller.py b/src/core/controller.py new file mode 100644 index 0000000..6c307fd --- /dev/null +++ b/src/core/controller.py @@ -0,0 +1,35 @@ +# Python imports + +# Lib imports + +# Application imports +from .mixins.processor_mixin import ProcessorMixin +from .controller_data import ControllerData +from .widgets.desktop_files import DdesktopFiles +from .widgets.menu import Menu + + + +class Controller(ProcessorMixin, ControllerData): + def __init__(self, args, unknownargs): + self.setup_controller_data() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets(args, unknownargs) + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + event_system.subscribe("execute_program", self.execute_program) + event_system.subscribe("clear_console", self.clear_console) + + def _load_widgets(self, args, unknownargs): + DdesktopFiles() + Menu(args, unknownargs) diff --git a/src/core/controller_data.py b/src/core/controller_data.py new file mode 100644 index 0000000..f744298 --- /dev/null +++ b/src/core/controller_data.py @@ -0,0 +1,51 @@ +# Python imports +import os +import subprocess + +# Lib imports + +# Application imports + + +class ControllerData: + ''' ControllerData contains most of the state of the app at ay given time. It also has some support methods. ''' + + def setup_controller_data(self) -> None: + ... + + def clear_console(self) -> None: + ''' Clears the terminal screen. ''' + os.system('cls' if os.name == 'nt' else 'clear') + + def call_method(self, _method_name: str, data: type) -> type: + ''' + Calls a method from scope of class. + + Parameters: + a (obj): self + b (str): method name to be called + c (*): Data (if any) to be passed to the method. + Note: It must be structured according to the given methods requirements. + + Returns: + Return data is that which the calling method gives. + ''' + method_name = str(_method_name) + method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}") + return method(*data) if data else method() + + def has_method(self, obj: type, method: type) -> type: + ''' Checks if a given method exists. ''' + return callable(getattr(obj, method, None)) + + def get_clipboard_data(self, encoding="utf-8") -> str: + proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE) + retcode = proc.wait() + data = proc.stdout.read() + return data.decode(encoding).strip() + + def set_clipboard_data(self, data: type, encoding="utf-8") -> None: + proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE) + proc.stdin.write(data.encode(encoding)) + proc.stdin.close() + retcode = proc.wait() diff --git a/src/core/mixins/__init__.py b/src/core/mixins/__init__.py new file mode 100644 index 0000000..4589fc7 --- /dev/null +++ b/src/core/mixins/__init__.py @@ -0,0 +1,3 @@ +""" + Generic Mixins Module +""" diff --git a/src/core/mixins/processor_mixin.py b/src/core/mixins/processor_mixin.py new file mode 100644 index 0000000..394fd0e --- /dev/null +++ b/src/core/mixins/processor_mixin.py @@ -0,0 +1,35 @@ +# Python imports +import os, subprocess + +# Lib imports + +# Application imports + + + +class ProcessorMixin: + def execute_program(self, exec_ops): + parts = exec_ops.split("||") + try_exec = parts[0].strip() + main_exec = parts[1].strip() + + self.pre_execute(try_exec, main_exec) + + def pre_execute(self, try_exec, main_exec): + try: + return self.execute(try_exec) + except Exception as e: + logger.debug(f"[Executing Program]\n\t\t Try Exec failed!\n{repr(e)}") + + try: + return self.execute(main_exec) + except Exception as e: + logger.debug(f"[Executing Program]\n\t\t Main Exec failed!\n{repr(e)}") + + + def execute(self, option): + DEVNULL = open(os.devnull, 'w') + command = option.split("%")[0] + + logger.debug(f"Command: {command}") + subprocess.Popen(command.split(), cwd=os.getenv("HOME"), start_new_session=True, stdout=DEVNULL, stderr=DEVNULL) diff --git a/src/core/widgets/__init__.py b/src/core/widgets/__init__.py new file mode 100644 index 0000000..72b072b --- /dev/null +++ b/src/core/widgets/__init__.py @@ -0,0 +1,3 @@ +""" + Widgets Module +""" diff --git a/src/core/widgets/desktop_files.py b/src/core/widgets/desktop_files.py new file mode 100644 index 0000000..8c05a10 --- /dev/null +++ b/src/core/widgets/desktop_files.py @@ -0,0 +1,146 @@ +# Python imports +import pickle +from os import listdir +from dataclasses import fields + +# Lib imports +from xdg.DesktopEntry import DesktopEntry + +# Application imports + + + +class DdesktopFiles: + def __init__(self): + + self.application_dirs = settings.config.application_dirs + self.desktop_enteries = [] + self.groups = {} + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self.reload_desktop_entries() + self.create_groups_mapping() + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + event_system.subscribe("reload_desktop_entries", self.reload_desktop_entries) + event_system.subscribe("get_desktop_entries", self.get_desktop_entries) + event_system.subscribe("get_search_results", self.get_search_results) + event_system.subscribe("get_favorites_results", self.get_favorites_results) + event_system.subscribe("get_sub_group", self.get_sub_group) + + + def reload_desktop_entries(self): + self.desktop_enteries.clear() + self.desktop_enteries = None + self.desktop_enteries = [] + self.collect_desktop_entries() + + self.groups = None + self.groups = {} + self.create_groups_mapping() + + def collect_desktop_entries(self): + for path in self.application_dirs: + for f in listdir(path): + if f.endswith(".desktop"): + self.create_desktop_entry(f"{path}/{f}") + + def create_desktop_entry(self, path): + xdg_object = DesktopEntry(path) + + if xdg_object.getHidden() or xdg_object.getNoDisplay(): + return + + type = xdg_object.getType() + if type == "Application": + self.desktop_enteries.append(xdg_object) + + def create_groups_mapping(self): + self.create_default_groups() + self.generation_primary_group_mapping() + self.cross_append_groups() + + def create_default_groups(self): + for slot in settings.filters.__slots__: + self.groups[slot.title()] = [] + + def generation_primary_group_mapping(self): + for entry in self.desktop_enteries: + groups = entry.getCategories() + if not groups: + self.groups["Other"].append(entry) + + for group in groups: + if not group in self.groups.keys(): + self.groups[group] = [] + + self.groups[group].append(entry) + + def cross_append_groups(self): + fields_data = fields(settings.filters) + for field in fields_data: + title = field.name.title() + to_merge = [] + + for group in field.default_factory(): + to_merge += self.groups[group] + + sub_map = {} + # NOTE: Will "hash" filters ("to_merge" var) first so that target self.groups[title] overrites with its own if any entry exists. + for entry in to_merge + self.groups[title]: + s1 = pickle.dumps(entry) + str_version = s1.decode('unicode_escape') + sub_map[str_version] = entry + + merged_set = [] + for key in sub_map.keys(): + merged_set.append(sub_map[key]) + + self.groups[title] = merged_set + + def get_desktop_entries(self) -> []: + return self.desktop_enteries + + def get_favorites_results(self, group): + results = [] + + for entry in self.desktop_enteries: + _entry = f"{entry.getName()} || {entry.getComment()}" + if _entry in settings.favorites["apps"]: + try_exec = entry.getTryExec() + main_exec = entry.getExec() + results.append( [_entry, f" {try_exec} || {main_exec}"] ) + + return results + + + def get_search_results(self, query): + logger.debug(f"Search Query: {query}") + + results = [] + for entry in self.desktop_enteries: + title = entry.getName() + comment = entry.getComment() + if query in title.lower() or query in comment.lower(): + try_exec = entry.getTryExec() + main_exec = entry.getExec() + results.append( [f"{title} || {comment}", f" {try_exec} || {main_exec}"] ) + + return results + + def get_sub_group(self, group): + results = [] + + for entry in self.groups[group]: + results.append( [f"{entry.getName()} || {entry.getComment()}", f" {entry.getTryExec()} || {entry.getExec()}"] ) + + return results diff --git a/src/core/widgets/menu.py b/src/core/widgets/menu.py new file mode 100644 index 0000000..32acc20 --- /dev/null +++ b/src/core/widgets/menu.py @@ -0,0 +1,134 @@ +# Python imports +import traceback + +# from pprint import pprint + +# Lib imports +from libs.PyInquirer import style_from_dict, Token, prompt, Separator + +# Application imports + + + +class Menu: + """ + The menu class has sub methods that are called per run. + """ + + def __init__(self, args, unknownargs): + self.theme = settings_manager.call_method(settings_manager.get_styles(), args.theme) + base_options = ["[ TO MAIN MENU ]", "Favorites"] + body_menu = [ x.title() for x in settings.filters.__slots__ ] + GROUPS = [ "Search...", "Favorites" ] + body_menu + [ "[ Set Favorites ]", "[ Exit ]" ] + query = "" + group = "" + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + while True: + try: + event_system.emit("clear_console") + results = None + group = self.main_menu(GROUPS)["group"] + event_system.emit("clear_console") + + match group: + case "Search...": + query = self.search_menu()["query"] + results = event_system.emit_and_await("get_search_results", (query.lower(),)) + case "Favorites": + results = event_system.emit_and_await("get_favorites_results", (group,)) + case "[ Set Favorites ]": + results = event_system.emit_and_await("get_search_results", ("",)) + programs_list = [{"name" : "[ TO MAIN MENU ]"}] + [ + {"name": prog, "checked": prog in settings.favorites["apps"]} for prog, exec in results + ] + favorites = self.set_favorites_menu(programs_list)["set_faves"] + settings.favorites["apps"] = favorites + continue + case "[ Exit ]": + break + case _: + results = event_system.emit_and_await("get_sub_group", (group,)) + + programs_list = ["[ TO MAIN MENU ]"] + [prog for prog, exec in results] + entry = self.sub_menu([group, programs_list])["prog"] + if entry not in base_options: + for prog, exec_ops in results: + if prog == entry: + event_system.emit("execute_program", (exec_ops,)) + break + except Exception as e: + logger.debug(f"Traceback: {traceback.print_exc()}") + logger.debug(f"Exception: {e}") + + settings_manager.save_settings() + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + ... + + + def main_menu(self, _group_list = None): + """ + Displays the main menu using the defined GROUPS list... + """ + group_list = GROUPS if not _group_list else _group_list + menu = { + 'type': 'list', + 'name': 'group', + 'message': '[ MAIN MENU ]', + 'choices': group_list + } + + return prompt(menu, style=self.theme) + + + def set_favorites_menu(self, _group_list = None): + GROUPS = [{'name': '[ TO MAIN MENU ]'}, {'name': 'This is a stub method for Favorites...'}] + group_list = GROUPS if not _group_list else _group_list + menu = { + 'type': 'checkbox', + 'qmark': '>', + 'message': 'Select Favorites', + 'name': 'set_faves', + 'choices': group_list + } + + return prompt(menu, style=self.theme) + + + def sub_menu(self, data = ["NO GROUP NAME", "NO PROGRAMS PASSED IN"]): + group = data[0] + prog_list = data[1] + + menu = { + 'type': 'list', + 'name': 'prog', + 'message': f'[ {group} ]', + 'choices': prog_list + } + + return prompt(menu, style=self.theme) + + + def search_menu(self): + menu = { + 'type': 'input', + 'name': 'query', + 'message': 'Program you\'re looking for: ', + } + + return prompt(menu, style=self.theme) diff --git a/src/core/window.py b/src/core/window.py new file mode 100644 index 0000000..64dc291 --- /dev/null +++ b/src/core/window.py @@ -0,0 +1,51 @@ +# Python imports + +# Lib imports + +# Application imports +from core.controller import Controller + + + +class ControllerStartExceptiom(Exception): + ... + + + +class ApplicationWindow: + """docstring for ApplicationWindow.""" + + def __init__(self): + ... + +class Window(ApplicationWindow): + """ docstring for Window. """ + + def __init__(self, args, unknownargs): + super(Window, self).__init__() + settings_manager.set_main_window(self) + + self._controller = None + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets(args, unknownargs) + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + event_system.subscribe("tear_down", self._tear_down) + + def _load_widgets(self, args, unknownargs): + self._controller = Controller(args, unknownargs) + if not self._controller: + raise ControllerStartException("Controller exited and doesn't exist...") + + def _tear_down(self, widget = None, eve = None): + settings_manager.save_settings() diff --git a/src/libs/PyInquirer/__init__.py b/src/libs/PyInquirer/__init__.py new file mode 100644 index 0000000..5cd0619 --- /dev/null +++ b/src/libs/PyInquirer/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function +import os + +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.styles import style_from_dict +from libs.prompt_toolkit.validation import Validator, ValidationError + +from .utils import print_json, format_json + + +__version__ = '1.0.2' + + +def here(p): + return os.path.abspath(os.path.join(os.path.dirname(__file__), p)) + + +class PromptParameterException(ValueError): + def __init__(self, message, errors=None): + + # Call the base class constructor with the parameters it needs + super(PromptParameterException, self).__init__( + 'You must provide a `%s` value' % message, errors) + +from .prompt import prompt +from .separator import Separator +from .prompts.common import default_style diff --git a/src/libs/PyInquirer/color_print.py b/src/libs/PyInquirer/color_print.py new file mode 100644 index 0000000..0841804 --- /dev/null +++ b/src/libs/PyInquirer/color_print.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +provide colorized output +""" +from __future__ import print_function, unicode_literals +import sys +from libs.prompt_toolkit.shortcuts import print_tokens, style_from_dict, Token + + +def _print_token_factory(col): + """Internal helper to provide color names.""" + def _helper(msg): + style = style_from_dict({ + Token.Color: col, + }) + tokens = [ + (Token.Color, msg) + ] + print_tokens(tokens, style=style) + + def _helper_no_terminal(msg): + # workaround if we have no terminal + print(msg) + if sys.stdout.isatty(): + return _helper + else: + return _helper_no_terminal + +# used this for color source: +# http://unix.stackexchange.com/questions/105568/how-can-i-list-the-available-color-names +yellow = _print_token_factory('#dfaf00') +blue = _print_token_factory('#0087ff') +gray = _print_token_factory('#6c6c6c') + +# TODO +#black +#red +#green +#magenta +#cyan +#white diff --git a/src/libs/PyInquirer/prompt.py b/src/libs/PyInquirer/prompt.py new file mode 100644 index 0000000..bbeab54 --- /dev/null +++ b/src/libs/PyInquirer/prompt.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function + +from libs.prompt_toolkit.shortcuts import run_application + +from . import PromptParameterException, prompts +from .prompts import list, confirm, input, password, checkbox, rawlist, expand, editor + + +def prompt(questions, answers=None, **kwargs): + if isinstance(questions, dict): + questions = [questions] + answers = answers or {} + + patch_stdout = kwargs.pop('patch_stdout', False) + return_asyncio_coroutine = kwargs.pop('return_asyncio_coroutine', False) + true_color = kwargs.pop('true_color', False) + refresh_interval = kwargs.pop('refresh_interval', 0) + eventloop = kwargs.pop('eventloop', None) + kbi_msg = kwargs.pop('keyboard_interrupt_msg', 'Cancelled by user') + + for question in questions: + # import the question + if 'type' not in question: + raise PromptParameterException('type') + if 'name' not in question: + raise PromptParameterException('name') + if 'message' not in question: + raise PromptParameterException('message') + try: + choices = question.get('choices') + if choices is not None and callable(choices): + question['choices'] = choices(answers) + + _kwargs = {} + _kwargs.update(kwargs) + _kwargs.update(question) + type = _kwargs.pop('type') + name = _kwargs.pop('name') + message = _kwargs.pop('message') + when = _kwargs.pop('when', None) + filter = _kwargs.pop('filter', None) + + if when: + # at least a little sanity check! + if callable(question['when']): + try: + if not question['when'](answers): + continue + except Exception as e: + raise ValueError( + 'Problem in \'when\' check of %s question: %s' % + (name, e)) + else: + raise ValueError('\'when\' needs to be function that ' \ + 'accepts a dict argument') + if filter: + # at least a little sanity check! + if not callable(question['filter']): + raise ValueError('\'filter\' needs to be function that ' \ + 'accepts an argument') + + if callable(question.get('default')): + _kwargs['default'] = question['default'](answers) + + application = getattr(prompts, type).question(message, **_kwargs) + + answer = run_application( + application, + patch_stdout=patch_stdout, + return_asyncio_coroutine=return_asyncio_coroutine, + true_color=true_color, + refresh_interval=refresh_interval, + eventloop=eventloop) + + if answer is not None: + if filter: + try: + answer = question['filter'](answer) + except Exception as e: + raise ValueError( + 'Problem processing \'filter\' of %s question: %s' % + (name, e)) + answers[name] = answer + except AttributeError as e: + print(e) + raise ValueError('No question type \'%s\'' % type) + except KeyboardInterrupt: + print('') + print(kbi_msg) + print('') + return {} + return answers + + +# TODO: +# Bottom Bar - inquirer.ui.BottomBar diff --git a/src/libs/PyInquirer/prompts/__init__.py b/src/libs/PyInquirer/prompts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/libs/PyInquirer/prompts/checkbox.py b/src/libs/PyInquirer/prompts/checkbox.py new file mode 100644 index 0000000..33ffdeb --- /dev/null +++ b/src/libs/PyInquirer/prompts/checkbox.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +""" +`checkbox` type question +""" +from __future__ import print_function, unicode_literals +from libs.prompt_toolkit.application import Application +from libs.prompt_toolkit.key_binding.manager import KeyBindingManager +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.layout.containers import Window +from libs.prompt_toolkit.filters import IsDone +from libs.prompt_toolkit.layout.controls import TokenListControl +from libs.prompt_toolkit.layout.containers import ConditionalContainer, \ + ScrollOffsets, HSplit +from libs.prompt_toolkit.layout.dimension import LayoutDimension as D +from libs.prompt_toolkit.token import Token + +from .. import PromptParameterException +from ..separator import Separator +from .common import setup_simple_validator, default_style, if_mousedown + + +# custom control based on TokenListControl + + +class InquirerControl(TokenListControl): + def __init__(self, choices, **kwargs): + self.pointer_index = 0 + self.selected_options = [] # list of names + self.answered = False + self._init_choices(choices) + super(InquirerControl, self).__init__(self._get_choice_tokens, + **kwargs) + + def _init_choices(self, choices): + # helper to convert from question format to internal format + self.choices = [] # list (name, value) + searching_first_choice = True + for i, c in enumerate(choices): + if isinstance(c, Separator): + self.choices.append(c) + else: + name = c['name'] + value = c.get('value', name) + disabled = c.get('disabled', None) + if 'checked' in c and c['checked'] and not disabled: + self.selected_options.append(c['name']) + self.choices.append((name, value, disabled)) + if searching_first_choice and not disabled: # find the first (available) choice + self.pointer_index = i + searching_first_choice = False + + @property + def choice_count(self): + return len(self.choices) + + def _get_choice_tokens(self, cli): + tokens = [] + T = Token + + def append(index, line): + if isinstance(line, Separator): + tokens.append((T.Separator, ' %s\n' % line)) + else: + line_name = line[0] + line_value = line[1] + selected = (line_value in self.selected_options) # use value to check if option has been selected + pointed_at = (index == self.pointer_index) + + @if_mousedown + def select_item(cli, mouse_event): + # bind option with this index to mouse event + if line_value in self.selected_options: + self.selected_options.remove(line_value) + else: + self.selected_options.append(line_value) + + if pointed_at: + tokens.append((T.Pointer, ' \u276f', select_item)) # ' >' + else: + tokens.append((T, ' ', select_item)) + # 'o ' - FISHEYE + if choice[2]: # disabled + tokens.append((T, '- %s (%s)' % (choice[0], choice[2]))) + else: + if selected: + tokens.append((T.Selected, '\u25cf ', select_item)) + else: + tokens.append((T, '\u25cb ', select_item)) + + if pointed_at: + tokens.append((Token.SetCursorPosition, '')) + + tokens.append((T, line_name, select_item)) + tokens.append((T, '\n')) + + # prepare the select choices + for i, choice in enumerate(self.choices): + append(i, choice) + tokens.pop() # Remove last newline. + return tokens + + def get_selected_values(self): + # get values not labels + return [c[1] for c in self.choices if not isinstance(c, Separator) and + c[1] in self.selected_options] + + @property + def line_count(self): + return len(self.choices) + + +def question(message, **kwargs): + # TODO add bottom-bar (Move up and down to reveal more choices) + # TODO extract common parts for list, checkbox, rawlist, expand + # TODO validate + if not 'choices' in kwargs: + raise PromptParameterException('choices') + # this does not implement default, use checked... + if 'default' in kwargs: + raise ValueError('Checkbox does not implement \'default\' ' + 'use \'checked\':True\' in choice!') + + choices = kwargs.pop('choices', None) + validator = setup_simple_validator(kwargs) + + # TODO style defaults on detail level + style = kwargs.pop('style', default_style) + + ic = InquirerControl(choices) + qmark = kwargs.pop('qmark', '?') + + def get_prompt_tokens(cli): + tokens = [] + + tokens.append((Token.QuestionMark, qmark)) + tokens.append((Token.Question, ' %s ' % message)) + if ic.answered: + nbr_selected = len(ic.selected_options) + if nbr_selected == 0: + tokens.append((Token.Answer, ' done')) + elif nbr_selected == 1: + tokens.append((Token.Answer, ' [%s]' % ic.selected_options[0])) + else: + tokens.append((Token.Answer, + ' done (%d selections)' % nbr_selected)) + else: + tokens.append((Token.Instruction, + ' (, to move, to select, ' + 'to toggle, to invert)')) + return tokens + + # assemble layout + layout = HSplit([ + Window(height=D.exact(1), + content=TokenListControl(get_prompt_tokens, align_center=False) + ), + ConditionalContainer( + Window( + ic, + width=D.exact(43), + height=D(min=3), + scroll_offsets=ScrollOffsets(top=1, bottom=1) + ), + filter=~IsDone() + ) + ]) + + # key bindings + manager = KeyBindingManager.for_prompt() + + @manager.registry.add_binding(Keys.ControlQ, eager=True) + @manager.registry.add_binding(Keys.ControlC, eager=True) + def _(event): + raise KeyboardInterrupt() + # event.cli.set_return_value(None) + + @manager.registry.add_binding(' ', eager=True) + def toggle(event): + pointed_choice = ic.choices[ic.pointer_index][1] # value + if pointed_choice in ic.selected_options: + ic.selected_options.remove(pointed_choice) + else: + ic.selected_options.append(pointed_choice) + + @manager.registry.add_binding('i', eager=True) + def invert(event): + inverted_selection = [c[1] for c in ic.choices if + not isinstance(c, Separator) and + c[1] not in ic.selected_options and + not c[2]] + ic.selected_options = inverted_selection + + @manager.registry.add_binding('a', eager=True) + def all(event): + all_selected = True # all choices have been selected + for c in ic.choices: + if not isinstance(c, Separator) and c[1] not in ic.selected_options and not c[2]: + # add missing ones + ic.selected_options.append(c[1]) + all_selected = False + if all_selected: + ic.selected_options = [] + + @manager.registry.add_binding(Keys.Down, eager=True) + def move_cursor_down(event): + def _next(): + ic.pointer_index = ((ic.pointer_index + 1) % ic.line_count) + _next() + while isinstance(ic.choices[ic.pointer_index], Separator) or \ + ic.choices[ic.pointer_index][2]: + _next() + + @manager.registry.add_binding(Keys.Up, eager=True) + def move_cursor_up(event): + def _prev(): + ic.pointer_index = ((ic.pointer_index - 1) % ic.line_count) + _prev() + while isinstance(ic.choices[ic.pointer_index], Separator) or \ + ic.choices[ic.pointer_index][2]: + _prev() + + @manager.registry.add_binding(Keys.Enter, eager=True) + def set_answer(event): + ic.answered = True + # TODO use validator + event.cli.set_return_value(ic.get_selected_values()) + + return Application( + layout=layout, + key_bindings_registry=manager.registry, + mouse_support=True, + style=style + ) diff --git a/src/libs/PyInquirer/prompts/common.py b/src/libs/PyInquirer/prompts/common.py new file mode 100644 index 0000000..cbecae4 --- /dev/null +++ b/src/libs/PyInquirer/prompts/common.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" +common prompt functionality +""" + +import sys + +from libs.prompt_toolkit.validation import Validator, ValidationError +from libs.prompt_toolkit.styles import style_from_dict +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.mouse_events import MouseEventTypes + + +PY3 = sys.version_info[0] >= 3 + +if PY3: + basestring = str + + +def if_mousedown(handler): + def handle_if_mouse_down(cli, mouse_event): + if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: + return handler(cli, mouse_event) + else: + return NotImplemented + + return handle_if_mouse_down + + +# TODO probably better to use base.Condition +def setup_validator(kwargs): + # this is an internal helper not meant for public consumption! + # note this works on a dictionary + validate_prompt = kwargs.pop('validate', None) + if validate_prompt: + if issubclass(validate_prompt, Validator): + kwargs['validator'] = validate_prompt() + elif callable(validate_prompt): + class _InputValidator(Validator): + def validate(self, document): + #print('validation!!') + verdict = validate_prompt(document.text) + if isinstance(verdict, basestring): + raise ValidationError( + message=verdict, + cursor_position=len(document.text)) + elif verdict is not True: + raise ValidationError( + message='invalid input', + cursor_position=len(document.text)) + kwargs['validator'] = _InputValidator() + return kwargs['validator'] + + +def setup_simple_validator(kwargs): + # this is an internal helper not meant for public consumption! + # note this works on a dictionary + # this validates the answer not a buffer + # TODO + # not sure yet how to deal with the validation result: + # https://github.com/jonathanslenders/python-prompt-toolkit/issues/430 + validate = kwargs.pop('validate', None) + if validate is None: + def _always(answer): + return True + return _always + elif not callable(validate): + raise ValueError('Here a simple validate function is expected, no class') + + def _validator(answer): + verdict = validate(answer) + if isinstance(verdict, basestring): + raise ValidationError( + message=verdict + ) + elif verdict is not True: + raise ValidationError( + message='invalid input' + ) + return _validator + + +# FIXME style defaults on detail level +default_style = style_from_dict({ + Token.Separator: '#6C6C6C', + Token.QuestionMark: '#5F819D', + Token.Selected: '', # default + Token.Pointer: '#FF9D00 bold', # AWS orange + Token.Instruction: '', # default + Token.Answer: '#FF9D00 bold', # AWS orange + Token.Question: 'bold', +}) diff --git a/src/libs/PyInquirer/prompts/confirm.py b/src/libs/PyInquirer/prompts/confirm.py new file mode 100644 index 0000000..3fbc607 --- /dev/null +++ b/src/libs/PyInquirer/prompts/confirm.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +""" +confirm type question +""" +from __future__ import print_function, unicode_literals +from libs.prompt_toolkit.application import Application +from libs.prompt_toolkit.key_binding.manager import KeyBindingManager +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.layout.containers import Window, HSplit +from libs.prompt_toolkit.layout.controls import TokenListControl +from libs.prompt_toolkit.layout.dimension import LayoutDimension as D +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.shortcuts import create_prompt_application +from libs.prompt_toolkit.styles import style_from_dict + + +# custom control based on TokenListControl + + +def question(message, **kwargs): + # TODO need ENTER confirmation + default = kwargs.pop('default', True) + + # TODO style defaults on detail level + style = kwargs.pop('style', style_from_dict({ + Token.QuestionMark: '#5F819D', + #Token.Selected: '#FF9D00', # AWS orange + Token.Instruction: '', # default + Token.Answer: '#FF9D00 bold', # AWS orange + Token.Question: 'bold', + })) + status = {'answer': None} + + qmark = kwargs.pop('qmark', '?') + + def get_prompt_tokens(cli): + tokens = [] + + tokens.append((Token.QuestionMark, qmark)) + tokens.append((Token.Question, ' %s ' % message)) + if isinstance(status['answer'], bool): + tokens.append((Token.Answer, ' Yes' if status['answer'] else ' No')) + else: + if default: + instruction = ' (Y/n)' + else: + instruction = ' (y/N)' + tokens.append((Token.Instruction, instruction)) + return tokens + + # key bindings + manager = KeyBindingManager.for_prompt() + + @manager.registry.add_binding(Keys.ControlQ, eager=True) + @manager.registry.add_binding(Keys.ControlC, eager=True) + def _(event): + raise KeyboardInterrupt() + + @manager.registry.add_binding('n') + @manager.registry.add_binding('N') + def key_n(event): + status['answer'] = False + event.cli.set_return_value(False) + + @manager.registry.add_binding('y') + @manager.registry.add_binding('Y') + def key_y(event): + status['answer'] = True + event.cli.set_return_value(True) + + @manager.registry.add_binding(Keys.Enter, eager=True) + def set_answer(event): + status['answer'] = default + event.cli.set_return_value(default) + + return create_prompt_application( + get_prompt_tokens=get_prompt_tokens, + key_bindings_registry=manager.registry, + mouse_support=False, + style=style, + erase_when_done=False, + ) diff --git a/src/libs/PyInquirer/prompts/editor.py b/src/libs/PyInquirer/prompts/editor.py new file mode 100644 index 0000000..0d4b8d6 --- /dev/null +++ b/src/libs/PyInquirer/prompts/editor.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +""" +`editor` type question +""" +from __future__ import print_function, unicode_literals +import os +import sys +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.shortcuts import create_prompt_application +from libs.prompt_toolkit.validation import Validator, ValidationError +from libs.prompt_toolkit.layout.lexers import SimpleLexer + +from .common import default_style + + +# use std prompt-toolkit control + +WIN = sys.platform.startswith('win') + +class EditorArgumentsError(Exception): + pass + +class Editor(object): + + def __init__(self, editor=None, env=None, require_save=True, extension='.txt'): + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self): + if self.editor is not None and self.editor.lower() != "default": + return self.editor + for key in 'VISUAL', 'EDITOR': + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return 'notepad' + for editor in 'vim', 'nano': + if os.system('which %s >/dev/null 2>&1' % editor) == 0: + return editor + return 'vi' + + def edit_file(self, filename): + import subprocess + editor = self.get_editor() + if self.env: + environ = os.environ.copy() + environ.update(self.env) + else: + environ = None + try: + c = subprocess.Popen('%s "%s"' % (editor, filename), + env=environ, shell=True) + exit_code = c.wait() + if exit_code != 0: + raise Exception('%s: Editing failed!' % editor) + except OSError as e: + raise Exception('%s: Editing failed: %s' % (editor, e)) + + def edit(self, text): + import tempfile + + text = text or '' + if text and not text.endswith('\n'): + text += '\n' + + fd, name = tempfile.mkstemp(prefix='editor-', suffix=self.extension) + try: + if WIN: + encoding = 'utf-8-sig' + text = text.replace('\n', '\r\n') + else: + encoding = 'utf-8' + text = text.encode(encoding) + + f = os.fdopen(fd, 'wb') + f.write(text) + f.close() + timestamp = os.path.getmtime(name) + + self.edit_file(name) + + if self.require_save \ + and os.path.getmtime(name) == timestamp: + return None + + f = open(name, 'rb') + try: + rv = f.read() + finally: + f.close() + return rv.decode('utf-8-sig').replace('\r\n', '\n') + finally: + os.unlink(name) + +def edit(text=None, editor=None, env=None, require_save=True, + extension='.txt', filename=None): + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. + """ + + editor = Editor(editor=editor, env=env, require_save=require_save, + extension=extension) + if filename is None: + return editor.edit(text) + editor.edit_file(filename) + +def question(message, **kwargs): + default = kwargs.pop('default', '') + eargs = kwargs.pop('eargs', {}) + validate_prompt = kwargs.pop('validate', None) + if validate_prompt: + if issubclass(validate_prompt, Validator): + kwargs['validator'] = validate_prompt() + elif callable(validate_prompt): + class _InputValidator(Validator): + def validate(self, document): + verdict = validate_prompt(document.text) + if not verdict == True: + if verdict == False: + verdict = 'invalid input' + raise ValidationError( + message=verdict, + cursor_position=len(document.text)) + kwargs['validator'] = _InputValidator() + + for k, v in eargs.items(): + if v == "" or v == " ": + raise EditorArgumentsError( + "Args '{}' value should not be empty".format(k) + ) + + editor = eargs.get("editor", None) + ext = eargs.get("ext", ".txt") + env = eargs.get("env", None) + text = default + filename = eargs.get("filename", None) + multiline = True if not editor else False + save = eargs.get("save", None) + + if editor: + _text = edit( + editor=editor, + extension=ext, + text=text, + env=env, + filename=filename, + require_save=save + ) + if filename: + default = filename + else: + default = _text + + # TODO style defaults on detail level + kwargs['style'] = kwargs.pop('style', default_style) + qmark = kwargs.pop('qmark', '?') + + def _get_prompt_tokens(cli): + return [ + (Token.QuestionMark, qmark), + (Token.Question, ' %s ' % message) + ] + + return create_prompt_application( + get_prompt_tokens=_get_prompt_tokens, + lexer=SimpleLexer(Token.Answer), + default=default, + multiline=multiline, + **kwargs + ) diff --git a/src/libs/PyInquirer/prompts/expand.py b/src/libs/PyInquirer/prompts/expand.py new file mode 100644 index 0000000..f3c0c38 --- /dev/null +++ b/src/libs/PyInquirer/prompts/expand.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +""" +`expand` type question +""" +from __future__ import print_function, unicode_literals + +import sys + +from libs.prompt_toolkit.application import Application +from libs.prompt_toolkit.key_binding.manager import KeyBindingManager +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.layout.containers import Window +from libs.prompt_toolkit.filters import IsDone +from libs.prompt_toolkit.layout.controls import TokenListControl +from libs.prompt_toolkit.layout.containers import ConditionalContainer, HSplit +from libs.prompt_toolkit.layout.dimension import LayoutDimension as D +from libs.prompt_toolkit.token import Token + +from .. import PromptParameterException +from ..separator import Separator +from .common import default_style +from .common import if_mousedown + + +PY3 = sys.version_info[0] >= 3 + +if PY3: + basestring = str + + +# custom control based on TokenListControl + +class InquirerControl(TokenListControl): + def __init__(self, choices, default=None, **kwargs): + self.pointer_index = 0 + self.answered = False + self._init_choices(choices, default) + self._help_active = False # help is activated via 'h' key + super(InquirerControl, self).__init__(self._get_choice_tokens, + **kwargs) + + def _init_choices(self, choices, default=None): + # helper to convert from question format to internal format + + self.choices = [] # list (key, name, value) + + if not default: + default = 'h' + for i, c in enumerate(choices): + if isinstance(c, Separator): + self.choices.append(c) + else: + if isinstance(c, basestring): + self.choices.append((key, c, c)) + else: + key = c.get('key') + name = c.get('name') + value = c.get('value', name) + if default and default == key: + self.pointer_index = i + key = key.upper() # default key is in uppercase + self.choices.append((key, name, value)) + # append the help choice + key = 'h' + if not default: + self.pointer_index = len(self.choices) + key = key.upper() # default key is in uppercase + self.choices.append((key, 'Help, list all options', None)) + + @property + def choice_count(self): + return len(self.choices) + + def _get_choice_tokens(self, cli): + tokens = [] + T = Token + + def _append(index, line): + if isinstance(line, Separator): + tokens.append((T.Separator, ' %s\n' % line)) + else: + key = line[0] + line = line[1] + pointed_at = (index == self.pointer_index) + + @if_mousedown + def select_item(cli, mouse_event): + # bind option with this index to mouse event + self.pointer_index = index + + if pointed_at: + tokens.append((T.Selected, ' %s) %s' % (key, line), + select_item)) + else: + tokens.append((T, ' %s) %s' % (key, line), + select_item)) + tokens.append((T, '\n')) + + if self._help_active: + # prepare the select choices + for i, choice in enumerate(self.choices): + _append(i, choice) + tokens.append((T, ' Answer: %s' % + self.choices[self.pointer_index][0])) + else: + tokens.append((T.Pointer, '>> ')) + tokens.append((T, self.choices[self.pointer_index][1])) + return tokens + + def get_selected_value(self): + # get value not label + return self.choices[self.pointer_index][2] + + +def question(message, **kwargs): + # TODO extract common parts for list, checkbox, rawlist, expand + # TODO up, down navigation + if not 'choices' in kwargs: + raise PromptParameterException('choices') + + choices = kwargs.pop('choices', None) + default = kwargs.pop('default', None) + qmark = kwargs.pop('qmark', '?') + # TODO style defaults on detail level + style = kwargs.pop('style', default_style) + + ic = InquirerControl(choices, default) + + def get_prompt_tokens(cli): + tokens = [] + T = Token + + tokens.append((T.QuestionMark, qmark)) + tokens.append((T.Question, ' %s ' % message)) + if not ic.answered: + tokens.append((T.Instruction, ' (%s)' % ''.join( + [k[0] for k in ic.choices if not isinstance(k, Separator)]))) + else: + tokens.append((T.Answer, ' %s' % ic.get_selected_value())) + return tokens + + #@Condition + #def is_help_active(cli): + # return ic._help_active + + # assemble layout + layout = HSplit([ + Window(height=D.exact(1), + content=TokenListControl(get_prompt_tokens) + ), + ConditionalContainer( + Window(ic), + #filter=is_help_active & ~IsDone() # ~ bitwise inverse + filter=~IsDone() # ~ bitwise inverse + ) + ]) + + # key bindings + manager = KeyBindingManager.for_prompt() + + @manager.registry.add_binding(Keys.ControlQ, eager=True) + @manager.registry.add_binding(Keys.ControlC, eager=True) + def _(event): + raise KeyboardInterrupt() + + # add key bindings for choices + for i, c in enumerate(ic.choices): + if not isinstance(c, Separator): + def _reg_binding(i, keys): + # trick out late evaluation with a "function factory": + # http://stackoverflow.com/questions/3431676/creating-functions-in-a-loop + @manager.registry.add_binding(keys, eager=True) + def select_choice(event): + ic.pointer_index = i + if c[0] not in ['h', 'H']: + _reg_binding(i, c[0]) + if c[0].isupper(): + _reg_binding(i, c[0].lower()) + + @manager.registry.add_binding('H', eager=True) + @manager.registry.add_binding('h', eager=True) + def help_choice(event): + ic._help_active = True + + @manager.registry.add_binding(Keys.Enter, eager=True) + def set_answer(event): + ic.answered = True + event.cli.set_return_value(ic.get_selected_value()) + + return Application( + layout=layout, + key_bindings_registry=manager.registry, + mouse_support=True, + style=style + ) diff --git a/src/libs/PyInquirer/prompts/input.py b/src/libs/PyInquirer/prompts/input.py new file mode 100644 index 0000000..fb134fa --- /dev/null +++ b/src/libs/PyInquirer/prompts/input.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +`input` type question +""" +from __future__ import print_function, unicode_literals +import inspect +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.shortcuts import create_prompt_application +from libs.prompt_toolkit.validation import Validator, ValidationError +from libs.prompt_toolkit.layout.lexers import SimpleLexer + +from .common import default_style + +# use std prompt-toolkit control + + +def question(message, **kwargs): + default = kwargs.pop('default', '') + validate_prompt = kwargs.pop('validate', None) + if validate_prompt: + if inspect.isclass(validate_prompt) and issubclass(validate_prompt, Validator): + kwargs['validator'] = validate_prompt() + elif callable(validate_prompt): + class _InputValidator(Validator): + def validate(self, document): + verdict = validate_prompt(document.text) + if not verdict == True: + if verdict == False: + verdict = 'invalid input' + raise ValidationError( + message=verdict, + cursor_position=len(document.text)) + kwargs['validator'] = _InputValidator() + + # TODO style defaults on detail level + kwargs['style'] = kwargs.pop('style', default_style) + qmark = kwargs.pop('qmark', '?') + + + def _get_prompt_tokens(cli): + return [ + (Token.QuestionMark, qmark), + (Token.Question, ' %s ' % message) + ] + + return create_prompt_application( + get_prompt_tokens=_get_prompt_tokens, + lexer=SimpleLexer(Token.Answer), + default=default, + **kwargs + ) diff --git a/src/libs/PyInquirer/prompts/list.py b/src/libs/PyInquirer/prompts/list.py new file mode 100644 index 0000000..9b18a94 --- /dev/null +++ b/src/libs/PyInquirer/prompts/list.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +""" +`list` type question +""" +from __future__ import print_function +from __future__ import unicode_literals + +import sys + +from libs.prompt_toolkit.application import Application +from libs.prompt_toolkit.key_binding.manager import KeyBindingManager +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.layout.containers import Window +from libs.prompt_toolkit.filters import IsDone +from libs.prompt_toolkit.layout.controls import TokenListControl +from libs.prompt_toolkit.layout.containers import ConditionalContainer, \ + ScrollOffsets, HSplit +from libs.prompt_toolkit.layout.dimension import LayoutDimension as D +from libs.prompt_toolkit.token import Token + +from .. import PromptParameterException +from ..separator import Separator +from .common import if_mousedown, default_style + +# custom control based on TokenListControl +# docu here: +# https://github.com/jonathanslenders/python-prompt-toolkit/issues/281 +# https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/examples/full-screen-layout.py +# https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/docs/pages/full_screen_apps.rst + + +PY3 = sys.version_info[0] >= 3 + +if PY3: + basestring = str + + +class InquirerControl(TokenListControl): + def __init__(self, choices, **kwargs): + self.selected_option_index = 0 + self.answered = False + self.choices = choices + self._init_choices(choices) + super(InquirerControl, self).__init__(self._get_choice_tokens, + **kwargs) + + def _init_choices(self, choices, default=None): + # helper to convert from question format to internal format + self.choices = [] # list (name, value, disabled) + searching_first_choice = True + for i, c in enumerate(choices): + if isinstance(c, Separator): + self.choices.append((c, None, None)) + else: + if isinstance(c, basestring): + self.choices.append((c, c, None)) + else: + name = c.get('name') + value = c.get('value', name) + disabled = c.get('disabled', None) + self.choices.append((name, value, disabled)) + if searching_first_choice: + self.selected_option_index = i # found the first choice + searching_first_choice = False + + @property + def choice_count(self): + return len(self.choices) + + def _get_choice_tokens(self, cli): + tokens = [] + T = Token + + def append(index, choice): + selected = (index == self.selected_option_index) + + @if_mousedown + def select_item(cli, mouse_event): + # bind option with this index to mouse event + self.selected_option_index = index + self.answered = True + cli.set_return_value(None) + + tokens.append((T.Pointer if selected else T, ' \u276f ' if selected + else ' ')) + if selected: + tokens.append((Token.SetCursorPosition, '')) + if choice[2]: # disabled + tokens.append((T.Selected if selected else T, + '- %s (%s)' % (choice[0], choice[2]))) + else: + try: + tokens.append((T.Selected if selected else T, str(choice[0]), + select_item)) + except: + tokens.append((T.Selected if selected else T, choice[0], + select_item)) + tokens.append((T, '\n')) + + # prepare the select choices + for i, choice in enumerate(self.choices): + append(i, choice) + tokens.pop() # Remove last newline. + return tokens + + def get_selection(self): + return self.choices[self.selected_option_index] + + +def question(message, **kwargs): + # TODO disabled, dict choices + if not 'choices' in kwargs: + raise PromptParameterException('choices') + + choices = kwargs.pop('choices', None) + default = kwargs.pop('default', 0) # TODO + qmark = kwargs.pop('qmark', '?') + # TODO style defaults on detail level + style = kwargs.pop('style', default_style) + + ic = InquirerControl(choices) + + def get_prompt_tokens(cli): + tokens = [] + + tokens.append((Token.QuestionMark, qmark)) + tokens.append((Token.Question, ' %s ' % message)) + if ic.answered: + tokens.append((Token.Answer, ' ' + ic.get_selection()[0])) + else: + tokens.append((Token.Instruction, ' (Use arrow keys)')) + return tokens + + # assemble layout + layout = HSplit([ + Window(height=D.exact(1), + content=TokenListControl(get_prompt_tokens) + ), + ConditionalContainer( + Window(ic), + filter=~IsDone() + ) + ]) + + # key bindings + manager = KeyBindingManager.for_prompt() + + @manager.registry.add_binding(Keys.ControlQ, eager=True) + @manager.registry.add_binding(Keys.ControlC, eager=True) + def _(event): + raise KeyboardInterrupt() + # event.cli.set_return_value(None) + + @manager.registry.add_binding(Keys.Down, eager=True) + def move_cursor_down(event): + def _next(): + ic.selected_option_index = ( + (ic.selected_option_index + 1) % ic.choice_count) + _next() + while isinstance(ic.choices[ic.selected_option_index][0], Separator) or\ + ic.choices[ic.selected_option_index][2]: + _next() + + @manager.registry.add_binding(Keys.Up, eager=True) + def move_cursor_up(event): + def _prev(): + ic.selected_option_index = ( + (ic.selected_option_index - 1) % ic.choice_count) + _prev() + while isinstance(ic.choices[ic.selected_option_index][0], Separator) or \ + ic.choices[ic.selected_option_index][2]: + _prev() + + @manager.registry.add_binding(Keys.Enter, eager=True) + def set_answer(event): + ic.answered = True + event.cli.set_return_value(ic.get_selection()[1]) + + return Application( + layout=layout, + key_bindings_registry=manager.registry, + mouse_support=True, + style=style + ) diff --git a/src/libs/PyInquirer/prompts/password.py b/src/libs/PyInquirer/prompts/password.py new file mode 100644 index 0000000..1bf294f --- /dev/null +++ b/src/libs/PyInquirer/prompts/password.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +`password` type question +""" +from __future__ import print_function, unicode_literals + +from . import input + +# use std prompt-toolkit control + + +def question(message, **kwargs): + kwargs['is_password'] = True + return input.question(message, **kwargs) diff --git a/src/libs/PyInquirer/prompts/rawlist.py b/src/libs/PyInquirer/prompts/rawlist.py new file mode 100644 index 0000000..976c2d1 --- /dev/null +++ b/src/libs/PyInquirer/prompts/rawlist.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +""" +`rawlist` type question +""" +from __future__ import print_function, unicode_literals +import sys + +from libs.prompt_toolkit.application import Application +from libs.prompt_toolkit.key_binding.manager import KeyBindingManager +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.layout.containers import Window +from libs.prompt_toolkit.filters import IsDone +from libs.prompt_toolkit.layout.controls import TokenListControl +from libs.prompt_toolkit.layout.containers import ConditionalContainer, HSplit +from libs.prompt_toolkit.layout.dimension import LayoutDimension as D +from libs.prompt_toolkit.token import Token + +from .. import PromptParameterException +from ..separator import Separator +from .common import default_style +from .common import if_mousedown + + +PY3 = sys.version_info[0] >= 3 + +if PY3: + basestring = str + + +# custom control based on TokenListControl + +class InquirerControl(TokenListControl): + def __init__(self, choices, **kwargs): + self.pointer_index = 0 + self.answered = False + self._init_choices(choices) + super(InquirerControl, self).__init__(self._get_choice_tokens, + **kwargs) + + def _init_choices(self, choices): + # helper to convert from question format to internal format + self.choices = [] # list (key, name, value) + searching_first_choice = True + key = 1 # used for numeric keys + for i, c in enumerate(choices): + if isinstance(c, Separator): + self.choices.append(c) + else: + if isinstance(c, basestring): + self.choices.append((key, c, c)) + key += 1 + if searching_first_choice: + self.pointer_index = i # found the first choice + searching_first_choice = False + + @property + def choice_count(self): + return len(self.choices) + + def _get_choice_tokens(self, cli): + tokens = [] + T = Token + + def _append(index, line): + if isinstance(line, Separator): + tokens.append((T.Separator, ' %s\n' % line)) + else: + key = line[0] + line = line[1] + pointed_at = (index == self.pointer_index) + + @if_mousedown + def select_item(cli, mouse_event): + # bind option with this index to mouse event + self.pointer_index = index + + if pointed_at: + tokens.append((T.Selected, ' %d) %s' % (key, line), + select_item)) + else: + tokens.append((T, ' %d) %s' % (key, line), + select_item)) + + tokens.append((T, '\n')) + + # prepare the select choices + for i, choice in enumerate(self.choices): + _append(i, choice) + tokens.append((T, ' Answer: %d' % self.choices[self.pointer_index][0])) + return tokens + + def get_selected_value(self): + # get value not label + return self.choices[self.pointer_index][2] + + +def question(message, **kwargs): + # TODO extract common parts for list, checkbox, rawlist, expand + if not 'choices' in kwargs: + raise PromptParameterException('choices') + # this does not implement default, use checked... + # TODO + #if 'default' in kwargs: + # raise ValueError('rawlist does not implement \'default\' ' + # 'use \'checked\':True\' in choice!') + qmark = kwargs.pop('qmark', '?') + choices = kwargs.pop('choices', None) + if len(choices) > 9: + raise ValueError('rawlist supports only a maximum of 9 choices!') + + # TODO style defaults on detail level + style = kwargs.pop('style', default_style) + + ic = InquirerControl(choices) + + def get_prompt_tokens(cli): + tokens = [] + T = Token + + tokens.append((T.QuestionMark, qmark)) + tokens.append((T.Question, ' %s ' % message)) + if ic.answered: + tokens.append((T.Answer, ' %s' % ic.get_selected_value())) + return tokens + + # assemble layout + layout = HSplit([ + Window(height=D.exact(1), + content=TokenListControl(get_prompt_tokens) + ), + ConditionalContainer( + Window(ic), + filter=~IsDone() + ) + ]) + + # key bindings + manager = KeyBindingManager.for_prompt() + + @manager.registry.add_binding(Keys.ControlQ, eager=True) + @manager.registry.add_binding(Keys.ControlC, eager=True) + def _(event): + raise KeyboardInterrupt() + + # add key bindings for choices + for i, c in enumerate(ic.choices): + if not isinstance(c, Separator): + def _reg_binding(i, keys): + # trick out late evaluation with a "function factory": + # http://stackoverflow.com/questions/3431676/creating-functions-in-a-loop + @manager.registry.add_binding(keys, eager=True) + def select_choice(event): + ic.pointer_index = i + _reg_binding(i, '%d' % c[0]) + + @manager.registry.add_binding(Keys.Enter, eager=True) + def set_answer(event): + ic.answered = True + event.cli.set_return_value(ic.get_selected_value()) + + return Application( + layout=layout, + key_bindings_registry=manager.registry, + mouse_support=True, + style=style + ) diff --git a/src/libs/PyInquirer/separator.py b/src/libs/PyInquirer/separator.py new file mode 100644 index 0000000..663aba7 --- /dev/null +++ b/src/libs/PyInquirer/separator.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +""" +Used to space/separate choices group +""" + + +class Separator(object): + line = '-' * 15 + + def __init__(self, line=None): + if line: + self.line = line + + def __str__(self): + return self.line diff --git a/src/libs/PyInquirer/utils.py b/src/libs/PyInquirer/utils.py new file mode 100644 index 0000000..91d8a9c --- /dev/null +++ b/src/libs/PyInquirer/utils.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +import json +import sys +from pprint import pprint + +from pygments import highlight, lexers, formatters + +__version__ = '0.1.2' + +PY3 = sys.version_info[0] >= 3 + + +def format_json(data): + return json.dumps(data, sort_keys=True, indent=4) + + +def colorize_json(data): + if PY3: + if isinstance(data, bytes): + data = data.decode('UTF-8') + else: + if not isinstance(data, unicode): + data = unicode(data, 'UTF-8') + colorful_json = highlight(data, + lexers.JsonLexer(), + formatters.TerminalFormatter()) + return colorful_json + + +def print_json(data): + #colorful_json = highlight(unicode(format_json(data), 'UTF-8'), + # lexers.JsonLexer(), + # formatters.TerminalFormatter()) + pprint(colorize_json(format_json(data))) diff --git a/src/libs/prompt_toolkit/__init__.py b/src/libs/prompt_toolkit/__init__.py new file mode 100644 index 0000000..13a098f --- /dev/null +++ b/src/libs/prompt_toolkit/__init__.py @@ -0,0 +1,22 @@ +""" +libs.prompt_toolkit +============== + +Author: Jonathan Slenders + +Description: libs.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 meight also want to have a look at +`libs.prompt_toolkit.shortcuts.prompt`. +""" +from .interface import CommandLineInterface +from .application import AbortAction, Application +from .shortcuts import prompt, prompt_async + + +# Don't forget to update in `docs/conf.py`! +__version__ = '1.0.14' diff --git a/src/libs/prompt_toolkit/application.py b/src/libs/prompt_toolkit/application.py new file mode 100644 index 0000000..5df8cfd --- /dev/null +++ b/src/libs/prompt_toolkit/application.py @@ -0,0 +1,192 @@ +from __future__ import unicode_literals + +from .buffer import Buffer, AcceptAction +from .buffer_mapping import BufferMapping +from .clipboard import Clipboard, InMemoryClipboard +from .enums import DEFAULT_BUFFER, EditingMode +from .filters import CLIFilter, to_cli_filter +from .key_binding.bindings.basic import load_basic_bindings +from .key_binding.bindings.emacs import load_emacs_bindings +from .key_binding.bindings.vi import load_vi_bindings +from .key_binding.registry import BaseRegistry +from .key_binding.defaults import load_key_bindings +from .layout import Window +from .layout.containers import Container +from .layout.controls import BufferControl +from .styles import DEFAULT_STYLE, Style +import six + +__all__ = ( + 'AbortAction', + 'Application', +) + + +class AbortAction(object): + """ + Actions to take on an Exit or Abort exception. + """ + RETRY = 'retry' + RAISE_EXCEPTION = 'raise-exception' + RETURN_NONE = 'return-none' + + _all = (RETRY, RAISE_EXCEPTION, RETURN_NONE) + + +class Application(object): + """ + Application class to be passed to a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface`. + + This contains all customizable logic that is not I/O dependent. + (So, what is independent of event loops, input and output.) + + This way, such an :class:`.Application` can run easily on several + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` instances, each + with a different I/O backends. that runs for instance over telnet, SSH or + any other I/O backend. + + :param layout: A :class:`~libs.prompt_toolkit.layout.containers.Container` instance. + :param buffer: A :class:`~libs.prompt_toolkit.buffer.Buffer` instance for the default buffer. + :param initial_focussed_buffer: Name of the buffer that is focussed during start-up. + :param key_bindings_registry: + :class:`~libs.prompt_toolkit.key_binding.registry.BaseRegistry` instance for + the key bindings. + :param clipboard: :class:`~libs.prompt_toolkit.clipboard.base.Clipboard` to use. + :param on_abort: What to do when Control-C is pressed. + :param on_exit: What to do when Control-D is pressed. + :param use_alternate_screen: When True, run the application on the alternate screen buffer. + :param get_title: Callable that returns the current title to be displayed in the terminal. + :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. + + Filters: + + :param mouse_support: (:class:`~libs.prompt_toolkit.filters.CLIFilter` or + boolean). When True, enable mouse support. + :param paste_mode: :class:`~libs.prompt_toolkit.filters.CLIFilter` or boolean. + :param ignore_case: :class:`~libs.prompt_toolkit.filters.CLIFilter` or boolean. + :param editing_mode: :class:`~libs.prompt_toolkit.enums.EditingMode`. + + Callbacks (all of these should accept a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` object as input.) + + :param on_input_timeout: Called when there is no input for x seconds. + (Fired when any eventloop.onInputTimeout is fired.) + :param on_start: Called when reading input starts. + :param on_stop: Called when reading input ends. + :param on_reset: Called during reset. + :param on_buffer_changed: Called when the content of a buffer has been changed. + :param on_initialize: Called after the + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` initializes. + :param on_render: Called right after rendering. + :param on_invalidate: Called when the UI has been invalidated. + """ + def __init__(self, layout=None, buffer=None, buffers=None, + initial_focussed_buffer=DEFAULT_BUFFER, + style=None, + key_bindings_registry=None, clipboard=None, + on_abort=AbortAction.RAISE_EXCEPTION, on_exit=AbortAction.RAISE_EXCEPTION, + use_alternate_screen=False, mouse_support=False, + get_title=None, + + paste_mode=False, ignore_case=False, editing_mode=EditingMode.EMACS, + erase_when_done=False, + reverse_vi_search_direction=False, + + on_input_timeout=None, on_start=None, on_stop=None, + on_reset=None, on_initialize=None, on_buffer_changed=None, + on_render=None, on_invalidate=None): + + paste_mode = to_cli_filter(paste_mode) + ignore_case = to_cli_filter(ignore_case) + mouse_support = to_cli_filter(mouse_support) + reverse_vi_search_direction = to_cli_filter(reverse_vi_search_direction) + + assert layout is None or isinstance(layout, Container) + assert buffer is None or isinstance(buffer, Buffer) + assert buffers is None or isinstance(buffers, (dict, BufferMapping)) + assert key_bindings_registry is None or isinstance(key_bindings_registry, BaseRegistry) + assert clipboard is None or isinstance(clipboard, Clipboard) + assert on_abort in AbortAction._all + assert on_exit in AbortAction._all + assert isinstance(use_alternate_screen, bool) + assert get_title is None or callable(get_title) + assert isinstance(paste_mode, CLIFilter) + assert isinstance(ignore_case, CLIFilter) + assert isinstance(editing_mode, six.string_types) + assert on_input_timeout is None or callable(on_input_timeout) + assert style is None or isinstance(style, Style) + assert isinstance(erase_when_done, bool) + + assert on_start is None or callable(on_start) + assert on_stop is None or callable(on_stop) + assert on_reset is None or callable(on_reset) + assert on_buffer_changed is None or callable(on_buffer_changed) + assert on_initialize is None or callable(on_initialize) + assert on_render is None or callable(on_render) + assert on_invalidate is None or callable(on_invalidate) + + self.layout = layout or Window(BufferControl()) + + # Make sure that the 'buffers' dictionary is a BufferMapping. + # NOTE: If no buffer is given, we create a default Buffer, with IGNORE as + # default accept_action. This is what makes sense for most users + # creating full screen applications. Doing nothing is the obvious + # default. Those creating a REPL would use the shortcuts module that + # passes in RETURN_DOCUMENT. + self.buffer = buffer or Buffer(accept_action=AcceptAction.IGNORE) + if not buffers or not isinstance(buffers, BufferMapping): + self.buffers = BufferMapping(buffers, initial=initial_focussed_buffer) + else: + self.buffers = buffers + + if buffer: + self.buffers[DEFAULT_BUFFER] = buffer + + self.initial_focussed_buffer = initial_focussed_buffer + + self.style = style or DEFAULT_STYLE + + if key_bindings_registry is None: + key_bindings_registry = load_key_bindings() + + if get_title is None: + get_title = lambda: None + + self.key_bindings_registry = key_bindings_registry + self.clipboard = clipboard or InMemoryClipboard() + self.on_abort = on_abort + self.on_exit = on_exit + self.use_alternate_screen = use_alternate_screen + self.mouse_support = mouse_support + self.get_title = get_title + + self.paste_mode = paste_mode + self.ignore_case = ignore_case + self.editing_mode = editing_mode + self.erase_when_done = erase_when_done + self.reverse_vi_search_direction = reverse_vi_search_direction + + def dummy_handler(cli): + " Dummy event handler. " + + self.on_input_timeout = on_input_timeout or dummy_handler + self.on_start = on_start or dummy_handler + self.on_stop = on_stop or dummy_handler + self.on_reset = on_reset or dummy_handler + self.on_initialize = on_initialize or dummy_handler + self.on_buffer_changed = on_buffer_changed or dummy_handler + self.on_render = on_render or dummy_handler + self.on_invalidate = on_invalidate or dummy_handler + + # List of 'extra' functions to execute before a CommandLineInterface.run. + # Note: It's important to keep this here, and not in the + # CommandLineInterface itself. shortcuts.run_application creates + # a new Application instance everytime. (Which is correct, it + # could be that we want to detach from one IO backend and attach + # the UI on a different backend.) But important is to keep as + # much state as possible between runs. + self.pre_run_callables = [] diff --git a/src/libs/prompt_toolkit/auto_suggest.py b/src/libs/prompt_toolkit/auto_suggest.py new file mode 100644 index 0000000..d785801 --- /dev/null +++ b/src/libs/prompt_toolkit/auto_suggest.py @@ -0,0 +1,88 @@ +""" +`Fish-style `_ 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. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +from .filters import to_cli_filter + +__all__ = ( + 'Suggestion', + 'AutoSuggest', + 'AutoSuggestFromHistory', + 'ConditionalAutoSuggest', +) + + +class Suggestion(object): + """ + Suggestion returned by an auto-suggest algorithm. + + :param text: The suggestion text. + """ + def __init__(self, text): + self.text = text + + def __repr__(self): + return 'Suggestion(%s)' % self.text + + +class AutoSuggest(with_metaclass(ABCMeta, object)): + """ + Base class for auto suggestion implementations. + """ + @abstractmethod + def get_suggestion(self, cli, buffer, document): + """ + Return `None` or a :class:`.Suggestion` instance. + + We receive both ``buffer`` and ``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:`~libs.prompt_toolkit.buffer.Buffer` instance. + :param document: The :class:`~libs.prompt_toolkit.document.Document` instance. + """ + + +class AutoSuggestFromHistory(AutoSuggest): + """ + Give suggestions based on the lines in the history. + """ + def get_suggestion(self, cli, buffer, document): + 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)): + for line in reversed(string.splitlines()): + if line.startswith(text): + return Suggestion(line[len(text):]) + + +class ConditionalAutoSuggest(AutoSuggest): + """ + Auto suggest that can be turned on and of according to a certain condition. + """ + def __init__(self, auto_suggest, filter): + assert isinstance(auto_suggest, AutoSuggest) + + self.auto_suggest = auto_suggest + self.filter = to_cli_filter(filter) + + def get_suggestion(self, cli, buffer, document): + if self.filter(cli): + return self.auto_suggest.get_suggestion(cli, buffer, document) diff --git a/src/libs/prompt_toolkit/buffer.py b/src/libs/prompt_toolkit/buffer.py new file mode 100644 index 0000000..5bc04f6 --- /dev/null +++ b/src/libs/prompt_toolkit/buffer.py @@ -0,0 +1,1415 @@ +""" +Data structures for the Buffer. +It holds the text, cursor position, history, etc... +""" +from __future__ import unicode_literals + +from .auto_suggest import AutoSuggest +from .clipboard import ClipboardData +from .completion import Completer, Completion, CompleteEvent +from .document import Document +from .enums import IncrementalSearchDirection +from .filters import to_simple_filter +from .history import History, InMemoryHistory +from .search_state import SearchState +from .selection import SelectionType, SelectionState, PasteMode +from .utils import Event +from .cache import FastDictCache +from .validation import ValidationError + +from six.moves import range + +import os +import re +import shlex +import six +import subprocess +import tempfile + +__all__ = ( + 'EditReadOnlyBuffer', + 'AcceptAction', + 'Buffer', + 'indent', + 'unindent', + 'reshape_text', +) + + +class EditReadOnlyBuffer(Exception): + " Attempt editing of read-only :class:`.Buffer`. " + + +class AcceptAction(object): + """ + What to do when the input is accepted by the user. + (When Enter was pressed in the command line.) + + :param handler: (optional) A callable which takes a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` and + :class:`~libs.prompt_toolkit.document.Document`. It is called when the user + accepts input. + """ + def __init__(self, handler=None): + assert handler is None or callable(handler) + self.handler = handler + + @classmethod + def run_in_terminal(cls, handler, render_cli_done=False): + """ + Create an :class:`.AcceptAction` that runs the given handler in the + terminal. + + :param render_cli_done: When True, render the interface in the 'Done' + state first, then execute the function. If False, erase the + interface instead. + """ + def _handler(cli, buffer): + cli.run_in_terminal(lambda: handler(cli, buffer), render_cli_done=render_cli_done) + return AcceptAction(handler=_handler) + + @property + def is_returnable(self): + """ + True when there is something handling accept. + """ + return bool(self.handler) + + def validate_and_handle(self, cli, buffer): + """ + Validate buffer and handle the accept action. + """ + if buffer.validate(): + if self.handler: + self.handler(cli, buffer) + + buffer.append_to_history() + + +def _return_document_handler(cli, buffer): + # Set return value. + cli.set_return_value(buffer.document) + + # Make sure that if we run this UI again, that we reset this buffer, next + # time. + def reset_this_buffer(): + buffer.reset() + cli.pre_run_callables.append(reset_this_buffer) + + +AcceptAction.RETURN_DOCUMENT = AcceptAction(_return_document_handler) +AcceptAction.IGNORE = AcceptAction(handler=None) + + +class ValidationState(object): + " The validation state of a buffer. This is set after the validation. " + VALID = 'VALID' + INVALID = 'INVALID' + UNKNOWN = 'UNKNOWN' + + +class CompletionState(object): + """ + Immutable class that contains a completion state. + """ + def __init__(self, original_document, current_completions=None, complete_index=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.current_completions = current_completions or [] + + #: Position in the `current_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): + return '%s(%r, <%r> completions, index=%r)' % ( + self.__class__.__name__, + self.original_document, len(self.current_completions), self.complete_index) + + def go_to_index(self, index): + """ + Create a new :class:`.CompletionState` object with the new index. + """ + return CompletionState(self.original_document, self.current_completions, complete_index=index) + + def new_text_and_position(self): + """ + 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.current_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): + """ + Return the current completion, or return `None` when no completion is + selected. + """ + if self.complete_index is not None: + return self.current_completions[self.complete_index] + + +_QUOTED_WORDS_RE = re.compile(r"""(\s+|".*?"|'.*?')""") + + +class YankNthArgState(object): + """ + For yank-last-arg/yank-nth-arg: Keep track of where we are in the history. + """ + def __init__(self, history_position=0, n=-1, previous_inserted_word=''): + self.history_position = history_position + self.previous_inserted_word = previous_inserted_word + self.n = n + + def __repr__(self): + return '%s(history_position=%r, n=%r, previous_inserted_word=%r)' % ( + self.__class__.__name__, self.history_position, self.n, + self.previous_inserted_word) + + +class Buffer(object): + """ + The core data structure that holds the text and cursor position of the + current input line and implements all text manupulations on top of it. It + also implements the history, undo stack and the completion state. + + :param completer: :class:`~libs.prompt_toolkit.completion.Completer` instance. + :param history: :class:`~libs.prompt_toolkit.history.History` instance. + :param tempfile_suffix: Suffix to be appended to the tempfile for the 'open + in editor' function. + + Events: + + :param on_text_changed: When the buffer text changes. (Callable on None.) + :param on_text_insert: When new text is inserted. (Callable on None.) + :param on_cursor_position_changed: When the cursor moves. (Callable on None.) + + Filters: + + :param is_multiline: :class:`~libs.prompt_toolkit.filters.SimpleFilter` to + indicate whether we should consider this buffer a multiline input. If + so, key bindings can decide to insert newlines when pressing [Enter]. + (Instead of accepting the input.) + :param complete_while_typing: :class:`~libs.prompt_toolkit.filters.SimpleFilter` + instance. Decide whether or not to do asynchronous autocompleting while + typing. + :param enable_history_search: :class:`~libs.prompt_toolkit.filters.SimpleFilter` + to indicate when up-arrow partial string matching is enabled. It is + adviced 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:`~libs.prompt_toolkit.filters.SimpleFilter`. When True, + changes will not be allowed. + """ + def __init__(self, completer=None, auto_suggest=None, history=None, + validator=None, tempfile_suffix='', + is_multiline=False, complete_while_typing=False, + enable_history_search=False, initial_document=None, + accept_action=AcceptAction.IGNORE, read_only=False, + on_text_changed=None, on_text_insert=None, on_cursor_position_changed=None): + + # Accept both filters and booleans as input. + enable_history_search = to_simple_filter(enable_history_search) + is_multiline = to_simple_filter(is_multiline) + complete_while_typing = to_simple_filter(complete_while_typing) + read_only = to_simple_filter(read_only) + + # Validate input. + assert completer is None or isinstance(completer, Completer) + assert auto_suggest is None or isinstance(auto_suggest, AutoSuggest) + assert history is None or isinstance(history, History) + assert on_text_changed is None or callable(on_text_changed) + assert on_text_insert is None or callable(on_text_insert) + assert on_cursor_position_changed is None or callable(on_cursor_position_changed) + + self.completer = completer + self.auto_suggest = auto_suggest + self.validator = validator + self.tempfile_suffix = tempfile_suffix + self.accept_action = accept_action + + # Filters. (Usually, used by the key bindings to drive the buffer.) + self.is_multiline = is_multiline + self.complete_while_typing = complete_while_typing + self.enable_history_search = enable_history_search + self.read_only = read_only + + # 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(self, on_text_changed) + self.on_text_insert = Event(self, on_text_insert) + self.on_cursor_position_changed = Event(self, on_cursor_position_changed) + + # Document cache. (Avoid creating new Document instances.) + self._document_cache = FastDictCache(Document, size=10) + + self.reset(initial_document=initial_document) + + def reset(self, initial_document=None, append_to_history=False): + """ + :param append_to_history: Append current input to history first. + """ + assert initial_document is None or isinstance(initial_document, Document) + + if append_to_history: + self.append_to_history() + + initial_document = initial_document or Document() + + self.__cursor_position = initial_document.cursor_position + + # `ValidationError` instance. (Will be set when the input is wrong.) + self.validation_error = None + self.validation_state = ValidationState.UNKNOWN + + # State of the selection. + self.selection_state = 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 = [] + + # When doing consecutive up/down movements, prefer to stay at this column. + self.preferred_column = None + + # State of complete browser + self.complete_state = None # For interactive completion through Ctrl-N/Ctrl-P. + + # State of Emacs yank-nth-arg completion. + self.yank_nth_arg_state = 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 = None + + # Current suggestion. + self.suggestion = None + + # The history search text. (Used for filtering the history when we + # browse through it.) + self.history_search_text = None + + # Undo/redo stacks + self._undo_stack = [] # Stack of (text, cursor_position) + self._redo_stack = [] + + #: 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 = self.history.strings[:] + self._working_lines.append(initial_document.text) + self.__working_index = len(self._working_lines) - 1 + + # + + def _set_text(self, value): + """ 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): + """ Set cursor position. Return whether it changed. """ + original_position = self.__cursor_position + self.__cursor_position = max(0, value) + + return value != original_position + + @property + def text(self): + return self._working_lines[self.working_index] + + @text.setter + def text(self, value): + """ + 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.) + """ + assert isinstance(value, six.text_type), 'Got %r' % value + assert 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. + self.history_search_text = None + + @property + def cursor_position(self): + return self.__cursor_position + + @cursor_position.setter + def cursor_position(self, value): + """ + Setting cursor position. + """ + assert isinstance(value, int) + assert value <= len(self.text) + + changed = self._set_cursor_position(value) + + if changed: + self._cursor_position_changed() + + @property + def working_index(self): + return self.__working_index + + @working_index.setter + def working_index(self, value): + if self.__working_index != value: + self.__working_index = value + self._text_changed() + + def _text_changed(self): + # 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() + + def _cursor_position_changed(self): + # 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 + + # 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): + """ + Return :class:`~libs.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): + """ + Set :class:`~libs.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, bypass_readonly=False): + """ + Set :class:`~libs.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. + """ + assert isinstance(value, Document) + + # 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() + + if cursor_position_changed: + self._cursor_position_changed() + + # End of + + def save_to_undo_stack(self, clear_redo_stack=True): + """ + 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, transform_callback): + """ + 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): + """ + 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_, to, transform_callback): + """ + 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=1): + self.cursor_position += self.document.get_cursor_left_position(count=count) + + def cursor_right(self, count=1): + self.cursor_position += self.document.get_cursor_right_position(count=count) + + def cursor_up(self, count=1): + """ (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=1): + """ (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=1, go_to_start_of_line_if_history_changes=False): + """ + 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=1, go_to_start_of_line_if_history_changes=False): + """ + 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=1): + """ + 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=1): + """ + 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=' '): + """ + 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=' '): + """ + 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): + """ + 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): + """ + 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=1, disable_wrap_around=False): + """ + Browse to the next completions. + (Does nothing if there are no completion.) + """ + if self.complete_state: + completions_count = len(self.complete_state.current_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=1, disable_wrap_around=False): + """ + Browse to the previous completions. + (Does nothing if there are no completion.) + """ + 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.current_completions) - 1 + else: + index = max(0, self.complete_state.complete_index - count) + + self.go_to_completion(index) + + def cancel_completion(self): + """ + 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, go_to_first=True, go_to_last=False): + """ + Start completions. (Generate list of completions and initialize.) + """ + assert not (go_to_first and go_to_last) + + # Generate list of all completions. + if completions is None: + if self.completer: + completions = list(self.completer.get_completions( + self.document, + CompleteEvent(completion_requested=True) + )) + else: + completions = [] + + # Set `complete_state`. + if completions: + self.complete_state = CompletionState( + original_document=self.document, + current_completions=completions) + if go_to_first: + self.go_to_completion(0) + elif go_to_last: + self.go_to_completion(len(completions) - 1) + else: + self.go_to_completion(None) + + else: + self.complete_state = None + + def start_history_lines_completion(self): + """ + Start a completion based on all the other lines in the document and the + history. + """ + found_completions = 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 = "History %s, line %s" % (i+1, j+1) + + completions.append(Completion( + l, + start_position=-len(current_line), + display_meta=display_meta)) + + self.set_completions(completions=completions[::-1]) + + def go_to_completion(self, index): + """ + Select a completion from the list of current completions. + """ + assert index is None or isinstance(index, int) + assert self.complete_state + + # Set new completion + state = self.complete_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): + """ + Insert a given completion. + """ + assert isinstance(completion, 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): + """ Set `history_search_text`. """ + if self.enable_history_search(): + if self.history_search_text is None: + self.history_search_text = self.text + else: + self.history_search_text = None + + def _history_matches(self, i): + """ + 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=1): + """ + 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=1): + """ + 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=None, _yank_last_arg=False): + """ + 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) + + if not len(self.history): + 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(self.history): + new_pos = -1 + + # Take argument from line. + line = self.history[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=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.CHARACTERS): + """ + 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=False): + """ + Copy selected text and return :class:`.ClipboardData` instance. + """ + 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): + """ + Delete selected text and return :class:`.ClipboardData` instance. + """ + return self.copy_selection(_cut=True) + + def paste_clipboard_data(self, data, paste_mode=PasteMode.EMACS, count=1): + """ + 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=True): + """ + 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=True): + """ + 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=True): + """ + 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, overwrite=False, move_cursor=True, fire_event=True): + """ + 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')] + + self.text = otext[:ocpos] + data + otext[ocpos + len(overwritten_text):] + else: + self.text = otext[:ocpos] + data + otext[ocpos:] + + if move_cursor: + self.cursor_position += len(data) + + # Fire 'on_text_insert' event. + if fire_event: + self.on_text_insert.fire() + + def undo(self): + # 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): + 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): + """ + Returns `True` if valid. + """ + # 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 + + # Validate first. If not valid, set validation exception. + if self.validator: + try: + self.validator.validate(self.document) + except ValidationError as e: + # Set cursor position (don't allow invalid values.) + cursor_position = e.cursor_position + self.cursor_position = min(max(0, cursor_position), len(self.text)) + + self.validation_state = ValidationState.INVALID + self.validation_error = e + return False + + self.validation_state = ValidationState.VALID + self.validation_error = None + return True + + def append_to_history(self): + """ + Append the current input to the history. + (Only if valid input.) + """ + # Validate first. If not valid, set validation exception. + if not self.validate(): + return + + # Save at the tail of the history. (But don't if the last entry the + # history is already the same.) + if self.text and (not len(self.history) or self.history[-1] != self.text): + self.history.append(self.text) + + def _search(self, search_state, include_current_position=False, count=1): + """ + Execute search. Return (working_index, cursor_position) tuple when this + search is applied. Returns `None` when this text cannot be found. + """ + assert isinstance(search_state, SearchState) + assert isinstance(count, int) and count > 0 + + text = search_state.text + direction = search_state.direction + ignore_case = search_state.ignore_case() + + def search_once(working_index, document): + """ + Do search one time. + Return (working_index, document) or `None` + """ + if direction == IncrementalSearchDirection.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)) + + # 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 # Nothing found. + else: + working_index, document = result + + return (working_index, document.cursor_position) + + def document_for_search(self, search_state): + """ + Return a :class:`~libs.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:`~libs.prompt_toolkit.layout.controls.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, include_current_position=True, count=1): + """ + 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, include_current_position=True, count=1): + """ + 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): + self.selection_state = None + + def open_in_editor(self, cli): + """ + Open code in editor. + + :param cli: :class:`~libs.prompt_toolkit.interface.CommandLineInterface` + instance. + """ + if self.read_only(): + raise EditReadOnlyBuffer() + + # Write to temporary file + descriptor, filename = tempfile.mkstemp(self.tempfile_suffix) + os.write(descriptor, self.text.encode('utf-8')) + os.close(descriptor) + + # Open in editor + # (We need to use `cli.run_in_terminal`, because not all editors go to + # the alternate screen buffer, and some could influence the cursor + # position.) + succes = cli.run_in_terminal(lambda: self._open_file_in_editor(filename)) + + # Read content again. + if succes: + 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)) + + # Clean up temp file. + os.remove(filename) + + def _open_file_in_editor(self, filename): + """ + 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 indent(buffer, from_row, to_row, count=1): + """ + Indent text of a :class:`.Buffer` object. + """ + current_row = buffer.document.cursor_position_row + line_range = range(from_row, to_row) + + # Apply transformation. + new_text = buffer.transform_lines(line_range, lambda l: ' ' * count + l) + buffer.document = Document( + new_text, + Document(new_text).translate_row_col_to_index(current_row, 0)) + + # Go to the start of the line. + buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True) + + +def unindent(buffer, from_row, to_row, count=1): + """ + Unindent text of a :class:`.Buffer` object. + """ + current_row = buffer.document.cursor_position_row + line_range = range(from_row, to_row) + + def transform(text): + remove = ' ' * count + 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)) + + # Go to the start of the line. + buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True) + + +def reshape_text(buffer, from_row, to_row): + """ + 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. + length = re.search(r'^\s*', lines_to_reformat[0]).end() + 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/libs/prompt_toolkit/buffer_mapping.py b/src/libs/prompt_toolkit/buffer_mapping.py new file mode 100644 index 0000000..fbc46f4 --- /dev/null +++ b/src/libs/prompt_toolkit/buffer_mapping.py @@ -0,0 +1,92 @@ +""" +The BufferMapping contains all the buffers for a command line interface, and it +keeps track of which buffer gets the focus. +""" +from __future__ import unicode_literals +from .enums import DEFAULT_BUFFER, SEARCH_BUFFER, SYSTEM_BUFFER, DUMMY_BUFFER +from .buffer import Buffer, AcceptAction +from .history import InMemoryHistory + +import six + +__all__ = ( + 'BufferMapping', +) + + +class BufferMapping(dict): + """ + Dictionary that maps the name of the buffers to the + :class:`~libs.prompt_toolkit.buffer.Buffer` instances. + + This mapping also keeps track of which buffer currently has the focus. + (Some methods receive a 'cli' parameter. This is useful for applications + where this `BufferMapping` is shared between several applications.) + """ + def __init__(self, buffers=None, initial=DEFAULT_BUFFER): + assert buffers is None or isinstance(buffers, dict) + + # Start with an empty dict. + super(BufferMapping, self).__init__() + + # Add default buffers. + self.update({ + # For the 'search' and 'system' buffers, 'returnable' is False, in + # order to block normal Enter/ControlC behaviour. + DEFAULT_BUFFER: Buffer(accept_action=AcceptAction.RETURN_DOCUMENT), + SEARCH_BUFFER: Buffer(history=InMemoryHistory(), accept_action=AcceptAction.IGNORE), + SYSTEM_BUFFER: Buffer(history=InMemoryHistory(), accept_action=AcceptAction.IGNORE), + DUMMY_BUFFER: Buffer(read_only=True), + }) + + # Add received buffers. + if buffers is not None: + self.update(buffers) + + # Focus stack. + self.focus_stack = [initial or DEFAULT_BUFFER] + + def current(self, cli): + """ + The active :class:`.Buffer`. + """ + return self[self.focus_stack[-1]] + + def current_name(self, cli): + """ + The name of the active :class:`.Buffer`. + """ + return self.focus_stack[-1] + + def previous(self, cli): + """ + Return the previously focussed :class:`.Buffer` or `None`. + """ + if len(self.focus_stack) > 1: + try: + return self[self.focus_stack[-2]] + except KeyError: + pass + + def focus(self, cli, buffer_name): + """ + Focus the buffer with the given name. + """ + assert isinstance(buffer_name, six.text_type) + self.focus_stack = [buffer_name] + + def push_focus(self, cli, buffer_name): + """ + Push buffer on the focus stack. + """ + assert isinstance(buffer_name, six.text_type) + self.focus_stack.append(buffer_name) + + def pop_focus(self, cli): + """ + Pop buffer from the focus stack. + """ + if len(self.focus_stack) > 1: + self.focus_stack.pop() + else: + raise IndexError('Cannot pop last item from the focus stack.') diff --git a/src/libs/prompt_toolkit/cache.py b/src/libs/prompt_toolkit/cache.py new file mode 100644 index 0000000..0648e97 --- /dev/null +++ b/src/libs/prompt_toolkit/cache.py @@ -0,0 +1,111 @@ +from __future__ import unicode_literals +from collections import deque +from functools import wraps + +__all__ = ( + 'SimpleCache', + 'FastDictCache', + 'memoized', +) + + +class SimpleCache(object): + """ + 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=8): + assert isinstance(maxsize, int) and maxsize > 0 + + self._data = {} + self._keys = deque() + self.maxsize = maxsize + + def get(self, key, getter_func): + """ + 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): + " Clear cache. " + self._data = {} + self._keys = deque() + + +class FastDictCache(dict): + """ + 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 `libs.prompt_toolkit.layout.screen.Char` and + # `libs.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=None, size=1000000): + assert callable(get_value) + assert isinstance(size, int) and size > 0 + + self._keys = deque() + self.get_value = get_value + self.size = size + + def __missing__(self, key): + # 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 + + +def memoized(maxsize=1024): + """ + Momoization decorator for immutable classes and pure functions. + """ + cache = SimpleCache(maxsize=maxsize) + + def decorator(obj): + @wraps(obj) + def new_callable(*a, **kw): + def create_new(): + return obj(*a, **kw) + + key = (a, tuple(kw.items())) + return cache.get(key, create_new) + return new_callable + return decorator diff --git a/src/libs/prompt_toolkit/clipboard/__init__.py b/src/libs/prompt_toolkit/clipboard/__init__.py new file mode 100644 index 0000000..56202dd --- /dev/null +++ b/src/libs/prompt_toolkit/clipboard/__init__.py @@ -0,0 +1,8 @@ +from .base import Clipboard, ClipboardData +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 diff --git a/src/libs/prompt_toolkit/clipboard/base.py b/src/libs/prompt_toolkit/clipboard/base.py new file mode 100644 index 0000000..71290d7 --- /dev/null +++ b/src/libs/prompt_toolkit/clipboard/base.py @@ -0,0 +1,62 @@ +""" +Clipboard for command line interface. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass +import six + +from libs.prompt_toolkit.selection import SelectionType + +__all__ = ( + 'Clipboard', + 'ClipboardData', +) + + +class ClipboardData(object): + """ + Text on the clipboard. + + :param text: string + :param type: :class:`~prompt_toolkit.selection.SelectionType` + """ + def __init__(self, text='', type=SelectionType.CHARACTERS): + assert isinstance(text, six.string_types) + assert type in (SelectionType.CHARACTERS, SelectionType.LINES, SelectionType.BLOCK) + + self.text = text + self.type = type + + +class Clipboard(with_metaclass(ABCMeta, object)): + """ + 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): + """ + Set data to the clipboard. + + :param data: :class:`~.ClipboardData` instance. + """ + + def set_text(self, text): # Not abstract. + """ + Shortcut for setting plain text on clipboard. + """ + assert isinstance(text, six.string_types) + self.set_data(ClipboardData(text)) + + def rotate(self): + """ + For Emacs mode, rotate the kill ring. + """ + + @abstractmethod + def get_data(self): + """ + Return clipboard data. + """ diff --git a/src/libs/prompt_toolkit/clipboard/in_memory.py b/src/libs/prompt_toolkit/clipboard/in_memory.py new file mode 100644 index 0000000..081666a --- /dev/null +++ b/src/libs/prompt_toolkit/clipboard/in_memory.py @@ -0,0 +1,42 @@ +from .base import Clipboard, ClipboardData + +from collections import deque + +__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=None, max_size=60): + assert data is None or isinstance(data, ClipboardData) + assert max_size >= 1 + + self.max_size = max_size + self._ring = deque() + if data is not None: + self.set_data(data) + + def set_data(self, data): + assert isinstance(data, ClipboardData) + self._ring.appendleft(data) + + while len(self._ring) > self.max_size: + self._ring.pop() + + def get_data(self): + if self._ring: + return self._ring[0] + else: + return ClipboardData() + + def rotate(self): + if self._ring: + # Add the very first item at the end. + self._ring.append(self._ring.popleft()) diff --git a/src/libs/prompt_toolkit/clipboard/pyperclip.py b/src/libs/prompt_toolkit/clipboard/pyperclip.py new file mode 100644 index 0000000..f2b3b53 --- /dev/null +++ b/src/libs/prompt_toolkit/clipboard/pyperclip.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, unicode_literals +import pyperclip + +from libs.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): + self._data = None + + def set_data(self, data): + assert isinstance(data, ClipboardData) + self._data = data + pyperclip.copy(data.text) + + def get_data(self): + 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.LINES) diff --git a/src/libs/prompt_toolkit/completion.py b/src/libs/prompt_toolkit/completion.py new file mode 100644 index 0000000..2d271dd --- /dev/null +++ b/src/libs/prompt_toolkit/completion.py @@ -0,0 +1,170 @@ +""" +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +__all__ = ( + 'Completion', + 'Completer', + 'CompleteEvent', + 'get_common_complete_suffix', +) + + +class Completion(object): + """ + :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) If the completion has to be displayed + differently in the completion menu. + :param display_meta: (Optional string) Meta information about the + completion, e.g. the path or source where it's coming from. + :param get_display_meta: Lazy `display_meta`. Retrieve meta information + only when meta is displayed. + """ + def __init__(self, text, start_position=0, display=None, display_meta=None, + get_display_meta=None): + self.text = text + self.start_position = start_position + self._display_meta = display_meta + self._get_display_meta = get_display_meta + + if display is None: + self.display = text + else: + self.display = display + + assert self.start_position <= 0 + + def __repr__(self): + if self.display == self.text: + return '%s(text=%r, start_position=%r)' % ( + self.__class__.__name__, self.text, self.start_position) + else: + return '%s(text=%r, start_position=%r, display=%r)' % ( + self.__class__.__name__, self.text, self.start_position, + self.display) + + def __eq__(self, other): + 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): + return hash((self.text, self.start_position, self.display, self.display_meta)) + + @property + def display_meta(self): + # Return meta-text. (This is lazy when using "get_display_meta".) + if self._display_meta is not None: + return self._display_meta + + elif self._get_display_meta: + self._display_meta = self._get_display_meta() + return self._display_meta + + else: + return '' + + def new_completion_from_position(self, position): + """ + (Only for internal use!) + Get a new completion by splitting this one. Used by + `CommandLineInterface` when it needs to have a list of new completions + after inserting the common prefix. + """ + assert isinstance(position, int) and position - self.start_position >= 0 + + return Completion( + text=self.text[position - self.start_position:], + display=self.display, + display_meta=self._display_meta, + get_display_meta=self._get_display_meta) + + +class CompleteEvent(object): + """ + 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 explicitely + pressed the `Tab` key in order to view the completions. + + These two flags can be used for instance to implemented 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=False, completion_requested=False): + assert not (text_inserted and completion_requested) + + #: Automatic completion while typing. + self.text_inserted = text_inserted + + #: Used explicitely requested completion by pressing 'tab'. + self.completion_requested = completion_requested + + def __repr__(self): + return '%s(text_inserted=%r, completion_requested=%r)' % ( + self.__class__.__name__, self.text_inserted, self.completion_requested) + + +class Completer(with_metaclass(ABCMeta, object)): + """ + Base class for completer implementations. + """ + @abstractmethod + def get_completions(self, document, complete_event): + """ + Yield :class:`.Completion` instances. + + :param document: :class:`~libs.prompt_toolkit.document.Document` instance. + :param complete_event: :class:`.CompleteEvent` instance. + """ + while False: + yield + + +def get_common_complete_suffix(document, completions): + """ + 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): + 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): + return completion.text[-completion.start_position:] + + return _commonprefix([get_suffix(c) for c in completions2]) + + +def _commonprefix(strings): + # 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/libs/prompt_toolkit/contrib/__init__.py b/src/libs/prompt_toolkit/contrib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/libs/prompt_toolkit/contrib/completers/__init__.py b/src/libs/prompt_toolkit/contrib/completers/__init__.py new file mode 100644 index 0000000..43893b8 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/completers/__init__.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + +from .filesystem import PathCompleter +from .base import WordCompleter +from .system import SystemCompleter diff --git a/src/libs/prompt_toolkit/contrib/completers/base.py b/src/libs/prompt_toolkit/contrib/completers/base.py new file mode 100644 index 0000000..65a69fe --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/completers/base.py @@ -0,0 +1,61 @@ +from __future__ import unicode_literals + +from six import string_types +from prompt_toolkit.completion import Completer, Completion + +__all__ = ( + 'WordCompleter', +) + + +class WordCompleter(Completer): + """ + Simple autocompletion on a list of words. + + :param words: List of words. + :param ignore_case: If True, case-insensitive completion. + :param meta_dict: Optional dict mapping words to their meta-information. + :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. + """ + def __init__(self, words, ignore_case=False, meta_dict=None, WORD=False, + sentence=False, match_middle=False): + assert not (WORD and sentence) + + self.words = list(words) + self.ignore_case = ignore_case + self.meta_dict = meta_dict or {} + self.WORD = WORD + self.sentence = sentence + self.match_middle = match_middle + assert all(isinstance(w, string_types) for w in self.words) + + def get_completions(self, document, complete_event): + # 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) + + if self.ignore_case: + word_before_cursor = word_before_cursor.lower() + + def word_matches(word): + """ 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 self.words: + if word_matches(a): + display_meta = self.meta_dict.get(a, '') + yield Completion(a, -len(word_before_cursor), display_meta=display_meta) diff --git a/src/libs/prompt_toolkit/contrib/completers/filesystem.py b/src/libs/prompt_toolkit/contrib/completers/filesystem.py new file mode 100644 index 0000000..cbd74d8 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/completers/filesystem.py @@ -0,0 +1,105 @@ +from __future__ import unicode_literals + +from prompt_toolkit.completion import Completer, Completion +import os + +__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=False, get_paths=None, file_filter=None, + min_input_len=0, expanduser=False): + assert get_paths is None or callable(get_paths) + assert file_filter is None or callable(file_filter) + assert isinstance(min_input_len, int) + assert isinstance(expanduser, bool) + + 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, complete_event): + 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 themself.) + filename += '/' + elif self.only_directories: + continue + + if not self.file_filter(full_name): + continue + + yield Completion(completion, 0, display=filename) + except OSError: + pass + + +class ExecutableCompleter(PathCompleter): + """ + Complete only excutable files in the current path. + """ + def __init__(self): + PathCompleter.__init__( + self, + 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/libs/prompt_toolkit/contrib/completers/system.py b/src/libs/prompt_toolkit/contrib/completers/system.py new file mode 100644 index 0000000..76d6c1f --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/completers/system.py @@ -0,0 +1,56 @@ +from __future__ import unicode_literals + +from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter +from prompt_toolkit.contrib.regular_languages.compiler import compile + +from .filesystem import PathCompleter, ExecutableCompleter + +__all__ = ( + 'SystemCompleter', +) + + +class SystemCompleter(GrammarCompleter): + """ + Completer for system commands. + """ + def __init__(self): + # Compile grammar. + g = compile( + r""" + # First we have an executable. + (?P[^\s]+) + + # Ignore literals in between. + ( + \s+ + ("[^"]*" | '[^']*' | [^'"]+ ) + )* + + \s+ + + # Filename as parameters. + ( + (?P[^\s]+) | + "(?P[^\s]+)" | + '(?P[^\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 enterily correct. + 'single_quoted_filename': (lambda string: string.replace("\\'", "'")), + }) + + # Create GrammarCompleter + super(SystemCompleter, self).__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/libs/prompt_toolkit/contrib/regular_languages/__init__.py b/src/libs/prompt_toolkit/contrib/regular_languages/__init__.py new file mode 100644 index 0000000..314cb1f --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/regular_languages/__init__.py @@ -0,0 +1,76 @@ +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 colour. +- 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...)`` 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: + + Ofter 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 prefixes +of strings that would match the first expression. (We translate it into +multiple expression, because we want to have each possible state the regex +could be in -- in case there are several or-clauses with each different +completers.) + + +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 .compiler import compile diff --git a/src/libs/prompt_toolkit/contrib/regular_languages/compiler.py b/src/libs/prompt_toolkit/contrib/regular_languages/compiler.py new file mode 100644 index 0000000..01476bf --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/regular_languages/compiler.py @@ -0,0 +1,408 @@ +r""" +Compiler for a regular grammar. + +Example usage:: + + # Create and compile grammar. + p = compile('add \s+ (?P[^\s]+) \s+ (?P[^\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[^\s]+) \s+ (?P[^\s]+) \s+ (?P[^\s]+)) | + + # Operators with only one arguments. + ((?P[^\s]+) \s+ (?P[^\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 unicode_literals +import re + +from six.moves import range +from .regex_parser import Any, Sequence, Regex, Variable, Repeat, Lookahead +from .regex_parser import 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' + + +class _CompiledGrammar(object): + """ + 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, escape_funcs=None, unescape_funcs=None): + self.root_node = root_node + self.escape_funcs = escape_funcs or {} + self.unescape_funcs = unescape_funcs or {} + + #: Dictionary that will map the redex names to Node instances. + self._group_names_to_nodes = {} # Maps regex group names to varnames. + counter = [0] + + def create_group_func(node): + 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'(?:%s)(?P<%s>.*?)$' % (t.rstrip('$'), _INVALID_TRAILING_INPUT), flags) + for t in self._re_prefix_patterns] + + def escape(self, varname, value): + """ + 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, value): + """ + Unescape `value`. + """ + f = self.unescape_funcs.get(varname) + return f(value) if f else value + + @classmethod + def _transform(cls, root_node, create_group_func): + """ + 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): + # Turn `Any` into an OR. + if isinstance(node, Any): + return '(?:%s)' % '|'.join(transform(c) for c in node.children) + + # Concatenate a `Sequence` + elif isinstance(node, Sequence): + 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 '(?P<%s>%s)' % (create_group_func(node), transform(node.childnode)) + + # `Repeat`. + elif isinstance(node, Repeat): + return '(?:%s){%i,%s}%s' % ( + transform(node.childnode), node.min_repeat, + ('' if node.max_repeat is None else str(node.max_repeat)), + ('' if node.greedy else '?') + ) + else: + raise TypeError('Got %r' % (node, )) + + return transform(root_node) + + @classmethod + def _transform_prefix(cls, root_node, create_group_func): + """ + Yield all the regular expressions matching a prefix of the grammar + defined by the `Node` instance. + + This can yield multiple expressions, because in the case of on OR + operation in the grammar, we can have another outcome depending on + which clause would appear first. E.g. "(A|B)C" is not the same as + "(B|A)C" because the regex engine is lazy and takes the first match. + However, because we the current input is actually a prefix of the + grammar which meight not yet contain the data for "C", we need to know + both intermediate states, in order to call the appropriate + autocompletion for both cases. + + :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): + # Generate regexes for all permutations of this OR. Each node + # should be in front once. + if isinstance(node, Any): + for c in node.children: + for r in transform(c): + yield '(?:%s)?' % r + + # For a sequence. We can either have a match for the sequence + # of all the children, or for an exact match of the first X + # children, followed by a partial match of the next children. + elif isinstance(node, Sequence): + for i in range(len(node.children)): + a = [cls._transform(c, create_group_func) for c in node.children[:i]] + for c in transform(node.children[i]): + yield '(?:%s)' % (''.join(a) + c) + + 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 in transform(node.childnode): + yield '(?P<%s>%s)' % (create_group_func(node), c) + + 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) + + for c in transform(node.childnode): + if node.max_repeat: + repeat_sign = '{,%i}' % (node.max_repeat - 1) + else: + repeat_sign = '*' + yield '(?:%s)%s%s(?:%s)?' % ( + prefix, + repeat_sign, + ('' if node.greedy else '?'), + c) + + else: + raise TypeError('Got %r' % node) + + for r in transform(root_node): + yield '^%s$' % r + + def match(self, string): + """ + 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) + + def match_prefix(self, string): + """ + 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] + matches = [(r, m) for r, m in matches if m] + + if matches != []: + return Match(string, matches, self._group_names_to_nodes, self.unescape_funcs) + + +class Match(object): + """ + :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, re_matches, group_names_to_nodes, unescape_funcs): + 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): + """ + Return a list of (varname, reg) tuples. + """ + def get_tuples(): + for r, re_match in self._re_matches: + for group_name, group_index in r.groupindex.items(): + if group_name != _INVALID_TRAILING_INPUT: + reg = re_match.regs[group_index] + node = self._group_names_to_nodes[group_name] + yield (node, reg) + + return list(get_tuples()) + + def _nodes_to_values(self): + """ + Returns list of list of (Node, string_value) tuples. + """ + def is_none(slice): + return slice[0] == -1 and slice[1] == -1 + + def get(slice): + return self.string[slice[0]:slice[1]] + + return [(varname, get(slice), slice) for varname, slice in self._nodes_to_regs() if not is_none(slice)] + + def _unescape(self, varname, value): + unwrapper = self._unescape_funcs.get(varname) + return unwrapper(value) if unwrapper else value + + def variables(self): + """ + 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): + """ + 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 = [] + + # 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('', value, slice) + + def end_nodes(self): + """ + 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(object): + def __init__(self, tuples): + #: List of (varname, value, slice) tuples. + self._tuples = tuples + + def __repr__(self): + return '%s(%s)' % ( + self.__class__.__name__, ', '.join('%s=%r' % (k, v) for k, v, _ in self._tuples)) + + def get(self, key, default=None): + items = self.getall(key) + return items[0] if items else default + + def getall(self, key): + return [v for k, v, _ in self._tuples if k == key] + + def __getitem__(self, key): + return self.get(key) + + def __iter__(self): + """ + Yield `MatchVariable` instances. + """ + for varname, value, slice in self._tuples: + yield MatchVariable(varname, value, slice) + + +class MatchVariable(object): + """ + 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, value, slice): + self.varname = varname + self.value = value + self.slice = slice + + self.start = self.slice[0] + self.stop = self.slice[1] + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.varname, self.value) + + +def compile(expression, escape_funcs=None, unescape_funcs=None): + """ + 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, *a, **kw): + """ + Compile grammar (given as parse tree), returning a `CompiledGrammar` + instance. + """ + return _CompiledGrammar(root_node, *a, **kw) diff --git a/src/libs/prompt_toolkit/contrib/regular_languages/completion.py b/src/libs/prompt_toolkit/contrib/regular_languages/completion.py new file mode 100644 index 0000000..bb49986 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/regular_languages/completion.py @@ -0,0 +1,84 @@ +""" +Completer for a regular grammar. +""" +from __future__ import unicode_literals + +from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.document import Document + +from .compiler import _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, completers): + assert isinstance(compiled_grammar, _CompiledGrammar) + assert isinstance(completers, dict) + + self.compiled_grammar = compiled_grammar + self.completers = completers + + def get_completions(self, document, complete_event): + m = self.compiled_grammar.match_prefix(document.text_before_cursor) + + if m: + completions = self._remove_duplicates( + self._get_completions_for_match(m, complete_event)) + + for c in completions: + yield c + + def _get_completions_for_match(self, match, complete_event): + """ + 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): + """ + Remove duplicates, while keeping the order. + (Sometimes we have duplicates, because the there several matches of the + same grammar, each yielding similar completions.) + """ + result = [] + for i in items: + if i not in result: + result.append(i) + return result diff --git a/src/libs/prompt_toolkit/contrib/regular_languages/lexer.py b/src/libs/prompt_toolkit/contrib/regular_languages/lexer.py new file mode 100644 index 0000000..c166d84 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/regular_languages/lexer.py @@ -0,0 +1,90 @@ +""" +`GrammarLexer` is compatible with Pygments lexers and can be used to highlight +the input using a regular grammar with token annotations. +""" +from __future__ import unicode_literals +from prompt_toolkit.document import Document +from prompt_toolkit.layout.lexers import Lexer +from prompt_toolkit.layout.utils import split_lines +from prompt_toolkit.token import Token + +from .compiler import _CompiledGrammar +from six.moves import range + +__all__ = ( + 'GrammarLexer', +) + + +class GrammarLexer(Lexer): + """ + Lexer which can be used for highlighting of tokens 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 token, use a + `prompt_toolkit.layout.lexers.SimpleLexer`. + """ + def __init__(self, compiled_grammar, default_token=None, lexers=None): + assert isinstance(compiled_grammar, _CompiledGrammar) + assert default_token is None or isinstance(default_token, tuple) + assert lexers is None or all(isinstance(v, Lexer) for k, v in lexers.items()) + assert lexers is None or isinstance(lexers, dict) + + self.compiled_grammar = compiled_grammar + self.default_token = default_token or Token + self.lexers = lexers or {} + + def _get_tokens(self, cli, text): + m = self.compiled_grammar.match_prefix(text) + + if m: + characters = [[self.default_token, 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(cli, document) + lexer_tokens = [] + for i in range(len(document.lines)): + lexer_tokens.extend(lexer_tokens_for_line(i)) + lexer_tokens.append((Token, '\n')) + if lexer_tokens: + lexer_tokens.pop() + + i = v.start + for t, s in lexer_tokens: + for c in s: + if characters[i][0] == self.default_token: + characters[i][0] = t + 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][0] = Token.TrailingInput + + return characters + else: + return [(Token, text)] + + def lex_document(self, cli, document): + lines = list(split_lines(self._get_tokens(cli, document.text))) + + def get_line(lineno): + try: + return lines[lineno] + except IndexError: + return [] + + return get_line diff --git a/src/libs/prompt_toolkit/contrib/regular_languages/regex_parser.py b/src/libs/prompt_toolkit/contrib/regular_languages/regex_parser.py new file mode 100644 index 0000000..e5909b2 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/regular_languages/regex_parser.py @@ -0,0 +1,262 @@ +""" +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 unicode_literals +import re + +__all__ = ( + 'Repeat', + 'Variable', + 'Regex', + 'Lookahead', + + 'tokenize_regex', + 'parse_regex', +) + + +class Node(object): + """ + Base class for all the grammar nodes. + (You don't initialize this one.) + """ + def __add__(self, other_node): + return Sequence([self, other_node]) + + def __or__(self, other_node): + return Any([self, other_node]) + + +class Any(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): + self.children = children + + def __or__(self, other_node): + return Any(self.children + [other_node]) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.children) + + +class Sequence(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): + self.children = children + + def __add__(self, other_node): + return Sequence(self.children + [other_node]) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.children) + + +class Regex(Node): + """ + Regular expression. + """ + def __init__(self, regex): + re.compile(regex) # Validate + + self.regex = regex + + def __repr__(self): + return '%s(/%s/)' % (self.__class__.__name__, self.regex) + + +class Lookahead(Node): + """ + Lookahead expression. + """ + def __init__(self, childnode, negative=False): + self.childnode = childnode + self.negative = negative + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.childnode) + + +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, varname=None): + self.childnode = childnode + self.varname = varname + + def __repr__(self): + return '%s(childnode=%r, varname=%r)' % ( + self.__class__.__name__, self.childnode, self.varname) + + +class Repeat(Node): + def __init__(self, childnode, min_repeat=0, max_repeat=None, greedy=True): + self.childnode = childnode + self.min_repeat = min_repeat + self.max_repeat = max_repeat + self.greedy = greedy + + def __repr__(self): + return '%s(childnode=%r)' % (self.__class__.__name__, self.childnode) + + +def tokenize_regex(input): + """ + 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): + """ + 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 = [')'] + regex_tokens[::-1] + + def wrap(lst): + """ Turn list into sequence when it contains several items. """ + if len(lst) == 1: + return lst[0] + else: + return Sequence(lst) + + def _parse(): + or_list = [] + result = [] + + def wrapped_result(): + if or_list == []: + return wrap(result) + else: + or_list.append(result) + return Any([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('{}-style repitition not yet supported' % t) + + 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 parantheses.") + else: + return result diff --git a/src/libs/prompt_toolkit/contrib/regular_languages/validation.py b/src/libs/prompt_toolkit/contrib/regular_languages/validation.py new file mode 100644 index 0000000..d5f8cfc --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/regular_languages/validation.py @@ -0,0 +1,57 @@ +""" +Validator for a regular langage. +""" +from __future__ import unicode_literals + +from prompt_toolkit.validation import Validator, ValidationError +from prompt_toolkit.document import Document + +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, validators): + assert isinstance(compiled_grammar, _CompiledGrammar) + assert isinstance(validators, dict) + + self.compiled_grammar = compiled_grammar + self.validators = validators + + def validate(self, document): + # 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) + else: + raise ValidationError(cursor_position=len(document.text), + message='Invalid command') diff --git a/src/libs/prompt_toolkit/contrib/telnet/__init__.py b/src/libs/prompt_toolkit/contrib/telnet/__init__.py new file mode 100644 index 0000000..7b7aeec --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/telnet/__init__.py @@ -0,0 +1,2 @@ +from .server import * +from .application import * diff --git a/src/libs/prompt_toolkit/contrib/telnet/application.py b/src/libs/prompt_toolkit/contrib/telnet/application.py new file mode 100644 index 0000000..7fe6cc9 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/telnet/application.py @@ -0,0 +1,32 @@ +""" +Interface for Telnet applications. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +__all__ = ( + 'TelnetApplication', +) + + +class TelnetApplication(with_metaclass(ABCMeta, object)): + """ + The interface which has to be implemented for any telnet application. + An instance of this class has to be passed to `TelnetServer`. + """ + @abstractmethod + def client_connected(self, telnet_connection): + """ + Called when a new client was connected. + + Probably you want to call `telnet_connection.set_cli` here to set a + the CommandLineInterface instance to be used. + Hint: Use the following shortcut: `prompt_toolkit.shortcuts.create_cli` + """ + + @abstractmethod + def client_leaving(self, telnet_connection): + """ + Called when a client quits. + """ diff --git a/src/libs/prompt_toolkit/contrib/telnet/log.py b/src/libs/prompt_toolkit/contrib/telnet/log.py new file mode 100644 index 0000000..10792ce --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/telnet/log.py @@ -0,0 +1,11 @@ +""" +Python logger for the telnet server. +""" +from __future__ import unicode_literals +import logging + +logger = logging.getLogger(__package__) + +__all__ = ( + 'logger', +) diff --git a/src/libs/prompt_toolkit/contrib/telnet/protocol.py b/src/libs/prompt_toolkit/contrib/telnet/protocol.py new file mode 100644 index 0000000..b1bb0cc --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/telnet/protocol.py @@ -0,0 +1,181 @@ +""" +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 unicode_literals + +import struct +from six import int2byte, binary_type, iterbytes + +from .log import logger + +__all__ = ( + 'TelnetProtocolParser', +) + +# 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) + +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(object): + """ + 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, size_received_callback): + self.data_received_callback = data_received_callback + self.size_received_callback = size_received_callback + + self._parser = self._parse_coroutine() + self._parser.send(None) + + def received_data(self, data): + self.data_received_callback(data) + + def do_received(self, data): + """ Received telnet DO command. """ + logger.info('DO %r', data) + + def dont_received(self, data): + """ Received telnet DONT command. """ + logger.info('DONT %r', data) + + def will_received(self, data): + """ Received telnet WILL command. """ + logger.info('WILL %r', data) + + def wont_received(self, data): + """ Received telnet WONT command. """ + logger.info('WONT %r', data) + + def command_received(self, command, data): + 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): + """ + 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(str('!HH'), data) + self.size_received_callback(rows, columns) + else: + logger.warning('Wrong number of NAWS bytes') + + def negotiate(self, data): + """ + Got negotiate data. + """ + command, payload = data[0:1], data[1:] + assert isinstance(command, bytes) + + if command == NAWS: + self.naws(payload) + else: + logger.info('Negotiate (%r got bytes)', len(data)) + + def _parse_coroutine(self): + """ + 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, None) + + # 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): + """ + Feed data to the parser. + """ + assert isinstance(data, binary_type) + for b in iterbytes(data): + self._parser.send(int2byte(b)) diff --git a/src/libs/prompt_toolkit/contrib/telnet/server.py b/src/libs/prompt_toolkit/contrib/telnet/server.py new file mode 100644 index 0000000..d75a957 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/telnet/server.py @@ -0,0 +1,407 @@ +""" +Telnet server. + +Example usage:: + + class MyTelnetApplication(TelnetApplication): + def client_connected(self, telnet_connection): + # Set CLI with simple prompt. + telnet_connection.set_application( + telnet_connection.create_prompt_application(...)) + + def handle_command(self, telnet_connection, document): + # When the client enters a command, just reply. + telnet_connection.send('You said: %r\n\n' % document.text) + + ... + + a = MyTelnetApplication() + TelnetServer(application=a, host='127.0.0.1', port=23).run() +""" +from __future__ import unicode_literals + +import socket +import select + +import threading +import os +import fcntl + +from six import int2byte, text_type, binary_type +from codecs import getincrementaldecoder + +from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.eventloop.base import EventLoop +from prompt_toolkit.interface import CommandLineInterface, Application +from prompt_toolkit.layout.screen import Size +from prompt_toolkit.shortcuts import create_prompt_application +from prompt_toolkit.terminal.vt100_input import InputStream +from prompt_toolkit.terminal.vt100_output import Vt100_Output + +from .log import logger +from .protocol import IAC, DO, LINEMODE, SB, MODE, SE, WILL, ECHO, NAWS, SUPPRESS_GO_AHEAD +from .protocol import TelnetProtocolParser +from .application import TelnetApplication + +__all__ = ( + 'TelnetServer', +) + + +def _initialize_telnet(connection): + 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) + + +class _ConnectionStdout(object): + """ + Wrapper around socket which provides `write` and `flush` methods for the + Vt100_Output output. + """ + def __init__(self, connection, encoding): + self._encoding = encoding + self._connection = connection + self._buffer = [] + + def write(self, data): + assert isinstance(data, text_type) + self._buffer.append(data.encode(self._encoding)) + self.flush() + + def flush(self): + try: + self._connection.send(b''.join(self._buffer)) + except socket.error as e: + logger.error("Couldn't send data over socket: %s" % e) + + self._buffer = [] + + +class TelnetConnection(object): + """ + Class that represents one Telnet connection. + """ + def __init__(self, conn, addr, application, server, encoding): + assert isinstance(addr, tuple) # (addr, port) tuple + assert isinstance(application, TelnetApplication) + assert isinstance(server, TelnetServer) + assert isinstance(encoding, text_type) # e.g. 'utf-8' + + self.conn = conn + self.addr = addr + self.application = application + self.closed = False + self.handling_command = True + self.server = server + self.encoding = encoding + self.callback = None # Function that handles the CLI result. + + # Create "Output" object. + self.size = Size(rows=40, columns=79) + + # Initialize. + _initialize_telnet(conn) + + # Create output. + def get_size(): + return self.size + self.stdout = _ConnectionStdout(conn, encoding=encoding) + self.vt100_output = Vt100_Output(self.stdout, get_size, write_binary=False) + + # Create an eventloop (adaptor) for the CommandLineInterface. + self.eventloop = _TelnetEventLoopInterface(server) + + # Set default CommandLineInterface. + self.set_application(create_prompt_application()) + + # Call client_connected + application.client_connected(self) + + # Draw for the first time. + self.handling_command = False + self.cli._redraw() + + def set_application(self, app, callback=None): + """ + Set ``CommandLineInterface`` instance for this connection. + (This can be replaced any time.) + + :param cli: CommandLineInterface instance. + :param callback: Callable that takes the result of the CLI. + """ + assert isinstance(app, Application) + assert callback is None or callable(callback) + + self.cli = CommandLineInterface( + application=app, + eventloop=self.eventloop, + output=self.vt100_output) + self.callback = callback + + # Create a parser, and parser callbacks. + cb = self.cli.create_eventloop_callbacks() + inputstream = InputStream(cb.feed_key) + + # Input decoder for stdin. (Required when working with multibyte + # characters, like chinese input.) + stdin_decoder_cls = getincrementaldecoder(self.encoding) + stdin_decoder = [stdin_decoder_cls()] # nonlocal + + # Tell the CLI that it's running. We don't start it through the run() + # call, but will still want _redraw() to work. + self.cli._is_running = True + + def data_received(data): + """ TelnetProtocolParser 'data_received' callback """ + assert isinstance(data, binary_type) + + try: + result = stdin_decoder[0].decode(data) + inputstream.feed(result) + except UnicodeDecodeError: + stdin_decoder[0] = stdin_decoder_cls() + return '' + + def size_received(rows, columns): + """ TelnetProtocolParser 'size_received' callback """ + self.size = Size(rows=rows, columns=columns) + cb.terminal_size_changed() + + self.parser = TelnetProtocolParser(data_received, size_received) + + def feed(self, data): + """ + Handler for incoming data. (Called by TelnetServer.) + """ + assert isinstance(data, binary_type) + + self.parser.feed(data) + + # Render again. + self.cli._redraw() + + # When a return value has been set (enter was pressed), handle command. + if self.cli.is_returning: + try: + return_value = self.cli.return_value() + except (EOFError, KeyboardInterrupt) as e: + # Control-D or Control-C was pressed. + logger.info('%s, closing connection.', type(e).__name__) + self.close() + return + + # Handle CLI command + self._handle_command(return_value) + + def _handle_command(self, command): + """ + Handle command. This will run in a separate thread, in order not + to block the event loop. + """ + logger.info('Handle command %r', command) + + def in_executor(): + self.handling_command = True + try: + if self.callback is not None: + self.callback(self, command) + finally: + self.server.call_from_executor(done) + + def done(): + self.handling_command = False + + # Reset state and draw again. (If the connection is still open -- + # the application could have called TelnetConnection.close() + if not self.closed: + self.cli.reset() + self.cli.buffers[DEFAULT_BUFFER].reset() + self.cli.renderer.request_absolute_cursor_position() + self.vt100_output.flush() + self.cli._redraw() + + self.server.run_in_executor(in_executor) + + def erase_screen(self): + """ + Erase output screen. + """ + self.vt100_output.erase_screen() + self.vt100_output.cursor_goto(0, 0) + self.vt100_output.flush() + + def send(self, data): + """ + Send text to the client. + """ + assert isinstance(data, text_type) + + # When data is send back to the client, we should replace the line + # endings. (We didn't allocate a real pseudo terminal, and the telnet + # connection is raw, so we are responsible for inserting \r.) + self.stdout.write(data.replace('\n', '\r\n')) + self.stdout.flush() + + def close(self): + """ + Close the connection. + """ + self.application.client_leaving(self) + + self.conn.close() + self.closed = True + + +class _TelnetEventLoopInterface(EventLoop): + """ + Eventloop object to be assigned to `CommandLineInterface`. + """ + def __init__(self, server): + self._server = server + + def close(self): + " Ignore. " + + def stop(self): + " Ignore. " + + def run_in_executor(self, callback): + self._server.run_in_executor(callback) + + def call_from_executor(self, callback, _max_postpone_until=None): + self._server.call_from_executor(callback) + + def add_reader(self, fd, callback): + raise NotImplementedError + + def remove_reader(self, fd): + raise NotImplementedError + + +class TelnetServer(object): + """ + Telnet server implementation. + """ + def __init__(self, host='127.0.0.1', port=23, application=None, encoding='utf-8'): + assert isinstance(host, text_type) + assert isinstance(port, int) + assert isinstance(application, TelnetApplication) + assert isinstance(encoding, text_type) + + self.host = host + self.port = port + self.application = application + self.encoding = encoding + + self.connections = set() + + self._calls_from_executor = [] + + # Create a pipe for inter thread communication. + self._schedule_pipe = os.pipe() + fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK) + + @classmethod + def create_socket(cls, host, port): + # 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 + + def run_in_executor(self, callback): + threading.Thread(target=callback).start() + + def call_from_executor(self, callback): + self._calls_from_executor.append(callback) + + if self._schedule_pipe: + os.write(self._schedule_pipe[1], b'x') + + def _process_callbacks(self): + """ + Process callbacks from `call_from_executor` in eventloop. + """ + # Flush all the pipe content. + os.read(self._schedule_pipe[0], 1024) + + # Process calls from executor. + calls_from_executor, self._calls_from_executor = self._calls_from_executor, [] + for c in calls_from_executor: + c() + + def run(self): + """ + Run the eventloop for the telnet server. + """ + listen_socket = self.create_socket(self.host, self.port) + logger.info('Listening for telnet connections on %s port %r', self.host, self.port) + + try: + while True: + # Removed closed connections. + self.connections = set([c for c in self.connections if not c.closed]) + + # Ignore connections handling commands. + connections = set([c for c in self.connections if not c.handling_command]) + + # Wait for next event. + read_list = ( + [listen_socket, self._schedule_pipe[0]] + + [c.conn for c in connections]) + + read, _, _ = select.select(read_list, [], []) + + for s in read: + # When the socket itself is ready, accept a new connection. + if s == listen_socket: + self._accept(listen_socket) + + # If we receive something on our "call_from_executor" pipe, process + # these callbacks in a thread safe way. + elif s == self._schedule_pipe[0]: + self._process_callbacks() + + # Handle incoming data on socket. + else: + self._handle_incoming_data(s) + finally: + listen_socket.close() + + def _accept(self, listen_socket): + """ + Accept new incoming connection. + """ + conn, addr = listen_socket.accept() + connection = TelnetConnection(conn, addr, self.application, self, encoding=self.encoding) + self.connections.add(connection) + + logger.info('New connection %r %r', *addr) + + def _handle_incoming_data(self, conn): + """ + Handle incoming data on socket. + """ + connection = [c for c in self.connections if c.conn == conn][0] + data = conn.recv(1024) + if data: + connection.feed(data) + else: + self.connections.remove(connection) diff --git a/src/libs/prompt_toolkit/contrib/validators/__init__.py b/src/libs/prompt_toolkit/contrib/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/libs/prompt_toolkit/contrib/validators/base.py b/src/libs/prompt_toolkit/contrib/validators/base.py new file mode 100644 index 0000000..16c1539 --- /dev/null +++ b/src/libs/prompt_toolkit/contrib/validators/base.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals +from prompt_toolkit.validation import Validator, ValidationError +from six import string_types + + +class SentenceValidator(Validator): + """ + Validate input only when it appears in this list of sentences. + + :param sentences: List of sentences. + :param ignore_case: If True, case-insensitive comparisons. + """ + def __init__(self, sentences, ignore_case=False, error_message='Invalid input', move_cursor_to_end=False): + assert all(isinstance(s, string_types) for s in sentences) + assert isinstance(ignore_case, bool) + assert isinstance(error_message, string_types) + + self.sentences = list(sentences) + self.ignore_case = ignore_case + self.error_message = error_message + self.move_cursor_to_end = move_cursor_to_end + + if ignore_case: + self.sentences = set([s.lower() for s in self.sentences]) + + def validate(self, document): + if document.text not in self.sentences: + if self.move_cursor_to_end: + index = len(document.text) + else: + index = 0 + + raise ValidationError(cursor_position=index, + message=self.error_message) diff --git a/src/libs/prompt_toolkit/document.py b/src/libs/prompt_toolkit/document.py new file mode 100644 index 0000000..0d70aa6 --- /dev/null +++ b/src/libs/prompt_toolkit/document.py @@ -0,0 +1,1001 @@ +""" +The `Document` that implements all the text operations/querying. +""" +from __future__ import unicode_literals + +import bisect +import re +import six +import string +import weakref +from six.moves import range, map + +from .selection import SelectionType, SelectionState, PasteMode +from .clipboard import ClipboardData + +__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 = weakref.WeakValueDictionary() # Maps document.text to DocumentCache instance. + + +class _ImmutableLineList(list): + """ + Some protection for our 'lines' list, which is assumed to be immutable in the cache. + (Useful for detecting obvious bugs.) + """ + def _error(self, *a, **kw): + raise NotImplementedError('Attempt to modifiy an immutable list.') + + __setitem__ = _error + append = _error + clear = _error + extend = _error + insert = _error + pop = _error + remove = _error + reverse = _error + sort = _error + + +class _DocumentCache(object): + def __init__(self): + #: List of lines for the Document text. + self.lines = None + + #: List of index positions, pointing to the start of all the lines. + self.line_indexes = None + + +class Document(object): + """ + 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:`~libs.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='', cursor_position=None, selection=None): + assert isinstance(text, six.text_type), 'Got %r' % text + assert selection is None or isinstance(selection, SelectionState) + + # 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( + 'cursor_position=%r, len_text=%r' % (cursor_position, len(text))) + + # 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): + return '%s(%r, %r)' % (self.__class__.__name__, self.text, self.cursor_position) + + @property + def text(self): + " The document text. " + return self._text + + @property + def cursor_position(self): + " The document cursor position. " + return self._cursor_position + + @property + def selection(self): + " :class:`.SelectionState` object. " + return self._selection + + @property + def current_char(self): + """ Return character under cursor or an empty string. """ + return self._get_char_relative_to_cursor(0) or '' + + @property + def char_before_cursor(self): + """ Return character before the cursor or an empty string. """ + return self._get_char_relative_to_cursor(-1) or '' + + @property + def text_before_cursor(self): + return self.text[:self.cursor_position:] + + @property + def text_after_cursor(self): + return self.text[self.cursor_position:] + + @property + def current_line_before_cursor(self): + """ 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): + """ Text from the cursor until the end of the line. """ + text, _, _ = self.text_after_cursor.partition('\n') + return text + + @property + def lines(self): + """ + 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): + """ + 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): + """ + 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): + 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): + """ 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): + """ 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=0): + """ + 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): + """ + True when we are at the first line. + """ + return self.cursor_position_row == 0 + + @property + def on_last_line(self): + """ + True when we are at the last line. + """ + return self.cursor_position_row == self.line_count - 1 + + @property + def cursor_position_row(self): + """ + Current row. (0-based.) + """ + row, _ = self._find_line_start_index(self.cursor_position) + return row + + @property + def cursor_position_col(self): + """ + 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): + """ + 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): + """ + 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, col): + """ + 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): + """ 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): + """ 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): + """ + `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, in_current_line=False, include_current_position=False, + ignore_case=False, count=1): + """ + Find `text` after the cursor, return position relative to the cursor + position. Return `None` if nothing was found. + + :param count: Find the n-th occurance. + """ + 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 # (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 + + def find_all(self, sub, ignore_case=False): + """ + Find all occurances 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, in_current_line=False, ignore_case=False, count=1): + """ + Find `text` before the cursor, return position relative to the cursor + position. Return `None` if nothing was found. + + :param count: Find the n-th occurance. + """ + 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 + + def get_word_before_cursor(self, WORD=False): + """ + Give the word before the cursor. + If we have whitespace before the cursor this returns an empty string. + """ + if self.text_before_cursor[-1:].isspace(): + return '' + else: + return self.text_before_cursor[self.find_start_of_previous_word(WORD=WORD):] + + def find_start_of_previous_word(self, count=1, WORD=False): + """ + Return an index relative to the cursor position pointing to the start + of the previous word. Return `None` if nothing was found. + """ + # Reverse the text before the cursor, in order to do an efficient + # backwards search. + text_before_cursor = 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): + if i + 1 == count: + return - match.end(1) + except StopIteration: + pass + + def find_boundaries_of_current_word(self, WORD=False, include_leading_whitespace=False, + include_trailing_whitespace=False): + """ + 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): + 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=False): + """ + 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=1, WORD=False): + """ + 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 + + def find_next_word_ending(self, include_current_position=False, count=1, WORD=False): + """ + 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 + + def find_previous_word_beginning(self, count=1, WORD=False): + """ + 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 + + def find_previous_word_ending(self, count=1, WORD=False): + """ + 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 + + def find_next_matching_line(self, match_func, count=1): + """ + 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, count=1): + """ + 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=1): + """ + 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=1): + """ + 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=1, preferred_column=None): + """ + 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=1, preferred_column=None): + """ + 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, right_ch, end_pos=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 + + def find_enclosing_bracket_left(self, left_ch, right_ch, start_pos=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 + + def find_matching_bracket_position(self, start_pos=None, end_pos=None): + """ + 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 A, B in '()', '[]', '{}', '<>': + 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): + """ Relative position for the start of the document. """ + return - self.cursor_position + + def get_end_of_document_position(self): + """ Relative position for the end of the document. """ + return len(self.text) - self.cursor_position + + def get_start_of_line_position(self, after_whitespace=False): + """ 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): + """ Relative position for the end of this line. """ + return len(self.current_line_after_cursor) + + def last_non_blank_of_current_line_position(self): + """ + 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): + """ + 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): # 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): + """ + Return a list of (from, to) tuples for the selection or none if nothing + was selected. start and end position are always included in the + selection. + + This will yield several (from, to) tuples in case of 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 + + 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 - 1, 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 + + yield from_, to + + def selection_range_at_line(self, row): + """ + If the selection spans a portion of the given line, return a (from, to) tuple. + Otherwise, return None. + """ + if self.selection: + row_start = self.translate_row_col_to_index(row, 0) + row_end = self.translate_row_col_to_index(row, max(0, len(self.lines[row]) - 1)) + + 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]) + 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) + + return from_column, to_column + + def cut_selection(self): + """ + 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 + 1]) + last_to = to + 1 + + 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, paste_mode=PasteMode.EMACS, count=1): + """ + 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. + """ + assert isinstance(data, ClipboardData) + assert paste_mode in (PasteMode.VI_BEFORE, PasteMode.VI_AFTER, PasteMode.EMACS) + + 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): + """ + 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=1, before=False): + """ + Return the start of the current paragraph. (Relative cursor position.) + """ + def match_func(text): + 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=1, after=False): + """ + Return the end of the current paragraph. (Relative cursor position.) + """ + def match_func(text): + 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): + """ + 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): + """ + 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/libs/prompt_toolkit/enums.py b/src/libs/prompt_toolkit/enums.py new file mode 100644 index 0000000..6945f44 --- /dev/null +++ b/src/libs/prompt_toolkit/enums.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + + +class IncrementalSearchDirection(object): + FORWARD = 'FORWARD' + BACKWARD = 'BACKWARD' + + +class EditingMode(object): + # 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' + +# Dummy buffer. This is the buffer returned by +# `CommandLineInterface.current_buffer` when the top of the `FocusStack` is +# `None`. This could be the case when there is some widget has the focus and no +# actual text editing is possible. This buffer should also never be displayed. +# (It will never contain any actual text.) +DUMMY_BUFFER = 'DUMMY_BUFFER' diff --git a/src/libs/prompt_toolkit/eventloop/__init__.py b/src/libs/prompt_toolkit/eventloop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/libs/prompt_toolkit/eventloop/asyncio_base.py b/src/libs/prompt_toolkit/eventloop/asyncio_base.py new file mode 100644 index 0000000..ace2b8d --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/asyncio_base.py @@ -0,0 +1,46 @@ +""" +Eventloop for integration with Python3 asyncio. + +Note that we can't use "yield from", because the package should be installable +under Python 2.6 as well, and it should contain syntactically valid Python 2.6 +code. +""" +from __future__ import unicode_literals + +__all__ = ( + 'AsyncioTimeout', +) + + +class AsyncioTimeout(object): + """ + Call the `timeout` function when the timeout expires. + Every call of the `reset` method, resets the timeout and starts a new + timer. + """ + def __init__(self, timeout, callback, loop): + self.timeout = timeout + self.callback = callback + self.loop = loop + + self.counter = 0 + self.running = True + + def reset(self): + """ + Reset the timeout. Starts a new timer. + """ + self.counter += 1 + local_counter = self.counter + + def timer_timeout(): + if self.counter == local_counter and self.running: + self.callback() + + self.loop.call_later(self.timeout, timer_timeout) + + def stop(self): + """ + Ignore timeout. Don't call the callback anymore. + """ + self.running = False diff --git a/src/libs/prompt_toolkit/eventloop/asyncio_posix.py b/src/libs/prompt_toolkit/eventloop/asyncio_posix.py new file mode 100644 index 0000000..426ed96 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/asyncio_posix.py @@ -0,0 +1,113 @@ +""" +Posix asyncio event loop. +""" +from __future__ import unicode_literals + +from ..terminal.vt100_input import InputStream +from .asyncio_base import AsyncioTimeout +from .base import EventLoop, INPUT_TIMEOUT +from .callbacks import EventLoopCallbacks +from .posix_utils import PosixStdinReader + +import asyncio +import signal + +__all__ = ( + 'PosixAsyncioEventLoop', +) + + +class PosixAsyncioEventLoop(EventLoop): + def __init__(self, loop=None): + self.loop = loop or asyncio.get_event_loop() + self.closed = False + + self._stopped_f = asyncio.Future(loop=self.loop) + + @asyncio.coroutine + def run_as_coroutine(self, stdin, callbacks): + """ + The input 'event loop'. + """ + assert isinstance(callbacks, EventLoopCallbacks) + + # Create reader class. + stdin_reader = PosixStdinReader(stdin.fileno()) + + if self.closed: + raise Exception('Event loop already closed.') + + inputstream = InputStream(callbacks.feed_key) + + try: + # Create a new Future every time. + self._stopped_f = asyncio.Future(loop=self.loop) + + # Handle input timouts + def timeout_handler(): + """ + When no input has been received for INPUT_TIMEOUT seconds, + flush the input stream and fire the timeout event. + """ + inputstream.flush() + + callbacks.input_timeout() + + timeout = AsyncioTimeout(INPUT_TIMEOUT, timeout_handler, self.loop) + + # Catch sigwinch + def received_winch(): + self.call_from_executor(callbacks.terminal_size_changed) + + self.loop.add_signal_handler(signal.SIGWINCH, received_winch) + + # Read input data. + def stdin_ready(): + data = stdin_reader.read() + inputstream.feed(data) + timeout.reset() + + # Quit when the input stream was closed. + if stdin_reader.closed: + self.stop() + + self.loop.add_reader(stdin.fileno(), stdin_ready) + + # Block this coroutine until stop() has been called. + for f in self._stopped_f: + yield f + + finally: + # Clean up. + self.loop.remove_reader(stdin.fileno()) + self.loop.remove_signal_handler(signal.SIGWINCH) + + # Don't trigger any timeout events anymore. + timeout.stop() + + def stop(self): + # Trigger the 'Stop' future. + self._stopped_f.set_result(True) + + def close(self): + # Note: we should not close the asyncio loop itself, because that one + # was not created here. + self.closed = True + + def run_in_executor(self, callback): + self.loop.run_in_executor(None, callback) + + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. + Similar to Twisted's ``callFromThread``. + """ + self.loop.call_soon_threadsafe(callback) + + def add_reader(self, fd, callback): + " Start watching the file descriptor for read availability. " + self.loop.add_reader(fd, callback) + + def remove_reader(self, fd): + " Stop watching the file descriptor for read availability. " + self.loop.remove_reader(fd) diff --git a/src/libs/prompt_toolkit/eventloop/asyncio_win32.py b/src/libs/prompt_toolkit/eventloop/asyncio_win32.py new file mode 100644 index 0000000..45f5f52 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/asyncio_win32.py @@ -0,0 +1,83 @@ +""" +Win32 asyncio event loop. + +Windows notes: +- Somehow it doesn't seem to work with the 'ProactorEventLoop'. +""" +from __future__ import unicode_literals + +from .base import EventLoop, INPUT_TIMEOUT +from ..terminal.win32_input import ConsoleInputReader +from .callbacks import EventLoopCallbacks +from .asyncio_base import AsyncioTimeout + +import asyncio + +__all__ = ( + 'Win32AsyncioEventLoop', +) + + +class Win32AsyncioEventLoop(EventLoop): + def __init__(self, loop=None): + self._console_input_reader = ConsoleInputReader() + self.running = False + self.closed = False + self.loop = loop or asyncio.get_event_loop() + + @asyncio.coroutine + def run_as_coroutine(self, stdin, callbacks): + """ + The input 'event loop'. + """ + # Note: We cannot use "yield from", because this package also + # installs on Python 2. + assert isinstance(callbacks, EventLoopCallbacks) + + if self.closed: + raise Exception('Event loop already closed.') + + timeout = AsyncioTimeout(INPUT_TIMEOUT, callbacks.input_timeout, self.loop) + self.running = True + + try: + while self.running: + timeout.reset() + + # Get keys + try: + g = iter(self.loop.run_in_executor(None, self._console_input_reader.read)) + while True: + yield next(g) + except StopIteration as e: + keys = e.args[0] + + # Feed keys to input processor. + for k in keys: + callbacks.feed_key(k) + finally: + timeout.stop() + + def stop(self): + self.running = False + + def close(self): + # Note: we should not close the asyncio loop itself, because that one + # was not created here. + self.closed = True + + self._console_input_reader.close() + + def run_in_executor(self, callback): + self.loop.run_in_executor(None, callback) + + def call_from_executor(self, callback, _max_postpone_until=None): + self.loop.call_soon_threadsafe(callback) + + def add_reader(self, fd, callback): + " Start watching the file descriptor for read availability. " + self.loop.add_reader(fd, callback) + + def remove_reader(self, fd): + " Stop watching the file descriptor for read availability. " + self.loop.remove_reader(fd) diff --git a/src/libs/prompt_toolkit/eventloop/base.py b/src/libs/prompt_toolkit/eventloop/base.py new file mode 100644 index 0000000..b851a21 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/base.py @@ -0,0 +1,85 @@ +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +__all__ = ( + 'EventLoop', + 'INPUT_TIMEOUT', +) + + +#: When to trigger the `onInputTimeout` event. +INPUT_TIMEOUT = .5 + + +class EventLoop(with_metaclass(ABCMeta, object)): + """ + Eventloop interface. + """ + def run(self, stdin, callbacks): + """ + Run the eventloop until stop() is called. Report all + input/timeout/terminal-resize events to the callbacks. + + :param stdin: :class:`~libs.prompt_toolkit.input.Input` instance. + :param callbacks: :class:`~libs.prompt_toolkit.eventloop.callbacks.EventLoopCallbacks` instance. + """ + raise NotImplementedError("This eventloop doesn't implement synchronous 'run()'.") + + def run_as_coroutine(self, stdin, callbacks): + """ + Similar to `run`, but this is a coroutine. (For asyncio integration.) + """ + raise NotImplementedError("This eventloop doesn't implement 'run_as_coroutine()'.") + + @abstractmethod + def stop(self): + """ + Stop the `run` call. (Normally called by + :class:`~libs.prompt_toolkit.interface.CommandLineInterface`, when a result + is available, or Abort/Quit has been called.) + """ + + @abstractmethod + def close(self): + """ + Clean up of resources. Eventloop cannot be reused a second time after + this call. + """ + + @abstractmethod + def add_reader(self, fd, callback): + """ + Start watching the file descriptor for read availability and then call + the callback. + """ + + @abstractmethod + def remove_reader(self, fd): + """ + Stop watching the file descriptor for read availability. + """ + + @abstractmethod + def run_in_executor(self, callback): + """ + Run a long running function in a background thread. (This is + recommended for code that could block the event loop.) + Similar to Twisted's ``deferToThread``. + """ + + @abstractmethod + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. Similar to Twisted's + ``callFromThread``. + + :param _max_postpone_until: `None` or `time.time` value. For interal + use. If the eventloop is saturated, consider this task to be low + priority and postpone maximum until this timestamp. (For instance, + repaint is done using low priority.) + + Note: In the past, this used to be a datetime.datetime instance, + but apparently, executing `time.time` is more efficient: it + does fewer system calls. (It doesn't read /etc/localtime.) + """ diff --git a/src/libs/prompt_toolkit/eventloop/callbacks.py b/src/libs/prompt_toolkit/eventloop/callbacks.py new file mode 100644 index 0000000..f96fee2 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/callbacks.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +__all__ = ( + 'EventLoopCallbacks', +) + + +class EventLoopCallbacks(with_metaclass(ABCMeta, object)): + """ + This is the glue between the :class:`~libs.prompt_toolkit.eventloop.base.EventLoop` + and :class:`~libs.prompt_toolkit.interface.CommandLineInterface`. + + :meth:`~libs.prompt_toolkit.eventloop.base.EventLoop.run` takes an + :class:`.EventLoopCallbacks` instance and operates on that one, driving the + interface. + """ + @abstractmethod + def terminal_size_changed(self): + pass + + @abstractmethod + def input_timeout(self): + pass + + @abstractmethod + def feed_key(self, key): + pass diff --git a/src/libs/prompt_toolkit/eventloop/inputhook.py b/src/libs/prompt_toolkit/eventloop/inputhook.py new file mode 100644 index 0000000..ca58ff8 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/inputhook.py @@ -0,0 +1,107 @@ +""" +Similar to `PyOS_InputHook` of the Python API. Some eventloops can have an +inputhook to allow easy integration with other event loops. + +When the eventloop of prompt-toolkit is idle, it can call such a hook. This +hook can call another eventloop that runs for a short while, for instance to +keep a graphical user interface responsive. + +It's the responsibility of this hook to exit when there is input ready. +There are two ways to detect when input is ready: + +- 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. + +An alternative to using input hooks, is to create a custom `EventLoop` class that +controls everything. +""" +from __future__ import unicode_literals +import os +import threading +from libs.prompt_toolkit.utils import is_windows +from .select import select_fds + +__all__ = ( + 'InputHookContext', +) + + +class InputHookContext(object): + """ + Given as a parameter to the inputhook. + """ + def __init__(self, inputhook): + assert callable(inputhook) + + self.inputhook = inputhook + self._input_is_ready = None + + self._r, self._w = os.pipe() + + def input_is_ready(self): + """ + Return True when the input is ready. + """ + return self._input_is_ready(wait=False) + + def fileno(self): + """ + File descriptor that will become ready when the event loop needs to go on. + """ + return self._r + + def call_inputhook(self, input_is_ready_func): + """ + Call the inputhook. (Called by a prompt-toolkit eventloop.) + """ + self._input_is_ready = input_is_ready_func + + # Start thread that activates this pipe when there is input to process. + def thread(): + input_is_ready_func(wait=True) + os.write(self._w, b'x') + + threading.Thread(target=thread).start() + + # Call inputhook. + self.inputhook(self) + + # 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 not is_windows(): + select_fds([self._r], timeout=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 + self._input_is_ready = None + + def close(self): + """ + Clean up resources. + """ + if self._r: + os.close(self._r) + os.close(self._w) + + self._r = self._w = None diff --git a/src/libs/prompt_toolkit/eventloop/posix.py b/src/libs/prompt_toolkit/eventloop/posix.py new file mode 100644 index 0000000..ac27bed --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/posix.py @@ -0,0 +1,311 @@ +from __future__ import unicode_literals +import fcntl +import os +import random +import signal +import threading +import time + +from libs.prompt_toolkit.terminal.vt100_input import InputStream +from libs.prompt_toolkit.utils import DummyContext, in_main_thread +from libs.prompt_toolkit.input import Input +from .base import EventLoop, INPUT_TIMEOUT +from .callbacks import EventLoopCallbacks +from .inputhook import InputHookContext +from .posix_utils import PosixStdinReader +from .utils import TimeIt +from .select import AutoSelector, Selector, fd_to_int + +__all__ = ( + 'PosixEventLoop', +) + +_now = time.time + + +class PosixEventLoop(EventLoop): + """ + Event loop for posix systems (Linux, Mac os X). + """ + def __init__(self, inputhook=None, selector=AutoSelector): + assert inputhook is None or callable(inputhook) + assert issubclass(selector, Selector) + + self.running = False + self.closed = False + self._running = False + self._callbacks = None + + self._calls_from_executor = [] + self._read_fds = {} # Maps fd to handler. + self.selector = selector() + + # Create a pipe for inter thread communication. + self._schedule_pipe = os.pipe() + fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK) + + # Create inputhook context. + self._inputhook_context = InputHookContext(inputhook) if inputhook else None + + def run(self, stdin, callbacks): + """ + The input 'event loop'. + """ + assert isinstance(stdin, Input) + assert isinstance(callbacks, EventLoopCallbacks) + assert not self._running + + if self.closed: + raise Exception('Event loop already closed.') + + self._running = True + self._callbacks = callbacks + + inputstream = InputStream(callbacks.feed_key) + current_timeout = [INPUT_TIMEOUT] # Nonlocal + + # Create reader class. + stdin_reader = PosixStdinReader(stdin.fileno()) + + # Only attach SIGWINCH signal handler in main thread. + # (It's not possible to attach signal handlers in other threads. In + # that case we should rely on a the main thread to call this manually + # instead.) + if in_main_thread(): + ctx = call_on_sigwinch(self.received_winch) + else: + ctx = DummyContext() + + def read_from_stdin(): + " Read user input. " + # Feed input text. + data = stdin_reader.read() + inputstream.feed(data) + + # Set timeout again. + current_timeout[0] = INPUT_TIMEOUT + + # Quit when the input stream was closed. + if stdin_reader.closed: + self.stop() + + self.add_reader(stdin, read_from_stdin) + self.add_reader(self._schedule_pipe[0], None) + + with ctx: + while self._running: + # Call inputhook. + if self._inputhook_context: + with TimeIt() as inputhook_timer: + def ready(wait): + " True when there is input ready. The inputhook should return control. " + return self._ready_for_reading(current_timeout[0] if wait else 0) != [] + self._inputhook_context.call_inputhook(ready) + inputhook_duration = inputhook_timer.duration + else: + inputhook_duration = 0 + + # Calculate remaining timeout. (The inputhook consumed some of the time.) + if current_timeout[0] is None: + remaining_timeout = None + else: + remaining_timeout = max(0, current_timeout[0] - inputhook_duration) + + # Wait until input is ready. + fds = self._ready_for_reading(remaining_timeout) + + # When any of the FDs are ready. Call the appropriate callback. + if fds: + # Create lists of high/low priority tasks. The main reason + # for this is to allow painting the UI to happen as soon as + # possible, but when there are many events happening, we + # don't want to call the UI renderer 1000x per second. If + # the eventloop is completely saturated with many CPU + # intensive tasks (like processing input/output), we say + # that drawing the UI can be postponed a little, to make + # CPU available. This will be a low priority task in that + # case. + tasks = [] + low_priority_tasks = [] + now = None # Lazy load time. (Fewer system calls.) + + for fd in fds: + # For the 'call_from_executor' fd, put each pending + # item on either the high or low priority queue. + if fd == self._schedule_pipe[0]: + for c, max_postpone_until in self._calls_from_executor: + if max_postpone_until is None: + # Execute now. + tasks.append(c) + else: + # Execute soon, if `max_postpone_until` is in the future. + now = now or _now() + if max_postpone_until < now: + tasks.append(c) + else: + low_priority_tasks.append((c, max_postpone_until)) + self._calls_from_executor = [] + + # Flush all the pipe content. + os.read(self._schedule_pipe[0], 1024) + else: + handler = self._read_fds.get(fd) + if handler: + tasks.append(handler) + + # Handle everything in random order. (To avoid starvation.) + random.shuffle(tasks) + random.shuffle(low_priority_tasks) + + # When there are high priority tasks, run all these. + # Schedule low priority tasks for the next iteration. + if tasks: + for t in tasks: + t() + + # Postpone low priority tasks. + for t, max_postpone_until in low_priority_tasks: + self.call_from_executor(t, _max_postpone_until=max_postpone_until) + else: + # Currently there are only low priority tasks -> run them right now. + for t, _ in low_priority_tasks: + t() + + else: + # Flush all pending keys on a timeout. (This is most + # important to flush the vt100 'Escape' key early when + # nothing else follows.) + inputstream.flush() + + # Fire input timeout event. + callbacks.input_timeout() + current_timeout[0] = None + + self.remove_reader(stdin) + self.remove_reader(self._schedule_pipe[0]) + + self._callbacks = None + + def _ready_for_reading(self, timeout=None): + """ + Return the file descriptors that are ready for reading. + """ + fds = self.selector.select(timeout) + return fds + + def received_winch(self): + """ + Notify the event loop that SIGWINCH has been received + """ + # Process signal asynchronously, because this handler can write to the + # output, and doing this inside the signal handler causes easily + # reentrant calls, giving runtime errors.. + + # Furthur, this has to be thread safe. When the CommandLineInterface + # runs not in the main thread, this function still has to be called + # from the main thread. (The only place where we can install signal + # handlers.) + def process_winch(): + if self._callbacks: + self._callbacks.terminal_size_changed() + + self.call_from_executor(process_winch) + + def run_in_executor(self, callback): + """ + Run a long running function in a background thread. + (This is recommended for code that could block the event loop.) + Similar to Twisted's ``deferToThread``. + """ + # Wait until the main thread is idle. + # We start the thread by using `call_from_executor`. The event loop + # favours processing input over `calls_from_executor`, so the thread + # will not start until there is no more input to process and the main + # thread becomes idle for an instant. This is good, because Python + # threading favours CPU over I/O -- an autocompletion thread in the + # background would cause a significantly slow down of the main thread. + # It is mostly noticable when pasting large portions of text while + # having real time autocompletion while typing on. + def start_executor(): + threading.Thread(target=callback).start() + self.call_from_executor(start_executor) + + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. + Similar to Twisted's ``callFromThread``. + + :param _max_postpone_until: `None` or `time.time` value. For interal + use. If the eventloop is saturated, consider this task to be low + priority and postpone maximum until this timestamp. (For instance, + repaint is done using low priority.) + """ + assert _max_postpone_until is None or isinstance(_max_postpone_until, float) + self._calls_from_executor.append((callback, _max_postpone_until)) + + if self._schedule_pipe: + try: + os.write(self._schedule_pipe[1], b'x') + except (AttributeError, IndexError, OSError): + # Handle race condition. We're in a different thread. + # - `_schedule_pipe` could have become None in the meantime. + # - We catch `OSError` (actually BrokenPipeError), because the + # main thread could have closed the pipe already. + pass + + def stop(self): + """ + Stop the event loop. + """ + self._running = False + + def close(self): + self.closed = True + + # Close pipes. + schedule_pipe = self._schedule_pipe + self._schedule_pipe = None + + if schedule_pipe: + os.close(schedule_pipe[0]) + os.close(schedule_pipe[1]) + + if self._inputhook_context: + self._inputhook_context.close() + + def add_reader(self, fd, callback): + " Add read file descriptor to the event loop. " + fd = fd_to_int(fd) + self._read_fds[fd] = callback + self.selector.register(fd) + + def remove_reader(self, fd): + " Remove read file descriptor from the event loop. " + fd = fd_to_int(fd) + + if fd in self._read_fds: + del self._read_fds[fd] + + self.selector.unregister(fd) + + +class call_on_sigwinch(object): + """ + Context manager which Installs a SIGWINCH callback. + (This signal occurs when the terminal size changes.) + """ + def __init__(self, callback): + self.callback = callback + self.previous_callback = None + + def __enter__(self): + self.previous_callback = signal.signal(signal.SIGWINCH, lambda *a: self.callback()) + + def __exit__(self, *a, **kw): + if self.previous_callback is None: + # Normally, `signal.signal` should never return `None`. + # For some reason it happens here: + # https://github.com/jonathanslenders/python-prompt-toolkit/pull/174 + signal.signal(signal.SIGWINCH, 0) + else: + signal.signal(signal.SIGWINCH, self.previous_callback) diff --git a/src/libs/prompt_toolkit/eventloop/posix_utils.py b/src/libs/prompt_toolkit/eventloop/posix_utils.py new file mode 100644 index 0000000..320df43 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/posix_utils.py @@ -0,0 +1,82 @@ +from __future__ import unicode_literals + +from codecs import getincrementaldecoder +import os +import six + +__all__ = ( + 'PosixStdinReader', +) + + +class PosixStdinReader(object): + """ + 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 + unrecognised 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, + errors=('ignore' if six.PY2 else 'surrogateescape')): + assert isinstance(stdin_fd, int) + 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('utf-8') + self._stdin_decoder = self._stdin_decoder_cls(errors=errors) + + #: True when there is nothing anymore to read. + self.closed = False + + def read(self, count=1024): + # 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 b'' + + # 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/libs/prompt_toolkit/eventloop/select.py b/src/libs/prompt_toolkit/eventloop/select.py new file mode 100644 index 0000000..f678f84 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/select.py @@ -0,0 +1,216 @@ +""" +Selectors for the Posix event loop. +""" +from __future__ import unicode_literals, absolute_import +import sys +import abc +import errno +import select +import six + +__all__ = ( + 'AutoSelector', + 'PollSelector', + 'SelectSelector', + 'Selector', + 'fd_to_int', +) + +def fd_to_int(fd): + assert isinstance(fd, int) or hasattr(fd, 'fileno') + + if isinstance(fd, int): + return fd + else: + return fd.fileno() + + +class Selector(six.with_metaclass(abc.ABCMeta, object)): + @abc.abstractmethod + def register(self, fd): + assert isinstance(fd, int) + + @abc.abstractmethod + def unregister(self, fd): + assert isinstance(fd, int) + + @abc.abstractmethod + def select(self, timeout): + pass + + @abc.abstractmethod + def close(self): + pass + + +class AutoSelector(Selector): + def __init__(self): + self._fds = [] + + self._select_selector = SelectSelector() + self._selectors = [self._select_selector] + + # When 'select.poll' exists, create a PollSelector. + if hasattr(select, 'poll'): + self._poll_selector = PollSelector() + self._selectors.append(self._poll_selector) + else: + self._poll_selector = None + + # Use of the 'select' module, that was introduced in Python3.4. We don't + # use it before 3.5 however, because this is the point where this module + # retries interrupted system calls. + if sys.version_info >= (3, 5): + self._py3_selector = Python3Selector() + self._selectors.append(self._py3_selector) + else: + self._py3_selector = None + + def register(self, fd): + assert isinstance(fd, int) + + self._fds.append(fd) + + for sel in self._selectors: + sel.register(fd) + + def unregister(self, fd): + assert isinstance(fd, int) + + self._fds.remove(fd) + + for sel in self._selectors: + sel.unregister(fd) + + def select(self, timeout): + # Try Python 3 selector first. + if self._py3_selector: + try: + return self._py3_selector.select(timeout) + except PermissionError: + # We had a situation (in pypager) where epoll raised a + # PermissionError when a local file descriptor was registered, + # however poll and select worked fine. So, in that case, just + # try using select below. + pass + + try: + # Prefer 'select.select', if we don't have much file descriptors. + # This is more universal. + return self._select_selector.select(timeout) + except ValueError: + # When we have more than 1024 open file descriptors, we'll always + # get a "ValueError: filedescriptor out of range in select()" for + # 'select'. In this case, try, using 'poll' instead. + if self._poll_selector is not None: + return self._poll_selector.select(timeout) + else: + raise + + def close(self): + for sel in self._selectors: + sel.close() + + +class Python3Selector(Selector): + """ + Use of the Python3 'selectors' module. + + NOTE: Only use on Python 3.5 or newer! + """ + def __init__(self): + assert sys.version_info >= (3, 5) + + import selectors # Inline import: Python3 only! + self._sel = selectors.DefaultSelector() + + def register(self, fd): + assert isinstance(fd, int) + import selectors # Inline import: Python3 only! + self._sel.register(fd, selectors.EVENT_READ, None) + + def unregister(self, fd): + assert isinstance(fd, int) + self._sel.unregister(fd) + + def select(self, timeout): + events = self._sel.select(timeout=timeout) + return [key.fileobj for key, mask in events] + + def close(self): + self._sel.close() + + +class PollSelector(Selector): + def __init__(self): + self._poll = select.poll() + + def register(self, fd): + assert isinstance(fd, int) + self._poll.register(fd, select.POLLIN) + + def unregister(self, fd): + assert isinstance(fd, int) + + def select(self, timeout): + tuples = self._poll.poll(timeout) # Returns (fd, event) tuples. + return [t[0] for t in tuples] + + def close(self): + pass # XXX + + +class SelectSelector(Selector): + """ + Wrapper around select.select. + + When the SIGWINCH signal is handled, other system calls, like select + are aborted in Python. This wrapper will retry the system call. + """ + def __init__(self): + self._fds = [] + + def register(self, fd): + self._fds.append(fd) + + def unregister(self, fd): + self._fds.remove(fd) + + def select(self, timeout): + while True: + try: + return select.select(self._fds, [], [], timeout)[0] + except select.error as e: + # Retry select call when EINTR + if e.args and e.args[0] == errno.EINTR: + continue + else: + raise + + def close(self): + pass + + +def select_fds(read_fds, timeout, selector=AutoSelector): + """ + Wait for a list of file descriptors (`read_fds`) to become ready for + reading. This chooses the most appropriate select-tool for use in + prompt-toolkit. + """ + # Map to ensure that we return the objects that were passed in originally. + # Whether they are a fd integer or an object that has a fileno(). + # (The 'poll' implementation for instance, returns always integers.) + fd_map = dict((fd_to_int(fd), fd) for fd in read_fds) + + # Wait, using selector. + sel = selector() + try: + for fd in read_fds: + sel.register(fd) + + result = sel.select(timeout) + + if result is not None: + return [fd_map[fd_to_int(fd)] for fd in result] + finally: + sel.close() diff --git a/src/libs/prompt_toolkit/eventloop/utils.py b/src/libs/prompt_toolkit/eventloop/utils.py new file mode 100644 index 0000000..ff3a4cf --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/utils.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals +import time + +__all__ = ( + 'TimeIt', +) + + +class TimeIt(object): + """ + Context manager that times the duration of the code body. + The `duration` attribute will contain the execution time in seconds. + """ + def __init__(self): + self.duration = None + + def __enter__(self): + self.start = time.time() + return self + + def __exit__(self, *args): + self.end = time.time() + self.duration = self.end - self.start diff --git a/src/libs/prompt_toolkit/eventloop/win32.py b/src/libs/prompt_toolkit/eventloop/win32.py new file mode 100644 index 0000000..9d4d642 --- /dev/null +++ b/src/libs/prompt_toolkit/eventloop/win32.py @@ -0,0 +1,187 @@ +""" +Win32 event loop. + +Windows notes: + - Somehow it doesn't seem to work with the 'ProactorEventLoop'. +""" +from __future__ import unicode_literals + +from ..terminal.win32_input import ConsoleInputReader +from ..win32_types import SECURITY_ATTRIBUTES +from .base import EventLoop, INPUT_TIMEOUT +from .inputhook import InputHookContext +from .utils import TimeIt + +from ctypes import windll, pointer +from ctypes.wintypes import DWORD, BOOL, HANDLE + +import msvcrt +import threading + +__all__ = ( + 'Win32EventLoop', +) + +WAIT_TIMEOUT = 0x00000102 +INPUT_TIMEOUT_MS = int(1000 * INPUT_TIMEOUT) + + +class Win32EventLoop(EventLoop): + """ + Event loop for Windows systems. + + :param recognize_paste: When True, try to discover paste actions and turn + the event into a BracketedPaste. + """ + def __init__(self, inputhook=None, recognize_paste=True): + assert inputhook is None or callable(inputhook) + + self._event = _create_event() + self._console_input_reader = ConsoleInputReader(recognize_paste=recognize_paste) + self._calls_from_executor = [] + + self.closed = False + self._running = False + + # Additional readers. + self._read_fds = {} # Maps fd to handler. + + # Create inputhook context. + self._inputhook_context = InputHookContext(inputhook) if inputhook else None + + def run(self, stdin, callbacks): + if self.closed: + raise Exception('Event loop already closed.') + + current_timeout = INPUT_TIMEOUT_MS + self._running = True + + while self._running: + # Call inputhook. + with TimeIt() as inputhook_timer: + if self._inputhook_context: + def ready(wait): + " True when there is input ready. The inputhook should return control. " + return bool(self._ready_for_reading(current_timeout if wait else 0)) + self._inputhook_context.call_inputhook(ready) + + # Calculate remaining timeout. (The inputhook consumed some of the time.) + if current_timeout == -1: + remaining_timeout = -1 + else: + remaining_timeout = max(0, current_timeout - int(1000 * inputhook_timer.duration)) + + # Wait for the next event. + handle = self._ready_for_reading(remaining_timeout) + + if handle == self._console_input_reader.handle: + # When stdin is ready, read input and reset timeout timer. + keys = self._console_input_reader.read() + for k in keys: + callbacks.feed_key(k) + current_timeout = INPUT_TIMEOUT_MS + + elif handle == self._event: + # When the Windows Event has been trigger, process the messages in the queue. + windll.kernel32.ResetEvent(self._event) + self._process_queued_calls_from_executor() + + elif handle in self._read_fds: + callback = self._read_fds[handle] + callback() + else: + # Fire input timeout event. + callbacks.input_timeout() + current_timeout = -1 + + def _ready_for_reading(self, timeout=None): + """ + Return the handle that is ready for reading or `None` on timeout. + """ + handles = [self._event, self._console_input_reader.handle] + handles.extend(self._read_fds.keys()) + return _wait_for_handles(handles, timeout) + + def stop(self): + self._running = False + + def close(self): + self.closed = True + + # Clean up Event object. + windll.kernel32.CloseHandle(self._event) + + if self._inputhook_context: + self._inputhook_context.close() + + self._console_input_reader.close() + + def run_in_executor(self, callback): + """ + Run a long running function in a background thread. + (This is recommended for code that could block the event loop.) + Similar to Twisted's ``deferToThread``. + """ + # Wait until the main thread is idle for an instant before starting the + # executor. (Like in eventloop/posix.py, we start the executor using + # `call_from_executor`.) + def start_executor(): + threading.Thread(target=callback).start() + self.call_from_executor(start_executor) + + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. + Similar to Twisted's ``callFromThread``. + """ + # Append to list of pending callbacks. + self._calls_from_executor.append(callback) + + # Set Windows event. + windll.kernel32.SetEvent(self._event) + + def _process_queued_calls_from_executor(self): + # Process calls from executor. + calls_from_executor, self._calls_from_executor = self._calls_from_executor, [] + for c in calls_from_executor: + c() + + def add_reader(self, fd, callback): + " Start watching the file descriptor for read availability. " + h = msvcrt.get_osfhandle(fd) + self._read_fds[h] = callback + + def remove_reader(self, fd): + " Stop watching the file descriptor for read availability. " + h = msvcrt.get_osfhandle(fd) + if h in self._read_fds: + del self._read_fds[h] + + +def _wait_for_handles(handles, timeout=-1): + """ + 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 + """ + arrtype = HANDLE * len(handles) + handle_array = arrtype(*handles) + + ret = windll.kernel32.WaitForMultipleObjects( + len(handle_array), handle_array, BOOL(False), DWORD(timeout)) + + if ret == WAIT_TIMEOUT: + return None + else: + h = handle_array[ret] + return h + + +def _create_event(): + """ + Creates a Win32 unnamed Event . + + http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx + """ + return windll.kernel32.CreateEventA(pointer(SECURITY_ATTRIBUTES()), BOOL(True), BOOL(False), None) diff --git a/src/libs/prompt_toolkit/filters/__init__.py b/src/libs/prompt_toolkit/filters/__init__.py new file mode 100644 index 0000000..d3f14ef --- /dev/null +++ b/src/libs/prompt_toolkit/filters/__init__.py @@ -0,0 +1,36 @@ +""" +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. + +One class of filters observe a `CommandLineInterface` instance. However, they +are not attached to such an instance. (We have to pass this instance to the +filter when calling it.) The reason for this is to allow declarative +programming: for key bindings, we can attach a filter to a key binding without +knowing yet which `CommandLineInterface` instance it will observe in the end. +Examples are `HasSearch` or `IsExiting`. + +Another class of filters doesn't take anything as input. And a third class of +filters are universal, for instance `Always` and `Never`. +It is impossible to mix the first and the second class, because that would mean +mixing filters with a different signature. + +Filters can be chained using ``&`` and ``|`` operations, and inverted using the +``~`` operator, for instance:: + + filter = HasFocus('default') & ~ HasSelection() +""" +from __future__ import unicode_literals + +from .base import * +from .cli import * +from .types import * +from .utils import * diff --git a/src/libs/prompt_toolkit/filters/base.py b/src/libs/prompt_toolkit/filters/base.py new file mode 100644 index 0000000..578617b --- /dev/null +++ b/src/libs/prompt_toolkit/filters/base.py @@ -0,0 +1,234 @@ +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +from libs.prompt_toolkit.utils import test_callable_args + + +__all__ = ( + 'Filter', + 'Never', + 'Always', + 'Condition', +) + + +class Filter(with_metaclass(ABCMeta, object)): + """ + Filter to activate/deactivate a feature, depending on a condition. + The return value of ``__call__`` will tell if the feature should be active. + """ + @abstractmethod + def __call__(self, *a, **kw): + """ + The actual call to evaluate the filter. + """ + return True + + def __and__(self, other): + """ + Chaining of filters using the & operator. + """ + return _and_cache[self, other] + + def __or__(self, other): + """ + Chaining of filters using the | operator. + """ + return _or_cache[self, other] + + def __invert__(self): + """ + Inverting of filters using the ~ operator. + """ + return _invert_cache[self] + + def __bool__(self): + """ + By purpose, we don't allow bool(...) operations directly on a filter, + because because the meaning is ambigue. + + 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 TypeError + + __nonzero__ = __bool__ # For Python 2. + + def test_args(self, *args): + """ + Test whether this filter can be called with the following argument list. + """ + return test_callable_args(self.__call__, args) + + +class _AndCache(dict): + """ + Cache for And operation between filters. + (Filter classes are stateless, so we can reuse them.) + + Note: This could be a memory leak if we keep creating filters at runtime. + If that is True, the filters should be weakreffed (not the tuple of + filters), and tuples should be removed when one of these filters is + removed. In practise however, there is a finite amount of filters. + """ + def __missing__(self, filters): + a, b = filters + assert isinstance(b, Filter), 'Expecting filter, got %r' % b + + if isinstance(b, Always) or isinstance(a, Never): + return a + elif isinstance(b, Never) or isinstance(a, Always): + return b + + result = _AndList(filters) + self[filters] = result + return result + + +class _OrCache(dict): + """ Cache for Or operation between filters. """ + def __missing__(self, filters): + a, b = filters + assert isinstance(b, Filter), 'Expecting filter, got %r' % b + + if isinstance(b, Always) or isinstance(a, Never): + return b + elif isinstance(b, Never) or isinstance(a, Always): + return a + + result = _OrList(filters) + self[filters] = result + return result + + +class _InvertCache(dict): + """ Cache for inversion operator. """ + def __missing__(self, filter): + result = _Invert(filter) + self[filter] = result + return result + + +_and_cache = _AndCache() +_or_cache = _OrCache() +_invert_cache = _InvertCache() + + +class _AndList(Filter): + """ + Result of &-operation between several filters. + """ + def __init__(self, filters): + all_filters = [] + + for f in filters: + if isinstance(f, _AndList): # Turn nested _AndLists into one. + all_filters.extend(f.filters) + else: + all_filters.append(f) + + self.filters = all_filters + + def test_args(self, *args): + return all(f.test_args(*args) for f in self.filters) + + def __call__(self, *a, **kw): + return all(f(*a, **kw) for f in self.filters) + + def __repr__(self): + return '&'.join(repr(f) for f in self.filters) + + +class _OrList(Filter): + """ + Result of |-operation between several filters. + """ + def __init__(self, filters): + all_filters = [] + + for f in filters: + if isinstance(f, _OrList): # Turn nested _OrLists into one. + all_filters.extend(f.filters) + else: + all_filters.append(f) + + self.filters = all_filters + + def test_args(self, *args): + return all(f.test_args(*args) for f in self.filters) + + def __call__(self, *a, **kw): + return any(f(*a, **kw) for f in self.filters) + + def __repr__(self): + return '|'.join(repr(f) for f in self.filters) + + +class _Invert(Filter): + """ + Negation of another filter. + """ + def __init__(self, filter): + self.filter = filter + + def __call__(self, *a, **kw): + return not self.filter(*a, **kw) + + def __repr__(self): + return '~%r' % self.filter + + def test_args(self, *args): + return self.filter.test_args(*args) + + +class Always(Filter): + """ + Always enable feature. + """ + def __call__(self, *a, **kw): + return True + + def __invert__(self): + return Never() + + +class Never(Filter): + """ + Never enable feature. + """ + def __call__(self, *a, **kw): + return False + + def __invert__(self): + return Always() + + +class Condition(Filter): + """ + Turn any callable (which takes a cli and returns a boolean) into a Filter. + + This can be used as a decorator:: + + @Condition + def feature_is_active(cli): # `feature_is_active` becomes a Filter. + return True + + :param func: Callable which takes either a + :class:`~prompt_toolkit.interface.CommandLineInterface` or nothing and + returns a boolean. (Depending on what it takes, this will become a + :class:`.Filter` or :class:`~prompt_toolkit.filters.CLIFilter`.) + """ + def __init__(self, func): + assert callable(func) + self.func = func + + def __call__(self, *a, **kw): + return self.func(*a, **kw) + + def __repr__(self): + return 'Condition(%r)' % self.func + + def test_args(self, *a): + return test_callable_args(self.func, a) diff --git a/src/libs/prompt_toolkit/filters/cli.py b/src/libs/prompt_toolkit/filters/cli.py new file mode 100644 index 0000000..c14c271 --- /dev/null +++ b/src/libs/prompt_toolkit/filters/cli.py @@ -0,0 +1,395 @@ +""" +Filters that accept a `CommandLineInterface` as argument. +""" +from __future__ import unicode_literals +from .base import Filter +from libs.prompt_toolkit.enums import EditingMode +from libs.prompt_toolkit.key_binding.vi_state import InputMode as ViInputMode +from libs.prompt_toolkit.cache import memoized + +__all__ = ( + 'HasArg', + 'HasCompletions', + 'HasFocus', + 'InFocusStack', + 'HasSearch', + 'HasSelection', + 'HasValidationError', + 'IsAborting', + 'IsDone', + 'IsMultiline', + 'IsReadOnly', + 'IsReturning', + 'RendererHeightIsKnown', + 'InEditingMode', + + # Vi modes. + 'ViMode', + 'ViNavigationMode', + 'ViInsertMode', + 'ViInsertMultipleMode', + 'ViReplaceMode', + 'ViSelectionMode', + 'ViWaitingForTextObjectMode', + 'ViDigraphMode', + + # Emacs modes. + 'EmacsMode', + 'EmacsInsertMode', + 'EmacsSelectionMode', +) + + +@memoized() +class HasFocus(Filter): + """ + Enable when this buffer has the focus. + """ + def __init__(self, buffer_name): + self._buffer_name = buffer_name + + @property + def buffer_name(self): + " The given buffer name. (Read-only) " + return self._buffer_name + + def __call__(self, cli): + return cli.current_buffer_name == self.buffer_name + + def __repr__(self): + return 'HasFocus(%r)' % self.buffer_name + + +@memoized() +class InFocusStack(Filter): + """ + Enable when this buffer appears on the focus stack. + """ + def __init__(self, buffer_name): + self._buffer_name = buffer_name + + @property + def buffer_name(self): + " The given buffer name. (Read-only) " + return self._buffer_name + + def __call__(self, cli): + return self.buffer_name in cli.buffers.focus_stack + + def __repr__(self): + return 'InFocusStack(%r)' % self.buffer_name + + +@memoized() +class HasSelection(Filter): + """ + Enable when the current buffer has a selection. + """ + def __call__(self, cli): + return bool(cli.current_buffer.selection_state) + + def __repr__(self): + return 'HasSelection()' + + +@memoized() +class HasCompletions(Filter): + """ + Enable when the current buffer has completions. + """ + def __call__(self, cli): + return cli.current_buffer.complete_state is not None + + def __repr__(self): + return 'HasCompletions()' + + +@memoized() +class IsMultiline(Filter): + """ + Enable in multiline mode. + """ + def __call__(self, cli): + return cli.current_buffer.is_multiline() + + def __repr__(self): + return 'IsMultiline()' + + +@memoized() +class IsReadOnly(Filter): + """ + True when the current buffer is read only. + """ + def __call__(self, cli): + return cli.current_buffer.read_only() + + def __repr__(self): + return 'IsReadOnly()' + + +@memoized() +class HasValidationError(Filter): + """ + Current buffer has validation error. + """ + def __call__(self, cli): + return cli.current_buffer.validation_error is not None + + def __repr__(self): + return 'HasValidationError()' + + +@memoized() +class HasArg(Filter): + """ + Enable when the input processor has an 'arg'. + """ + def __call__(self, cli): + return cli.input_processor.arg is not None + + def __repr__(self): + return 'HasArg()' + + +@memoized() +class HasSearch(Filter): + """ + Incremental search is active. + """ + def __call__(self, cli): + return cli.is_searching + + def __repr__(self): + return 'HasSearch()' + + +@memoized() +class IsReturning(Filter): + """ + When a return value has been set. + """ + def __call__(self, cli): + return cli.is_returning + + def __repr__(self): + return 'IsReturning()' + + +@memoized() +class IsAborting(Filter): + """ + True when aborting. (E.g. Control-C pressed.) + """ + def __call__(self, cli): + return cli.is_aborting + + def __repr__(self): + return 'IsAborting()' + + +@memoized() +class IsExiting(Filter): + """ + True when exiting. (E.g. Control-D pressed.) + """ + def __call__(self, cli): + return cli.is_exiting + + def __repr__(self): + return 'IsExiting()' + + +@memoized() +class IsDone(Filter): + """ + True when the CLI is returning, aborting or exiting. + """ + def __call__(self, cli): + return cli.is_done + + def __repr__(self): + return 'IsDone()' + + +@memoized() +class RendererHeightIsKnown(Filter): + """ + 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.) + """ + def __call__(self, cli): + return cli.renderer.height_is_known + + def __repr__(self): + return 'RendererHeightIsKnown()' + + +@memoized() +class InEditingMode(Filter): + """ + Check whether a given editing mode is active. (Vi or Emacs.) + """ + def __init__(self, editing_mode): + self._editing_mode = editing_mode + + @property + def editing_mode(self): + " The given editing mode. (Read-only) " + return self._editing_mode + + def __call__(self, cli): + return cli.editing_mode == self.editing_mode + + def __repr__(self): + return 'InEditingMode(%r)' % (self.editing_mode, ) + + +@memoized() +class ViMode(Filter): + def __call__(self, cli): + return cli.editing_mode == EditingMode.VI + + def __repr__(self): + return 'ViMode()' + + +@memoized() +class ViNavigationMode(Filter): + """ + Active when the set for Vi navigation key bindings are active. + """ + def __call__(self, cli): + if (cli.editing_mode != EditingMode.VI + or cli.vi_state.operator_func + or cli.vi_state.waiting_for_digraph + or cli.current_buffer.selection_state): + return False + + return (cli.vi_state.input_mode == ViInputMode.NAVIGATION or + cli.current_buffer.read_only()) + + def __repr__(self): + return 'ViNavigationMode()' + + +@memoized() +class ViInsertMode(Filter): + def __call__(self, cli): + if (cli.editing_mode != EditingMode.VI + or cli.vi_state.operator_func + or cli.vi_state.waiting_for_digraph + or cli.current_buffer.selection_state + or cli.current_buffer.read_only()): + return False + + return cli.vi_state.input_mode == ViInputMode.INSERT + + def __repr__(self): + return 'ViInputMode()' + + +@memoized() +class ViInsertMultipleMode(Filter): + def __call__(self, cli): + if (cli.editing_mode != EditingMode.VI + or cli.vi_state.operator_func + or cli.vi_state.waiting_for_digraph + or cli.current_buffer.selection_state + or cli.current_buffer.read_only()): + return False + + return cli.vi_state.input_mode == ViInputMode.INSERT_MULTIPLE + + def __repr__(self): + return 'ViInsertMultipleMode()' + + +@memoized() +class ViReplaceMode(Filter): + def __call__(self, cli): + if (cli.editing_mode != EditingMode.VI + or cli.vi_state.operator_func + or cli.vi_state.waiting_for_digraph + or cli.current_buffer.selection_state + or cli.current_buffer.read_only()): + return False + + return cli.vi_state.input_mode == ViInputMode.REPLACE + + def __repr__(self): + return 'ViReplaceMode()' + + +@memoized() +class ViSelectionMode(Filter): + def __call__(self, cli): + if cli.editing_mode != EditingMode.VI: + return False + + return bool(cli.current_buffer.selection_state) + + def __repr__(self): + return 'ViSelectionMode()' + + +@memoized() +class ViWaitingForTextObjectMode(Filter): + def __call__(self, cli): + if cli.editing_mode != EditingMode.VI: + return False + + return cli.vi_state.operator_func is not None + + def __repr__(self): + return 'ViWaitingForTextObjectMode()' + + +@memoized() +class ViDigraphMode(Filter): + def __call__(self, cli): + if cli.editing_mode != EditingMode.VI: + return False + + return cli.vi_state.waiting_for_digraph + + def __repr__(self): + return 'ViDigraphMode()' + + +@memoized() +class EmacsMode(Filter): + " When the Emacs bindings are active. " + def __call__(self, cli): + return cli.editing_mode == EditingMode.EMACS + + def __repr__(self): + return 'EmacsMode()' + + +@memoized() +class EmacsInsertMode(Filter): + def __call__(self, cli): + if (cli.editing_mode != EditingMode.EMACS + or cli.current_buffer.selection_state + or cli.current_buffer.read_only()): + return False + return True + + def __repr__(self): + return 'EmacsInsertMode()' + + +@memoized() +class EmacsSelectionMode(Filter): + def __call__(self, cli): + return (cli.editing_mode == EditingMode.EMACS + and cli.current_buffer.selection_state) + + def __repr__(self): + return 'EmacsSelectionMode()' diff --git a/src/libs/prompt_toolkit/filters/types.py b/src/libs/prompt_toolkit/filters/types.py new file mode 100644 index 0000000..3e89c39 --- /dev/null +++ b/src/libs/prompt_toolkit/filters/types.py @@ -0,0 +1,55 @@ +from __future__ import unicode_literals +from six import with_metaclass +from collections import defaultdict +import weakref + +__all__ = ( + 'CLIFilter', + 'SimpleFilter', +) + +# Cache for _FilterTypeMeta. (Don't test the same __instancecheck__ twice as +# long as the object lives. -- We do this a lot and calling 'test_args' is +# expensive.) +_instance_check_cache = defaultdict(weakref.WeakKeyDictionary) + + +class _FilterTypeMeta(type): + def __instancecheck__(cls, instance): + cache = _instance_check_cache[tuple(cls.arguments_list)] + + def get(): + " The actual test. " + if not hasattr(instance, 'test_args'): + return False + return instance.test_args(*cls.arguments_list) + + try: + return cache[instance] + except KeyError: + result = get() + cache[instance] = result + return result + + +class _FilterType(with_metaclass(_FilterTypeMeta)): + def __new__(cls): + raise NotImplementedError('This class should not be initiated.') + + +class CLIFilter(_FilterType): + """ + Abstract base class for filters that accept a + :class:`~prompt_toolkit.interface.CommandLineInterface` argument. It cannot + be instantiated, it's only to be used for instance assertions, e.g.:: + + isinstance(my_filter, CliFilter) + """ + arguments_list = ['cli'] + + +class SimpleFilter(_FilterType): + """ + Abstract base class for filters that don't accept any arguments. + """ + arguments_list = [] diff --git a/src/libs/prompt_toolkit/filters/utils.py b/src/libs/prompt_toolkit/filters/utils.py new file mode 100644 index 0000000..836d295 --- /dev/null +++ b/src/libs/prompt_toolkit/filters/utils.py @@ -0,0 +1,39 @@ +from __future__ import unicode_literals +from .base import Always, Never +from .types import SimpleFilter, CLIFilter + +__all__ = ( + 'to_cli_filter', + 'to_simple_filter', +) + +_always = Always() +_never = Never() + + +def to_simple_filter(bool_or_filter): + """ + Accept both booleans and CLIFilters as input and + turn it into a SimpleFilter. + """ + if not isinstance(bool_or_filter, (bool, SimpleFilter)): + raise TypeError('Expecting a bool or a SimpleFilter instance. Got %r' % bool_or_filter) + + return { + True: _always, + False: _never, + }.get(bool_or_filter, bool_or_filter) + + +def to_cli_filter(bool_or_filter): + """ + Accept both booleans and CLIFilters as input and + turn it into a CLIFilter. + """ + if not isinstance(bool_or_filter, (bool, CLIFilter)): + raise TypeError('Expecting a bool or a CLIFilter instance. Got %r' % bool_or_filter) + + return { + True: _always, + False: _never, + }.get(bool_or_filter, bool_or_filter) diff --git a/src/libs/prompt_toolkit/history.py b/src/libs/prompt_toolkit/history.py new file mode 100644 index 0000000..d1eb5f2 --- /dev/null +++ b/src/libs/prompt_toolkit/history.py @@ -0,0 +1,120 @@ +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +import datetime +import os + +__all__ = ( + 'FileHistory', + 'History', + 'InMemoryHistory', +) + + +class History(with_metaclass(ABCMeta, object)): + """ + Base ``History`` interface. + """ + @abstractmethod + def append(self, string): + " Append string to history. " + + @abstractmethod + def __getitem__(self, key): + " Return one item of the history. It should be accessible like a `list`. " + + @abstractmethod + def __iter__(self): + " Iterate through all the items of the history. Cronologically. " + + @abstractmethod + def __len__(self): + " Return the length of the history. " + + def __bool__(self): + """ + Never evaluate to False, even when the history is empty. + (Python calls __len__ if __bool__ is not implemented.) + This is mainly to allow lazy evaluation:: + + x = history or InMemoryHistory() + """ + return True + + __nonzero__ = __bool__ # For Python 2. + + +class InMemoryHistory(History): + """ + :class:`.History` class that keeps a list of all strings in memory. + """ + def __init__(self): + self.strings = [] + + def append(self, string): + self.strings.append(string) + + def __getitem__(self, key): + return self.strings[key] + + def __iter__(self): + return iter(self.strings) + + def __len__(self): + return len(self.strings) + + +class FileHistory(History): + """ + :class:`.History` class that stores all strings in a file. + """ + def __init__(self, filename): + self.strings = [] + self.filename = filename + + self._load() + + def _load(self): + lines = [] + + def add(): + if lines: + # Join and drop trailing newline. + string = ''.join(lines)[:-1] + + self.strings.append(string) + + if os.path.exists(self.filename): + with open(self.filename, 'rb') as f: + for line in f: + line = line.decode('utf-8') + + if line.startswith('+'): + lines.append(line[1:]) + else: + add() + lines = [] + + add() + + def append(self, string): + self.strings.append(string) + + # Save to file. + with open(self.filename, 'ab') as f: + def write(t): + f.write(t.encode('utf-8')) + + write('\n# %s\n' % datetime.datetime.now()) + for line in string.split('\n'): + write('+%s\n' % line) + + def __getitem__(self, key): + return self.strings[key] + + def __iter__(self): + return iter(self.strings) + + def __len__(self): + return len(self.strings) diff --git a/src/libs/prompt_toolkit/input.py b/src/libs/prompt_toolkit/input.py new file mode 100644 index 0000000..a6467aa --- /dev/null +++ b/src/libs/prompt_toolkit/input.py @@ -0,0 +1,135 @@ +""" +Abstraction of CLI Input. +""" +from __future__ import unicode_literals + +from .utils import DummyContext, is_windows +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +import io +import os +import sys + +if is_windows(): + from .terminal.win32_input import raw_mode, cooked_mode +else: + from .terminal.vt100_input import raw_mode, cooked_mode + +__all__ = ( + 'Input', + 'StdinInput', + 'PipeInput', +) + + +class Input(with_metaclass(ABCMeta, object)): + """ + Abstraction for any input. + + An instance of this class can be given to the constructor of a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` and will also be + passed to the :class:`~libs.prompt_toolkit.eventloop.base.EventLoop`. + """ + @abstractmethod + def fileno(self): + """ + Fileno for putting this in an event loop. + """ + + @abstractmethod + def read(self): + """ + Return text from the input. + """ + + @abstractmethod + def raw_mode(self): + """ + Context manager that turns the input into raw mode. + """ + + @abstractmethod + def cooked_mode(self): + """ + Context manager that turns the input into cooked mode. + """ + + +class StdinInput(Input): + """ + Simple wrapper around stdin. + """ + def __init__(self, stdin=None): + self.stdin = stdin or sys.stdin + + # The input object should be a TTY. + assert self.stdin.isatty() + + # 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. + self.stdin.fileno() + except io.UnsupportedOperation: + if 'idlelib.run' in sys.modules: + raise io.UnsupportedOperation( + 'Stdin is not a terminal. Running from Idle is not supported.') + else: + raise io.UnsupportedOperation('Stdin is not a terminal.') + + def __repr__(self): + return 'StdinInput(stdin=%r)' % (self.stdin,) + + def raw_mode(self): + return raw_mode(self.stdin.fileno()) + + def cooked_mode(self): + return cooked_mode(self.stdin.fileno()) + + def fileno(self): + return self.stdin.fileno() + + def read(self): + return self.stdin.read() + + +class PipeInput(Input): + """ + Input that is send through a pipe. + This is useful if we want to send the input programatically into the + interface, but still use the eventloop. + + Usage:: + + input = PipeInput() + input.send('inputdata') + """ + def __init__(self): + self._r, self._w = os.pipe() + + def fileno(self): + return self._r + + def read(self): + return os.read(self._r) + + def send_text(self, data): + " Send text to the input. " + os.write(self._w, data.encode('utf-8')) + + # Deprecated alias for `send_text`. + send = send_text + + def raw_mode(self): + return DummyContext() + + def cooked_mode(self): + return DummyContext() + + def close(self): + " Close pipe fds. " + os.close(self._r) + os.close(self._w) + self._r = None + self._w = None diff --git a/src/libs/prompt_toolkit/interface.py b/src/libs/prompt_toolkit/interface.py new file mode 100644 index 0000000..1437870 --- /dev/null +++ b/src/libs/prompt_toolkit/interface.py @@ -0,0 +1,1185 @@ +""" +The main `CommandLineInterface` class and logic. +""" +from __future__ import unicode_literals + +import functools +import os +import signal +import six +import sys +import textwrap +import threading +import time +import types +import weakref + +from subprocess import Popen + +from .application import Application, AbortAction +from .buffer import Buffer +from .buffer_mapping import BufferMapping +from .completion import CompleteEvent, get_common_complete_suffix +from .enums import SEARCH_BUFFER +from .eventloop.base import EventLoop +from .eventloop.callbacks import EventLoopCallbacks +from .filters import Condition +from .input import StdinInput, Input +from .key_binding.input_processor import InputProcessor +from .key_binding.input_processor import KeyPress +from .key_binding.registry import Registry +from .key_binding.vi_state import ViState +from .keys import Keys +from .output import Output +from .renderer import Renderer, print_tokens +from .search_state import SearchState +from .utils import Event + +# Following import is required for backwards compatibility. +from .buffer import AcceptAction + +__all__ = ( + 'AbortAction', + 'CommandLineInterface', +) + + +class CommandLineInterface(object): + """ + Wrapper around all the other classes, tying everything together. + + Typical usage:: + + application = Application(...) + cli = CommandLineInterface(application, eventloop) + result = cli.run() + print(result) + + :param application: :class:`~libs.prompt_toolkit.application.Application` instance. + :param eventloop: The :class:`~libs.prompt_toolkit.eventloop.base.EventLoop` to + be used when `run` is called. The easiest way to create + an eventloop is by calling + :meth:`~libs.prompt_toolkit.shortcuts.create_eventloop`. + :param input: :class:`~libs.prompt_toolkit.input.Input` instance. + :param output: :class:`~libs.prompt_toolkit.output.Output` instance. (Probably + Vt100_Output or Win32Output.) + """ + def __init__(self, application, eventloop=None, input=None, output=None): + assert isinstance(application, Application) + assert isinstance(eventloop, EventLoop), 'Passing an eventloop is required.' + assert output is None or isinstance(output, Output) + assert input is None or isinstance(input, Input) + + from .shortcuts import create_output + + self.application = application + self.eventloop = eventloop + self._is_running = False + + # Inputs and outputs. + self.output = output or create_output() + self.input = input or StdinInput(sys.stdin) + + #: The input buffers. + assert isinstance(application.buffers, BufferMapping) + self.buffers = application.buffers + + #: EditingMode.VI or EditingMode.EMACS + self.editing_mode = application.editing_mode + + #: 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() + + #: The `Renderer` instance. + # Make sure that the same stdout is used, when a custom renderer has been passed. + self.renderer = Renderer( + self.application.style, + self.output, + use_alternate_screen=application.use_alternate_screen, + mouse_support=application.mouse_support) + + #: 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 + + #: When there is high CPU, postpone the renderering max x seconds. + #: '0' means: don't postpone. '.5' means: try to draw at least twice a second. + self.max_render_postpone_time = 0 # E.g. .5 + + # Invalidate flag. When 'True', a repaint has been scheduled. + self._invalidated = False + + #: The `InputProcessor` instance. + self.input_processor = InputProcessor(application.key_bindings_registry, weakref.ref(self)) + + self._async_completers = {} # Map buffer name to completer function. + + # Pointer to sub CLI. (In chain of CLI instances.) + self._sub_cli = None # None or other CommandLineInterface instance. + + # Call `add_buffer` for each buffer. + for name, b in self.buffers.items(): + self.add_buffer(name, b) + + # Events. + self.on_buffer_changed = Event(self, application.on_buffer_changed) + self.on_initialize = Event(self, application.on_initialize) + self.on_input_timeout = Event(self, application.on_input_timeout) + self.on_invalidate = Event(self, application.on_invalidate) + self.on_render = Event(self, application.on_render) + self.on_reset = Event(self, application.on_reset) + self.on_start = Event(self, application.on_start) + self.on_stop = Event(self, application.on_stop) + + # Trigger initialize callback. + self.reset() + self.on_initialize += self.application.on_initialize + self.on_initialize.fire() + + @property + def layout(self): + return self.application.layout + + @property + def clipboard(self): + return self.application.clipboard + + @property + def pre_run_callables(self): + return self.application.pre_run_callables + + def add_buffer(self, name, buffer, focus=False): + """ + Insert a new buffer. + """ + assert isinstance(buffer, Buffer) + self.buffers[name] = buffer + + if focus: + self.buffers.focus(name) + + # Create asynchronous completer / auto suggestion. + auto_suggest_function = self._create_auto_suggest_function(buffer) + completer_function = self._create_async_completer(buffer) + self._async_completers[name] = completer_function + + # Complete/suggest on text insert. + def create_on_insert_handler(): + """ + Wrapper around the asynchronous completer and auto suggestion, that + ensures that it's only called while typing if the + `complete_while_typing` filter is enabled. + """ + def on_text_insert(_): + # Only complete when "complete_while_typing" is enabled. + if buffer.completer and buffer.complete_while_typing(): + completer_function() + + # Call auto_suggest. + if buffer.auto_suggest: + auto_suggest_function() + + return on_text_insert + + buffer.on_text_insert += create_on_insert_handler() + + def buffer_changed(_): + """ + When the text in a buffer changes. + (A paste event is also a change, but not an insert. So we don't + want to do autocompletions in this case, but we want to propagate + the on_buffer_changed event.) + """ + # Trigger on_buffer_changed. + self.on_buffer_changed.fire() + + buffer.on_text_changed += buffer_changed + + def start_completion(self, buffer_name=None, select_first=False, + select_last=False, insert_common_part=False, + complete_event=None): + """ + Start asynchronous autocompletion of this buffer. + (This will do nothing if a previous completion was still in progress.) + """ + buffer_name = buffer_name or self.current_buffer_name + completer = self._async_completers.get(buffer_name) + + if completer: + completer(select_first=select_first, + select_last=select_last, + insert_common_part=insert_common_part, + complete_event=CompleteEvent(completion_requested=True)) + + @property + def current_buffer_name(self): + """ + The name of the current :class:`.Buffer`. (Or `None`.) + """ + return self.buffers.current_name(self) + + @property + def current_buffer(self): + """ + The currently focussed :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.buffers.current(self) + + def focus(self, buffer_name): + """ + Focus the buffer with the given name on the focus stack. + """ + self.buffers.focus(self, buffer_name) + + def push_focus(self, buffer_name): + """ + Push to the focus stack. + """ + self.buffers.push_focus(self, buffer_name) + + def pop_focus(self): + """ + Pop from the focus stack. + """ + self.buffers.pop_focus(self) + + @property + def terminal_title(self): + """ + Return the current title to be displayed in the terminal. + When this in `None`, the terminal title remains the original. + """ + result = self.application.get_title() + + # Make sure that this function returns a unicode object, + # and not a byte string. + assert result is None or isinstance(result, six.text_type) + return result + + @property + def is_searching(self): + """ + True when we are searching. + """ + return self.current_buffer_name == SEARCH_BUFFER + + def reset(self, reset_current_buffer=False): + """ + Reset everything, for reading the next input. + + :param reset_current_buffer: XXX: not used anymore. The reason for + having this option in the past was when this CommandLineInterface + is run multiple times, that we could reset the buffer content from + the previous run. This is now handled in the AcceptAction. + """ + # 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_flag = False + self._abort_flag = False + + self._return_value = None + + self.renderer.reset() + self.input_processor.reset() + self.layout.reset() + self.vi_state.reset() + + # Search new search state. (Does also remember what has to be + # highlighted.) + self.search_state = SearchState(ignore_case=Condition(lambda: self.is_ignoring_case)) + + # Trigger reset event. + self.on_reset.fire() + + @property + def in_paste_mode(self): + """ True when we are in paste mode. """ + return self.application.paste_mode(self) + + @property + def is_ignoring_case(self): + """ True when we currently ignore casing. """ + return self.application.ignore_case(self) + + def invalidate(self): + """ + Thread safe way of sending a repaint trigger to the input event loop. + """ + # 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.on_invalidate.fire() + + if self.eventloop is not None: + def redraw(): + self._invalidated = False + self._redraw() + + # Call redraw in the eventloop (thread safe). + # Usually with the high priority, in order to make the application + # feel responsive, but this can be tuned by changing the value of + # `max_render_postpone_time`. + if self.max_render_postpone_time: + _max_postpone_until = time.time() + self.max_render_postpone_time + else: + _max_postpone_until = None + + self.eventloop.call_from_executor( + redraw, _max_postpone_until=_max_postpone_until) + + # Depracated alias for 'invalidate'. + request_redraw = invalidate + + def _redraw(self): + """ + Render the command line again. (Not thread safe!) (From other threads, + or if unsure, use :meth:`.CommandLineInterface.invalidate`.) + """ + # Only draw when no sub application was started. + if self._is_running and self._sub_cli is None: + self.render_counter += 1 + self.renderer.render(self, self.layout, is_done=self.is_done) + + # Fire render event. + self.on_render.fire() + + def _on_resize(self): + """ + 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, erase_title=False) + self.renderer.request_absolute_cursor_position() + self._redraw() + + def _load_next_buffer_indexes(self): + for buff, index in self._next_buffer_indexes.items(): + if buff in self.buffers: + self.buffers[buff].working_index = index + + def _pre_run(self, pre_run=None): + " Called during `run`. " + 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[:] + + def run(self, reset_current_buffer=False, pre_run=None): + """ + Read input from the command line. + This runs the eventloop until a return value has been set. + + :param reset_current_buffer: XXX: Not used anymore. + :param pre_run: Callable that is called right after the reset has taken + place. This allows custom initialisation. + """ + assert pre_run is None or callable(pre_run) + + try: + self._is_running = True + + self.on_start.fire() + self.reset() + + # Call pre_run. + self._pre_run(pre_run) + + # Run eventloop in raw mode. + with self.input.raw_mode(): + self.renderer.request_absolute_cursor_position() + self._redraw() + + self.eventloop.run(self.input, self.create_eventloop_callbacks()) + finally: + # Clean up renderer. (This will leave the alternate screen, if we use + # that.) + + # If exit/abort haven't been called set, but another exception was + # thrown instead for some reason, make sure that we redraw in exit + # mode. + if not self.is_done: + self._exit_flag = True + self._redraw() + + self.renderer.reset() + self.on_stop.fire() + self._is_running = False + + # Return result. + return self.return_value() + + try: + # The following `run_async` function is compiled at runtime + # because it contains syntax which is not supported on older Python + # versions. (A 'return' inside a generator.) + six.exec_(textwrap.dedent(''' + def run_async(self, reset_current_buffer=True, pre_run=None): + """ + Same as `run`, but this returns a coroutine. + + This is only available on Python >3.3, with asyncio. + """ + # Inline import, because it slows down startup when asyncio is not + # needed. + import asyncio + + @asyncio.coroutine + def run(): + assert pre_run is None or callable(pre_run) + + try: + self._is_running = True + + self.on_start.fire() + self.reset() + + # Call pre_run. + self._pre_run(pre_run) + + with self.input.raw_mode(): + self.renderer.request_absolute_cursor_position() + self._redraw() + + yield from self.eventloop.run_as_coroutine( + self.input, self.create_eventloop_callbacks()) + + return self.return_value() + finally: + if not self.is_done: + self._exit_flag = True + self._redraw() + + self.renderer.reset() + self.on_stop.fire() + self._is_running = False + + return run() + ''')) + except SyntaxError: + # Python2, or early versions of Python 3. + def run_async(self, reset_current_buffer=True, pre_run=None): + """ + Same as `run`, but this returns a coroutine. + + This is only available on Python >3.3, with asyncio. + """ + raise NotImplementedError + + def run_sub_application(self, application, done_callback=None, erase_when_done=False, + _from_application_generator=False): + # `erase_when_done` is deprecated, set Application.erase_when_done instead. + """ + Run a sub :class:`~libs.prompt_toolkit.application.Application`. + + This will suspend the main application and display the sub application + until that one returns a value. The value is returned by calling + `done_callback` with the result. + + The sub application will share the same I/O of the main application. + That means, it uses the same input and output channels and it shares + the same event loop. + + .. note:: Technically, it gets another Eventloop instance, but that is + only a proxy to our main event loop. The reason is that calling + 'stop' --which returns the result of an application when it's + done-- is handled differently. + """ + assert isinstance(application, Application) + assert done_callback is None or callable(done_callback) + + if self._sub_cli is not None: + raise RuntimeError('Another sub application started already.') + + # Erase current application. + if not _from_application_generator: + self.renderer.erase() + + # Callback when the sub app is done. + def done(): + # Redraw sub app in done state. + # and reset the renderer. (This reset will also quit the alternate + # screen, if the sub application used that.) + sub_cli._redraw() + if erase_when_done or application.erase_when_done: + sub_cli.renderer.erase() + sub_cli.renderer.reset() + sub_cli._is_running = False # Don't render anymore. + + self._sub_cli = None + + # Restore main application. + if not _from_application_generator: + self.renderer.request_absolute_cursor_position() + self._redraw() + + # Deliver result. + if done_callback: + done_callback(sub_cli.return_value()) + + # Create sub CommandLineInterface. + sub_cli = CommandLineInterface( + application=application, + eventloop=_SubApplicationEventLoop(self, done), + input=self.input, + output=self.output) + sub_cli._is_running = True # Allow rendering of sub app. + + sub_cli._redraw() + self._sub_cli = sub_cli + + def exit(self): + """ + Set exit. When Control-D has been pressed. + """ + on_exit = self.application.on_exit + self._exit_flag = True + self._redraw() + + if on_exit == AbortAction.RAISE_EXCEPTION: + def eof_error(): + raise EOFError() + self._set_return_callable(eof_error) + + elif on_exit == AbortAction.RETRY: + self.reset() + self.renderer.request_absolute_cursor_position() + self.current_buffer.reset() + + elif on_exit == AbortAction.RETURN_NONE: + self.set_return_value(None) + + def abort(self): + """ + Set abort. When Control-C has been pressed. + """ + on_abort = self.application.on_abort + self._abort_flag = True + self._redraw() + + if on_abort == AbortAction.RAISE_EXCEPTION: + def keyboard_interrupt(): + raise KeyboardInterrupt() + self._set_return_callable(keyboard_interrupt) + + elif on_abort == AbortAction.RETRY: + self.reset() + self.renderer.request_absolute_cursor_position() + self.current_buffer.reset() + + elif on_abort == AbortAction.RETURN_NONE: + self.set_return_value(None) + + # Deprecated aliase for exit/abort. + set_exit = exit + set_abort = abort + + def set_return_value(self, document): + """ + Set a return value. The eventloop can retrieve the result it by calling + `return_value`. + """ + self._set_return_callable(lambda: document) + self._redraw() # Redraw in "done" state, after the return value has been set. + + def _set_return_callable(self, value): + assert callable(value) + self._return_value = value + + if self.eventloop: + self.eventloop.stop() + + def run_in_terminal(self, func, render_cli_done=False): + """ + Run function on the terminal above the 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. + + :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. + + :returns: the result of `func`. + """ + # Draw interface in 'done' state, or erase. + if render_cli_done: + self._return_value = True + self._redraw() + self.renderer.reset() # Make sure to disable mouse mode, etc... + else: + self.renderer.erase() + self._return_value = None + + # Run system command. + with self.input.cooked_mode(): + result = func() + + # Redraw interface again. + self.renderer.reset() + self.renderer.request_absolute_cursor_position() + self._redraw() + + return result + + def run_application_generator(self, coroutine, render_cli_done=False): + """ + EXPERIMENTAL + Like `run_in_terminal`, but takes a generator that can yield Application instances. + + Example: + + def f(): + yield Application1(...) + print('...') + yield Application2(...) + cli.run_in_terminal_async(f) + + The values which are yielded by the given coroutine are supposed to be + `Application` instances that run in the current CLI, all other code is + supposed to be CPU bound, so except for yielding the applications, + there should not be any user interaction or I/O in the given function. + """ + # Draw interface in 'done' state, or erase. + if render_cli_done: + self._return_value = True + self._redraw() + self.renderer.reset() # Make sure to disable mouse mode, etc... + else: + self.renderer.erase() + self._return_value = None + + # Loop through the generator. + g = coroutine() + assert isinstance(g, types.GeneratorType) + + def step_next(send_value=None): + " Execute next step of the coroutine." + try: + # Run until next yield, in cooked mode. + with self.input.cooked_mode(): + result = g.send(send_value) + except StopIteration: + done() + except: + done() + raise + else: + # Process yielded value from coroutine. + assert isinstance(result, Application) + self.run_sub_application(result, done_callback=step_next, + _from_application_generator=True) + + def done(): + # Redraw interface again. + self.renderer.reset() + self.renderer.request_absolute_cursor_position() + self._redraw() + + # Start processing coroutine. + step_next() + + def run_system_command(self, command): + """ + Run system command (While hiding the prompt. When finished, all the + output will scroll above the prompt.) + + :param command: Shell command to be executed. + """ + def wait_for_enter(): + """ + 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 .shortcuts import create_prompt_application + + registry = Registry() + + @registry.add_binding(Keys.ControlJ) + @registry.add_binding(Keys.ControlM) + def _(event): + event.cli.set_return_value(None) + + application = create_prompt_application( + message='Press ENTER to continue...', + key_bindings_registry=registry) + self.run_sub_application(application) + + def run(): + # 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. + # XXX: This will still block the event loop. + p = Popen(command, shell=True, + stdin=input_fd, stdout=output_fd) + p.wait() + + # Wait for the user to press enter. + wait_for_enter() + + self.run_in_terminal(run) + + def suspend_to_background(self, suspend_group=True): + """ + (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 opperating system supports it. + # (Not on Windows.) + if hasattr(signal, 'SIGTSTP'): + def run(): + # Send `SIGSTP` 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.SIGTSTP) + else: + os.kill(os.getpid(), signal.SIGTSTP) + + self.run_in_terminal(run) + + def print_tokens(self, tokens, style=None): + """ + Print a list of (Token, 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 style: Style class to use. Defaults to the active style in the CLI. + """ + print_tokens(self.output, tokens, style or self.application.style) + + @property + def is_exiting(self): + """ + ``True`` when the exit flag as been set. + """ + return self._exit_flag + + @property + def is_aborting(self): + """ + ``True`` when the abort flag as been set. + """ + return self._abort_flag + + @property + def is_returning(self): + """ + ``True`` when a return value has been set. + """ + return self._return_value is not None + + def return_value(self): + """ + Get the return value. Not that this method can throw an exception. + """ + # Note that it's a method, not a property, because it can throw + # exceptions. + if self._return_value: + return self._return_value() + + @property + def is_done(self): + return self.is_exiting or self.is_aborting or self.is_returning + + def _create_async_completer(self, buffer): + """ + Create function for asynchronous autocompletion. + (Autocomplete in other thread.) + """ + complete_thread_running = [False] # By ref. + + def completion_does_nothing(document, completion): + """ + 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 + + def async_completer(select_first=False, select_last=False, + insert_common_part=False, complete_event=None): + document = buffer.document + complete_event = complete_event or CompleteEvent(text_inserted=True) + + # Don't start two threads at the same time. + if complete_thread_running[0]: + return + + # Don't complete when we already have completions. + if buffer.complete_state or not buffer.completer: + return + + # Otherwise, get completions in other thread. + complete_thread_running[0] = True + + def run(): + completions = list(buffer.completer.get_completions(document, complete_event)) + + def callback(): + """ + Set the new complete_state in a safe way. Don't replace an + existing complete_state if we had one. (The user could have + pressed 'Tab' in the meantime. Also don't set it if the text + was changed in the meantime. + """ + complete_thread_running[0] = False + + # 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 buffer.text == document.text and \ + buffer.cursor_position == document.cursor_position and \ + not buffer.complete_state: + + set_completions = True + select_first_anyway = False + + # When the common part has to be inserted, and there + # is a common part. + if insert_common_part: + common_part = get_common_complete_suffix(document, completions) + if common_part: + # Insert the common part, update completions. + buffer.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] + else: + set_completions = False + 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: + select_first_anyway = True + + if set_completions: + buffer.set_completions( + completions=completions, + go_to_first=select_first or select_first_anyway, + go_to_last=select_last) + self.invalidate() + elif not buffer.complete_state: + # Otherwise, restart thread. + async_completer() + + if self.eventloop: + self.eventloop.call_from_executor(callback) + + self.eventloop.run_in_executor(run) + return async_completer + + def _create_auto_suggest_function(self, buffer): + """ + Create function for asynchronous auto suggestion. + (AutoSuggest in other thread.) + """ + suggest_thread_running = [False] # By ref. + + def async_suggestor(): + document = buffer.document + + # Don't start two threads at the same time. + if suggest_thread_running[0]: + return + + # Don't suggest when we already have a suggestion. + if buffer.suggestion or not buffer.auto_suggest: + return + + # Otherwise, get completions in other thread. + suggest_thread_running[0] = True + + def run(): + suggestion = buffer.auto_suggest.get_suggestion(self, buffer, document) + + def callback(): + suggest_thread_running[0] = False + + # Set suggestion only if the text was not yet changed. + if buffer.text == document.text and \ + buffer.cursor_position == document.cursor_position: + + # Set suggestion and redraw interface. + buffer.suggestion = suggestion + self.invalidate() + else: + # Otherwise, restart thread. + async_suggestor() + + if self.eventloop: + self.eventloop.call_from_executor(callback) + + self.eventloop.run_in_executor(run) + return async_suggestor + + def stdout_proxy(self, raw=False): + """ + Create an :class:`_StdoutProxy` class which can be used as a patch for + `sys.stdout`. 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. + + :param raw: (`bool`) When True, vt100 terminal escape sequences are not + removed/escaped. + """ + return _StdoutProxy(self, raw=raw) + + def patch_stdout_context(self, raw=False, patch_stdout=True, patch_stderr=True): + """ + Return a context manager that will replace ``sys.stdout`` with a proxy + that makes sure that all printed text will appear above the prompt, and + that it doesn't destroy the output from the renderer. + + :param patch_stdout: Replace `sys.stdout`. + :param patch_stderr: Replace `sys.stderr`. + """ + return _PatchStdoutContext( + self.stdout_proxy(raw=raw), + patch_stdout=patch_stdout, patch_stderr=patch_stderr) + + def create_eventloop_callbacks(self): + return _InterfaceEventLoopCallbacks(self) + + +class _InterfaceEventLoopCallbacks(EventLoopCallbacks): + """ + Callbacks on the :class:`.CommandLineInterface` object, to which an + eventloop can talk. + """ + def __init__(self, cli): + assert isinstance(cli, CommandLineInterface) + self.cli = cli + + @property + def _active_cli(self): + """ + Return the active `CommandLineInterface`. + """ + cli = self.cli + + # If there is a sub CLI. That one is always active. + while cli._sub_cli: + cli = cli._sub_cli + + return cli + + def terminal_size_changed(self): + """ + Report terminal size change. This will trigger a redraw. + """ + self._active_cli._on_resize() + + def input_timeout(self): + cli = self._active_cli + cli.on_input_timeout.fire() + + def feed_key(self, key_press): + """ + Feed a key press to the CommandLineInterface. + """ + assert isinstance(key_press, KeyPress) + cli = self._active_cli + + # Feed the key and redraw. + # (When the CLI is in 'done' state, it should return to the event loop + # as soon as possible. Ignore all key presses beyond this point.) + if not cli.is_done: + cli.input_processor.feed(key_press) + cli.input_processor.process_keys() + + +class _PatchStdoutContext(object): + def __init__(self, new_stdout, patch_stdout=True, patch_stderr=True): + self.new_stdout = new_stdout + self.patch_stdout = patch_stdout + self.patch_stderr = patch_stderr + + def __enter__(self): + self.original_stdout = sys.stdout + self.original_stderr = sys.stderr + + if self.patch_stdout: + sys.stdout = self.new_stdout + if self.patch_stderr: + sys.stderr = self.new_stdout + + def __exit__(self, *a, **kw): + if self.patch_stdout: + sys.stdout = self.original_stdout + + if self.patch_stderr: + sys.stderr = self.original_stderr + + +class _StdoutProxy(object): + """ + Proxy for stdout, as returned by + :class:`CommandLineInterface.stdout_proxy`. + """ + def __init__(self, cli, raw=False): + assert isinstance(cli, CommandLineInterface) + assert isinstance(raw, bool) + + self._lock = threading.RLock() + self._cli = cli + self._raw = raw + self._buffer = [] + + self.errors = sys.__stdout__.errors + self.encoding = sys.__stdout__.encoding + + def _do(self, func): + if self._cli._is_running: + run_in_terminal = functools.partial(self._cli.run_in_terminal, func) + self._cli.eventloop.call_from_executor(run_in_terminal) + else: + func() + + def _write(self, data): + """ + 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 overwritter 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] + + def run(): + for s in to_write: + if self._raw: + self._cli.output.write_raw(s) + else: + self._cli.output.write(s) + self._do(run) + else: + # Otherwise, cache in buffer. + self._buffer.append(data) + + def write(self, data): + with self._lock: + self._write(data) + + def _flush(self): + def run(): + for s in self._buffer: + if self._raw: + self._cli.output.write_raw(s) + else: + self._cli.output.write(s) + self._buffer = [] + self._cli.output.flush() + self._do(run) + + def flush(self): + """ + Flush buffered output. + """ + with self._lock: + self._flush() + + +class _SubApplicationEventLoop(EventLoop): + """ + Eventloop used by sub applications. + + A sub application is an `Application` that is "spawned" by a parent + application. The parent application is suspended temporarily and the sub + application is displayed instead. + + It doesn't need it's own event loop. The `EventLoopCallbacks` from the + parent application are redirected to the sub application. So if the event + loop that is run by the parent application detects input, the callbacks + will make sure that it's forwarded to the sub application. + + When the sub application has a return value set, it will terminate + by calling the `stop` method of this event loop. This is used to + transfer control back to the parent application. + """ + def __init__(self, cli, stop_callback): + assert isinstance(cli, CommandLineInterface) + assert callable(stop_callback) + + self.cli = cli + self.stop_callback = stop_callback + + def stop(self): + self.stop_callback() + + def close(self): + pass + + def run_in_executor(self, callback): + self.cli.eventloop.run_in_executor(callback) + + def call_from_executor(self, callback, _max_postpone_until=None): + self.cli.eventloop.call_from_executor( + callback, _max_postpone_until=_max_postpone_until) + + def add_reader(self, fd, callback): + self.cli.eventloop.add_reader(fd, callback) + + def remove_reader(self, fd): + self.cli.eventloop.remove_reader(fd) diff --git a/src/libs/prompt_toolkit/key_binding/__init__.py b/src/libs/prompt_toolkit/key_binding/__init__.py new file mode 100644 index 0000000..baffc48 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/src/libs/prompt_toolkit/key_binding/bindings/__init__.py b/src/libs/prompt_toolkit/key_binding/bindings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/libs/prompt_toolkit/key_binding/bindings/basic.py b/src/libs/prompt_toolkit/key_binding/bindings/basic.py new file mode 100644 index 0000000..63ca074 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/bindings/basic.py @@ -0,0 +1,407 @@ +# pylint: disable=function-redefined +from __future__ import unicode_literals + +from libs.prompt_toolkit.enums import DEFAULT_BUFFER +from libs.prompt_toolkit.filters import HasSelection, Condition, EmacsInsertMode, ViInsertMode +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.layout.screen import Point +from libs.prompt_toolkit.mouse_events import MouseEventType, MouseEvent +from libs.prompt_toolkit.renderer import HeightIsUnknownError +from libs.prompt_toolkit.utils import suspend_to_background_supported, is_windows + +from .named_commands import get_by_name +from ..registry import Registry + + +__all__ = ( + 'load_basic_bindings', + 'load_abort_and_exit_bindings', + 'load_basic_system_bindings', + 'load_auto_suggestion_bindings', +) + +def if_no_repeat(event): + """ Callable that returns True when the previous event was delivered to + another handler. """ + return not event.is_repeat + + +def load_basic_bindings(): + registry = Registry() + insert_mode = ViInsertMode() | EmacsInsertMode() + handle = registry.add_binding + has_selection = HasSelection() + + @handle(Keys.ControlA) + @handle(Keys.ControlB) + @handle(Keys.ControlC) + @handle(Keys.ControlD) + @handle(Keys.ControlE) + @handle(Keys.ControlF) + @handle(Keys.ControlG) + @handle(Keys.ControlH) + @handle(Keys.ControlI) + @handle(Keys.ControlJ) + @handle(Keys.ControlK) + @handle(Keys.ControlL) + @handle(Keys.ControlM) + @handle(Keys.ControlN) + @handle(Keys.ControlO) + @handle(Keys.ControlP) + @handle(Keys.ControlQ) + @handle(Keys.ControlR) + @handle(Keys.ControlS) + @handle(Keys.ControlT) + @handle(Keys.ControlU) + @handle(Keys.ControlV) + @handle(Keys.ControlW) + @handle(Keys.ControlX) + @handle(Keys.ControlY) + @handle(Keys.ControlZ) + @handle(Keys.F1) + @handle(Keys.F2) + @handle(Keys.F3) + @handle(Keys.F4) + @handle(Keys.F5) + @handle(Keys.F6) + @handle(Keys.F7) + @handle(Keys.F8) + @handle(Keys.F9) + @handle(Keys.F10) + @handle(Keys.F11) + @handle(Keys.F12) + @handle(Keys.F13) + @handle(Keys.F14) + @handle(Keys.F15) + @handle(Keys.F16) + @handle(Keys.F17) + @handle(Keys.F18) + @handle(Keys.F19) + @handle(Keys.F20) + @handle(Keys.ControlSpace) + @handle(Keys.ControlBackslash) + @handle(Keys.ControlSquareClose) + @handle(Keys.ControlCircumflex) + @handle(Keys.ControlUnderscore) + @handle(Keys.Backspace) + @handle(Keys.Up) + @handle(Keys.Down) + @handle(Keys.Right) + @handle(Keys.Left) + @handle(Keys.ShiftUp) + @handle(Keys.ShiftDown) + @handle(Keys.ShiftRight) + @handle(Keys.ShiftLeft) + @handle(Keys.Home) + @handle(Keys.End) + @handle(Keys.Delete) + @handle(Keys.ShiftDelete) + @handle(Keys.ControlDelete) + @handle(Keys.PageUp) + @handle(Keys.PageDown) + @handle(Keys.BackTab) + @handle(Keys.Tab) + @handle(Keys.ControlLeft) + @handle(Keys.ControlRight) + @handle(Keys.ControlUp) + @handle(Keys.ControlDown) + @handle(Keys.Insert) + @handle(Keys.Ignore) + def _(event): + """ + 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(Keys.Home)(get_by_name('beginning-of-line')) + handle(Keys.End)(get_by_name('end-of-line')) + handle(Keys.Left)(get_by_name('backward-char')) + handle(Keys.Right)(get_by_name('forward-char')) + handle(Keys.ControlUp)(get_by_name('previous-history')) + handle(Keys.ControlDown)(get_by_name('next-history')) + handle(Keys.ControlL)(get_by_name('clear-screen')) + + handle(Keys.ControlK, filter=insert_mode)(get_by_name('kill-line')) + handle(Keys.ControlU, filter=insert_mode)(get_by_name('unix-line-discard')) + handle(Keys.ControlH, filter=insert_mode, save_before=if_no_repeat)( + get_by_name('backward-delete-char')) + handle(Keys.Backspace, filter=insert_mode, save_before=if_no_repeat)( + get_by_name('backward-delete-char')) + handle(Keys.Delete, filter=insert_mode, save_before=if_no_repeat)( + get_by_name('delete-char')) + handle(Keys.ShiftDelete, 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(Keys.ControlT, filter=insert_mode)(get_by_name('transpose-chars')) + handle(Keys.ControlW, filter=insert_mode)(get_by_name('unix-word-rubout')) + handle(Keys.ControlI, filter=insert_mode)(get_by_name('menu-complete')) + handle(Keys.BackTab, filter=insert_mode)(get_by_name('menu-complete-backward')) + + handle(Keys.PageUp, filter= ~has_selection)(get_by_name('previous-history')) + handle(Keys.PageDown, filter= ~has_selection)(get_by_name('next-history')) + + # CTRL keys. + + text_before_cursor = Condition(lambda cli: cli.current_buffer.text) + handle(Keys.ControlD, filter=text_before_cursor & insert_mode)(get_by_name('delete-char')) + + is_multiline = Condition(lambda cli: cli.current_buffer.is_multiline()) + is_returnable = Condition(lambda cli: cli.current_buffer.accept_action.is_returnable) + + @handle(Keys.ControlJ, filter=is_multiline & insert_mode) + def _(event): + " Newline (in case of multiline input. " + event.current_buffer.newline(copy_margin=not event.cli.in_paste_mode) + + @handle(Keys.ControlJ, filter=~is_multiline & is_returnable) + def _(event): + " Enter, accept input. " + buff = event.current_buffer + buff.accept_action.validate_and_handle(event.cli, buff) + + # Delete the word before the cursor. + + @handle(Keys.Up) + def _(event): + event.current_buffer.auto_up(count=event.arg) + + @handle(Keys.Down) + def _(event): + event.current_buffer.auto_down(count=event.arg) + + @handle(Keys.Delete, filter=has_selection) + def _(event): + data = event.current_buffer.cut_selection() + event.cli.clipboard.set_data(data) + + # Global bindings. + + @handle(Keys.ControlZ) + def _(event): + """ + 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.CPRResponse, save_before=lambda e: False) + def _(event): + """ + 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.cli.renderer.report_absolute_cursor_row(row) + + @handle(Keys.BracketedPaste) + def _(event): + " Pasting from clipboard. " + data = event.data + + # Be sure to use \n as line ending. + # Some terminals (Like iTerm2) seem to paste \r\n line endings in a + # bracketed paste. See: https://github.com/ipython/ipython/issues/9737 + data = data.replace('\r\n', '\n') + data = data.replace('\r', '\n') + + event.current_buffer.insert_text(data) + + @handle(Keys.Any, filter=Condition(lambda cli: cli.quoted_insert), eager=True) + def _(event): + """ + Handle quoted insert. + """ + event.current_buffer.insert_text(event.data, overwrite=False) + event.cli.quoted_insert = False + + return registry + + +def load_mouse_bindings(): + """ + Key bindings, required for mouse support. + (Mouse events enter through the key binding system.) + """ + registry = Registry() + + @registry.add_binding(Keys.Vt100MouseEvent) + def _(event): + """ + 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:]) + mouse_event = { + 32: MouseEventType.MOUSE_DOWN, + 35: MouseEventType.MOUSE_UP, + 96: MouseEventType.SCROLL_UP, + 97: MouseEventType.SCROLL_DOWN, + }.get(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: + mouse_event = { + (0, 'M'): MouseEventType.MOUSE_DOWN, + (0, 'm'): MouseEventType.MOUSE_UP, + (64, 'M'): MouseEventType.SCROLL_UP, + (65, 'M'): MouseEventType.SCROLL_DOWN, + }.get((mouse_event, m)) + else: + mouse_event = { + 32: MouseEventType.MOUSE_DOWN, + 35: MouseEventType.MOUSE_UP, + 96: MouseEventType.SCROLL_UP, + 97: MouseEventType.SCROLL_DOWN, + }.get(mouse_event) + + x -= 1 + y -= 1 + + # Only handle mouse events when we know the window height. + if event.cli.renderer.height_is_known and mouse_event is not None: + # Take region above the layout into account. The reported + # coordinates are absolute to the visible part of the terminal. + try: + y -= event.cli.renderer.rows_above_layout + except HeightIsUnknownError: + return + + # Call the mouse handler from the renderer. + handler = event.cli.renderer.mouse_handlers.mouse_handlers[x,y] + handler(event.cli, MouseEvent(position=Point(x=x, y=y), + event_type=mouse_event)) + + @registry.add_binding(Keys.WindowsMouseEvent) + def _(event): + """ + Handling of mouse events for Windows. + """ + assert is_windows() # This key binding should only exist for Windows. + + # Parse data. + event_type, x, y = event.data.split(';') + x = int(x) + y = int(y) + + # Make coordinates absolute to the visible part of the terminal. + screen_buffer_info = event.cli.renderer.output.get_win32_screen_buffer_info() + rows_above_cursor = screen_buffer_info.dwCursorPosition.Y - event.cli.renderer._cursor_pos.y + y -= rows_above_cursor + + # Call the mouse event handler. + handler = event.cli.renderer.mouse_handlers.mouse_handlers[x,y] + handler(event.cli, MouseEvent(position=Point(x=x, y=y), + event_type=event_type)) + + return registry + + +def load_abort_and_exit_bindings(): + """ + Basic bindings for abort (Ctrl-C) and exit (Ctrl-D). + """ + registry = Registry() + handle = registry.add_binding + + @handle(Keys.ControlC) + def _(event): + " Abort when Control-C has been pressed. " + event.cli.abort() + + @Condition + def ctrl_d_condition(cli): + """ Ctrl-D binding is only active when the default buffer is selected + and empty. """ + return (cli.current_buffer_name == DEFAULT_BUFFER and + not cli.current_buffer.text) + + handle(Keys.ControlD, filter=ctrl_d_condition)(get_by_name('end-of-file')) + + return registry + + +def load_basic_system_bindings(): + """ + Basic system bindings (For both Emacs and Vi mode.) + """ + registry = Registry() + + suspend_supported = Condition( + lambda cli: suspend_to_background_supported()) + + @registry.add_binding(Keys.ControlZ, filter=suspend_supported) + def _(event): + """ + Suspend process to background. + """ + event.cli.suspend_to_background() + + return registry + + +def load_auto_suggestion_bindings(): + """ + Key bindings for accepting auto suggestion text. + """ + registry = Registry() + handle = registry.add_binding + + suggestion_available = Condition( + lambda cli: + cli.current_buffer.suggestion is not None and + cli.current_buffer.document.is_cursor_at_the_end) + + @handle(Keys.ControlF, filter=suggestion_available) + @handle(Keys.ControlE, filter=suggestion_available) + @handle(Keys.Right, filter=suggestion_available) + def _(event): + " Accept suggestion. " + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + b.insert_text(suggestion.text) + + return registry diff --git a/src/libs/prompt_toolkit/key_binding/bindings/completion.py b/src/libs/prompt_toolkit/key_binding/bindings/completion.py new file mode 100644 index 0000000..63069ee --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/bindings/completion.py @@ -0,0 +1,161 @@ +""" +Key binding handlers for displaying completions. +""" +from __future__ import unicode_literals +from libs.prompt_toolkit.completion import CompleteEvent, get_common_complete_suffix +from libs.prompt_toolkit.utils import get_cwidth +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.key_binding.registry import Registry + +import math + +__all__ = ( + 'generate_completions', + 'display_completions_like_readline', +) + +def generate_completions(event): + 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: + event.cli.start_completion(insert_common_part=True, select_first=False) + + +def display_completions_like_readline(event): + """ + 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. + registry.add_binding(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.cli, completions) + + +def _display_completions_like_readline(cli, completions): + """ + 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 libs.prompt_toolkit.shortcuts import create_confirm_application + assert isinstance(completions, list) + + # Get terminal dimensions. + term_size = cli.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.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): + # 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 = [] + for r in range(page_row_count): + for c in range(column_count): + try: + result.append(page_columns[c][r].text.ljust(max_compl_width)) + except IndexError: + pass + result.append('\n') + cli.output.write(''.join(result)) + cli.output.flush() + + # User interaction through an application generator function. + def run(): + if len(completions) > completions_per_page: + # Ask confirmation if it doesn't fit on the screen. + message = 'Display all {} possibilities? (y on n) '.format(len(completions)) + confirm = yield create_confirm_application(message) + + 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 = yield _create_more_application() + if not show_more: + return + else: + cli.output.write('\n'); cli.output.flush() + else: + # Display all completions. + display(0) + + cli.run_application_generator(run, render_cli_done=True) + + +def _create_more_application(): + """ + Create an `Application` instance that displays the "--MORE--". + """ + from libs.prompt_toolkit.shortcuts import create_prompt_application + registry = Registry() + + @registry.add_binding(' ') + @registry.add_binding('y') + @registry.add_binding('Y') + @registry.add_binding(Keys.ControlJ) + @registry.add_binding(Keys.ControlI) # Tab. + def _(event): + event.cli.set_return_value(True) + + @registry.add_binding('n') + @registry.add_binding('N') + @registry.add_binding('q') + @registry.add_binding('Q') + @registry.add_binding(Keys.ControlC) + def _(event): + event.cli.set_return_value(False) + + return create_prompt_application( + '--MORE--', key_bindings_registry=registry, erase_when_done=True) diff --git a/src/libs/prompt_toolkit/key_binding/bindings/emacs.py b/src/libs/prompt_toolkit/key_binding/bindings/emacs.py new file mode 100644 index 0000000..4234e4a --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/bindings/emacs.py @@ -0,0 +1,451 @@ +# pylint: disable=function-redefined +from __future__ import unicode_literals +from libs.prompt_toolkit.buffer import SelectionType, indent, unindent +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER, SYSTEM_BUFFER +from libs.prompt_toolkit.filters import Condition, EmacsMode, HasSelection, EmacsInsertMode, HasFocus, HasArg +from libs.prompt_toolkit.completion import CompleteEvent + +from .scroll import scroll_page_up, scroll_page_down +from .named_commands import get_by_name +from ..registry import Registry, ConditionalRegistry + +__all__ = ( + 'load_emacs_bindings', + 'load_emacs_search_bindings', + 'load_emacs_system_bindings', + 'load_extra_emacs_page_navigation_bindings', +) + + +def load_emacs_bindings(): + """ + Some e-macs extensions. + """ + # Overview of Readline emacs commands: + # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf + registry = ConditionalRegistry(Registry(), EmacsMode()) + handle = registry.add_binding + + insert_mode = EmacsInsertMode() + has_selection = HasSelection() + + @handle(Keys.Escape) + def _(event): + """ + 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(Keys.ControlA)(get_by_name('beginning-of-line')) + handle(Keys.ControlB)(get_by_name('backward-char')) + handle(Keys.ControlDelete, filter=insert_mode)(get_by_name('kill-word')) + handle(Keys.ControlE)(get_by_name('end-of-line')) + handle(Keys.ControlF)(get_by_name('forward-char')) + handle(Keys.ControlLeft)(get_by_name('backward-word')) + handle(Keys.ControlRight)(get_by_name('forward-word')) + handle(Keys.ControlX, 'r', 'y', filter=insert_mode)(get_by_name('yank')) + handle(Keys.ControlY, filter=insert_mode)(get_by_name('yank')) + handle(Keys.Escape, 'b')(get_by_name('backward-word')) + handle(Keys.Escape, 'c', filter=insert_mode)(get_by_name('capitalize-word')) + handle(Keys.Escape, 'd', filter=insert_mode)(get_by_name('kill-word')) + handle(Keys.Escape, 'f')(get_by_name('forward-word')) + handle(Keys.Escape, 'l', filter=insert_mode)(get_by_name('downcase-word')) + handle(Keys.Escape, 'u', filter=insert_mode)(get_by_name('uppercase-word')) + handle(Keys.Escape, 'y', filter=insert_mode)(get_by_name('yank-pop')) + handle(Keys.Escape, Keys.ControlH, filter=insert_mode)(get_by_name('backward-kill-word')) + handle(Keys.Escape, Keys.Backspace, filter=insert_mode)(get_by_name('backward-kill-word')) + handle(Keys.Escape, '\\', filter=insert_mode)(get_by_name('delete-horizontal-space')) + + handle(Keys.ControlUnderscore, save_before=(lambda e: False), filter=insert_mode)( + get_by_name('undo')) + + handle(Keys.ControlX, Keys.ControlU, save_before=(lambda e: False), filter=insert_mode)( + get_by_name('undo')) + + + handle(Keys.Escape, '<', filter= ~has_selection)(get_by_name('beginning-of-history')) + handle(Keys.Escape, '>', filter= ~has_selection)(get_by_name('end-of-history')) + + handle(Keys.Escape, '.', filter=insert_mode)(get_by_name('yank-last-arg')) + handle(Keys.Escape, '_', filter=insert_mode)(get_by_name('yank-last-arg')) + handle(Keys.Escape, Keys.ControlY, filter=insert_mode)(get_by_name('yank-nth-arg')) + handle(Keys.Escape, '#', filter=insert_mode)(get_by_name('insert-comment')) + handle(Keys.ControlO)(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(Keys.ControlQ, filter= ~has_selection)(get_by_name('quoted-insert')) + + handle(Keys.ControlX, '(')(get_by_name('start-kbd-macro')) + handle(Keys.ControlX, ')')(get_by_name('end-kbd-macro')) + handle(Keys.ControlX, 'e')(get_by_name('call-last-kbd-macro')) + + @handle(Keys.ControlN) + def _(event): + " Next line. " + event.current_buffer.auto_down() + + @handle(Keys.ControlP) + def _(event): + " Previous line. " + event.current_buffer.auto_up(count=event.arg) + + def handle_digit(c): + """ + Handle input of arguments. + The first number needs to be preceeded by escape. + """ + @handle(c, filter=HasArg()) + @handle(Keys.Escape, c) + def _(event): + event.append_to_arg_count(c) + + for c in '0123456789': + handle_digit(c) + + @handle(Keys.Escape, '-', filter=~HasArg()) + def _(event): + """ + """ + if event._arg is None: + event.append_to_arg_count('-') + + @handle('-', filter=Condition(lambda cli: cli.input_processor.arg == '-')) + def _(event): + """ + When '-' is typed again, after exactly '-' has been given as an + argument, ignore this. + """ + event.cli.input_processor.arg = '-' + + is_returnable = Condition( + lambda cli: cli.current_buffer.accept_action.is_returnable) + + # Meta + Newline: always accept input. + handle(Keys.Escape, Keys.ControlJ, filter=insert_mode & is_returnable)( + get_by_name('accept-line')) + + def character_search(buff, char, count): + 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(Keys.ControlSquareClose, Keys.Any) + def _(event): + " When Ctl-] + a character is pressed. go to that character. " + # Also named 'character-search' + character_search(event.current_buffer, event.data, event.arg) + + @handle(Keys.Escape, Keys.ControlSquareClose, Keys.Any) + def _(event): + " Like Ctl-], but backwards. " + # Also named 'character-search-backward' + character_search(event.current_buffer, event.data, -event.arg) + + @handle(Keys.Escape, 'a') + def _(event): + " Previous sentence. " + # TODO: + + @handle(Keys.Escape, 'e') + def _(event): + " Move to end of sentence. " + # TODO: + + @handle(Keys.Escape, 't', filter=insert_mode) + def _(event): + """ + Swap the last two words before the cursor. + """ + # TODO + + @handle(Keys.Escape, '*', filter=insert_mode) + def _(event): + """ + `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(Keys.ControlX, Keys.ControlX) + def _(event): + """ + 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(Keys.ControlSpace) + def _(event): + """ + 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(Keys.ControlG, filter= ~has_selection) + def _(event): + """ + Control + G: Cancel completion menu and validation state. + """ + event.current_buffer.complete_state = None + event.current_buffer.validation_error = None + + @handle(Keys.ControlG, filter=has_selection) + def _(event): + """ + Cancel selection. + """ + event.current_buffer.exit_selection() + + @handle(Keys.ControlW, filter=has_selection) + @handle(Keys.ControlX, 'r', 'k', filter=has_selection) + def _(event): + """ + Cut selected text. + """ + data = event.current_buffer.cut_selection() + event.cli.clipboard.set_data(data) + + @handle(Keys.Escape, 'w', filter=has_selection) + def _(event): + """ + Copy selected text. + """ + data = event.current_buffer.copy_selection() + event.cli.clipboard.set_data(data) + + @handle(Keys.Escape, Keys.Left) + def _(event): + """ + 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(Keys.Escape, Keys.Right) + def _(event): + """ + 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(Keys.Escape, '/', filter=insert_mode) + def _(event): + """ + M-/: Complete. + """ + b = event.current_buffer + if b.complete_state: + b.complete_next() + else: + event.cli.start_completion(select_first=True) + + @handle(Keys.ControlC, '>', filter=has_selection) + def _(event): + """ + 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(Keys.ControlC, '<', filter=has_selection) + def _(event): + """ + 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 registry + + +def load_emacs_open_in_editor_bindings(): + """ + Pressing C-X C-E will open the buffer in an external editor. + """ + registry = Registry() + + registry.add_binding(Keys.ControlX, Keys.ControlE, + filter=EmacsMode() & ~HasSelection())( + get_by_name('edit-and-execute-command')) + + return registry + + +def load_emacs_system_bindings(): + registry = ConditionalRegistry(Registry(), EmacsMode()) + handle = registry.add_binding + + has_focus = HasFocus(SYSTEM_BUFFER) + + @handle(Keys.Escape, '!', filter= ~has_focus) + def _(event): + """ + M-'!' opens the system prompt. + """ + event.cli.push_focus(SYSTEM_BUFFER) + + @handle(Keys.Escape, filter=has_focus) + @handle(Keys.ControlG, filter=has_focus) + @handle(Keys.ControlC, filter=has_focus) + def _(event): + """ + Cancel system prompt. + """ + event.cli.buffers[SYSTEM_BUFFER].reset() + event.cli.pop_focus() + + @handle(Keys.ControlJ, filter=has_focus) + def _(event): + """ + Run system command. + """ + system_line = event.cli.buffers[SYSTEM_BUFFER] + event.cli.run_system_command(system_line.text) + system_line.reset(append_to_history=True) + + # Focus previous buffer again. + event.cli.pop_focus() + + return registry + + +def load_emacs_search_bindings(get_search_state=None): + registry = ConditionalRegistry(Registry(), EmacsMode()) + handle = registry.add_binding + + has_focus = HasFocus(SEARCH_BUFFER) + + assert get_search_state is None or callable(get_search_state) + + if not get_search_state: + def get_search_state(cli): return cli.search_state + + @handle(Keys.ControlG, filter=has_focus) + @handle(Keys.ControlC, filter=has_focus) + # NOTE: the reason for not also binding Escape to this one, is that we want + # Alt+Enter to accept input directly in incremental search mode. + def _(event): + """ + Abort an incremental search and restore the original line. + """ + search_buffer = event.cli.buffers[SEARCH_BUFFER] + + search_buffer.reset() + event.cli.pop_focus() + + @handle(Keys.ControlJ, filter=has_focus) + def _(event): + """ + When enter pressed in isearch, quit isearch mode. (Multiline + isearch would be too complicated.) + """ + input_buffer = event.cli.buffers.previous(event.cli) + search_buffer = event.cli.buffers[SEARCH_BUFFER] + + # Update search state. + if search_buffer.text: + get_search_state(event.cli).text = search_buffer.text + + # Apply search. + input_buffer.apply_search(get_search_state(event.cli), include_current_position=True) + + # Add query to history of search line. + search_buffer.append_to_history() + search_buffer.reset() + + # Focus previous document again. + event.cli.pop_focus() + + @handle(Keys.ControlR, filter= ~has_focus) + def _(event): + get_search_state(event.cli).direction = IncrementalSearchDirection.BACKWARD + event.cli.push_focus(SEARCH_BUFFER) + + @handle(Keys.ControlS, filter= ~has_focus) + def _(event): + get_search_state(event.cli).direction = IncrementalSearchDirection.FORWARD + event.cli.push_focus(SEARCH_BUFFER) + + def incremental_search(cli, direction, count=1): + " Apply search, but keep search buffer focussed. " + # Update search_state. + search_state = get_search_state(cli) + direction_changed = search_state.direction != direction + + search_state.text = cli.buffers[SEARCH_BUFFER].text + search_state.direction = direction + + # Apply search to current buffer. + if not direction_changed: + input_buffer = cli.buffers.previous(cli) + input_buffer.apply_search(search_state, + include_current_position=False, count=count) + + @handle(Keys.ControlR, filter=has_focus) + @handle(Keys.Up, filter=has_focus) + def _(event): + incremental_search(event.cli, IncrementalSearchDirection.BACKWARD, count=event.arg) + + @handle(Keys.ControlS, filter=has_focus) + @handle(Keys.Down, filter=has_focus) + def _(event): + incremental_search(event.cli, IncrementalSearchDirection.FORWARD, count=event.arg) + + return registry + + +def load_extra_emacs_page_navigation_bindings(): + """ + Key bindings, for scrolling up and down through pages. + This are separate bindings, because GNU readline doesn't have them. + """ + registry = ConditionalRegistry(Registry(), EmacsMode()) + handle = registry.add_binding + + handle(Keys.ControlV)(scroll_page_down) + handle(Keys.PageDown)(scroll_page_down) + handle(Keys.Escape, 'v')(scroll_page_up) + handle(Keys.PageUp)(scroll_page_up) + + return registry diff --git a/src/libs/prompt_toolkit/key_binding/bindings/named_commands.py b/src/libs/prompt_toolkit/key_binding/bindings/named_commands.py new file mode 100644 index 0000000..d7421b4 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/bindings/named_commands.py @@ -0,0 +1,578 @@ +""" +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 unicode_literals +from libs.prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER +from libs.prompt_toolkit.selection import PasteMode +from six.moves import range +import six + +from .completion import generate_completions, display_completions_like_readline +from libs.prompt_toolkit.document import Document +from libs.prompt_toolkit.enums import EditingMode +from libs.prompt_toolkit.key_binding.input_processor import KeyPress +from libs.prompt_toolkit.keys import Keys + +__all__ = ( + 'get_by_name', +) + + +# Registry that maps the Readline command names to their handlers. +_readline_commands = {} + +def register(name): + """ + Store handler in the `_readline_commands` dictionary. + """ + assert isinstance(name, six.text_type) + def decorator(handler): + assert callable(handler) + + _readline_commands[name] = handler + return handler + return decorator + + +def get_by_name(name): + """ + Return the handler for the (Readline) command with the given name. + """ + try: + return _readline_commands[name] + except KeyError: + raise KeyError('Unknown readline command: %r' % name) + +# +# Commands for moving +# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html +# + +@register('beginning-of-line') +def beginning_of_line(event): + " 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): + " 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): + " 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): + " 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): + """ + 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): + """ + 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): + """ + Clear the screen and redraw everything at the top of the screen. + """ + event.cli.renderer.clear() + + +@register('redraw-current-line') +def redraw_current_line(event): + """ + 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): + " Accept the line regardless of where the cursor is. " + b = event.current_buffer + b.accept_action.validate_and_handle(event.cli, b) + + +@register('previous-history') +def previous_history(event): + " 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): + " 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): + " Move to the first line in the history. " + event.current_buffer.go_to_history(0) + + +@register('end-of-history') +def end_of_history(event): + """ + 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): + """ + Search backward starting at the current line and moving `up` through + the history as necessary. This is an incremental search. + """ + event.cli.current_search_state.direction = IncrementalSearchDirection.BACKWARD + event.cli.push_focus(SEARCH_BUFFER) + + +# +# Commands for changing text +# + +@register('end-of-file') +def end_of_file(event): + """ + Exit. + """ + event.cli.exit() + + +@register('delete-char') +def delete_char(event): + " Delete character before the cursor. " + deleted = event.current_buffer.delete(count=event.arg) + if not deleted: + event.cli.output.bell() + + +@register('backward-delete-char') +def backward_delete_char(event): + " 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.cli.output.bell() + + +@register('self-insert') +def self_insert(event): + " Insert yourself. " + event.current_buffer.insert_text(event.data * event.arg) + + +@register('transpose-chars') +def transpose_chars(event): + """ + 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): + """ + 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): + """ + 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): + """ + 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): + """ + Add the next character typed to the line verbatim. This is how to insert + key sequences like C-q, for example. + """ + event.cli.quoted_insert = True + + +# +# Killing and yanking. +# + +@register('kill-line') +def kill_line(event): + """ + 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.cli.clipboard.set_text(deleted) + + +@register('kill-word') +def kill_word(event): + """ + 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) + event.cli.clipboard.set_text(deleted) + + +@register('unix-word-rubout') +def unix_word_rubout(event, WORD=True): + """ + 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.cli.clipboard.get_data().text + + event.cli.clipboard.set_text(deleted) + else: + # Nothing to delete. Bell. + event.cli.output.bell() + + +@register('backward-kill-word') +def backward_kill_word(event): + """ + 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): + " 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): + """ + 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.cli.clipboard.set_text(deleted) + + +@register('yank') +def yank(event): + """ + Paste before cursor. + """ + event.current_buffer.paste_clipboard_data( + event.cli.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS) + +@register('yank-nth-arg') +def yank_nth_arg(event): + """ + 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): + """ + 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): + """ + 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.cli.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): + " Attempt to perform completion. " + display_completions_like_readline(event) + + +@register('menu-complete') +def menu_complete(event): + """ + Generate completions, or go to the next completion. (This is the default + way of completing input in libs.prompt_toolkit.) + """ + generate_completions(event) + + +@register('menu-complete-backward') +def menu_complete_backward(event): + " Move backward through the list of possible completions. " + event.current_buffer.complete_previous() + +# +# Keyboard macros. +# + +@register('start-kbd-macro') +def start_kbd_macro(event): + """ + Begin saving the characters typed into the current keyboard macro. + """ + event.cli.input_processor.start_macro() + + +@register('end-kbd-macro') +def start_kbd_macro(event): + """ + Stop saving the characters typed into the current keyboard macro and save + the definition. + """ + event.cli.input_processor.end_macro() + + +@register('call-last-kbd-macro') +def start_kbd_macro(event): + """ + Re-execute the last keyboard macro defined, by making the characters in the + macro appear as if typed at the keyboard. + """ + event.cli.input_processor.call_macro() + + +@register('print-last-kbd-macro') +def print_last_kbd_macro(event): + " Print the last keboard macro. " + # TODO: Make the format suitable for the inputrc file. + def print_macro(): + for k in event.cli.input_processor.macro: + print(k) + event.cli.run_in_terminal(print_macro) + +# +# Miscellaneous Commands. +# + +@register('undo') +def undo(event): + " Incremental undo. " + event.current_buffer.undo() + + +@register('insert-comment') +def insert_comment(event): + """ + 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): + return line[1:] if line.startswith('#') else line + else: + def change(line): + return '#' + line + + buff.document = Document( + text='\n'.join(map(change, buff.text.splitlines())), + cursor_position=0) + + # Accept input. + buff.accept_action.validate_and_handle(event.cli, buff) + + +@register('vi-editing-mode') +def vi_editing_mode(event): + " Switch to Vi editing mode. " + event.cli.editing_mode = EditingMode.VI + + +@register('emacs-editing-mode') +def emacs_editing_mode(event): + " Switch to Emacs editing mode. " + event.cli.editing_mode = EditingMode.EMACS + + +@register('prefix-meta') +def prefix_meta(event): + """ + 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':: + + registry.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta) + """ + event.cli.input_processor.feed(KeyPress(Keys.Escape)) + + +@register('operate-and-get-next') +def operate_and_get_next(event): + """ + 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.accept_action.validate_and_handle(event.cli, buff) + + # Set the new index at the start of the next run. + def set_working_index(): + if new_index < len(buff._working_lines): + buff.working_index = new_index + + event.cli.pre_run_callables.append(set_working_index) + + +@register('edit-and-execute-command') +def edit_and_execute(event): + """ + Invoke an editor on the current command line, and accept the result. + """ + buff = event.current_buffer + + buff.open_in_editor(event.cli) + buff.accept_action.validate_and_handle(event.cli, buff) diff --git a/src/libs/prompt_toolkit/key_binding/bindings/scroll.py b/src/libs/prompt_toolkit/key_binding/bindings/scroll.py new file mode 100644 index 0000000..498da16 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/bindings/scroll.py @@ -0,0 +1,185 @@ +""" +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 unicode_literals + +from libs.prompt_toolkit.layout.utils import find_window_for_buffer_name +from six.moves import range + +__all__ = ( + 'scroll_forward', + 'scroll_backward', + 'scroll_half_page_up', + 'scroll_half_page_down', + 'scroll_one_line_up', + 'scroll_one_line_down', +) + + +def _current_window_for_event(event): + """ + Return the `Window` for the currently focussed Buffer. + """ + return find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + + +def scroll_forward(event, half=False): + """ + Scroll window down. + """ + w = _current_window_for_event(event) + b = event.cli.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, half=False): + """ + Scroll window up. + """ + w = _current_window_for_event(event) + b = event.cli.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): + """ + Same as ControlF, but only scroll half a page. + """ + scroll_forward(event, half=True) + + +def scroll_half_page_up(event): + """ + Same as ControlB, but only scroll half a page. + """ + scroll_backward(event, half=True) + + +def scroll_one_line_down(event): + """ + scroll_offset += 1 + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + b = event.cli.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): + """ + scroll_offset -= 1 + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + b = event.cli.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): + """ + Scroll page down. (Prefer the cursor at the top of the page, after scrolling.) + """ + w = _current_window_for_event(event) + b = event.cli.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): + """ + Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.) + """ + w = _current_window_for_event(event) + b = event.cli.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/libs/prompt_toolkit/key_binding/bindings/utils.py b/src/libs/prompt_toolkit/key_binding/bindings/utils.py new file mode 100644 index 0000000..5eeda35 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/bindings/utils.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals +from libs.prompt_toolkit.filters import CLIFilter, Always + +__all__ = ( + 'create_handle_decorator', +) + +def create_handle_decorator(registry, filter=Always()): + """ + Create a key handle decorator, which is compatible with `Registry.handle`, + but will chain the given filter to every key binding. + + :param filter: `CLIFilter` + """ + assert isinstance(filter, CLIFilter) + + def handle(*keys, **kw): + # Chain the given filter to the filter of this specific binding. + if 'filter' in kw: + kw['filter'] = kw['filter'] & filter + else: + kw['filter'] = filter + + return registry.add_binding(*keys, **kw) + return handle diff --git a/src/libs/prompt_toolkit/key_binding/bindings/vi.py b/src/libs/prompt_toolkit/key_binding/bindings/vi.py new file mode 100644 index 0000000..6573acf --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/bindings/vi.py @@ -0,0 +1,1903 @@ +# pylint: disable=function-redefined +from __future__ import unicode_literals +from libs.prompt_toolkit.buffer import ClipboardData, indent, unindent, reshape_text +from libs.prompt_toolkit.document import Document +from libs.prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER, SYSTEM_BUFFER +from libs.prompt_toolkit.filters import Filter, Condition, HasArg, Always, IsReadOnly +from libs.prompt_toolkit.filters.cli import ViNavigationMode, ViInsertMode, ViInsertMultipleMode, ViReplaceMode, ViSelectionMode, ViWaitingForTextObjectMode, ViDigraphMode, ViMode +from libs.prompt_toolkit.key_binding.digraphs import DIGRAPHS +from libs.prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.layout.utils import find_window_for_buffer_name +from libs.prompt_toolkit.selection import SelectionType, SelectionState, PasteMode + +from .scroll import scroll_forward, scroll_backward, scroll_half_page_up, scroll_half_page_down, scroll_one_line_up, scroll_one_line_down, scroll_page_up, scroll_page_down +from .named_commands import get_by_name +from ..registry import Registry, ConditionalRegistry, BaseRegistry + +import libs.prompt_toolkit.filters as filters +from six.moves import range +import codecs +import six +import string + +try: + from itertools import accumulate +except ImportError: # < Python 3.2 + def accumulate(iterable): + " Super simpel 'accumulate' implementation. " + total = 0 + for item in iterable: + total += item + yield total + +__all__ = ( + 'load_vi_bindings', + 'load_vi_search_bindings', + 'load_vi_system_bindings', + 'load_extra_vi_page_navigation_bindings', +) + +if six.PY2: + ascii_lowercase = string.ascii_lowercase.decode('ascii') +else: + ascii_lowercase = string.ascii_lowercase + +vi_register_names = ascii_lowercase + '0123456789' + + +class TextObjectType(object): + EXCLUSIVE = 'EXCLUSIVE' + INCLUSIVE = 'INCLUSIVE' + LINEWISE = 'LINEWISE' + BLOCK = 'BLOCK' + + +class TextObject(object): + """ + Return struct for functions wrapped in ``text_object``. + Both `start` and `end` are relative to the current cursor position. + """ + def __init__(self, start, end=0, type=TextObjectType.EXCLUSIVE): + self.start = start + self.end = end + self.type = type + + @property + def selection_type(self): + if self.type == TextObjectType.LINEWISE: + return SelectionType.LINES + if self.type == TextObjectType.BLOCK: + return SelectionType.BLOCK + else: + return SelectionType.CHARACTERS + + def sorted(self): + """ + 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): + """ + 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. + """ + 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): + """ + 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): + """ + Turn text object into `ClipboardData` instance. + """ + from_, to = self.operator_range(buffer.document) + + from_ += buffer.cursor_position + to += buffer.cursor_position + to -= 1 # SelectionState does not include the end position, `operator_range` does. + + 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 + + +def create_text_object_decorator(registry): + """ + Create a decorator that can be used to register Vi text object implementations. + """ + assert isinstance(registry, BaseRegistry) + + operator_given = ViWaitingForTextObjectMode() + navigation_mode = ViNavigationMode() + selection_mode = ViSelectionMode() + + def text_object_decorator(*keys, **kw): + """ + 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.) + """ + filter = kw.pop('filter', Always()) + no_move_handler = kw.pop('no_move_handler', False) + no_selection_handler = kw.pop('no_selection_handler', False) + eager = kw.pop('eager', False) + assert not kw + + def decorator(text_object_func): + assert callable(text_object_func) + + @registry.add_binding(*keys, filter=operator_given & filter, eager=eager) + def _(event): + # Arguments are multiplied. + vi_state = event.cli.vi_state + event._arg = (vi_state.operator_arg or 1) * (event.arg or 1) + + # Call the text object handler. + text_obj = text_object_func(event) + if text_obj is not None: + assert isinstance(text_obj, TextObject) + + # Call the operator function with the text object. + vi_state.operator_func(event, text_obj) + + # Clear operator. + event.cli.vi_state.operator_func = None + event.cli.vi_state.operator_arg = None + + # Register a move operation. (Doesn't need an operator.) + if not no_move_handler: + @registry.add_binding(*keys, filter=~operator_given & filter & navigation_mode, eager=eager) + def _(event): + " 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: + @registry.add_binding(*keys, filter=~operator_given & filter & selection_mode, eager=eager) + def _(event): + " Move handler for selection mode. " + text_object = text_object_func(event) + buff = event.current_buffer + + # 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 + + buff.selection_state.original_cursor_position = start + buff.cursor_position = end + + # Take selection type from text object. + if text_object.type == TextObjectType.LINEWISE: + buff.selection_state.type = SelectionType.LINES + else: + buff.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 + + +def create_operator_decorator(registry): + """ + Create a decorator that can be used for registering Vi operators. + """ + assert isinstance(registry, BaseRegistry) + + operator_given = ViWaitingForTextObjectMode() + navigation_mode = ViNavigationMode() + selection_mode = ViSelectionMode() + + def operator_decorator(*keys, **kw): + """ + Register a Vi operator. + + Usage:: + + @operator('d', filter=...) + def handler(cli, text_object): + # Do something with the text object here. + """ + filter = kw.pop('filter', Always()) + eager = kw.pop('eager', False) + assert not kw + + def decorator(operator_func): + @registry.add_binding(*keys, filter=~operator_given & filter & navigation_mode, eager=eager) + def _(event): + """ + 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.cli.vi_state.operator_func = operator_func + event.cli.vi_state.operator_arg = event.arg + + @registry.add_binding(*keys, filter=~operator_given & filter & selection_mode, eager=eager) + def _(event): + """ + Handle operator in selection mode. + """ + buff = event.current_buffer + selection_state = buff.selection_state + + # 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(get_search_state=None): + """ + Vi extensions. + + # Overview of Readline Vi commands: + # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf + + :param get_search_state: None or a callable that takes a + CommandLineInterface and returns a SearchState. + """ + # 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. + + registry = ConditionalRegistry(Registry(), ViMode()) + handle = registry.add_binding + + # Default get_search_state. + if get_search_state is None: + def get_search_state(cli): return cli.search_state + + # (Note: Always take the navigation bindings in read-only mode, even when + # ViState says different.) + navigation_mode = ViNavigationMode() + insert_mode = ViInsertMode() + insert_multiple_mode = ViInsertMultipleMode() + replace_mode = ViReplaceMode() + selection_mode = ViSelectionMode() + operator_given = ViWaitingForTextObjectMode() + digraph_mode = ViDigraphMode() + + vi_transform_functions = [ + # 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 cli: cli.vi_state.tilde_operator), lambda string: string.swapcase()), + ] + + # Insert a character literally (quoted insert). + handle(Keys.ControlV, filter=insert_mode)(get_by_name('quoted-insert')) + + @handle(Keys.Escape) + def _(event): + """ + Escape goes to vi navigation mode. + """ + buffer = event.current_buffer + vi_state = event.cli.vi_state + + if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): + buffer.cursor_position += buffer.document.get_cursor_left_position() + + vi_state.reset(InputMode.NAVIGATION) + + if bool(buffer.selection_state): + buffer.exit_selection() + + @handle('k', filter=selection_mode) + def _(event): + """ + Arrow up in selection mode. + """ + event.current_buffer.cursor_up(count=event.arg) + + @handle('j', filter=selection_mode) + def _(event): + """ + Arrow down in selection mode. + """ + event.current_buffer.cursor_down(count=event.arg) + + @handle(Keys.Up, filter=navigation_mode) + @handle(Keys.ControlP, filter=navigation_mode) + def _(event): + """ + Arrow up and ControlP in navigation mode go up. + """ + event.current_buffer.auto_up(count=event.arg) + + @handle('k', filter=navigation_mode) + def _(event): + """ + 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(Keys.Down, filter=navigation_mode) + @handle(Keys.ControlN, filter=navigation_mode) + def _(event): + """ + Arrow down and Control-N in navigation mode. + """ + event.current_buffer.auto_down(count=event.arg) + + @handle('j', filter=navigation_mode) + def _(event): + """ + 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(Keys.ControlH, filter=navigation_mode) + @handle(Keys.Backspace, filter=navigation_mode) + def _(event): + """ + In navigation-mode, move cursor. + """ + event.current_buffer.cursor_position += \ + event.current_buffer.document.get_cursor_left_position(count=event.arg) + + @handle(Keys.ControlN, filter=insert_mode) + def _(event): + b = event.current_buffer + + if b.complete_state: + b.complete_next() + else: + event.cli.start_completion(select_first=True) + + @handle(Keys.ControlP, filter=insert_mode) + def _(event): + """ + Control-P: To previous completion. + """ + b = event.current_buffer + + if b.complete_state: + b.complete_previous() + else: + event.cli.start_completion(select_last=True) + + @handle(Keys.ControlY, filter=insert_mode) + def _(event): + """ + Accept current completion. + """ + event.current_buffer.complete_state = None + + @handle(Keys.ControlE, filter=insert_mode) + def _(event): + """ + Cancel completion. Go back to originally typed text. + """ + event.current_buffer.cancel_completion() + + @handle(Keys.ControlJ, filter=navigation_mode) # XXX: only if the selected buffer has a return handler. + def _(event): + """ + In navigation mode, pressing enter will always return the input. + """ + b = event.current_buffer + + if b.accept_action.is_returnable: + b.accept_action.validate_and_handle(event.cli, b) + + # ** In navigation mode ** + + # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html + + @handle(Keys.Insert, filter=navigation_mode) + def _(event): + " Presing the Insert key. " + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('a', filter=navigation_mode & ~IsReadOnly()) + # ~IsReadOnly, because we want to stay in navigation mode for + # read-only buffers. + def _(event): + event.current_buffer.cursor_position += event.current_buffer.document.get_cursor_right_position() + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('A', filter=navigation_mode & ~IsReadOnly()) + def _(event): + event.current_buffer.cursor_position += event.current_buffer.document.get_end_of_line_position() + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('C', filter=navigation_mode & ~IsReadOnly()) + def _(event): + """ + # 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.cli.clipboard.set_text(deleted) + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('c', 'c', filter=navigation_mode & ~IsReadOnly()) + @handle('S', filter=navigation_mode & ~IsReadOnly()) + def _(event): # TODO: implement 'arg' + """ + Change current line + """ + buffer = event.current_buffer + + # We copy the whole line. + data = ClipboardData(buffer.document.current_line, SelectionType.LINES) + event.cli.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.cli.vi_state.input_mode = InputMode.INSERT + + @handle('D', filter=navigation_mode) + def _(event): + buffer = event.current_buffer + deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) + event.cli.clipboard.set_text(deleted) + + @handle('d', 'd', filter=navigation_mode) + def _(event): + """ + 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.cli.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) + + @handle('x', filter=selection_mode) + def _(event): + """ + Cut selection. + ('x' is not an operator.) + """ + clipboard_data = event.current_buffer.cut_selection() + event.cli.clipboard.set_data(clipboard_data) + + @handle('i', filter=navigation_mode & ~IsReadOnly()) + def _(event): + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('I', filter=navigation_mode & ~IsReadOnly()) + def _(event): + event.cli.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(cli): + buff = cli.current_buffer + return buff.selection_state and buff.selection_state.type == SelectionType.BLOCK + + @handle('I', filter=in_block_selection & ~IsReadOnly()) + def go_to_block_selection(event, after=False): + " Insert in block selection mode. " + buff = event.current_buffer + + # Store all cursor positions. + positions = [] + + if after: + def get_pos(from_to): + return from_to[1] + 1 + else: + def get_pos(from_to): + 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.cli.vi_state.input_mode = InputMode.INSERT_MULTIPLE + buff.exit_selection() + + @handle('A', filter=in_block_selection & ~IsReadOnly()) + def _(event): + go_to_block_selection(event, after=True) + + @handle('J', filter=navigation_mode & ~IsReadOnly()) + def _(event): + " Join lines. " + for i in range(event.arg): + event.current_buffer.join_next_line() + + @handle('g', 'J', filter=navigation_mode & ~IsReadOnly()) + def _(event): + " Join lines without space. " + for i in range(event.arg): + event.current_buffer.join_next_line(separator='') + + @handle('J', filter=selection_mode & ~IsReadOnly()) + def _(event): + " Join selected lines. " + event.current_buffer.join_selected_lines() + + @handle('g', 'J', filter=selection_mode & ~IsReadOnly()) + def _(event): + " Join selected lines without space. " + event.current_buffer.join_selected_lines(separator='') + + @handle('p', filter=navigation_mode) + def _(event): + """ + Paste after + """ + event.current_buffer.paste_clipboard_data( + event.cli.clipboard.get_data(), + count=event.arg, + paste_mode=PasteMode.VI_AFTER) + + @handle('P', filter=navigation_mode) + def _(event): + """ + Paste before + """ + event.current_buffer.paste_clipboard_data( + event.cli.clipboard.get_data(), + count=event.arg, + paste_mode=PasteMode.VI_BEFORE) + + @handle('"', Keys.Any, 'p', filter=navigation_mode) + def _(event): + " Paste from named register. " + c = event.key_sequence[1].data + if c in vi_register_names: + data = event.cli.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=navigation_mode) + def _(event): + " Paste (before) from named register. " + c = event.key_sequence[1].data + if c in vi_register_names: + data = event.cli.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', Keys.Any, filter=navigation_mode) + def _(event): + """ + Replace single character under cursor + """ + event.current_buffer.insert_text(event.data * event.arg, overwrite=True) + event.current_buffer.cursor_position -= 1 + + @handle('R', filter=navigation_mode) + def _(event): + """ + Go to 'replace'-mode. + """ + event.cli.vi_state.input_mode = InputMode.REPLACE + + @handle('s', filter=navigation_mode & ~IsReadOnly()) + def _(event): + """ + Substitute with new text + (Delete character(s) and go to insert mode.) + """ + text = event.current_buffer.delete(count=event.arg) + event.cli.clipboard.set_text(text) + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('u', filter=navigation_mode, save_before=(lambda e: False)) + def _(event): + for i in range(event.arg): + event.current_buffer.undo() + + @handle('V', filter=navigation_mode) + def _(event): + """ + Start lines selection. + """ + event.current_buffer.start_selection(selection_type=SelectionType.LINES) + + @handle(Keys.ControlV, filter=navigation_mode) + def _(event): + " Enter block selection mode. " + event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) + + @handle('V', filter=selection_mode) + def _(event): + """ + 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.type != SelectionType.LINES: + selection_state.type = SelectionType.LINES + else: + event.current_buffer.exit_selection() + + @handle('v', filter=navigation_mode) + def _(event): + " Enter character selection mode. " + event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) + + @handle('v', filter=selection_mode) + def _(event): + """ + 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.type != SelectionType.CHARACTERS: + selection_state.type = SelectionType.CHARACTERS + else: + event.current_buffer.exit_selection() + + @handle(Keys.ControlV, filter=selection_mode) + def _(event): + """ + 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.type != SelectionType.BLOCK: + selection_state.type = SelectionType.BLOCK + else: + event.current_buffer.exit_selection() + + + @handle('a', 'w', filter=selection_mode) + @handle('a', 'W', filter=selection_mode) + def _(event): + """ + 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=navigation_mode) + def _(event): + """ + Delete character. + """ + text = event.current_buffer.delete(count=event.arg) + event.cli.clipboard.set_text(text) + + @handle('X', filter=navigation_mode) + def _(event): + text = event.current_buffer.delete_before_cursor() + event.cli.clipboard.set_text(text) + + @handle('y', 'y', filter=navigation_mode) + @handle('Y', filter=navigation_mode) + def _(event): + """ + Yank the whole line. + """ + text = '\n'.join(event.current_buffer.document.lines_from_current[:event.arg]) + event.cli.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) + + @handle('+', filter=navigation_mode) + def _(event): + """ + 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=navigation_mode) + def _(event): + """ + 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=navigation_mode) + def _(event): + """ + Indent lines. + """ + buffer = event.current_buffer + current_row = buffer.document.cursor_position_row + indent(buffer, current_row, current_row + event.arg) + + @handle('<', '<', filter=navigation_mode) + def _(event): + """ + Unindent lines. + """ + current_row = event.current_buffer.document.cursor_position_row + unindent(event.current_buffer, current_row, current_row + event.arg) + + @handle('O', filter=navigation_mode & ~IsReadOnly()) + def _(event): + """ + Open line above and enter insertion mode + """ + event.current_buffer.insert_line_above( + copy_margin=not event.cli.in_paste_mode) + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('o', filter=navigation_mode & ~IsReadOnly()) + def _(event): + """ + Open line below and enter insertion mode + """ + event.current_buffer.insert_line_below( + copy_margin=not event.cli.in_paste_mode) + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle('~', filter=navigation_mode) + def _(event): + """ + 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=navigation_mode & ~IsReadOnly()) + def _(event): + " Lowercase current line. " + buff = event.current_buffer + buff.transform_current_line(lambda s: s.lower()) + + @handle('g', 'U', 'U', filter=navigation_mode & ~IsReadOnly()) + def _(event): + " Uppercase current line. " + buff = event.current_buffer + buff.transform_current_line(lambda s: s.upper()) + + @handle('g', '~', '~', filter=navigation_mode & ~IsReadOnly()) + def _(event): + " Swap case of the current line. " + buff = event.current_buffer + buff.transform_current_line(lambda s: s.swapcase()) + + @handle('#', filter=navigation_mode) + def _(event): + """ + Go to previous occurence of this word. + """ + b = event.cli.current_buffer + + search_state = get_search_state(event.cli) + search_state.text = b.document.get_word_under_cursor() + search_state.direction = IncrementalSearchDirection.BACKWARD + + b.apply_search(search_state, count=event.arg, + include_current_position=False) + + @handle('*', filter=navigation_mode) + def _(event): + """ + Go to next occurence of this word. + """ + b = event.cli.current_buffer + + search_state = get_search_state(event.cli) + search_state.text = b.document.get_word_under_cursor() + search_state.direction = IncrementalSearchDirection.FORWARD + + b.apply_search(search_state, count=event.arg, + include_current_position=False) + + @handle('(', filter=navigation_mode) + def _(event): + # TODO: go to begin of sentence. + # XXX: should become text_object. + pass + + @handle(')', filter=navigation_mode) + def _(event): + # TODO: go to end of sentence. + # XXX: should become text_object. + pass + + operator = create_operator_decorator(registry) + text_object = create_text_object_decorator(registry) + + @text_object(Keys.Any, filter=operator_given) + def _(event): + """ + Unknown key binding while waiting for a text object. + """ + event.cli.output.bell() + + # + # *** Operators *** + # + + def create_delete_and_change_operators(delete_only, with_register=False): + """ + 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. + """ + if with_register: + handler_keys = ('"', Keys.Any, 'cd'[delete_only]) + else: + handler_keys = 'cd'[delete_only] + + @operator(*handler_keys, filter=~IsReadOnly()) + def delete_or_change_operator(event, text_object): + 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.cli.vi_state.named_registers[reg_name] = clipboard_data + else: + event.cli.clipboard.set_data(clipboard_data) + + # Only go back to insert mode in case of 'change'. + if not delete_only: + event.cli.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, transform_func, *a): + @operator(*a, filter=filter & ~IsReadOnly()) + def _(event, text_object): + """ + 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_handler(event, text_object): + """ + Yank operator. (Copy text.) + """ + _, clipboard_data = text_object.cut(event.current_buffer) + if clipboard_data.text: + event.cli.clipboard.set_data(clipboard_data) + + @operator('"', Keys.Any, 'y') + def _(event, text_object): + " 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.cli.vi_state.named_registers[c] = clipboard_data + + @operator('>') + def _(event, text_object): + """ + Indent. + """ + buff = event.current_buffer + from_, to = text_object.get_line_numbers(buff) + indent(buff, from_, to + 1, count=event.arg) + + @operator('<') + def _(event, text_object): + """ + 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 _(event, text_object): + """ + 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 _(event): + """ 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 _(event): + """ 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 key_dollar(event): + """ '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 _(event): + """ '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 _(event): + """ '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 _(event): + """ 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 _(event): + """ 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 _(event): + """ 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 _(event): + """ 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 _(event): + """ 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 _(event): + """ 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 _(event): + """ + 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 key_circumflex(event): + """ '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 key_zero(event): + """ + '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, ci_end, inner, key=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): + 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 _(event): + """ + 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 _(event): + """ + 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 _(event): + """ + Go to next occurance of character. Typing 'fx' will move the + cursor to the next occurance of character. 'x'. + """ + event.cli.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 _(event): + """ + Go to previous occurance of character. Typing 'Fx' will move the + cursor to the previous occurance of character. 'x'. + """ + event.cli.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 _(event): + """ + Move right to the next occurance of c, then one char backward. + """ + event.cli.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 _(event): + """ + Move left to the previous occurance of c, then one char forward. + """ + event.cli.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): + """ + Create ',' and ';' commands. + """ + @text_object(',' if reverse else ';') + def _(event): + # Repeat the last 'f'/'F'/'t'/'T' command. + pos = 0 + vi_state = event.cli.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(Keys.Left) + def _(event): + """ 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 _(event): + """ 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 _(event): + """ 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(Keys.Right) + def _(event): + """ 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 _(event): + """ + Moves to the start of the visible region. (Below the scroll offset.) + Implements 'cH', 'dH', 'H'. + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + 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 _(event): + """ + Moves cursor to the vertical center of the visible region. + Implements 'cM', 'dM', 'M'. + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + 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 _(event): + """ + Moves to the end of the visible region. (Above the scroll offset.) + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + 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 _(event): + " Search next. " + buff = event.current_buffer + cursor_position = buff.get_search_position( + get_search_state(event.cli), include_current_position=False, + count=event.arg) + return TextObject(cursor_position - buff.cursor_position) + + @handle('n', filter=navigation_mode) + def _(event): + " Search next in navigation mode. (This goes through the history.) " + event.current_buffer.apply_search( + get_search_state(event.cli), include_current_position=False, + count=event.arg) + + @text_object('N', no_move_handler=True) + def _(event): + " Search previous. " + buff = event.current_buffer + cursor_position = buff.get_search_position( + ~get_search_state(event.cli), include_current_position=False, + count=event.arg) + return TextObject(cursor_position - buff.cursor_position) + + @handle('N', filter=navigation_mode) + def _(event): + " Search previous in navigation mode. (This goes through the history.) " + event.current_buffer.apply_search( + ~get_search_state(event.cli), include_current_position=False, + count=event.arg) + + @handle('z', '+', filter=navigation_mode|selection_mode) + @handle('z', 't', filter=navigation_mode|selection_mode) + @handle('z', Keys.ControlJ, filter=navigation_mode|selection_mode) + def _(event): + """ + Scrolls the window to makes the current line the first line in the visible region. + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + b = event.cli.current_buffer + w.vertical_scroll = b.document.cursor_position_row + + @handle('z', '-', filter=navigation_mode|selection_mode) + @handle('z', 'b', filter=navigation_mode|selection_mode) + def _(event): + """ + Scrolls the window to makes the current line the last line in the visible region. + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + + # We can safely set the scroll offset to zero; the Window will meke + # sure that it scrolls at least enough to make the cursor visible + # again. + w.vertical_scroll = 0 + + @handle('z', 'z', filter=navigation_mode|selection_mode) + def _(event): + """ + Center Window vertically around cursor. + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + b = event.cli.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 _(event): + """ + 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 _(event): + # 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 _(event): + """ + 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 _(event): + """ + 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 _(event): + """ + 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 _(event): + """ + 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 _(event): + """ + Like g0, but half a screenwidth to the right. (Or as much as possible.) + """ + w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name) + 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 _(event): + """ + 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=HasArg()) + def _(event): + """ + 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=navigation_mode|selection_mode|operator_given) + def _(event): + """ + Always handle numberics in navigation mode as arg. + """ + event.append_to_arg_count(event.data) + + @handle('0', filter=(navigation_mode|selection_mode|operator_given) & HasArg()) + def _(event): + " Zero when an argument was already give. " + event.append_to_arg_count(event.data) + + @handle(Keys.Any, filter=replace_mode) + def _(event): + """ + Insert data at cursor position. + """ + event.current_buffer.insert_text(event.data, overwrite=True) + + @handle(Keys.Any, filter=insert_multiple_mode, + save_before=(lambda e: not e.is_repeat)) + def _(event): + """ + 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 = [ + p + i + 1 for i, p in enumerate(buff.multiple_cursor_positions)] + + # Set result. + buff.text = ''.join(text) + buff.multiple_cursor_positions = new_cursor_positions + buff.cursor_position += 1 + + @handle(Keys.Backspace, filter=insert_multiple_mode) + def _(event): + " 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.cli.output.bell() + + @handle(Keys.Delete, filter=insert_multiple_mode) + def _(event): + " 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.cli.output.bell() + + + @handle(Keys.ControlX, Keys.ControlL, filter=insert_mode) + def _(event): + """ + 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(Keys.ControlX, Keys.ControlF, filter=insert_mode) + def _(event): + """ + Complete file names. + """ + # TODO + pass + + @handle(Keys.ControlK, filter=insert_mode|replace_mode) + def _(event): + " Go into digraph mode. " + event.cli.vi_state.waiting_for_digraph = True + + @Condition + def digraph_symbol_1_given(cli): + return cli.vi_state.digraph_symbol1 is not None + + @handle(Keys.Any, filter=digraph_mode & ~digraph_symbol_1_given) + def _(event): + event.cli.vi_state.digraph_symbol1 = event.data + + @handle(Keys.Any, filter=digraph_mode & digraph_symbol_1_given) + def _(event): + " Insert digraph. " + try: + # Lookup. + code = (event.cli.vi_state.digraph_symbol1, event.data) + if code not in DIGRAPHS: + code = code[::-1] # Try reversing. + symbol = DIGRAPHS[code] + except KeyError: + # Unkown digraph. + event.cli.output.bell() + else: + # Insert digraph. + overwrite = event.cli.vi_state.input_mode == InputMode.REPLACE + event.current_buffer.insert_text( + six.unichr(symbol), overwrite=overwrite) + event.cli.vi_state.waiting_for_digraph = False + finally: + event.cli.vi_state.waiting_for_digraph = False + event.cli.vi_state.digraph_symbol1 = None + + return registry + + +def load_vi_open_in_editor_bindings(): + """ + Pressing 'v' in navigation mode will open the buffer in an external editor. + """ + registry = Registry() + navigation_mode = ViNavigationMode() + + registry.add_binding('v', filter=navigation_mode)( + get_by_name('edit-and-execute-command')) + return registry + + +def load_vi_system_bindings(): + registry = ConditionalRegistry(Registry(), ViMode()) + handle = registry.add_binding + + has_focus = filters.HasFocus(SYSTEM_BUFFER) + navigation_mode = ViNavigationMode() + + @handle('!', filter=~has_focus & navigation_mode) + def _(event): + """ + '!' opens the system prompt. + """ + event.cli.push_focus(SYSTEM_BUFFER) + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle(Keys.Escape, filter=has_focus) + @handle(Keys.ControlC, filter=has_focus) + def _(event): + """ + Cancel system prompt. + """ + event.cli.vi_state.input_mode = InputMode.NAVIGATION + event.cli.buffers[SYSTEM_BUFFER].reset() + event.cli.pop_focus() + + @handle(Keys.ControlJ, filter=has_focus) + def _(event): + """ + Run system command. + """ + event.cli.vi_state.input_mode = InputMode.NAVIGATION + + system_buffer = event.cli.buffers[SYSTEM_BUFFER] + event.cli.run_system_command(system_buffer.text) + system_buffer.reset(append_to_history=True) + + # Focus previous buffer again. + event.cli.pop_focus() + + return registry + + +def load_vi_search_bindings(get_search_state=None, + search_buffer_name=SEARCH_BUFFER): + assert get_search_state is None or callable(get_search_state) + + if not get_search_state: + def get_search_state(cli): return cli.search_state + + registry = ConditionalRegistry(Registry(), ViMode()) + handle = registry.add_binding + + has_focus = filters.HasFocus(search_buffer_name) + navigation_mode = ViNavigationMode() + selection_mode = ViSelectionMode() + + reverse_vi_search_direction = Condition( + lambda cli: cli.application.reverse_vi_search_direction(cli)) + + @handle('/', filter=(navigation_mode|selection_mode)&~reverse_vi_search_direction) + @handle('?', filter=(navigation_mode|selection_mode)&reverse_vi_search_direction) + @handle(Keys.ControlS, filter=~has_focus) + def _(event): + """ + Vi-style forward search. + """ + # Set the ViState. + get_search_state(event.cli).direction = IncrementalSearchDirection.FORWARD + event.cli.vi_state.input_mode = InputMode.INSERT + + # Focus search buffer. + event.cli.push_focus(search_buffer_name) + + @handle('?', filter=(navigation_mode|selection_mode)&~reverse_vi_search_direction) + @handle('/', filter=(navigation_mode|selection_mode)&reverse_vi_search_direction) + @handle(Keys.ControlR, filter=~has_focus) + def _(event): + """ + Vi-style backward search. + """ + # Set the ViState. + get_search_state(event.cli).direction = IncrementalSearchDirection.BACKWARD + + # Focus search buffer. + event.cli.push_focus(search_buffer_name) + event.cli.vi_state.input_mode = InputMode.INSERT + + @handle(Keys.ControlJ, filter=has_focus) + def _(event): + """ + Apply the search. (At the / or ? prompt.) + """ + input_buffer = event.cli.buffers.previous(event.cli) + search_buffer = event.cli.buffers[search_buffer_name] + + # Update search state. + if search_buffer.text: + get_search_state(event.cli).text = search_buffer.text + + # Apply search. + input_buffer.apply_search(get_search_state(event.cli)) + + # Add query to history of search line. + search_buffer.append_to_history() + search_buffer.reset() + + # Focus previous document again. + event.cli.vi_state.input_mode = InputMode.NAVIGATION + event.cli.pop_focus() + + def incremental_search(cli, direction, count=1): + " Apply search, but keep search buffer focussed. " + # Update search_state. + search_state = get_search_state(cli) + direction_changed = search_state.direction != direction + + search_state.text = cli.buffers[search_buffer_name].text + search_state.direction = direction + + # Apply search to current buffer. + if not direction_changed: + input_buffer = cli.buffers.previous(cli) + input_buffer.apply_search(search_state, + include_current_position=False, count=count) + + @handle(Keys.ControlR, filter=has_focus) + def _(event): + incremental_search(event.cli, IncrementalSearchDirection.BACKWARD, count=event.arg) + + @handle(Keys.ControlS, filter=has_focus) + def _(event): + incremental_search(event.cli, IncrementalSearchDirection.FORWARD, count=event.arg) + + def search_buffer_is_empty(cli): + """ Returns True when the search buffer is empty. """ + return cli.buffers[search_buffer_name].text == '' + + @handle(Keys.Escape, filter=has_focus) + @handle(Keys.ControlC, filter=has_focus) + @handle(Keys.ControlH, filter=has_focus & Condition(search_buffer_is_empty)) + @handle(Keys.Backspace, filter=has_focus & Condition(search_buffer_is_empty)) + def _(event): + """ + Cancel search. + """ + event.cli.vi_state.input_mode = InputMode.NAVIGATION + + event.cli.pop_focus() + event.cli.buffers[search_buffer_name].reset() + + return registry + + +def load_extra_vi_page_navigation_bindings(): + """ + Key bindings, for scrolling up and down through pages. + This are separate bindings, because GNU readline doesn't have them. + """ + registry = ConditionalRegistry(Registry(), ViMode()) + handle = registry.add_binding + + handle(Keys.ControlF)(scroll_forward) + handle(Keys.ControlB)(scroll_backward) + handle(Keys.ControlD)(scroll_half_page_down) + handle(Keys.ControlU)(scroll_half_page_up) + handle(Keys.ControlE)(scroll_one_line_down) + handle(Keys.ControlY)(scroll_one_line_up) + handle(Keys.PageDown)(scroll_page_down) + handle(Keys.PageUp)(scroll_page_up) + + return registry + + +class ViStateFilter(Filter): + " Deprecated! " + def __init__(self, get_vi_state, mode): + self.get_vi_state = get_vi_state + self.mode = mode + + def __call__(self, cli): + return self.get_vi_state(cli).input_mode == self.mode diff --git a/src/libs/prompt_toolkit/key_binding/defaults.py b/src/libs/prompt_toolkit/key_binding/defaults.py new file mode 100644 index 0000000..4a1fb64 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/defaults.py @@ -0,0 +1,119 @@ +""" +Default key bindings.:: + + registry = load_key_bindings() + app = Application(key_bindings_registry=registry) +""" +from __future__ import unicode_literals +from libs.prompt_toolkit.key_binding.registry import ConditionalRegistry, MergedRegistry +from libs.prompt_toolkit.key_binding.bindings.basic import load_basic_bindings, load_abort_and_exit_bindings, load_basic_system_bindings, load_auto_suggestion_bindings, load_mouse_bindings +from libs.prompt_toolkit.key_binding.bindings.emacs import load_emacs_bindings, load_emacs_system_bindings, load_emacs_search_bindings, load_emacs_open_in_editor_bindings, load_extra_emacs_page_navigation_bindings +from libs.prompt_toolkit.key_binding.bindings.vi import load_vi_bindings, load_vi_system_bindings, load_vi_search_bindings, load_vi_open_in_editor_bindings, load_extra_vi_page_navigation_bindings +from libs.prompt_toolkit.filters import to_cli_filter + +__all__ = ( + 'load_key_bindings', + 'load_key_bindings_for_prompt', +) + + +def load_key_bindings( + get_search_state=None, + enable_abort_and_exit_bindings=False, + enable_system_bindings=False, + enable_search=False, + enable_open_in_editor=False, + enable_extra_page_navigation=False, + enable_auto_suggest_bindings=False): + """ + Create a Registry object that contains the default key bindings. + + :param enable_abort_and_exit_bindings: Filter to enable Ctrl-C and Ctrl-D. + :param enable_system_bindings: Filter to enable the system bindings (meta-! + prompt and Control-Z suspension.) + :param enable_search: Filter to enable the search bindings. + :param enable_open_in_editor: Filter to enable open-in-editor. + :param enable_open_in_editor: Filter to enable open-in-editor. + :param enable_extra_page_navigation: Filter for enabling extra page + navigation. (Bindings for up/down scrolling through long pages, like in + Emacs or Vi.) + :param enable_auto_suggest_bindings: Filter to enable fish-style suggestions. + """ + + assert get_search_state is None or callable(get_search_state) + + # Accept both Filters and booleans as input. + enable_abort_and_exit_bindings = to_cli_filter(enable_abort_and_exit_bindings) + enable_system_bindings = to_cli_filter(enable_system_bindings) + enable_search = to_cli_filter(enable_search) + enable_open_in_editor = to_cli_filter(enable_open_in_editor) + enable_extra_page_navigation = to_cli_filter(enable_extra_page_navigation) + enable_auto_suggest_bindings = to_cli_filter(enable_auto_suggest_bindings) + + registry = MergedRegistry([ + # Load basic bindings. + load_basic_bindings(), + load_mouse_bindings(), + + ConditionalRegistry(load_abort_and_exit_bindings(), + enable_abort_and_exit_bindings), + + ConditionalRegistry(load_basic_system_bindings(), + enable_system_bindings), + + # Load emacs bindings. + load_emacs_bindings(), + + ConditionalRegistry(load_emacs_open_in_editor_bindings(), + enable_open_in_editor), + + ConditionalRegistry(load_emacs_search_bindings(get_search_state=get_search_state), + enable_search), + + ConditionalRegistry(load_emacs_system_bindings(), + enable_system_bindings), + + ConditionalRegistry(load_extra_emacs_page_navigation_bindings(), + enable_extra_page_navigation), + + # Load Vi bindings. + load_vi_bindings(get_search_state=get_search_state), + + ConditionalRegistry(load_vi_open_in_editor_bindings(), + enable_open_in_editor), + + ConditionalRegistry(load_vi_search_bindings(get_search_state=get_search_state), + enable_search), + + ConditionalRegistry(load_vi_system_bindings(), + enable_system_bindings), + + ConditionalRegistry(load_extra_vi_page_navigation_bindings(), + enable_extra_page_navigation), + + # Suggestion bindings. + # (This has to come at the end, because the Vi bindings also have an + # implementation for the "right arrow", but we really want the + # suggestion binding when a suggestion is available.) + ConditionalRegistry(load_auto_suggestion_bindings(), + enable_auto_suggest_bindings), + ]) + + return registry + + +def load_key_bindings_for_prompt(**kw): + """ + Create a ``Registry`` object with the defaults key bindings for an input + prompt. + + This activates the key bindings for abort/exit (Ctrl-C/Ctrl-D), + incremental search and auto suggestions. + + (Not for full screen applications.) + """ + kw.setdefault('enable_abort_and_exit_bindings', True) + kw.setdefault('enable_search', True) + kw.setdefault('enable_auto_suggest_bindings', True) + + return load_key_bindings(**kw) diff --git a/src/libs/prompt_toolkit/key_binding/digraphs.py b/src/libs/prompt_toolkit/key_binding/digraphs.py new file mode 100644 index 0000000..36c6b15 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/digraphs.py @@ -0,0 +1,1378 @@ +# encoding: utf-8 +from __future__ import unicode_literals +""" +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 +""" +__all__ = ('DIGRAPHS', ) + +# digraphs for Unicode from RFC1345 +# (also work for ISO-8859-1 aka latin1) +DIGRAPHS = { + ('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/libs/prompt_toolkit/key_binding/input_processor.py b/src/libs/prompt_toolkit/key_binding/input_processor.py new file mode 100644 index 0000000..383e9f5 --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/input_processor.py @@ -0,0 +1,372 @@ +# *** encoding: utf-8 *** +""" +An :class:`~.InputProcessor` receives callbacks for the keystrokes parsed from +the input in the :class:`~libs.prompt_toolkit.inputstream.InputStream` instance. + +The `InputProcessor` will according to the implemented keybindings call the +correct callbacks when new key presses are feed through `feed`. +""" +from __future__ import unicode_literals +from libs.prompt_toolkit.buffer import EditReadOnlyBuffer +from libs.prompt_toolkit.filters.cli import ViNavigationMode +from libs.prompt_toolkit.keys import Keys, Key +from libs.prompt_toolkit.utils import Event + +from .registry import BaseRegistry + +from collections import deque +from six.moves import range +import weakref +import six + +__all__ = ( + 'InputProcessor', + 'KeyPress', +) + + +class KeyPress(object): + """ + :param key: A `Keys` instance or text (one character). + :param data: The received string on stdin. (Often vt100 escape codes.) + """ + def __init__(self, key, data=None): + assert isinstance(key, (six.text_type, Key)) + assert data is None or isinstance(data, six.text_type) + + if data is None: + data = key.name if isinstance(key, Key) else key + + self.key = key + self.data = data + + def __repr__(self): + return '%s(key=%r, data=%r)' % ( + self.__class__.__name__, self.key, self.data) + + def __eq__(self, other): + return self.key == other.key and self.data == other.data + + +class InputProcessor(object): + """ + Statemachine that receives :class:`KeyPress` instances and according to the + key bindings in the given :class:`Registry`, calls the matching handlers. + + :: + + p = InputProcessor(registry) + + # 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 registry. + + :param registry: `BaseRegistry` instance. + :param cli_ref: weakref to `CommandLineInterface`. + """ + def __init__(self, registry, cli_ref): + assert isinstance(registry, BaseRegistry) + + self._registry = registry + self._cli_ref = cli_ref + + self.beforeKeyPress = Event(self) + self.afterKeyPress = Event(self) + + # The queue of keys not yet send to our _process generator/state machine. + self.input_queue = 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 = [] + + # Simple macro recording. (Like readline does.) + self.record_macro = False + self.macro = [] + + self.reset() + + def reset(self): + self._previous_key_sequence = [] + self._previous_handler = None + + self._process_coroutine = self._process() + self._process_coroutine.send(None) + + #: Readline argument (for repetition of commands.) + #: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html + self.arg = None + + def start_macro(self): + " Start recording macro. " + self.record_macro = True + self.macro = [] + + def end_macro(self): + " End recording macro. " + self.record_macro = False + + def call_macro(self): + for k in self.macro: + self.feed(k) + + def _get_matches(self, key_presses): + """ + For a list of :class:`KeyPress` instances. Give the matching handlers + that would handle this. + """ + keys = tuple(k.key for k in key_presses) + cli = self._cli_ref() + + # Try match, with mode flag + return [b for b in self._registry.get_bindings_for_keys(keys) if b.filter(cli)] + + def _is_prefix_of_longer_match(self, key_presses): + """ + 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) + cli = self._cli_ref() + + # 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 = set(b.filter for b in self._registry.get_bindings_starting_with_keys(keys)) + + # When any key binding is active, return True. + return any(f(cli) for f in filters) + + def _process(self): + """ + 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: + if retry: + retry = False + else: + buffer.append((yield)) + + # If we have some key presses, check for matches. + if buffer: + is_prefix_of_longer_match = self._is_prefix_of_longer_match(buffer) + matches = self._get_matches(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(self._cli_ref())] + + 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): + """ + Add a new :class:`KeyPress` to the input queue. + (Don't forget to call `process_keys` in order to process the queue.) + """ + assert isinstance(key_press, KeyPress) + self.input_queue.append(key_press) + + def process_keys(self): + """ + 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. + """ + while self.input_queue: + key_press = self.input_queue.popleft() + + if key_press.key != Keys.CPRResponse: + self.beforeKeyPress.fire() + + self._process_coroutine.send(key_press) + + if key_press.key != Keys.CPRResponse: + self.afterKeyPress.fire() + + # Invalidate user interface. + cli = self._cli_ref() + if cli: + cli.invalidate() + + def _call_handler(self, handler, key_sequence=None): + was_recording = self.record_macro + 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. + cli = event.cli # Can be `None` (In unit-tests only.) + + if handler.save_before(event) and cli: + cli.current_buffer.save_to_undo_stack() + + # Call handler. + 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 just silently ignore that. + pass + + 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 self.record_macro and was_recording: + self.macro.extend(key_sequence) + + def _fix_vi_cursor_position(self, event): + """ + 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.) + """ + cli = self._cli_ref() + if cli: + buff = cli.current_buffer + preferred_column = buff.preferred_column + + if (ViNavigationMode()(event.cli) 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 + + + +class KeyPressEvent(object): + """ + Key press event, delivered to key bindings. + + :param input_processor_ref: Weak reference to the `InputProcessor`. + :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, input_processor_ref, arg=None, key_sequence=None, + previous_key_sequence=None, is_repeat=False): + self._input_processor_ref = input_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 + + def __repr__(self): + return 'KeyPressEvent(arg=%r, key_sequence=%r, is_repeat=%r)' % ( + self.arg, self.key_sequence, self.is_repeat) + + @property + def data(self): + return self.key_sequence[-1].data + + @property + def input_processor(self): + return self._input_processor_ref() + + @property + def cli(self): + """ + Command line interface. + """ + return self.input_processor._cli_ref() + + @property + def current_buffer(self): + """ + The current buffer. + """ + return self.cli.current_buffer + + @property + def arg(self): + """ + 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): + """ + True if repetition argument was explicitly provided. + """ + return self._arg is not None + + def append_to_arg_count(self, data): + """ + 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 = "%s%s" % (current, data) + + self.input_processor.arg = result diff --git a/src/libs/prompt_toolkit/key_binding/manager.py b/src/libs/prompt_toolkit/key_binding/manager.py new file mode 100644 index 0000000..8ed7a4e --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/manager.py @@ -0,0 +1,96 @@ +""" +DEPRECATED: +Use `libs.prompt_toolkit.key_binding.defaults.load_key_bindings` instead. + +:class:`KeyBindingManager` is a utility (or shortcut) for loading all the key +bindings in a key binding registry, with a logic set of filters to quickly to +quickly change from Vi to Emacs key bindings at runtime. + +You don't have to use this, but it's practical. + +Usage:: + + manager = KeyBindingManager() + app = Application(key_bindings_registry=manager.registry) +""" +from __future__ import unicode_literals +from .defaults import load_key_bindings +from libs.prompt_toolkit.filters import to_cli_filter +from libs.prompt_toolkit.key_binding.registry import Registry, ConditionalRegistry, MergedRegistry + +__all__ = ( + 'KeyBindingManager', +) + + +class KeyBindingManager(object): + """ + Utility for loading all key bindings into memory. + + :param registry: Optional `Registry` instance. + :param enable_abort_and_exit_bindings: Filter to enable Ctrl-C and Ctrl-D. + :param enable_system_bindings: Filter to enable the system bindings + (meta-! prompt and Control-Z suspension.) + :param enable_search: Filter to enable the search bindings. + :param enable_open_in_editor: Filter to enable open-in-editor. + :param enable_open_in_editor: Filter to enable open-in-editor. + :param enable_extra_page_navigation: Filter for enabling extra page navigation. + (Bindings for up/down scrolling through long pages, like in Emacs or Vi.) + :param enable_auto_suggest_bindings: Filter to enable fish-style suggestions. + + :param enable_vi_mode: Deprecated! + """ + def __init__(self, + registry=None, # XXX: not used anymore. + enable_vi_mode=None, # (`enable_vi_mode` is deprecated.) + enable_all=True, # + get_search_state=None, + enable_abort_and_exit_bindings=False, + enable_system_bindings=False, + enable_search=False, + enable_open_in_editor=False, + enable_extra_page_navigation=False, + enable_auto_suggest_bindings=False): + + assert registry is None or isinstance(registry, Registry) + assert get_search_state is None or callable(get_search_state) + enable_all = to_cli_filter(enable_all) + + defaults = load_key_bindings( + get_search_state=get_search_state, + enable_abort_and_exit_bindings=enable_abort_and_exit_bindings, + enable_system_bindings=enable_system_bindings, + enable_search=enable_search, + enable_open_in_editor=enable_open_in_editor, + enable_extra_page_navigation=enable_extra_page_navigation, + enable_auto_suggest_bindings=enable_auto_suggest_bindings) + + # Note, we wrap this whole thing again in a MergedRegistry, because we + # don't want the `enable_all` settings to apply on items that were + # added to the registry as a whole. + self.registry = MergedRegistry([ + ConditionalRegistry(defaults, enable_all) + ]) + + @classmethod + def for_prompt(cls, **kw): + """ + Create a ``KeyBindingManager`` with the defaults for an input prompt. + This activates the key bindings for abort/exit (Ctrl-C/Ctrl-D), + incremental search and auto suggestions. + + (Not for full screen applications.) + """ + kw.setdefault('enable_abort_and_exit_bindings', True) + kw.setdefault('enable_search', True) + kw.setdefault('enable_auto_suggest_bindings', True) + + return cls(**kw) + + def reset(self, cli): + # For backwards compatibility. + pass + + def get_vi_state(self, cli): + # Deprecated! + return cli.vi_state diff --git a/src/libs/prompt_toolkit/key_binding/registry.py b/src/libs/prompt_toolkit/key_binding/registry.py new file mode 100644 index 0000000..f3cf1ee --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/registry.py @@ -0,0 +1,350 @@ +""" +Key bindings registry. + +A `Registry` 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:: + + r = Registry() + + @r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT) + def handler(event): + # Handle ControlX-ControlC key sequence. + pass + + +It is also possible to combine multiple registries. We do this in the default +key bindings. There are some registries that contain Emacs bindings, while +others contain the Vi bindings. They are merged together using a +`MergedRegistry`. + +We also have a `ConditionalRegistry` object that can enable/disable a group of +key bindings at once. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod + +from libs.prompt_toolkit.cache import SimpleCache +from libs.prompt_toolkit.filters import CLIFilter, to_cli_filter, Never +from libs.prompt_toolkit.keys import Key, Keys + +from six import text_type, with_metaclass + +__all__ = ( + 'BaseRegistry', + 'Registry', + 'ConditionalRegistry', + 'MergedRegistry', +) + + +class _Binding(object): + """ + (Immutable binding class.) + """ + def __init__(self, keys, handler, filter=None, eager=None, save_before=None): + assert isinstance(keys, tuple) + assert callable(handler) + assert isinstance(filter, CLIFilter) + assert isinstance(eager, CLIFilter) + assert callable(save_before) + + self.keys = keys + self.handler = handler + self.filter = filter + self.eager = eager + self.save_before = save_before + + def call(self, event): + return self.handler(event) + + def __repr__(self): + return '%s(keys=%r, handler=%r)' % ( + self.__class__.__name__, self.keys, self.handler) + + +class BaseRegistry(with_metaclass(ABCMeta, object)): + """ + Interface for a Registry. + """ + _version = 0 # For cache invalidation. + + @abstractmethod + def get_bindings_for_keys(self, keys): + pass + + @abstractmethod + def get_bindings_starting_with_keys(self, keys): + pass + + # `add_binding` and `remove_binding` don't have to be part of this + # interface. + + +class Registry(BaseRegistry): + """ + Key binding registry. + """ + def __init__(self): + self.key_bindings = [] + self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000) + self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000) + self._version = 0 # For cache invalidation. + + def _clear_cache(self): + self._version += 1 + self._get_bindings_for_keys_cache.clear() + self._get_bindings_starting_with_keys_cache.clear() + + def add_binding(self, *keys, **kwargs): + """ + Decorator for annotating key bindings. + + :param filter: :class:`~libs.prompt_toolkit.filters.CLIFilter` to determine + when this key binding is active. + :param eager: :class:`~libs.prompt_toolkit.filters.CLIFilter` 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 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.) + """ + filter = to_cli_filter(kwargs.pop('filter', True)) + eager = to_cli_filter(kwargs.pop('eager', False)) + save_before = kwargs.pop('save_before', lambda e: True) + to_cli_filter(kwargs.pop('invalidate_ui', True)) # Deprecated! (ignored.) + + assert not kwargs + assert keys + assert all(isinstance(k, (Key, text_type)) for k in keys), \ + 'Key bindings should consist of Key and string (unicode) instances.' + assert callable(save_before) + + 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 registry. It will slow down every key + # press otherwise. + def decorator(func): + return func + else: + def decorator(func): + self.key_bindings.append( + _Binding(keys, func, filter=filter, eager=eager, + save_before=save_before)) + self._clear_cache() + + return func + return decorator + + def remove_binding(self, function): + """ + Remove a key binding. + + This expects a function that was given to `add_binding` method as + parameter. Raises `ValueError` when the given function was not + registered before. + """ + assert callable(function) + + for b in self.key_bindings: + if b.handler == function: + self.key_bindings.remove(b) + self._clear_cache() + return + + # No key binding found for this function. Raise ValueError. + raise ValueError('Binding not found: %r' % (function, )) + + def get_bindings_for_keys(self, keys): + """ + 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(): + result = [] + for b in self.key_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' occurences 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): + """ + 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(): + result = [] + for b in self.key_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) + + +class _AddRemoveMixin(BaseRegistry): + """ + Common part for ConditionalRegistry and MergedRegistry. + """ + def __init__(self): + # `Registry` to be synchronized with all the others. + self._registry2 = Registry() + self._last_version = None + + # The 'extra' registry. Mostly for backwards compatibility. + self._extra_registry = Registry() + + def _update_cache(self): + raise NotImplementedError + + # For backwards, compatibility, we allow adding bindings to both + # ConditionalRegistry and MergedRegistry. This is however not the + # recommended way. Better is to create a new registry and merge them + # together using MergedRegistry. + + def add_binding(self, *k, **kw): + return self._extra_registry.add_binding(*k, **kw) + + def remove_binding(self, *k, **kw): + return self._extra_registry.remove_binding(*k, **kw) + + # Proxy methods to self._registry2. + + @property + def key_bindings(self): + self._update_cache() + return self._registry2.key_bindings + + @property + def _version(self): + self._update_cache() + return self._last_version + + def get_bindings_for_keys(self, *a, **kw): + self._update_cache() + return self._registry2.get_bindings_for_keys(*a, **kw) + + def get_bindings_starting_with_keys(self, *a, **kw): + self._update_cache() + return self._registry2.get_bindings_starting_with_keys(*a, **kw) + + +class ConditionalRegistry(_AddRemoveMixin): + """ + Wraps around a `Registry`. Disable/enable all the key bindings according to + the given (additional) filter.:: + + @Condition + def setting_is_true(cli): + return True # or False + + registy = ConditionalRegistry(registry, 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 `Registry` objects. + :param filter: `CLIFilter` object. + """ + def __init__(self, registry=None, filter=True): + registry = registry or Registry() + assert isinstance(registry, BaseRegistry) + + _AddRemoveMixin.__init__(self) + + self.registry = registry + self.filter = to_cli_filter(filter) + + def _update_cache(self): + " If the original registry was changed. Update our copy version. " + expected_version = (self.registry._version, self._extra_registry._version) + + if self._last_version != expected_version: + registry2 = Registry() + + # Copy all bindings from `self.registry`, adding our condition. + for reg in (self.registry, self._extra_registry): + for b in reg.key_bindings: + registry2.key_bindings.append( + _Binding( + keys=b.keys, + handler=b.handler, + filter=self.filter & b.filter, + eager=b.eager, + save_before=b.save_before)) + + self._registry2 = registry2 + self._last_version = expected_version + + +class MergedRegistry(_AddRemoveMixin): + """ + Merge multiple registries of key bindings into one. + + This class acts as a proxy to multiple `Registry` objects, but behaves as + if this is just one bigger `Registry`. + + :param registries: List of `Registry` objects. + """ + def __init__(self, registries): + assert all(isinstance(r, BaseRegistry) for r in registries) + + _AddRemoveMixin.__init__(self) + + self.registries = registries + + def _update_cache(self): + """ + If one of the original registries was changed. Update our merged + version. + """ + expected_version = ( + tuple(r._version for r in self.registries) + + (self._extra_registry._version, )) + + if self._last_version != expected_version: + registry2 = Registry() + + for reg in self.registries: + registry2.key_bindings.extend(reg.key_bindings) + + # Copy all bindings from `self._extra_registry`. + registry2.key_bindings.extend(self._extra_registry.key_bindings) + + self._registry2 = registry2 + self._last_version = expected_version diff --git a/src/libs/prompt_toolkit/key_binding/vi_state.py b/src/libs/prompt_toolkit/key_binding/vi_state.py new file mode 100644 index 0000000..92ce3cb --- /dev/null +++ b/src/libs/prompt_toolkit/key_binding/vi_state.py @@ -0,0 +1,61 @@ +from __future__ import unicode_literals + +__all__ = ( + 'InputMode', + 'CharacterFind', + 'ViState', +) + + +class InputMode(object): + INSERT = 'vi-insert' + INSERT_MULTIPLE = 'vi-insert-multiple' + NAVIGATION = 'vi-navigation' + REPLACE = 'vi-replace' + + +class CharacterFind(object): + def __init__(self, character, backwards=False): + self.character = character + self.backwards = backwards + + +class ViState(object): + """ + Mutable class to hold the state of the Vi navigation. + """ + def __init__(self): + #: 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 = 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 + self.operator_arg = None + + #: Named registers. Maps register name (e.g. 'a') to + #: :class:`ClipboardData` instances. + self.named_registers = {} + + #: The Vi mode we're currently in to. + self.input_mode = InputMode.INSERT + + #: Waiting for digraph. + self.waiting_for_digraph = False + self.digraph_symbol1 = None # (None or a symbol.) + + #: When true, make ~ act as an operator. + self.tilde_operator = False + + def reset(self, mode=InputMode.INSERT): + """ + Reset state, go back to the given mode. INSERT by default. + """ + # Go back to insert mode. + self.input_mode = mode + + self.waiting_for_digraph = False + self.operator_func = None + self.operator_arg = None diff --git a/src/libs/prompt_toolkit/keys.py b/src/libs/prompt_toolkit/keys.py new file mode 100644 index 0000000..9ca4416 --- /dev/null +++ b/src/libs/prompt_toolkit/keys.py @@ -0,0 +1,129 @@ +from __future__ import unicode_literals + +__all__ = ( + 'Key', + 'Keys', +) + + +class Key(object): + def __init__(self, name): + + #: Descriptive way of writing keys in configuration files. e.g. + #: for ``Control-A``. + self.name = name + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.name) + + +class Keys(object): + Escape = Key('') + + ControlA = Key('') + ControlB = Key('') + ControlC = Key('') + ControlD = Key('') + ControlE = Key('') + ControlF = Key('') + ControlG = Key('') + ControlH = Key('') + ControlI = Key('') # Tab + ControlJ = Key('') # Enter + ControlK = Key('') + ControlL = Key('') + ControlM = Key('') # Enter + ControlN = Key('') + ControlO = Key('') + ControlP = Key('') + ControlQ = Key('') + ControlR = Key('') + ControlS = Key('') + ControlT = Key('') + ControlU = Key('') + ControlV = Key('') + ControlW = Key('') + ControlX = Key('') + ControlY = Key('') + ControlZ = Key('') + + ControlSpace = Key('') + ControlBackslash = Key('') + ControlSquareClose = Key('') + ControlCircumflex = Key('') + ControlUnderscore = Key('') + ControlLeft = Key('') + ControlRight = Key('') + ControlUp = Key('') + ControlDown = Key('') + + Up = Key('') + Down = Key('') + Right = Key('') + Left = Key('') + + ShiftLeft = Key('') + ShiftUp = Key('') + ShiftDown = Key('') + ShiftRight = Key('') + + Home = Key('') + End = Key('') + Delete = Key('') + ShiftDelete = Key('') + ControlDelete = Key('') + PageUp = Key('') + PageDown = Key('') + BackTab = Key('') # shift + tab + Insert = Key('') + Backspace = Key('') + + # Aliases. + Tab = ControlI + Enter = ControlJ + # XXX: Actually Enter equals ControlM, not ControlJ, + # However, in libs.prompt_toolkit, we made the mistake of translating + # \r into \n during the input, so everyone is now handling the + # enter key by binding ControlJ. + + # From now on, it's better to bind `Keys.Enter` everywhere, + # because that's future compatible, and will still work when we + # stop replacing \r by \n. + + F1 = Key('') + F2 = Key('') + F3 = Key('') + F4 = Key('') + F5 = Key('') + F6 = Key('') + F7 = Key('') + F8 = Key('') + F9 = Key('') + F10 = Key('') + F11 = Key('') + F12 = Key('') + F13 = Key('') + F14 = Key('') + F15 = Key('') + F16 = Key('') + F17 = Key('') + F18 = Key('') + F19 = Key('') + F20 = Key('') + F21 = Key('') + F22 = Key('') + F23 = Key('') + F24 = Key('') + + # Matches any key. + Any = Key('') + + # Special + CPRResponse = Key('') + Vt100MouseEvent = Key('') + WindowsMouseEvent = Key('') + BracketedPaste = Key('') + + # Key which is ignored. (The key binding for this key should not do + # anything.) + Ignore = Key('') diff --git a/src/libs/prompt_toolkit/layout/__init__.py b/src/libs/prompt_toolkit/layout/__init__.py new file mode 100644 index 0000000..0dec5ec --- /dev/null +++ b/src/libs/prompt_toolkit/layout/__init__.py @@ -0,0 +1,51 @@ +""" +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) + |- TokenListControl (Renders a simple list of tokens) + |- FillControl (Fills control with one token/character.) + `- 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 unicode_literals + +from .containers import Float, FloatContainer, HSplit, VSplit, Window, ConditionalContainer +from .controls import TokenListControl, FillControl, BufferControl diff --git a/src/libs/prompt_toolkit/layout/containers.py b/src/libs/prompt_toolkit/layout/containers.py new file mode 100644 index 0000000..11591bb --- /dev/null +++ b/src/libs/prompt_toolkit/layout/containers.py @@ -0,0 +1,1665 @@ +""" +Container for the layout. +(Containers can contain other containers or user interface controls.) +""" +from __future__ import unicode_literals + +from abc import ABCMeta, abstractmethod +from six import with_metaclass +from six.moves import range + +from .controls import UIControl, TokenListControl, UIContent +from .dimension import LayoutDimension, sum_layout_dimensions, max_layout_dimensions +from .margins import Margin +from .screen import Point, WritePosition, _CHAR_CACHE +from .utils import token_list_to_text, explode_tokens +from libs.prompt_toolkit.cache import SimpleCache +from libs.prompt_toolkit.filters import to_cli_filter, ViInsertMode, EmacsInsertMode +from libs.prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from libs.prompt_toolkit.reactive import Integer +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.utils import take_using_weights, get_cwidth + +__all__ = ( + 'Container', + 'HSplit', + 'VSplit', + 'FloatContainer', + 'Float', + 'Window', + 'WindowRenderInfo', + 'ConditionalContainer', + 'ScrollOffsets', + 'ColorColumn', +) + +Transparent = Token.Transparent + + +class Container(with_metaclass(ABCMeta, object)): + """ + Base class for user interface layout. + """ + @abstractmethod + def reset(self): + """ + Reset the state of this container and all the children. + (E.g. reset scroll offsets, etc...) + """ + + @abstractmethod + def preferred_width(self, cli, max_available_width): + """ + Return a :class:`~libs.prompt_toolkit.layout.dimension.LayoutDimension` that + represents the desired width for this container. + + :param cli: :class:`~libs.prompt_toolkit.interface.CommandLineInterface`. + """ + + @abstractmethod + def preferred_height(self, cli, width, max_available_height): + """ + Return a :class:`~libs.prompt_toolkit.layout.dimension.LayoutDimension` that + represents the desired height for this container. + + :param cli: :class:`~libs.prompt_toolkit.interface.CommandLineInterface`. + """ + + @abstractmethod + def write_to_screen(self, cli, screen, mouse_handlers, write_position): + """ + Write the actual content to the screen. + + :param cli: :class:`~libs.prompt_toolkit.interface.CommandLineInterface`. + :param screen: :class:`~libs.prompt_toolkit.layout.screen.Screen` + :param mouse_handlers: :class:`~libs.prompt_toolkit.layout.mouse_handlers.MouseHandlers`. + """ + + @abstractmethod + def walk(self, cli): + """ + Walk through all the layout nodes (and their children) and yield them. + """ + + +def _window_too_small(): + " Create a `Window` that displays the 'Window too small' text. " + return Window(TokenListControl.static( + [(Token.WindowTooSmall, ' Window too small... ')])) + + +class HSplit(Container): + """ + Several layouts, one stacked above/under the other. + + :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 get_dimensions: (`None` or a callable that takes a + `CommandLineInterface` and returns a list of `LayoutDimension` + instances.) By default the dimensions are taken from the children and + divided by the available space. However, when `get_dimensions` is specified, + this is taken instead. + :param report_dimensions_callback: When rendering, this function is called + with the `CommandLineInterface` and the list of used dimensions. (As a + list of integers.) + """ + def __init__(self, children, window_too_small=None, + get_dimensions=None, report_dimensions_callback=None): + assert all(isinstance(c, Container) for c in children) + assert window_too_small is None or isinstance(window_too_small, Container) + assert get_dimensions is None or callable(get_dimensions) + assert report_dimensions_callback is None or callable(report_dimensions_callback) + + self.children = children + self.window_too_small = window_too_small or _window_too_small() + self.get_dimensions = get_dimensions + self.report_dimensions_callback = report_dimensions_callback + + def preferred_width(self, cli, max_available_width): + if self.children: + dimensions = [c.preferred_width(cli, max_available_width) for c in self.children] + return max_layout_dimensions(dimensions) + else: + return LayoutDimension(0) + + def preferred_height(self, cli, width, max_available_height): + dimensions = [c.preferred_height(cli, width, max_available_height) for c in self.children] + return sum_layout_dimensions(dimensions) + + def reset(self): + for c in self.children: + c.reset() + + def write_to_screen(self, cli, screen, mouse_handlers, write_position): + """ + Render the prompt to a `Screen` instance. + + :param screen: The :class:`~libs.prompt_toolkit.layout.screen.Screen` class + to which the output has to be written. + """ + sizes = self._divide_heigths(cli, write_position) + + if self.report_dimensions_callback: + self.report_dimensions_callback(cli, sizes) + + if sizes is None: + self.window_too_small.write_to_screen( + cli, screen, mouse_handlers, write_position) + else: + # Draw child panes. + ypos = write_position.ypos + xpos = write_position.xpos + width = write_position.width + + for s, c in zip(sizes, self.children): + c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, width, s)) + ypos += s + + def _divide_heigths(self, cli, write_position): + """ + Return the heights for all rows. + Or None when there is not enough space. + """ + if not self.children: + return [] + + # Calculate heights. + given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None + + def get_dimension_for_child(c, index): + if given_dimensions and given_dimensions[index] is not None: + return given_dimensions[index] + else: + return c.preferred_height(cli, write_position.width, write_position.extended_height) + + dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.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 > write_position.extended_height: + return + + # 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) + + while sum(sizes) < min(write_position.extended_height, sum_dimensions.preferred): + # Increase until we meet at least the 'preferred' size. + if sizes[i] < dimensions[i].preferred: + sizes[i] += 1 + i = next(child_generator) + + if not any([cli.is_returning, cli.is_exiting, cli.is_aborting]): + while sum(sizes) < min(write_position.height, sum_dimensions.max): + # Increase until we use all the available space. (or until "max") + if sizes[i] < dimensions[i].max: + sizes[i] += 1 + i = next(child_generator) + + return sizes + + def walk(self, cli): + """ Walk through children. """ + yield self + for c in self.children: + for i in c.walk(cli): + yield i + + +class VSplit(Container): + """ + Several layouts, one stacked left/right of the other. + + :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 get_dimensions: (`None` or a callable that takes a + `CommandLineInterface` and returns a list of `LayoutDimension` + instances.) By default the dimensions are taken from the children and + divided by the available space. However, when `get_dimensions` is specified, + this is taken instead. + :param report_dimensions_callback: When rendering, this function is called + with the `CommandLineInterface` and the list of used dimensions. (As a + list of integers.) + """ + def __init__(self, children, window_too_small=None, + get_dimensions=None, report_dimensions_callback=None): + assert all(isinstance(c, Container) for c in children) + assert window_too_small is None or isinstance(window_too_small, Container) + assert get_dimensions is None or callable(get_dimensions) + assert report_dimensions_callback is None or callable(report_dimensions_callback) + + self.children = children + self.window_too_small = window_too_small or _window_too_small() + self.get_dimensions = get_dimensions + self.report_dimensions_callback = report_dimensions_callback + + def preferred_width(self, cli, max_available_width): + dimensions = [c.preferred_width(cli, max_available_width) for c in self.children] + return sum_layout_dimensions(dimensions) + + def preferred_height(self, cli, width, max_available_height): + sizes = self._divide_widths(cli, width) + if sizes is None: + return LayoutDimension() + else: + dimensions = [c.preferred_height(cli, s, max_available_height) + for s, c in zip(sizes, self.children)] + return max_layout_dimensions(dimensions) + + def reset(self): + for c in self.children: + c.reset() + + def _divide_widths(self, cli, width): + """ + Return the widths for all columns. + Or None when there is not enough space. + """ + if not self.children: + return [] + + # Calculate widths. + given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None + + def get_dimension_for_child(c, index): + if given_dimensions and given_dimensions[index] is not None: + return given_dimensions[index] + else: + return c.preferred_width(cli, width) + + dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.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 > width: + return + + # 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) + + while sum(sizes) < min(width, sum_dimensions.preferred): + # Increase until we meet at least the 'preferred' size. + if sizes[i] < dimensions[i].preferred: + sizes[i] += 1 + i = next(child_generator) + + while sum(sizes) < min(width, sum_dimensions.max): + # Increase until we use all the available space. + if sizes[i] < dimensions[i].max: + sizes[i] += 1 + i = next(child_generator) + + return sizes + + def write_to_screen(self, cli, screen, mouse_handlers, write_position): + """ + Render the prompt to a `Screen` instance. + + :param screen: The :class:`~libs.prompt_toolkit.layout.screen.Screen` class + to which the output has to be written. + """ + if not self.children: + return + + sizes = self._divide_widths(cli, write_position.width) + + if self.report_dimensions_callback: + self.report_dimensions_callback(cli, sizes) + + # If there is not enough space. + if sizes is None: + self.window_too_small.write_to_screen( + cli, screen, mouse_handlers, write_position) + return + + # Calculate heights, take the largest possible, but not larger than write_position.extended_height. + heights = [child.preferred_height(cli, width, write_position.extended_height).preferred + for width, child in zip(sizes, self.children)] + height = max(write_position.height, min(write_position.extended_height, max(heights))) + + # Draw child panes. + ypos = write_position.ypos + xpos = write_position.xpos + + for s, c in zip(sizes, self.children): + c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, s, height)) + xpos += s + + def walk(self, cli): + """ Walk through children. """ + yield self + for c in self.children: + for i in c.walk(cli): + yield i + + +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, + layout=CompletionMenu(...)) + ]) + """ + def __init__(self, content, floats): + assert isinstance(content, Container) + assert all(isinstance(f, Float) for f in floats) + + self.content = content + self.floats = floats + + def reset(self): + self.content.reset() + + for f in self.floats: + f.content.reset() + + def preferred_width(self, cli, write_position): + return self.content.preferred_width(cli, write_position) + + def preferred_height(self, cli, width, max_available_height): + """ + 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(cli, width, max_available_height) + + def write_to_screen(self, cli, screen, mouse_handlers, write_position): + self.content.write_to_screen(cli, screen, mouse_handlers, write_position) + + for fl in self.floats: + # 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. + cursor_position = screen.menu_position or screen.cursor_position + cursor_position = Point(x=cursor_position.x - write_position.xpos, + y=cursor_position.y - write_position.ypos) + + fl_width = fl.get_width(cli) + fl_height = fl.get_height(cli) + + # 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 + elif fl.xcursor: + width = fl_width + if width is None: + width = fl.content.preferred_width(cli, write_position.width).preferred + width = min(write_position.width, 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(cli, 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 + 1 + + height = fl_height + if height is None: + height = fl.content.preferred_height( + cli, width, write_position.extended_height).preferred + + # Reduce height if not enough space. (We can use the + # extended_height when the content requires it.) + if height > write_position.extended_height - ypos: + if write_position.extended_height - ypos + 1 >= ypos: + # When the space below the cursor is more than + # the space above, just reduce the height. + height = write_position.extended_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_width: + ypos = int((write_position.height - fl_height) / 2) + height = fl_height + # Otherwise, take preferred height from content. + else: + height = fl.content.preferred_height( + cli, width, write_position.extended_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(cli, screen, mouse_handlers, wp) + + def _area_is_empty(self, screen, write_position): + """ + Return True when the area below the write position is still empty. + (For floats that should not hide content underneath.) + """ + wp = write_position + Transparent = Token.Transparent + + 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 != ' ' or c.token != Transparent: + return False + + return True + + def walk(self, cli): + """ Walk through children. """ + yield self + + for i in self.content.walk(cli): + yield i + + for f in self.floats: + for i in f.content.walk(cli): + yield i + + +class Float(object): + """ + Float for use in a :class:`.FloatContainer`. + + :param content: :class:`.Container` instance. + :param hide_when_covering_content: Hide the float when it covers content underneath. + """ + def __init__(self, top=None, right=None, bottom=None, left=None, + width=None, height=None, get_width=None, get_height=None, + xcursor=False, ycursor=False, content=None, + hide_when_covering_content=False): + assert isinstance(content, Container) + assert width is None or get_width is None + assert height is None or get_height is None + + self.left = left + self.right = right + self.top = top + self.bottom = bottom + + self._width = width + self._height = height + + self._get_width = get_width + self._get_height = get_height + + self.xcursor = xcursor + self.ycursor = ycursor + + self.content = content + self.hide_when_covering_content = hide_when_covering_content + + def get_width(self, cli): + if self._width: + return self._width + if self._get_width: + return self._get_width(cli) + + def get_height(self, cli): + if self._height: + return self._height + if self._get_height: + return self._get_height(cli) + + def __repr__(self): + return 'Float(content=%r)' % self.content + + +class WindowRenderInfo(object): + """ + Render information, for the last render time of this control. + It stores mapping information between the input buffers (in case of a + :class:`~libs.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, ui_content, horizontal_scroll, vertical_scroll, + window_width, window_height, + configured_scroll_offsets, + visible_line_to_row_col, rowcol_to_yx, + x_offset, y_offset, wrap_lines): + assert isinstance(ui_content, UIContent) + assert isinstance(horizontal_scroll, int) + assert isinstance(vertical_scroll, int) + assert isinstance(window_width, int) + assert isinstance(window_height, int) + assert isinstance(configured_scroll_offsets, ScrollOffsets) + assert isinstance(visible_line_to_row_col, dict) + assert isinstance(rowcol_to_yx, dict) + assert isinstance(x_offset, int) + assert isinstance(y_offset, int) + assert isinstance(wrap_lines, bool) + + 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): + return dict( + (visible_line, rowcol[0]) + for visible_line, rowcol in self.visible_line_to_row_col.items()) + + @property + def cursor_position(self): + """ + Return the cursor position coordinates, relative to the left/top corner + of the rendered screen. + """ + cpos = self.ui_content.cursor_position + y, x = self._rowcol_to_yx[cpos.y, cpos.x] + return Point(x=x - self._x_offset, y=y - self._y_offset) + + @property + def applied_scroll_offsets(self): + """ + 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 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): + """ + 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 = {} + 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=False): + """ + 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=False): + """ + 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=False, + after_scroll_offset=False): + """ + 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): + """ + The full height of the user control. + """ + return self.ui_content.line_count + + @property + def full_height_visible(self): + """ + 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): + """ + True when the top of the buffer is visible. + """ + return self.vertical_scroll == 0 + + @property + def bottom_visible(self): + """ + True when the bottom of the buffer is visible. + """ + return self.last_visible_line() == self.content_height - 1 + + @property + def vertical_scroll_percentage(self): + """ + 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): + """ + 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) + else: + return 1 + + +class ScrollOffsets(object): + """ + Scroll offsets for the :class:`.Window` class. + + Note that left/right offsets only make sense if line wrapping is disabled. + """ + def __init__(self, top=0, bottom=0, left=0, right=0): + assert isinstance(top, Integer) + assert isinstance(bottom, Integer) + assert isinstance(left, Integer) + assert isinstance(right, Integer) + + self._top = top + self._bottom = bottom + self._left = left + self._right = right + + @property + def top(self): + return int(self._top) + + @property + def bottom(self): + return int(self._bottom) + + @property + def left(self): + return int(self._left) + + @property + def right(self): + return int(self._right) + + def __repr__(self): + return 'ScrollOffsets(top=%r, bottom=%r, left=%r, right=%r)' % ( + self.top, self.bottom, self.left, self.right) + + +class ColorColumn(object): + def __init__(self, position, token=Token.ColorColumn): + self.position = position + self.token = token + + +_in_insert_mode = ViInsertMode() | EmacsInsertMode() + + +class Window(Container): + """ + Container that holds a control. + + :param content: :class:`~libs.prompt_toolkit.layout.controls.UIControl` instance. + :param width: :class:`~libs.prompt_toolkit.layout.dimension.LayoutDimension` instance. + :param height: :class:`~libs.prompt_toolkit.layout.dimension.LayoutDimension` instance. + :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`. + :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`. + :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 left_margins: A list of :class:`~libs.prompt_toolkit.layout.margins.Margin` + instance to be displayed on the left. For instance: + :class:`~libs.prompt_toolkit.layout.margins.NumberredMargin` 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:`~libs.prompt_toolkit.filters.CLIFilter` 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:`~libs.prompt_toolkit.filters.CLIFilter` + 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:`~libs.prompt_toolkit.filters.CLIFilter` instance. When True, never + display the cursor, even when the user control specifies a cursor + position. + :param cursorline: A `bool` or :class:`~libs.prompt_toolkit.filters.CLIFilter` + instance. When True, display a cursorline. + :param cursorcolumn: A `bool` or :class:`~libs.prompt_toolkit.filters.CLIFilter` + instance. When True, display a cursorcolumn. + :param get_colorcolumns: A callable that takes a `CommandLineInterface` and + returns a a list of :class:`.ColorColumn` instances that describe the + columns to be highlighted. + :param cursorline_token: The token to be used for highlighting the current line, + if `cursorline` is True. + :param cursorcolumn_token: The token to be used for highlighting the current line, + if `cursorcolumn` is True. + """ + def __init__(self, content, width=None, height=None, get_width=None, + get_height=None, dont_extend_width=False, dont_extend_height=False, + left_margins=None, right_margins=None, scroll_offsets=None, + allow_scroll_beyond_bottom=False, wrap_lines=False, + get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False, + cursorline=False, cursorcolumn=False, get_colorcolumns=None, + cursorline_token=Token.CursorLine, cursorcolumn_token=Token.CursorColumn): + assert isinstance(content, UIControl) + assert width is None or isinstance(width, LayoutDimension) + assert height is None or isinstance(height, LayoutDimension) + assert get_width is None or callable(get_width) + assert get_height is None or callable(get_height) + assert width is None or get_width is None + assert height is None or get_height is None + assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets) + assert left_margins is None or all(isinstance(m, Margin) for m in left_margins) + assert right_margins is None or all(isinstance(m, Margin) for m in right_margins) + assert get_vertical_scroll is None or callable(get_vertical_scroll) + assert get_horizontal_scroll is None or callable(get_horizontal_scroll) + assert get_colorcolumns is None or callable(get_colorcolumns) + + self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom) + self.always_hide_cursor = to_cli_filter(always_hide_cursor) + self.wrap_lines = to_cli_filter(wrap_lines) + self.cursorline = to_cli_filter(cursorline) + self.cursorcolumn = to_cli_filter(cursorcolumn) + + self.content = content + self.dont_extend_width = dont_extend_width + self.dont_extend_height = dont_extend_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._width = get_width or (lambda cli: width) + self._height = get_height or (lambda cli: height) + self.get_colorcolumns = get_colorcolumns or (lambda cli: []) + self.cursorline_token = cursorline_token + self.cursorcolumn_token = cursorcolumn_token + + # Cache for the screens generated by the margin. + self._ui_content_cache = SimpleCache(maxsize=8) + self._margin_width_cache = SimpleCache(maxsize=1) + + self.reset() + + def __repr__(self): + return 'Window(content=%r)' % self.content + + def reset(self): + 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 = None + + def _get_margin_width(self, cli, margin): + """ + 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(): + return self._get_ui_content(cli, width=0, height=0) + + def get_width(): + return margin.get_width(cli, get_ui_content) + + key = (margin, cli.render_counter) + return self._margin_width_cache.get(key, get_width) + + def preferred_width(self, cli, max_available_width): + # Calculate the width of the margin. + total_margin_width = sum(self._get_margin_width(cli, m) for m in + self.left_margins + self.right_margins) + + # Window of the content. (Can be `None`.) + preferred_width = self.content.preferred_width( + cli, max_available_width - total_margin_width) + + if preferred_width is not None: + # Include width of the margins. + preferred_width += total_margin_width + + # Merge. + return self._merge_dimensions( + dimension=self._width(cli), + preferred=preferred_width, + dont_extend=self.dont_extend_width) + + def preferred_height(self, cli, width, max_available_height): + total_margin_width = sum(self._get_margin_width(cli, m) for m in + self.left_margins + self.right_margins) + wrap_lines = self.wrap_lines(cli) + + return self._merge_dimensions( + dimension=self._height(cli), + preferred=self.content.preferred_height( + cli, width - total_margin_width, max_available_height, wrap_lines), + dont_extend=self.dont_extend_height) + + @staticmethod + def _merge_dimensions(dimension, preferred=None, dont_extend=False): + """ + Take the LayoutDimension from this `Window` class and the received + preferred size from the `UIControl` and return a `LayoutDimension` to + report to the parent container. + """ + dimension = dimension or LayoutDimension() + + # When a preferred dimension was explicitly given to the Window, + # ignore the UIControl. + if dimension.preferred_specified: + preferred = dimension.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: + preferred = min(preferred, dimension.max) + + if dimension.min: + preferred = max(preferred, dimension.min) + + # When a `dont_extend` flag has been given, use the preferred dimension + # also as the max dimension. + if dont_extend and preferred is not None: + max_ = min(dimension.max, preferred) + else: + max_ = dimension.max + + return LayoutDimension( + min=dimension.min, max=max_, + preferred=preferred, weight=dimension.weight) + + def _get_ui_content(self, cli, width, height): + """ + Create a `UIContent` instance. + """ + def get_content(): + return self.content.create_content(cli, width=width, height=height) + + key = (cli.render_counter, width, height) + return self._ui_content_cache.get(key, get_content) + + def _get_digraph_char(self, cli): + " Return `False`, or the Digraph symbol to be used. " + if cli.quoted_insert: + return '^' + if cli.vi_state.waiting_for_digraph: + if cli.vi_state.digraph_symbol1: + return cli.vi_state.digraph_symbol1 + return '?' + return False + + def write_to_screen(self, cli, screen, mouse_handlers, write_position): + """ + Write window to screen. This renders the user control, the margins and + copies everything over to the absolute position at the given screen. + """ + # Calculate margin sizes. + left_margin_widths = [self._get_margin_width(cli, m) for m in self.left_margins] + right_margin_widths = [self._get_margin_width(cli, 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( + cli, write_position.width - total_margin_width, write_position.height) + assert isinstance(ui_content, UIContent) + + # Scroll content. + wrap_lines = self.wrap_lines(cli) + scroll_func = self._scroll_when_linewrapping if wrap_lines else self._scroll_without_linewrapping + + scroll_func( + ui_content, write_position.width - total_margin_width, write_position.height, cli) + + # Write body + visible_line_to_row_col, rowcol_to_yx = self._copy_body( + cli, ui_content, screen, write_position, + sum(left_margin_widths), write_position.width - total_margin_width, + self.vertical_scroll, self.horizontal_scroll, + has_focus=self.content.has_focus(cli), + wrap_lines=wrap_lines, highlight_lines=True, + vertical_scroll_2=self.vertical_scroll_2, + always_hide_cursor=self.always_hide_cursor(cli)) + + # 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 + + self.render_info = WindowRenderInfo( + 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) + + # Set mouse handlers. + def mouse_handler(cli, mouse_event): + """ Wrapper around the mouse_handler of the `UIControl` that turns + screen coordinates into line coordinates. """ + # Find row/col position first. + yx_to_rowcol = dict((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) + + 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( + cli, MouseEvent(position=Point(x=col, y=row), + event_type=mouse_event.event_type)) + break + else: + # nobreak. + # (No x/y coordinate found for the content. This happens in + # case of a FillControl, that only specifies a background, but + # doesn't have a content. Report (0,0) instead.) + result = self.content.mouse_handler( + cli, MouseEvent(position=Point(x=0, y=0), + event_type=mouse_event.event_type)) + + # If it returns NotImplemented, handle it here. + if result == NotImplemented: + return self._mouse_handler(cli, 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, width): + " Render margin. Return `Screen`. " + # Retrieve margin tokens. + tokens = m.create_margin(cli, self.render_info, width, write_position.height) + + # Turn it into a UIContent object. + # already rendered those tokens using this size.) + return TokenListControl.static(tokens).create_content( + cli, width + 1, write_position.height) + + for m, width in zip(self.left_margins, left_margin_widths): + # Create screen for margin. + margin_screen = render_margin(m, width) + + # Copy and shift X. + self._copy_margin(cli, margin_screen, 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_screen = render_margin(m, width) + + # Copy and shift X. + self._copy_margin(cli, margin_screen, screen, write_position, move_x, width) + move_x += width + + def _copy_body(self, cli, ui_content, new_screen, write_position, move_x, + width, vertical_scroll=0, horizontal_scroll=0, + has_focus=False, wrap_lines=False, highlight_lines=False, + vertical_scroll_2=0, always_hide_cursor=False): + """ + Copy the UIContent into the output screen. + """ + 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['', Token] + ZeroWidthEscape = Token.ZeroWidthEscape + + # Map visible line number to (row, col) of input. + # 'col' will always be zero if line wrapping is off. + visible_line_to_row_col = {} + rowcol_to_yx = {} # Maps (row, col) from the input to (y, x) screen coordinates. + + # Fill background with default_char first. + default_char = ui_content.default_char + + if default_char: + for y in range(ypos, ypos + write_position.height): + new_buffer_row = new_buffer[y] + for x in range(xpos, xpos + width): + new_buffer_row[x] = default_char + + # Copy content. + def copy(): + 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) + + col = 0 + x = -horizontal_scroll + + visible_line_to_row_col[y] = (lineno, horizontal_scroll) + new_buffer_row = new_buffer[y + ypos] + + for token, text in line: + # Remember raw VT escape sequences. (E.g. FinalTerm's + # escape sequences.) + if token == ZeroWidthEscape: + new_screen.zero_width_escapes[y + ypos][x + xpos] += text + continue + + for c in text: + char = _CHAR_CACHE[c, token] + 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 + x = -horizontal_scroll # This would be equal to zero. + # (horizontal_scroll=0 when wrap_lines.) + new_buffer_row = new_buffer[y + ypos] + + if y >= write_position.height: + return y # Break out of all for loops. + + # Set character in screen and shift 'x'. + if x >= 0 and y >= 0 and x < write_position.width: + new_buffer_row[x + xpos] = char + + # When we print a multi width character, make sure + # to erase the neighbous 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 and x - 1 >= 0: + prev_char = new_buffer_row[x + xpos - 1] + char2 = _CHAR_CACHE[prev_char.char + c, prev_char.token] + new_buffer_row[x + xpos - 1] = char2 + + # Keep track of write position for each character. + rowcol_to_yx[lineno, col] = (y + ypos, x + xpos) + + col += 1 + x += char_width + + lineno += 1 + y += 1 + return y + + y = copy() + + def cursor_pos_to_screen_pos(row, col): + " 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(y=0, x=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(y=y, x=x) + + # 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.cursor_position = screen_cursor_position + + if always_hide_cursor: + new_screen.show_cursor = False + else: + new_screen.show_cursor = ui_content.show_cursor + + self._highlight_digraph(cli, new_screen) + + if highlight_lines: + self._highlight_cursorlines( + cli, 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_input_processor_key_buffer(cli, new_screen) + + # Set menu position. + if not new_screen.menu_position and ui_content.menu_position: + new_screen.menu_position = cursor_pos_to_screen_pos( + ui_content.menu_position.y, ui_content.menu_position.x) + + # Update output screne height. + new_screen.height = max(new_screen.height, ypos + write_position.height) + + return visible_line_to_row_col, rowcol_to_yx + + def _highlight_digraph(self, cli, new_screen): + """ + When we are in Vi digraph mode, put a question mark underneath the + cursor. + """ + digraph_char = self._get_digraph_char(cli) + if digraph_char: + cpos = new_screen.cursor_position + new_screen.data_buffer[cpos.y][cpos.x] = \ + _CHAR_CACHE[digraph_char, Token.Digraph] + + def _show_input_processor_key_buffer(self, cli, new_screen): + """ + 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. + """ + key_buffer = cli.input_processor.key_buffer + + if key_buffer and _in_insert_mode(cli) and not cli.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.cursor_position + new_screen.data_buffer[cpos.y][cpos.x] = \ + _CHAR_CACHE[data, Token.PartialKeyBinding] + + def _highlight_cursorlines(self, cli, new_screen, cpos, x, y, width, height): + """ + Highlight cursor row/column. + """ + cursor_line_token = (':', ) + self.cursorline_token + cursor_column_token = (':', ) + self.cursorcolumn_token + + data_buffer = new_screen.data_buffer + + # Highlight cursor line. + if self.cursorline(cli): + 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.token + cursor_line_token] + + # Highlight cursor column. + if self.cursorcolumn(cli): + 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.token + cursor_column_token] + + # Highlight color columns + for cc in self.get_colorcolumns(cli): + assert isinstance(cc, ColorColumn) + color_column_token = (':', ) + cc.token + column = cc.position + + for y2 in range(y, y + height): + row = data_buffer[y2] + original_char = row[column] + row[column] = _CHAR_CACHE[ + original_char.char, original_char.token + color_column_token] + + def _copy_margin(self, cli, lazy_screen, new_screen, write_position, move_x, width): + """ + 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(cli, lazy_screen, new_screen, margin_write_position, 0, width) + + def _scroll_when_linewrapping(self, ui_content, width, height, cli): + """ + 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 + + # 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. + if ui_content.get_height_for_line(ui_content.cursor_position.y, width) > height - scroll_offsets_top: + # Calculate the height of the text before the cursor, with the line + # containing the cursor included, and the character belowe the + # cursor included as well. + line = explode_tokens(ui_content.get_line(ui_content.cursor_position.y)) + text_before_cursor = token_list_to_text(line[:ui_content.cursor_position.x + 1]) + text_before_height = UIContent.get_height_for_text(text_before_cursor, width) + + # Adjust scroll offset. + self.vertical_scroll = ui_content.cursor_position.y + self.vertical_scroll_2 = min(text_before_height - 1, 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(): + # 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 += ui_content.get_height_for_line(lineno, width) + + if used_height > height - scroll_offsets_bottom: + return prev_lineno + else: + prev_lineno = lineno + return 0 + + def get_max_vertical_scroll(): + # 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 += ui_content.get_height_for_line(lineno, width) + + if used_height > scroll_offsets_top: + return prev_lineno + else: + prev_lineno = lineno + return prev_lineno + + def get_topmost_visible(): + """ + 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 += ui_content.get_height_for_line(lineno, width) + 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(cli): + self.vertical_scroll = min(self.vertical_scroll, topmost_visible) + + def _scroll_without_linewrapping(self, ui_content, width, height, cli): + """ + 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(0, 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 = token_list_to_text(ui_content.get_line(cursor_position.y)) + + def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end, + cursor_pos, window_size, content_size): + " 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(cli) 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) + + 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, + # We can only analyse 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, cli, mouse_event): + """ + Mouse handler. Called when the UI control doesn't handle this + particular event. + """ + if mouse_event.event_type == MouseEventType.SCROLL_DOWN: + self._scroll_down(cli) + elif mouse_event.event_type == MouseEventType.SCROLL_UP: + self._scroll_up(cli) + + def _scroll_down(self, cli): + " Scroll window down. " + info = self.render_info + + 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(cli) + + self.vertical_scroll += 1 + + def _scroll_up(self, cli): + " Scroll window up. " + info = self.render_info + + 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(cli) + + self.vertical_scroll -= 1 + + def walk(self, cli): + # Only yield self. A window doesn't have children. + yield self + + +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:`~libs.prompt_toolkit.filters.CLIFilter` instance. + """ + def __init__(self, content, filter): + assert isinstance(content, Container) + + self.content = content + self.filter = to_cli_filter(filter) + + def __repr__(self): + return 'ConditionalContainer(%r, filter=%r)' % (self.content, self.filter) + + def reset(self): + self.content.reset() + + def preferred_width(self, cli, max_available_width): + if self.filter(cli): + return self.content.preferred_width(cli, max_available_width) + else: + return LayoutDimension.exact(0) + + def preferred_height(self, cli, width, max_available_height): + if self.filter(cli): + return self.content.preferred_height(cli, width, max_available_height) + else: + return LayoutDimension.exact(0) + + def write_to_screen(self, cli, screen, mouse_handlers, write_position): + if self.filter(cli): + return self.content.write_to_screen(cli, screen, mouse_handlers, write_position) + + def walk(self, cli): + return self.content.walk(cli) + + +# Deprecated alias for 'Container'. +Layout = Container diff --git a/src/libs/prompt_toolkit/layout/controls.py b/src/libs/prompt_toolkit/layout/controls.py new file mode 100644 index 0000000..89bcb96 --- /dev/null +++ b/src/libs/prompt_toolkit/layout/controls.py @@ -0,0 +1,730 @@ +""" +User interface Controls for the layout. +""" +from __future__ import unicode_literals + +from abc import ABCMeta, abstractmethod +from collections import namedtuple +from six import with_metaclass +from six.moves import range + +from libs.prompt_toolkit.cache import SimpleCache +from libs.prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER +from libs.prompt_toolkit.filters import to_cli_filter +from libs.prompt_toolkit.mouse_events import MouseEventType +from libs.prompt_toolkit.search_state import SearchState +from libs.prompt_toolkit.selection import SelectionType +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.utils import get_cwidth + +from .lexers import Lexer, SimpleLexer +from .processors import Processor +from .screen import Char, Point +from .utils import token_list_width, split_lines, token_list_to_text + +import six +import time + + +__all__ = ( + 'BufferControl', + 'FillControl', + 'TokenListControl', + 'UIControl', + 'UIContent', +) + + +class UIControl(with_metaclass(ABCMeta, object)): + """ + Base class for all user interface controls. + """ + def reset(self): + # Default reset. (Doesn't have to be implemented.) + pass + + def preferred_width(self, cli, max_available_width): + return None + + def preferred_height(self, cli, width, max_available_height, wrap_lines): + return None + + def has_focus(self, cli): + """ + Return ``True`` when this user control has the focus. + + If so, the cursor will be displayed according to the cursor position + reported by :meth:`.UIControl.create_content`. If the created content + has the property ``show_cursor=False``, the cursor will be hidden from + the output. + """ + return False + + @abstractmethod + def create_content(self, cli, width, height): + """ + Generate the content for this user control. + + Returns a :class:`.UIContent` instance. + """ + + def mouse_handler(self, cli, mouse_event): + """ + 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 cli: `CommandLineInterface` instance. + :param mouse_event: `MouseEvent` instance. + """ + return NotImplemented + + def move_cursor_down(self, cli): + """ + Request to move the cursor down. + This happens when scrolling down and the cursor is completely at the + top. + """ + + def move_cursor_up(self, cli): + """ + Request to move the cursor up. + """ + + +class UIContent(object): + """ + Content generated by a user control. This content consists of a list of + lines. + + :param get_line: Callable that returns the current line. This is a list of + (Token, 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. + :param default_char: The default :class:`.Char` for filling the background. + """ + def __init__(self, get_line=None, line_count=0, + cursor_position=None, menu_position=None, show_cursor=True, + default_char=None): + assert callable(get_line) + assert isinstance(line_count, six.integer_types) + assert cursor_position is None or isinstance(cursor_position, Point) + assert menu_position is None or isinstance(menu_position, Point) + assert default_char is None or isinstance(default_char, Char) + + self.get_line = get_line + self.line_count = line_count + self.cursor_position = cursor_position or Point(0, 0) + self.menu_position = menu_position + self.show_cursor = show_cursor + self.default_char = default_char + + # Cache for line heights. Maps (lineno, width) -> height. + self._line_heights = {} + + def __getitem__(self, lineno): + " 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, width): + """ + Return the height that a given line would need if it is rendered in a + space with the given width. + """ + try: + return self._line_heights[lineno, width] + except KeyError: + text = token_list_to_text(self.get_line(lineno)) + result = self.get_height_for_text(text, width) + + # Cache and return + self._line_heights[lineno, width] = result + return result + + @staticmethod + def get_height_for_text(text, width): + # Get text width for this line. + line_width = get_cwidth(text) + + # Calculate height. + try: + quotient, remainder = divmod(line_width, width) + except ZeroDivisionError: + # Return something very big. + # (This can happen, when the Window gets very small.) + return 10 ** 10 + else: + if remainder: + quotient += 1 # Like math.ceil. + return max(1, quotient) + + +class TokenListControl(UIControl): + """ + Control that displays a list of (Token, text) tuples. + (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) + + Mouse support: + + The list of tokens can also contain tuples of three items, looking like: + (Token, text, handler). When mouse support is enabled and the user + clicks on this token, then the given handler is called. That handler + should accept two inputs: (CommandLineInterface, MouseEvent) and it + should either handle the event or return `NotImplemented` in case we + want the containing Window to handle this event. + + :param get_tokens: Callable that takes a `CommandLineInterface` instance + and returns the list of (Token, text) tuples to be displayed right now. + :param default_char: default :class:`.Char` (character and Token) to use + for the background when there is more space available than `get_tokens` + returns. + :param get_default_char: Like `default_char`, but this is a callable that + takes a :class:`libs.prompt_toolkit.interface.CommandLineInterface` and + returns a :class:`.Char` instance. + :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`, + this UI control will take the focus. The cursor will be shown in the + upper left corner of this control, unless `get_token` returns a + ``Token.SetCursorPosition`` token somewhere in the token list, then the + cursor will be shown there. + """ + def __init__(self, get_tokens, default_char=None, get_default_char=None, + align_right=False, align_center=False, has_focus=False): + assert callable(get_tokens) + assert default_char is None or isinstance(default_char, Char) + assert get_default_char is None or callable(get_default_char) + assert not (default_char and get_default_char) + + self.align_right = to_cli_filter(align_right) + self.align_center = to_cli_filter(align_center) + self._has_focus_filter = to_cli_filter(has_focus) + + self.get_tokens = get_tokens + + # Construct `get_default_char` callable. + if default_char: + get_default_char = lambda _: default_char + elif not get_default_char: + get_default_char = lambda _: Char(' ', Token.Transparent) + + self.get_default_char = get_default_char + + #: Cache for the content. + self._content_cache = SimpleCache(maxsize=18) + self._token_cache = SimpleCache(maxsize=1) + # Only cache one token list. We don't need the previous item. + + # Render info for the mouse support. + self._tokens = None + + def reset(self): + self._tokens = None + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.get_tokens) + + def _get_tokens_cached(self, cli): + """ + Get tokens, but only retrieve tokens 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._token_cache.get( + cli.render_counter, lambda: self.get_tokens(cli)) + + def has_focus(self, cli): + return self._has_focus_filter(cli) + + def preferred_width(self, cli, max_available_width): + """ + Return the preferred width for this control. + That is the width of the longest line. + """ + text = token_list_to_text(self._get_tokens_cached(cli)) + line_lengths = [get_cwidth(l) for l in text.split('\n')] + return max(line_lengths) + + def preferred_height(self, cli, width, max_available_height, wrap_lines): + content = self.create_content(cli, width, None) + return content.line_count + + def create_content(self, cli, width, height): + # Get tokens + tokens_with_mouse_handlers = self._get_tokens_cached(cli) + + default_char = self.get_default_char(cli) + + # Wrap/align right/center parameters. + right = self.align_right(cli) + center = self.align_center(cli) + + def process_line(line): + " Center or right align a single line. " + used_width = token_list_width(line) + padding = width - used_width + if center: + padding = int(padding / 2) + return [(default_char.token, default_char.char * padding)] + line + + if right or center: + token_lines_with_mouse_handlers = [] + + for line in split_lines(tokens_with_mouse_handlers): + token_lines_with_mouse_handlers.append(process_line(line)) + else: + token_lines_with_mouse_handlers = list(split_lines(tokens_with_mouse_handlers)) + + # Strip mouse handlers from tokens. + token_lines = [ + [tuple(item[:2]) for item in line] + for line in token_lines_with_mouse_handlers + ] + + # Keep track of the tokens with mouse handler, for later use in + # `mouse_handler`. + self._tokens = tokens_with_mouse_handlers + + # If there is a `Token.SetCursorPosition` in the token list, set the + # cursor position here. + def get_cursor_position(): + SetCursorPosition = Token.SetCursorPosition + + for y, line in enumerate(token_lines): + x = 0 + for token, text in line: + if token == SetCursorPosition: + return Point(x=x, y=y) + x += len(text) + return None + + # Create content, or take it from the cache. + key = (default_char.char, default_char.token, + tuple(tokens_with_mouse_handlers), width, right, center) + + def get_content(): + return UIContent(get_line=lambda i: token_lines[i], + line_count=len(token_lines), + default_char=default_char, + cursor_position=get_cursor_position()) + + return self._content_cache.get(key, get_content) + + @classmethod + def static(cls, tokens): + def get_static_tokens(cli): + return tokens + return cls(get_static_tokens) + + def mouse_handler(self, cli, mouse_event): + """ + Handle mouse events. + + (When the token 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 `Window` to handle this + particular event.) + """ + if self._tokens: + # Read the generator. + tokens_for_line = list(split_lines(self._tokens)) + + try: + tokens = tokens_for_line[mouse_event.position.y] + except IndexError: + return NotImplemented + else: + # Find position in the token list. + xpos = mouse_event.position.x + + # Find mouse handler for this character. + count = 0 + for item in tokens: + 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(cli, mouse_event) + else: + break + + # Otherwise, don't handle here. + return NotImplemented + + +class FillControl(UIControl): + """ + Fill whole control with characters with this token. + (Also helpful for debugging.) + + :param char: :class:`.Char` instance to use for filling. + :param get_char: A callable that takes a CommandLineInterface and returns a + :class:`.Char` object. + """ + def __init__(self, character=None, token=Token, char=None, get_char=None): # 'character' and 'token' parameters are deprecated. + assert char is None or isinstance(char, Char) + assert get_char is None or callable(get_char) + assert not (char and get_char) + + self.char = char + + if character: + # Passing (character=' ', token=token) is deprecated. + self.character = character + self.token = token + + self.get_char = lambda cli: Char(character, token) + elif get_char: + # When 'get_char' is given. + self.get_char = get_char + else: + # When 'char' is given. + self.char = self.char or Char() + self.get_char = lambda cli: self.char + self.char = char + + def __repr__(self): + if self.char: + return '%s(char=%r)' % (self.__class__.__name__, self.char) + else: + return '%s(get_char=%r)' % (self.__class__.__name__, self.get_char) + + def reset(self): + pass + + def has_focus(self, cli): + return False + + def create_content(self, cli, width, height): + def get_line(i): + return [] + + return UIContent( + get_line=get_line, + line_count=100 ** 100, # Something very big. + default_char=self.get_char(cli)) + + +_ProcessedLine = namedtuple('_ProcessedLine', 'tokens source_to_display display_to_source') + + +class BufferControl(UIControl): + """ + Control for visualising the content of a `Buffer`. + + :param input_processors: list of :class:`~libs.prompt_toolkit.layout.processors.Processor`. + :param lexer: :class:`~libs.prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting. + :param preview_search: `bool` or `CLIFilter`: Show search while typing. + :param get_search_state: Callable that takes a CommandLineInterface and + returns the SearchState to be used. (If not CommandLineInterface.search_state.) + :param buffer_name: String representing the name of the buffer to display. + :param default_char: :class:`.Char` instance to use to fill the background. This is + transparent by default. + :param focus_on_click: Focus this buffer when it's click, but not yet focussed. + """ + def __init__(self, + buffer_name=DEFAULT_BUFFER, + input_processors=None, + lexer=None, + preview_search=False, + search_buffer_name=SEARCH_BUFFER, + get_search_state=None, + menu_position=None, + default_char=None, + focus_on_click=False): + assert input_processors is None or all(isinstance(i, Processor) for i in input_processors) + assert menu_position is None or callable(menu_position) + assert lexer is None or isinstance(lexer, Lexer) + assert get_search_state is None or callable(get_search_state) + assert default_char is None or isinstance(default_char, Char) + + self.preview_search = to_cli_filter(preview_search) + self.get_search_state = get_search_state + self.focus_on_click = to_cli_filter(focus_on_click) + + self.input_processors = input_processors or [] + self.buffer_name = buffer_name + self.menu_position = menu_position + self.lexer = lexer or SimpleLexer() + self.default_char = default_char or Char(token=Token.Transparent) + self.search_buffer_name = search_buffer_name + + #: 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 faily easy way to cache such an expensive operation. + self._token_cache = SimpleCache(maxsize=8) + + self._xy_to_cursor_position = None + self._last_click_timestamp = None + self._last_get_processed_line = None + + def _buffer(self, cli): + """ + The buffer object that contains the 'main' content. + """ + return cli.buffers[self.buffer_name] + + def has_focus(self, cli): + # This control gets the focussed if the actual `Buffer` instance has the + # focus or when any of the `InputProcessor` classes tells us that it + # wants the focus. (E.g. in case of a reverse-search, where the actual + # search buffer may not be displayed, but the "reverse-i-search" text + # should get the focus.) + return cli.current_buffer_name == self.buffer_name or \ + any(i.has_focus(cli) for i in self.input_processors) + + def preferred_width(self, cli, max_available_width): + """ + 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 behaviour. + """ + return None + + def preferred_height(self, cli, width, max_available_height, wrap_lines): + # Calculate the content height, if it was drawn on a screen with the + # given width. + height = 0 + content = self.create_content(cli, width, None) + + # 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) + + if height >= max_available_height: + return max_available_height + + return height + + def _get_tokens_for_line_func(self, cli, document): + """ + Create a function that returns the tokens for a given line. + """ + # Cache using `document.text`. + def get_tokens_for_line(): + return self.lexer.lex_document(cli, document) + + return self._token_cache.get(document.text, get_tokens_for_line) + + def _create_get_processed_line_func(self, cli, document): + """ + Create a function that takes a line number of the current document and + returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source) + tuple. + """ + def transform(lineno, tokens): + " Transform the tokens for a given line number. " + source_to_display_functions = [] + display_to_source_functions = [] + + # Get cursor position at this line. + if document.cursor_position_row == lineno: + cursor_column = document.cursor_position_col + else: + cursor_column = None + + def source_to_display(i): + """ Translate x position from the buffer to the x position in the + processed token list. """ + for f in source_to_display_functions: + i = f(i) + return i + + # Apply each processor. + for p in self.input_processors: + transformation = p.apply_transformation( + cli, document, lineno, source_to_display, tokens) + tokens = transformation.tokens + + if cursor_column: + cursor_column = transformation.source_to_display(cursor_column) + + display_to_source_functions.append(transformation.display_to_source) + source_to_display_functions.append(transformation.source_to_display) + + def display_to_source(i): + for f in reversed(display_to_source_functions): + i = f(i) + return i + + return _ProcessedLine(tokens, source_to_display, display_to_source) + + def create_func(): + get_line = self._get_tokens_for_line_func(cli, document) + cache = {} + + def get_processed_line(i): + 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, cli, width, height): + """ + Create a UIContent. + """ + buffer = self._buffer(cli) + + # 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.) + def preview_now(): + """ True when we should preview a search. """ + return bool(self.preview_search(cli) and + cli.buffers[self.search_buffer_name].text) + + if preview_now(): + if self.get_search_state: + ss = self.get_search_state(cli) + else: + ss = cli.search_state + + document = buffer.document_for_search(SearchState( + text=cli.current_buffer.text, + direction=ss.direction, + ignore_case=ss.ignore_case)) + else: + document = buffer.document + + get_processed_line = self._create_get_processed_line_func(cli, document) + self._last_get_processed_line = get_processed_line + + def translate_rowcol(row, col): + " Return the content column for this coordinate. " + return Point(y=row, x=get_processed_line(row).source_to_display(col)) + + def get_line(i): + " Return the tokens for a given line number. " + tokens = get_processed_line(i).tokens + + # 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.) + tokens = tokens + [(self.default_char.token, ' ')] + return tokens + + content = UIContent( + get_line=get_line, + line_count=document.line_count, + cursor_position=translate_rowcol(document.cursor_position_row, + document.cursor_position_col), + default_char=self.default_char) + + # 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 focussed buffer.) + if cli.current_buffer_name == self.buffer_name: + menu_position = self.menu_position(cli) 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, cli, mouse_event): + """ + Mouse handler for this control. + """ + buffer = self._buffer(cli) + position = mouse_event.position + + # Focus buffer when clicked. + if self.has_focus(cli): + 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_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: + 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 < .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 focussed, but focussing on click events. + else: + if self.focus_on_click(cli) 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 + # focussed and be handled anyway.) + cli.focus(self.buffer_name) + else: + return NotImplemented + + def move_cursor_down(self, cli): + b = self._buffer(cli) + b.cursor_position += b.document.get_cursor_down_position() + + def move_cursor_up(self, cli): + b = self._buffer(cli) + b.cursor_position += b.document.get_cursor_up_position() diff --git a/src/libs/prompt_toolkit/layout/dimension.py b/src/libs/prompt_toolkit/layout/dimension.py new file mode 100644 index 0000000..717ad7a --- /dev/null +++ b/src/libs/prompt_toolkit/layout/dimension.py @@ -0,0 +1,92 @@ +""" +Layout dimensions are used to give the minimum, maximum and preferred +dimensions for containers and controls. +""" +from __future__ import unicode_literals + +__all__ = ( + 'LayoutDimension', + 'sum_layout_dimensions', + 'max_layout_dimensions', +) + + +class LayoutDimension(object): + """ + 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 width 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=None, max=None, weight=1, preferred=None): + assert isinstance(weight, int) and weight > 0 # Cannot be a float. + + self.min_specified = min is not None + self.max_specified = max is not None + self.preferred_specified = preferred 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 + + self.min = min + self.max = max + self.preferred = preferred + self.weight = weight + + # 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): + """ + Return a :class:`.LayoutDimension` with an exact size. (min, max and + preferred set to ``amount``). + """ + return cls(min=amount, max=amount, preferred=amount) + + def __repr__(self): + return 'LayoutDimension(min=%r, max=%r, preferred=%r, weight=%r)' % ( + self.min, self.max, self.preferred, self.weight) + + def __add__(self, other): + return sum_layout_dimensions([self, other]) + + +def sum_layout_dimensions(dimensions): + """ + Sum a list of :class:`.LayoutDimension` instances. + """ + min = sum([d.min for d in dimensions if d.min is not None]) + max = sum([d.max for d in dimensions if d.max is not None]) + preferred = sum([d.preferred for d in dimensions]) + + return LayoutDimension(min=min, max=max, preferred=preferred) + + +def max_layout_dimensions(dimensions): + """ + Take the maximum of a list of :class:`.LayoutDimension` instances. + """ + min_ = max([d.min for d in dimensions if d.min is not None]) + max_ = max([d.max for d in dimensions if d.max is not None]) + preferred = max([d.preferred for d in dimensions]) + + return LayoutDimension(min=min_, max=max_, preferred=preferred) diff --git a/src/libs/prompt_toolkit/layout/lexers.py b/src/libs/prompt_toolkit/layout/lexers.py new file mode 100644 index 0000000..b50c3f6 --- /dev/null +++ b/src/libs/prompt_toolkit/layout/lexers.py @@ -0,0 +1,320 @@ +""" +Lexer interface and implementation. +Used for syntax highlighting. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass +from six.moves import range + +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.filters import to_cli_filter +from .utils import split_lines + +import re +import six + +__all__ = ( + 'Lexer', + 'SimpleLexer', + 'PygmentsLexer', + 'SyntaxSync', + 'SyncFromStart', + 'RegexSync', +) + + +class Lexer(with_metaclass(ABCMeta, object)): + """ + Base class for all lexers. + """ + @abstractmethod + def lex_document(self, cli, document): + """ + Takes a :class:`~libs.prompt_toolkit.document.Document` and returns a + callable that takes a line number and returns the tokens for that line. + """ + + +class SimpleLexer(Lexer): + """ + Lexer that doesn't do any tokenizing and returns the whole input as one token. + + :param token: The `Token` for this lexer. + """ + # `default_token` parameter is deprecated! + def __init__(self, token=Token, default_token=None): + self.token = token + + if default_token is not None: + self.token = default_token + + def lex_document(self, cli, document): + lines = document.lines + + def get_line(lineno): + " Return the tokens for the given line. " + try: + return [(self.token, lines[lineno])] + except IndexError: + return [] + return get_line + + +class SyntaxSync(with_metaclass(ABCMeta, object)): + """ + Syntax synchroniser. 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, lineno): + """ + 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, lineno): + 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 synchronisation. + # That would be too CPU intensive. + MAX_BACKWARDS = 500 + + # Start lexing at the start, if we are in the first 'n' lines and no + # synchronisation position was found. + FROM_START_IF_NO_SYNC_POS_FOUND = 100 + + def __init__(self, pattern): + assert isinstance(pattern, six.text_type) + self._compiled_pattern = re.compile(pattern) + + def get_sync_start_position(self, document, lineno): + " 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 + # synchronisation. + for i in range(lineno, max(-1, lineno - self.MAX_BACKWARDS), -1): + match = pattern.match(lines[i]) + if match: + return i, match.start() + + # No synchronisation 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): + """ + 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 PygmentsLexer(Lexer): + """ + Lexer that calls a pygments lexer. + + Example:: + + from pygments.lexers import HtmlLexer + lexer = PygmentsLexer(HtmlLexer) + + Note: Don't forget to also load a Pygments compatible style. E.g.:: + + from libs.prompt_toolkit.styles.from_pygments import style_from_pygments + from pygments.styles import get_style_by_name + style = style_from_pygments(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, sync_from_start=True, syntax_sync=None): + assert syntax_sync is None or isinstance(syntax_sync, SyntaxSync) + + self.pygments_lexer_cls = pygments_lexer_cls + self.sync_from_start = to_cli_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, sync_from_start=True): + """ + Create a `Lexer` from a filename. + """ + # Inline imports: the Pygments dependency is optional! + from pygments.util import ClassNotFound + from pygments.lexers import get_lexer_for_filename + + 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, cli, document): + """ + Create a lexer function that takes a line number and returns the list + of (Token, text) tuples as the Pygments lexer returns for that line. + """ + # Cache of already lexed lines. + cache = {} + + # Pygments generators that are currently lexing. + line_generators = {} # Map lexer generator to the line number. + + def get_syntax_sync(): + " The Syntax synchronisation objcet that we currently use. " + if self.sync_from_start(cli): + return SyncFromStart() + else: + return self.syntax_sync + + def find_closest_generator(i): + " Return a generator close to line 'i', or None if none was fonud. " + for generator, lineno in line_generators.items(): + if lineno < i and i - lineno < self.REUSE_GENERATOR_MAX_DISTANCE: + return generator + + def create_line_generator(start_lineno, column=0): + """ + Create a generator that yields the lexed lines. + Each iteration it yields a (line_number, [(token, text), ...]) tuple. + """ + def get_tokens(): + text = '\n'.join(document.lines[start_lineno:])[column:] + + # We call `get_tokens_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): + yield t, v + + return enumerate(split_lines(get_tokens()), start_lineno) + + def get_generator(i): + """ + 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 + # synchronisation 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 synchronisation 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): + " 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 synchronise 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/libs/prompt_toolkit/layout/margins.py b/src/libs/prompt_toolkit/layout/margins.py new file mode 100644 index 0000000..9a25834 --- /dev/null +++ b/src/libs/prompt_toolkit/layout/margins.py @@ -0,0 +1,253 @@ +""" +Margin implementations for a :class:`~libs.prompt_toolkit.layout.containers.Window`. +""" +from __future__ import unicode_literals + +from abc import ABCMeta, abstractmethod +from six import with_metaclass +from six.moves import range + +from libs.prompt_toolkit.filters import to_cli_filter +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.utils import get_cwidth +from .utils import token_list_to_text + +__all__ = ( + 'Margin', + 'NumberredMargin', + 'ScrollbarMargin', + 'ConditionalMargin', + 'PromptMargin', +) + + +class Margin(with_metaclass(ABCMeta, object)): + """ + Base interface for a margin. + """ + @abstractmethod + def get_width(self, cli, get_ui_content): + """ + Return the width that this margin is going to consume. + + :param cli: :class:`.CommandLineInterface` instance. + :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, cli, window_render_info, width, height): + """ + Creates a margin. + This should return a list of (Token, text) tuples. + + :param cli: :class:`.CommandLineInterface` instance. + :param window_render_info: + :class:`~libs.prompt_toolkit.layout.containers.WindowRenderInfo` + instance, generated after rendering and copying the visible part of + the :class:`~libs.prompt_toolkit.layout.controls.UIControl` into the + :class:`~libs.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:`~libs.prompt_toolkit.layout.containers.Window`.) + """ + return [] + + +class NumberredMargin(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=False, display_tildes=False): + self.relative = to_cli_filter(relative) + self.display_tildes = to_cli_filter(display_tildes) + + def get_width(self, cli, get_ui_content): + line_count = get_ui_content().line_count + return max(3, len('%s' % line_count) + 1) + + def create_margin(self, cli, window_render_info, width, height): + relative = self.relative(cli) + + token = Token.LineNumber + token_current = Token.LineNumber.Current + + # Get current line number. + current_lineno = window_render_info.ui_content.cursor_position.y + + # Construct margin. + result = [] + 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((token_current, '%i' % (lineno + 1))) + else: + result.append((token_current, ('%i ' % (lineno + 1)).rjust(width))) + else: + # Other lines. + if relative: + lineno = abs(lineno - current_lineno) - 1 + + result.append((token, ('%i ' % (lineno + 1)).rjust(width))) + + last_lineno = lineno + result.append((Token, '\n')) + + # Fill with tildes. + if self.display_tildes(cli): + while y < window_render_info.window_height: + result.append((Token.Tilde, '~\n')) + y += 1 + + return result + + +class ConditionalMargin(Margin): + """ + Wrapper around other :class:`.Margin` classes to show/hide them. + """ + def __init__(self, margin, filter): + assert isinstance(margin, Margin) + + self.margin = margin + self.filter = to_cli_filter(filter) + + def get_width(self, cli, ui_content): + if self.filter(cli): + return self.margin.get_width(cli, ui_content) + else: + return 0 + + def create_margin(self, cli, window_render_info, width, height): + if width and self.filter(cli): + return self.margin.create_margin(cli, 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=False): + self.display_arrows = to_cli_filter(display_arrows) + + def get_width(self, cli, ui_content): + return 1 + + def create_margin(self, cli, window_render_info, width, height): + total_height = window_render_info.content_height + display_arrows = self.display_arrows(cli) + + window_height = window_render_info.window_height + if display_arrows: + window_height -= 2 + + try: + items_per_row = float(total_height) / min(total_height, window_height) + except ZeroDivisionError: + return [] + else: + def is_scroll_button(row): + " True if we should display a button on this row. " + current_row_middle = int((row + .5) * items_per_row) + return current_row_middle in window_render_info.displayed_lines + + # Up arrow. + result = [] + if display_arrows: + result.extend([ + (Token.Scrollbar.Arrow, '^'), + (Token.Scrollbar, '\n') + ]) + + # Scrollbar body. + for i in range(window_height): + if is_scroll_button(i): + result.append((Token.Scrollbar.Button, ' ')) + else: + result.append((Token.Scrollbar, ' ')) + result.append((Token, '\n')) + + # Down arrow + if display_arrows: + result.append((Token.Scrollbar.Arrow, 'v')) + + return result + + +class PromptMargin(Margin): + """ + 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. + + :param get_prompt_tokens: Callable that takes a CommandLineInterface as + input and returns a list of (Token, type) tuples to be shown as the + prompt at the first line. + :param get_continuation_tokens: Callable that takes a CommandLineInterface + and a width as input and returns a list of (Token, type) tuples for the + next lines of the input. + :param show_numbers: (bool or :class:`~libs.prompt_toolkit.filters.CLIFilter`) + Display line numbers instead of the continuation prompt. + """ + def __init__(self, get_prompt_tokens, get_continuation_tokens=None, + show_numbers=False): + assert callable(get_prompt_tokens) + assert get_continuation_tokens is None or callable(get_continuation_tokens) + show_numbers = to_cli_filter(show_numbers) + + self.get_prompt_tokens = get_prompt_tokens + self.get_continuation_tokens = get_continuation_tokens + self.show_numbers = show_numbers + + def get_width(self, cli, ui_content): + " Width to report to the `Window`. " + # Take the width from the first line. + text = token_list_to_text(self.get_prompt_tokens(cli)) + return get_cwidth(text) + + def create_margin(self, cli, window_render_info, width, height): + # First line. + tokens = self.get_prompt_tokens(cli)[:] + + # Next lines. (Show line numbering when numbering is enabled.) + if self.get_continuation_tokens: + # Note: we turn this into a list, to make sure that we fail early + # in case `get_continuation_tokens` returns something else, + # like `None`. + tokens2 = list(self.get_continuation_tokens(cli, width)) + else: + tokens2 = [] + + show_numbers = self.show_numbers(cli) + last_y = None + + for y in window_render_info.displayed_lines[1:]: + tokens.append((Token, '\n')) + if show_numbers: + if y != last_y: + tokens.append((Token.LineNumber, ('%i ' % (y + 1)).rjust(width))) + else: + tokens.extend(tokens2) + last_y = y + + return tokens diff --git a/src/libs/prompt_toolkit/layout/menus.py b/src/libs/prompt_toolkit/layout/menus.py new file mode 100644 index 0000000..cd79b8f --- /dev/null +++ b/src/libs/prompt_toolkit/layout/menus.py @@ -0,0 +1,496 @@ +from __future__ import unicode_literals + +from six.moves import zip_longest, range +from libs.prompt_toolkit.filters import HasCompletions, IsDone, Condition, to_cli_filter +from libs.prompt_toolkit.mouse_events import MouseEventType +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.utils import get_cwidth + +from .containers import Window, HSplit, ConditionalContainer, ScrollOffsets +from .controls import UIControl, UIContent +from .dimension import LayoutDimension +from .margins import ScrollbarMargin +from .screen import Point, Char + +import math + +__all__ = ( + 'CompletionsMenu', + 'MultiColumnCompletionsMenu', +) + + +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 __init__(self): + self.token = Token.Menu.Completions + + def has_focus(self, cli): + return False + + def preferred_width(self, cli, max_available_width): + complete_state = cli.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, cli, width, max_available_height, wrap_lines): + complete_state = cli.current_buffer.complete_state + if complete_state: + return len(complete_state.current_completions) + else: + return 0 + + def create_content(self, cli, width, height): + """ + Create a UIContent object for this control. + """ + complete_state = cli.current_buffer.complete_state + if complete_state: + completions = complete_state.current_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): + c = completions[i] + is_current_completion = (i == index) + result = self._get_menu_item_tokens(c, is_current_completion, menu_width) + + if show_meta: + result += self._get_menu_item_meta_tokens(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), + default_char=Char(' ', self.token)) + + return UIContent() + + def _show_meta(self, complete_state): + """ + Return ``True`` if we need to show a column with meta information. + """ + return any(c.display_meta for c in complete_state.current_completions) + + def _get_menu_width(self, max_width, complete_state): + """ + Return the width of the main column. + """ + return min(max_width, max(self.MIN_WIDTH, max(get_cwidth(c.display) + for c in complete_state.current_completions) + 2)) + + def _get_menu_meta_width(self, max_width, complete_state): + """ + Return the width of the meta column. + """ + if self._show_meta(complete_state): + return min(max_width, max(get_cwidth(c.display_meta) + for c in complete_state.current_completions) + 2) + else: + return 0 + + def _get_menu_item_tokens(self, completion, is_current_completion, width): + if is_current_completion: + token = self.token.Completion.Current + else: + token = self.token.Completion + + text, tw = _trim_text(completion.display, width - 2) + padding = ' ' * (width - 2 - tw) + return [(token, ' %s%s ' % (text, padding))] + + def _get_menu_item_meta_tokens(self, completion, is_current_completion, width): + if is_current_completion: + token = self.token.Meta.Current + else: + token = self.token.Meta + + text, tw = _trim_text(completion.display_meta, width - 2) + padding = ' ' * (width - 2 - tw) + return [(token, ' %s%s ' % (text, padding))] + + def mouse_handler(self, cli, mouse_event): + """ + Handle mouse events: clicking and scrolling. + """ + b = cli.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) + + +def _trim_text(text, max_width): + """ + Trim the text to `max_width`, append dots when the text is too long. + Returns (text, width) tuple. + """ + width = get_cwidth(text) + + # When the text is too wide, trim it. + if width > max_width: + # When there are no double width characters, just use slice operation. + if len(text) == width: + trimmed_text = (text[:max(1, max_width-3)] + '...')[:max_width] + return trimmed_text, len(trimmed_text) + + # Otherwise, loop until we have the desired width. (Rather + # inefficient, but ok for now.) + else: + trimmed_text = '' + for c in text: + if get_cwidth(trimmed_text + c) <= max_width - 3: + trimmed_text += c + trimmed_text += '...' + + return (trimmed_text, get_cwidth(trimmed_text)) + else: + return text, width + + +class CompletionsMenu(ConditionalContainer): + def __init__(self, max_height=None, scroll_offset=0, extra_filter=True, display_arrows=False): + extra_filter = to_cli_filter(extra_filter) + display_arrows = to_cli_filter(display_arrows) + + super(CompletionsMenu, self).__init__( + content=Window( + content=CompletionsMenuControl(), + width=LayoutDimension(min=8), + height=LayoutDimension(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, + ), + # Show when there are completions but not at the point we are + # returning the input. + filter=HasCompletions() & ~IsDone() & extra_filter) + + +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 langer than one, in 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=3, suggested_max_column_width=30): + assert isinstance(min_rows, int) and min_rows >= 1 + + self.min_rows = min_rows + self.suggested_max_column_width = suggested_max_column_width + self.token = Token.Menu.Completions + self.scroll = 0 + + # Info of last rendering. + self._rendered_rows = 0 + self._rendered_columns = 0 + self._total_columns = 0 + self._render_pos_to_completion = {} + self._render_left_arrow = False + self._render_right_arrow = False + self._render_width = 0 + + def reset(self): + self.scroll = 0 + + def has_focus(self, cli): + return False + + def preferred_width(self, cli, max_available_width): + """ + Preferred width: prefer to use at least min_rows, but otherwise as much + as possible horizontally. + """ + complete_state = cli.current_buffer.complete_state + column_width = self._get_column_width(complete_state) + result = int(column_width * math.ceil(len(complete_state.current_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, cli, width, max_available_height, wrap_lines): + """ + Preferred height: as much as needed in order to display all the completions. + """ + complete_state = cli.current_buffer.complete_state + 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.current_completions) / float(column_count))) + + def create_content(self, cli, width, height): + """ + Create a UIContent object for this menu. + """ + complete_state = cli.current_buffer.complete_state + column_width = self._get_column_width(complete_state) + self._render_pos_to_completion = {} + + def grouper(n, iterable, fillvalue=None): + " grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx " + args = [iter(iterable)] * n + return zip_longest(fillvalue=fillvalue, *args) + + def is_current_completion(completion): + " Returns True when this completion is the currently selected one. " + return 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 + + if complete_state: + # 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.current_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. + tokens_for_line = [] + + for row_index, row in enumerate(rows_): + tokens = [] + middle_row = row_index == len(rows_) // 2 + + # Draw left arrow if we have hidden completions on the left. + if render_left_arrow: + tokens += [(Token.Scrollbar, '<' if middle_row else ' ')] + + # Draw row content. + for column_index, c in enumerate(row[self.scroll:][:visible_columns]): + if c is not None: + tokens += self._get_menu_item_tokens(c, is_current_completion(c), column_width) + + # 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: + tokens += [(self.token.Completion, ' ' * column_width)] + + # Draw trailing padding. (_get_menu_item_tokens only returns padding on the left.) + tokens += [(self.token.Completion, ' ')] + + # Draw right arrow if we have hidden completions on the right. + if render_right_arrow: + tokens += [(Token.Scrollbar, '>' if middle_row else ' ')] + + # Newline. + tokens_for_line.append(tokens) + + else: + tokens = [] + + 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): + return tokens_for_line[i] + + return UIContent(get_line=get_line, line_count=len(rows_)) + + def _get_column_width(self, complete_state): + """ + Return the width of each column. + """ + return max(get_cwidth(c.display) for c in complete_state.current_completions) + 1 + + def _get_menu_item_tokens(self, completion, is_current_completion, width): + if is_current_completion: + token = self.token.Completion.Current + else: + token = self.token.Completion + + text, tw = _trim_text(completion.display, width) + padding = ' ' * (width - tw - 1) + + return [(token, ' %s%s' % (text, padding))] + + def mouse_handler(self, cli, mouse_event): + """ + Handle scoll and click events. + """ + b = cli.current_buffer + + def scroll_left(): + b.complete_previous(count=self._rendered_rows, disable_wrap_around=True) + self.scroll = max(0, self.scroll - 1) + + def scroll_right(): + 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) + + +class MultiColumnCompletionsMenu(HSplit): + """ + Container that displays the completions in several columns. + When `show_meta` (a :class:`~libs.prompt_toolkit.filters.CLIFilter`) evaluates + to True, it shows the meta information at the bottom. + """ + def __init__(self, min_rows=3, suggested_max_column_width=30, show_meta=True, extra_filter=True): + show_meta = to_cli_filter(show_meta) + extra_filter = to_cli_filter(extra_filter) + + # Display filter: show when there are completions but not at the point + # we are returning the input. + full_filter = HasCompletions() & ~IsDone() & extra_filter + + any_completion_has_meta = Condition(lambda cli: + any(c.display_meta for c in cli.current_buffer.complete_state.current_completions)) + + # Create child windows. + completions_window = ConditionalContainer( + content=Window( + content=MultiColumnCompletionMenuControl( + min_rows=min_rows, suggested_max_column_width=suggested_max_column_width), + width=LayoutDimension(min=8), + height=LayoutDimension(min=1)), + filter=full_filter) + + meta_window = ConditionalContainer( + content=Window(content=_SelectedCompletionMetaControl()), + filter=show_meta & full_filter & any_completion_has_meta) + + # Initialise split. + super(MultiColumnCompletionsMenu, self).__init__([ + completions_window, + meta_window + ]) + + +class _SelectedCompletionMetaControl(UIControl): + """ + Control that shows the meta information of the selected token. + """ + def preferred_width(self, cli, max_available_width): + """ + 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.) + """ + if cli.current_buffer.complete_state: + state = cli.current_buffer.complete_state + return 2 + max(get_cwidth(c.display_meta) for c in state.current_completions) + else: + return 0 + + def preferred_height(self, cli, width, max_available_height, wrap_lines): + return 1 + + def create_content(self, cli, width, height): + tokens = self._get_tokens(cli) + + def get_line(i): + return tokens + + return UIContent(get_line=get_line, line_count=1 if tokens else 0) + + def _get_tokens(self, cli): + token = Token.Menu.Completions.MultiColumnMeta + state = cli.current_buffer.complete_state + + if state and state.current_completion and state.current_completion.display_meta: + return [(token, ' %s ' % state.current_completion.display_meta)] + + return [] diff --git a/src/libs/prompt_toolkit/layout/mouse_handlers.py b/src/libs/prompt_toolkit/layout/mouse_handlers.py new file mode 100644 index 0000000..d443bf8 --- /dev/null +++ b/src/libs/prompt_toolkit/layout/mouse_handlers.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + +from itertools import product +from collections import defaultdict + +__all__ = ( + 'MouseHandlers', +) + + +class MouseHandlers(object): + """ + Two dimentional raster of callbacks for mouse events. + """ + def __init__(self): + def dummy_callback(cli, mouse_event): + """ + :param mouse_event: `MouseEvent` instance. + """ + + # Map (x,y) tuples to handlers. + self.mouse_handlers = defaultdict(lambda: dummy_callback) + + def set_mouse_handler_for_range(self, x_min, x_max, y_min, y_max, handler=None): + """ + Set mouse handler for a region. + """ + for x, y in product(range(x_min, x_max), range(y_min, y_max)): + self.mouse_handlers[x,y] = handler diff --git a/src/libs/prompt_toolkit/layout/processors.py b/src/libs/prompt_toolkit/layout/processors.py new file mode 100644 index 0000000..3eff14c --- /dev/null +++ b/src/libs/prompt_toolkit/layout/processors.py @@ -0,0 +1,605 @@ +""" +Processors are little transformation blocks that transform the token list from +a buffer before the BufferControl will render it to the screen. + +They can insert tokens before or after, or highlight fragments by replacing the +token types. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass +from six.moves import range + +from libs.prompt_toolkit.cache import SimpleCache +from libs.prompt_toolkit.document import Document +from libs.prompt_toolkit.enums import SEARCH_BUFFER +from libs.prompt_toolkit.filters import to_cli_filter, ViInsertMultipleMode +from libs.prompt_toolkit.layout.utils import token_list_to_text +from libs.prompt_toolkit.reactive import Integer +from libs.prompt_toolkit.token import Token + +from .utils import token_list_len, explode_tokens + +import re + +__all__ = ( + 'Processor', + 'Transformation', + + 'HighlightSearchProcessor', + 'HighlightSelectionProcessor', + 'PasswordProcessor', + 'HighlightMatchingBracketProcessor', + 'DisplayMultipleCursors', + 'BeforeInput', + 'AfterInput', + 'AppendAutoSuggestion', + 'ConditionalProcessor', + 'ShowLeadingWhiteSpaceProcessor', + 'ShowTrailingWhiteSpaceProcessor', + 'TabsProcessor', +) + + +class Processor(with_metaclass(ABCMeta, object)): + """ + Manipulate the tokens for a given line in a + :class:`~libs.prompt_toolkit.layout.controls.BufferControl`. + """ + @abstractmethod + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + """ + Apply transformation. Returns a :class:`.Transformation` instance. + + :param cli: :class:`.CommandLineInterface` 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 + `tokens` for any position in the source string. (This takes + previous processors into account.) + :param tokens: List of tokens that we can transform. (Received from the + previous processor.) + """ + return Transformation(tokens) + + def has_focus(self, cli): + """ + Processors can override the focus. + (Used for the reverse-i-search prefix in DefaultPrompt.) + """ + return False + + +class Transformation(object): + """ + 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 `tokens`! + + :param tokens: The transformed tokens. 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, tokens, source_to_display=None, display_to_source=None): + self.tokens = tokens + self.source_to_display = source_to_display or (lambda i: i) + self.display_to_source = display_to_source or (lambda i: i) + + +class HighlightSearchProcessor(Processor): + """ + Processor that highlights search matches in the document. + Note that this doesn't support multiline search matches yet. + + :param preview_search: A Filter; when active it indicates that we take + the search text in real time while the user is typing, instead of the + last active search state. + """ + def __init__(self, preview_search=False, search_buffer_name=SEARCH_BUFFER, + get_search_state=None): + self.preview_search = to_cli_filter(preview_search) + self.search_buffer_name = search_buffer_name + self.get_search_state = get_search_state or (lambda cli: cli.search_state) + + def _get_search_text(self, cli): + """ + The text we are searching for. + """ + # When the search buffer has focus, take that text. + if self.preview_search(cli) and cli.buffers[self.search_buffer_name].text: + return cli.buffers[self.search_buffer_name].text + # Otherwise, take the text of the last active search. + else: + return self.get_search_state(cli).text + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + search_text = self._get_search_text(cli) + searchmatch_current_token = (':', ) + Token.SearchMatch.Current + searchmatch_token = (':', ) + Token.SearchMatch + + if search_text and not cli.is_returning: + # For each search match, replace the Token. + line_text = token_list_to_text(tokens) + tokens = explode_tokens(tokens) + + flags = re.IGNORECASE if cli.is_ignoring_case else 0 + + # Get cursor column. + 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_token, text = tokens[i] + if on_cursor: + tokens[i] = (old_token + searchmatch_current_token, tokens[i][1]) + else: + tokens[i] = (old_token + searchmatch_token, tokens[i][1]) + + return Transformation(tokens) + + +class HighlightSelectionProcessor(Processor): + """ + Processor that highlights the selection in the document. + """ + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + selected_token = (':', ) + Token.SelectedText + + # 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) + + tokens = explode_tokens(tokens) + + if from_ == 0 and to == 0 and len(tokens) == 0: + # When this is an empty line, insert a space in order to + # visualiase the selection. + return Transformation([(Token.SelectedText, ' ')]) + else: + for i in range(from_, to + 1): + if i < len(tokens): + old_token, old_text = tokens[i] + tokens[i] = (old_token + selected_token, old_text) + + return Transformation(tokens) + + +class PasswordProcessor(Processor): + """ + Processor that turns masks the input. (For passwords.) + + :param char: (string) Character to be used. "*" by default. + """ + def __init__(self, char='*'): + self.char = char + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + tokens = [(token, self.char * len(text)) for token, text in tokens] + return Transformation(tokens) + + +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='[](){}<>', max_cursor_distance=1000): + self.chars = chars + self.max_cursor_distance = max_cursor_distance + + self._positions_cache = SimpleCache(maxsize=8) + + def _get_positions_to_highlight(self, document): + """ + Return a list of (row, col) tuples that need to be highlighted. + """ + # 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, cli, document, lineno, source_to_display, tokens): + # Get the highlight positions. + key = (cli.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) + tokens = explode_tokens(tokens) + token, text = tokens[col] + + if col == document.cursor_position_col: + token += (':', ) + Token.MatchingBracket.Cursor + else: + token += (':', ) + Token.MatchingBracket.Other + + tokens[col] = (token, text) + + return Transformation(tokens) + + +class DisplayMultipleCursors(Processor): + """ + When we're in Vi block insert mode, display all the cursors. + """ + _insert_multiple = ViInsertMultipleMode() + + def __init__(self, buffer_name): + self.buffer_name = buffer_name + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + buff = cli.buffers[self.buffer_name] + + if self._insert_multiple(cli): + positions = buff.multiple_cursor_positions + tokens = explode_tokens(tokens) + + # 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]) + + token_suffix = (':', ) + Token.MultipleCursors.Cursor + + for p in positions: + if start_pos <= p < end_pos: + column = source_to_display(p - start_pos) + + # Replace token. + token, text = tokens[column] + token += token_suffix + tokens[column] = (token, text) + elif p == end_pos: + tokens.append((token_suffix, ' ')) + + return Transformation(tokens) + else: + return Transformation(tokens) + + +class BeforeInput(Processor): + """ + Insert tokens before the input. + + :param get_tokens: Callable that takes a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` and returns the + list of tokens to be inserted. + """ + def __init__(self, get_tokens): + assert callable(get_tokens) + self.get_tokens = get_tokens + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + if lineno == 0: + tokens_before = self.get_tokens(cli) + tokens = tokens_before + tokens + + shift_position = token_list_len(tokens_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 + + return Transformation(tokens, source_to_display=source_to_display, + display_to_source=display_to_source) + + @classmethod + def static(cls, text, token=Token): + """ + Create a :class:`.BeforeInput` instance that always inserts the same + text. + """ + def get_static_tokens(cli): + return [(token, text)] + return cls(get_static_tokens) + + def __repr__(self): + return '%s(get_tokens=%r)' % ( + self.__class__.__name__, self.get_tokens) + + +class AfterInput(Processor): + """ + Insert tokens after the input. + + :param get_tokens: Callable that takes a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` and returns the + list of tokens to be appended. + """ + def __init__(self, get_tokens): + assert callable(get_tokens) + self.get_tokens = get_tokens + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + # Insert tokens after the last line. + if lineno == document.line_count - 1: + return Transformation(tokens=tokens + self.get_tokens(cli)) + else: + return Transformation(tokens=tokens) + + @classmethod + def static(cls, text, token=Token): + """ + Create a :class:`.AfterInput` instance that always inserts the same + text. + """ + def get_static_tokens(cli): + return [(token, text)] + return cls(get_static_tokens) + + def __repr__(self): + return '%s(get_tokens=%r)' % ( + self.__class__.__name__, self.get_tokens) + + +class AppendAutoSuggestion(Processor): + """ + Append the auto suggestion to the input. + (The user can then press the right arrow the insert the suggestion.) + + :param buffer_name: The name of the buffer from where we should take the + auto suggestion. If not given, we take the current buffer. + """ + def __init__(self, buffer_name=None, token=Token.AutoSuggestion): + self.buffer_name = buffer_name + self.token = token + + def _get_buffer(self, cli): + if self.buffer_name: + return cli.buffers[self.buffer_name] + else: + return cli.current_buffer + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + # Insert tokens after the last line. + if lineno == document.line_count - 1: + buffer = self._get_buffer(cli) + + if buffer.suggestion and buffer.document.is_cursor_at_the_end: + suggestion = buffer.suggestion.text + else: + suggestion = '' + + return Transformation(tokens=tokens + [(self.token, suggestion)]) + else: + return Transformation(tokens=tokens) + + +class ShowLeadingWhiteSpaceProcessor(Processor): + """ + Make leading whitespace visible. + + :param get_char: Callable that takes a :class:`CommandLineInterface` + instance and returns one character. + :param token: Token to be used. + """ + def __init__(self, get_char=None, token=Token.LeadingWhiteSpace): + assert get_char is None or callable(get_char) + + if get_char is None: + def get_char(cli): + if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?': + return '.' + else: + return '\xb7' + + self.token = token + self.get_char = get_char + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + # Walk through all te tokens. + if tokens and token_list_to_text(tokens).startswith(' '): + t = (self.token, self.get_char(cli)) + tokens = explode_tokens(tokens) + + for i in range(len(tokens)): + if tokens[i][1] == ' ': + tokens[i] = t + else: + break + + return Transformation(tokens) + + +class ShowTrailingWhiteSpaceProcessor(Processor): + """ + Make trailing whitespace visible. + + :param get_char: Callable that takes a :class:`CommandLineInterface` + instance and returns one character. + :param token: Token to be used. + """ + def __init__(self, get_char=None, token=Token.TrailingWhiteSpace): + assert get_char is None or callable(get_char) + + if get_char is None: + def get_char(cli): + if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?': + return '.' + else: + return '\xb7' + + self.token = token + self.get_char = get_char + + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + if tokens and tokens[-1][1].endswith(' '): + t = (self.token, self.get_char(cli)) + tokens = explode_tokens(tokens) + + # Walk backwards through all te tokens and replace whitespace. + for i in range(len(tokens) - 1, -1, -1): + char = tokens[i][1] + if char == ' ': + tokens[i] = t + else: + break + + return Transformation(tokens) + + +class TabsProcessor(Processor): + """ + Render tabs as spaces (instead of ^I) or make them visible (for instance, + by replacing them with dots.) + + :param tabstop: (Integer) Horizontal space taken by a tab. + :param get_char1: Callable that takes a `CommandLineInterface` and return a + character (text of length one). This one is used for the first space + taken by the tab. + :param get_char2: Like `get_char1`, but for the rest of the space. + """ + def __init__(self, tabstop=4, get_char1=None, get_char2=None, token=Token.Tab): + assert isinstance(tabstop, Integer) + assert get_char1 is None or callable(get_char1) + assert get_char2 is None or callable(get_char2) + + self.get_char1 = get_char1 or get_char2 or (lambda cli: '|') + self.get_char2 = get_char2 or get_char1 or (lambda cli: '\u2508') + self.tabstop = tabstop + self.token = token + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + tabstop = int(self.tabstop) + token = self.token + + # Create separator for tabs. + separator1 = self.get_char1(cli) + separator2 = self.get_char2(cli) + + # Transform tokens. + tokens = explode_tokens(tokens) + + position_mappings = {} + result_tokens = [] + pos = 0 + + for i, token_and_text in enumerate(tokens): + position_mappings[i] = pos + + if token_and_text[1] == '\t': + # Calculate how many characters we have to insert. + count = tabstop - (pos % tabstop) + if count == 0: + count = tabstop + + # Insert tab. + result_tokens.append((token, separator1)) + result_tokens.append((token, separator2 * (count - 1))) + pos += count + else: + result_tokens.append(token_and_text) + pos += 1 + + position_mappings[len(tokens)] = pos + + def source_to_display(from_position): + " Maps original cursor position to the new one. " + return position_mappings[from_position] + + def display_to_source(display_pos): + " Maps display cursor position to the original one. " + position_mappings_reversed = dict((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_tokens, + 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(cli): + return true_or_false + + # Wrapt 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:`~libs.prompt_toolkit.filters.CLIFilter` instance. + """ + def __init__(self, processor, filter): + assert isinstance(processor, Processor) + + self.processor = processor + self.filter = to_cli_filter(filter) + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + # Run processor when enabled. + if self.filter(cli): + return self.processor.apply_transformation( + cli, document, lineno, source_to_display, tokens) + else: + return Transformation(tokens) + + def has_focus(self, cli): + if self.filter(cli): + return self.processor.has_focus(cli) + else: + return False + + def __repr__(self): + return '%s(processor=%r, filter=%r)' % ( + self.__class__.__name__, self.processor, self.filter) diff --git a/src/libs/prompt_toolkit/layout/prompt.py b/src/libs/prompt_toolkit/layout/prompt.py new file mode 100644 index 0000000..345b3c6 --- /dev/null +++ b/src/libs/prompt_toolkit/layout/prompt.py @@ -0,0 +1,111 @@ +from __future__ import unicode_literals + +from six import text_type + +from libs.prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER +from libs.prompt_toolkit.token import Token + +from .utils import token_list_len +from .processors import Processor, Transformation + +__all__ = ( + 'DefaultPrompt', +) + + +class DefaultPrompt(Processor): + """ + Default prompt. This one shows the 'arg' and reverse search like + Bash/readline normally do. + + There are two ways to instantiate a ``DefaultPrompt``. For a prompt + with a static message, do for instance:: + + prompt = DefaultPrompt.from_message('prompt> ') + + For a dynamic prompt, generated from a token list function:: + + def get_tokens(cli): + return [(Token.A, 'text'), (Token.B, 'text2')] + + prompt = DefaultPrompt(get_tokens) + """ + def __init__(self, get_tokens): + assert callable(get_tokens) + self.get_tokens = get_tokens + + @classmethod + def from_message(cls, message='> '): + """ + Create a default prompt with a static message text. + """ + assert isinstance(message, text_type) + + def get_message_tokens(cli): + return [(Token.Prompt, message)] + return cls(get_message_tokens) + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + # Get text before cursor. + if cli.is_searching: + before = _get_isearch_tokens(cli) + + elif cli.input_processor.arg is not None: + before = _get_arg_tokens(cli) + + else: + before = self.get_tokens(cli) + + # Insert before buffer text. + shift_position = token_list_len(before) + + # Only show the prompt before the first line. For the following lines, + # only indent using spaces. + if lineno != 0: + before = [(Token.Prompt, ' ' * shift_position)] + + return Transformation( + tokens=before + tokens, + source_to_display=lambda i: i + shift_position, + display_to_source=lambda i: i - shift_position) + + def has_focus(self, cli): + # Obtain focus when the CLI is searching. + + # Usually, when using this `DefaultPrompt`, we don't have a + # `BufferControl` instance that displays the content of the search + # buffer. Instead the search text is displayed before the current text. + # So, we can still show the cursor here, while it's actually not this + # buffer that's focussed. + return cli.is_searching + + +def _get_isearch_tokens(cli): + def before(): + if cli.search_state.direction == IncrementalSearchDirection.BACKWARD: + text = 'reverse-i-search' + else: + text = 'i-search' + + return [(Token.Prompt.Search, '(%s)`' % text)] + + def text(): + return [(Token.Prompt.Search.Text, cli.buffers[SEARCH_BUFFER].text)] + + def after(): + return [(Token.Prompt.Search, '`: ')] + + return before() + text() + after() + + +def _get_arg_tokens(cli): + """ + Tokens for the arg-prompt. + """ + arg = cli.input_processor.arg + + return [ + (Token.Prompt.Arg, '(arg: '), + (Token.Prompt.Arg.Text, str(arg)), + (Token.Prompt.Arg, ') '), + ] diff --git a/src/libs/prompt_toolkit/layout/screen.py b/src/libs/prompt_toolkit/layout/screen.py new file mode 100644 index 0000000..099c085 --- /dev/null +++ b/src/libs/prompt_toolkit/layout/screen.py @@ -0,0 +1,151 @@ +from __future__ import unicode_literals + +from libs.prompt_toolkit.cache import FastDictCache +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.utils import get_cwidth + +from collections import defaultdict, namedtuple + +__all__ = ( + 'Point', + 'Size', + 'Screen', + 'Char', +) + + +Point = namedtuple('Point', 'y x') +Size = namedtuple('Size', 'rows columns') + + +class Char(object): + """ + Represent a single character in a :class:`.Screen`. + + This should be considered immutable. + """ + __slots__ = ('char', 'token', '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 = { + '\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': '^]', + '\x1f': '^_', + '\x7f': '^?', # Backspace + } + + def __init__(self, char=' ', token=Token): + # If this character has to be displayed otherwise, take that one. + char = self.display_mappings.get(char, char) + + self.char = char + self.token = token + + # Calculate width. (We always need this, so better to store it directly + # as a member for performance.) + self.width = get_cwidth(char) + + def __eq__(self, other): + return self.char == other.char and self.token == other.token + + def __ne__(self, other): + # 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.token != other.token + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.char, self.token) + + +_CHAR_CACHE = FastDictCache(Char, size=1000 * 1000) +Transparent = Token.Transparent + + +class Screen(object): + """ + Two dimentional buffer of :class:`.Char` instances. + """ + def __init__(self, default_char=None, initial_width=0, initial_height=0): + if default_char is None: + default_char = _CHAR_CACHE[' ', Transparent] + + self.data_buffer = defaultdict(lambda: defaultdict(lambda: default_char)) + + #: Escape sequences to be injected. + self.zero_width_escapes = defaultdict(lambda: defaultdict(lambda: '')) + + #: Position of the cursor. + self.cursor_position = Point(y=0, x=0) + + #: 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_position = None + + #: 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 + + def replace_all_tokens(self, token): + """ + For all the characters in the screen. Set the token to the given `token`. + """ + b = self.data_buffer + + for y, row in b.items(): + for x, char in row.items(): + b[y][x] = _CHAR_CACHE[char.char, token] + + +class WritePosition(object): + def __init__(self, xpos, ypos, width, height, extended_height=None): + assert height >= 0 + assert extended_height is None or extended_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 + self.extended_height = extended_height or height + + def __repr__(self): + return '%s(%r, %r, %r, %r, %r)' % ( + self.__class__.__name__, + self.xpos, self.ypos, self.width, self.height, self.extended_height) diff --git a/src/libs/prompt_toolkit/layout/toolbars.py b/src/libs/prompt_toolkit/layout/toolbars.py new file mode 100644 index 0000000..321c64f --- /dev/null +++ b/src/libs/prompt_toolkit/layout/toolbars.py @@ -0,0 +1,209 @@ +from __future__ import unicode_literals + +from ..enums import IncrementalSearchDirection + +from .processors import BeforeInput + +from .lexers import SimpleLexer +from .dimension import LayoutDimension +from .controls import BufferControl, TokenListControl, UIControl, UIContent +from .containers import Window, ConditionalContainer +from .screen import Char +from .utils import token_list_len +from libs.prompt_toolkit.enums import SEARCH_BUFFER, SYSTEM_BUFFER +from libs.prompt_toolkit.filters import HasFocus, HasArg, HasCompletions, HasValidationError, HasSearch, Always, IsDone +from libs.prompt_toolkit.token import Token + +__all__ = ( + 'TokenListToolbar', + 'ArgToolbar', + 'CompletionsToolbar', + 'SearchToolbar', + 'SystemToolbar', + 'ValidationToolbar', +) + + +class TokenListToolbar(ConditionalContainer): + def __init__(self, get_tokens, filter=Always(), **kw): + super(TokenListToolbar, self).__init__( + content=Window( + TokenListControl(get_tokens, **kw), + height=LayoutDimension.exact(1)), + filter=filter) + + +class SystemToolbarControl(BufferControl): + def __init__(self): + token = Token.Toolbar.System + + super(SystemToolbarControl, self).__init__( + buffer_name=SYSTEM_BUFFER, + default_char=Char(token=token), + lexer=SimpleLexer(token=token.Text), + input_processors=[BeforeInput.static('Shell command: ', token)],) + + +class SystemToolbar(ConditionalContainer): + def __init__(self): + super(SystemToolbar, self).__init__( + content=Window( + SystemToolbarControl(), + height=LayoutDimension.exact(1)), + filter=HasFocus(SYSTEM_BUFFER) & ~IsDone()) + + +class ArgToolbarControl(TokenListControl): + def __init__(self): + def get_tokens(cli): + arg = cli.input_processor.arg + if arg == '-': + arg = '-1' + + return [ + (Token.Toolbar.Arg, 'Repeat: '), + (Token.Toolbar.Arg.Text, arg), + ] + + super(ArgToolbarControl, self).__init__(get_tokens) + + +class ArgToolbar(ConditionalContainer): + def __init__(self): + super(ArgToolbar, self).__init__( + content=Window( + ArgToolbarControl(), + height=LayoutDimension.exact(1)), + filter=HasArg()) + + +class SearchToolbarControl(BufferControl): + """ + :param vi_mode: Display '/' and '?' instead of I-search. + """ + def __init__(self, vi_mode=False): + token = Token.Toolbar.Search + + def get_before_input(cli): + if not cli.is_searching: + text = '' + elif cli.search_state.direction == IncrementalSearchDirection.BACKWARD: + text = ('?' if vi_mode else 'I-search backward: ') + else: + text = ('/' if vi_mode else 'I-search: ') + + return [(token, text)] + + super(SearchToolbarControl, self).__init__( + buffer_name=SEARCH_BUFFER, + input_processors=[BeforeInput(get_before_input)], + default_char=Char(token=token), + lexer=SimpleLexer(token=token.Text)) + + +class SearchToolbar(ConditionalContainer): + def __init__(self, vi_mode=False): + super(SearchToolbar, self).__init__( + content=Window( + SearchToolbarControl(vi_mode=vi_mode), + height=LayoutDimension.exact(1)), + filter=HasSearch() & ~IsDone()) + + +class CompletionsToolbarControl(UIControl): + token = Token.Toolbar.Completions + + def create_content(self, cli, width, height): + complete_state = cli.current_buffer.complete_state + if complete_state: + completions = complete_state.current_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. + tokens = [] + + for i, c in enumerate(completions): + # When there is no more place for the next completion + if token_list_len(tokens) + len(c.display) >= content_width: + # If the current one was not yet displayed, page to the next sequence. + if i <= (index or 0): + tokens = [] + cut_left = True + # If the current one is visible, stop here. + else: + cut_right = True + break + + tokens.append((self.token.Completion.Current if i == index else self.token.Completion, c.display)) + tokens.append((self.token, ' ')) + + # Extend/strip until the content width. + tokens.append((self.token, ' ' * (content_width - token_list_len(tokens)))) + tokens = tokens[:content_width] + + # Return tokens + all_tokens = [ + (self.token, ' '), + (self.token.Arrow, '<' if cut_left else ' '), + (self.token, ' '), + ] + tokens + [ + (self.token, ' '), + (self.token.Arrow, '>' if cut_right else ' '), + (self.token, ' '), + ] + else: + all_tokens = [] + + def get_line(i): + return all_tokens + + return UIContent(get_line=get_line, line_count=1) + + +class CompletionsToolbar(ConditionalContainer): + def __init__(self, extra_filter=Always()): + super(CompletionsToolbar, self).__init__( + content=Window( + CompletionsToolbarControl(), + height=LayoutDimension.exact(1)), + filter=HasCompletions() & ~IsDone() & extra_filter) + + +class ValidationToolbarControl(TokenListControl): + def __init__(self, show_position=False): + token = Token.Toolbar.Validation + + def get_tokens(cli): + buffer = cli.current_buffer + + if buffer.validation_error: + row, column = buffer.document.translate_index_to_position( + buffer.validation_error.cursor_position) + + if show_position: + text = '%s (line=%s column=%s)' % ( + buffer.validation_error.message, row + 1, column + 1) + else: + text = buffer.validation_error.message + + return [(token, text)] + else: + return [] + + super(ValidationToolbarControl, self).__init__(get_tokens) + + +class ValidationToolbar(ConditionalContainer): + def __init__(self, show_position=False): + super(ValidationToolbar, self).__init__( + content=Window( + ValidationToolbarControl(show_position=show_position), + height=LayoutDimension.exact(1)), + filter=HasValidationError() & ~IsDone()) diff --git a/src/libs/prompt_toolkit/layout/utils.py b/src/libs/prompt_toolkit/layout/utils.py new file mode 100644 index 0000000..6e9f01a --- /dev/null +++ b/src/libs/prompt_toolkit/layout/utils.py @@ -0,0 +1,181 @@ +from __future__ import unicode_literals + +from libs.prompt_toolkit.utils import get_cwidth +from libs.prompt_toolkit.token import Token + +__all__ = ( + 'token_list_len', + 'token_list_width', + 'token_list_to_text', + 'explode_tokens', + 'split_lines', + 'find_window_for_buffer_name', +) + + +def token_list_len(tokenlist): + """ + Return the amount of characters in this token list. + + :param tokenlist: List of (token, text) or (token, text, mouse_handler) + tuples. + """ + ZeroWidthEscape = Token.ZeroWidthEscape + return sum(len(item[1]) for item in tokenlist if item[0] != ZeroWidthEscape) + + +def token_list_width(tokenlist): + """ + Return the character width of this token list. + (Take double width characters into account.) + + :param tokenlist: List of (token, text) or (token, text, mouse_handler) + tuples. + """ + ZeroWidthEscape = Token.ZeroWidthEscape + return sum(get_cwidth(c) for item in tokenlist for c in item[1] if item[0] != ZeroWidthEscape) + + +def token_list_to_text(tokenlist): + """ + Concatenate all the text parts again. + """ + ZeroWidthEscape = Token.ZeroWidthEscape + return ''.join(item[1] for item in tokenlist if item[0] != ZeroWidthEscape) + + +def iter_token_lines(tokenlist): + """ + Iterator that yields tokenlists for each line. + """ + line = [] + for token, c in explode_tokens(tokenlist): + line.append((token, c)) + + if c == '\n': + yield line + line = [] + + yield line + + +def split_lines(tokenlist): + """ + Take a single list of (Token, text) tuples and yield one such list for each + line. Just like str.split, this will yield at least one item. + + :param tokenlist: List of (token, text) or (token, text, mouse_handler) + tuples. + """ + line = [] + + for item in tokenlist: + # For (token, text) tuples. + if len(item) == 2: + token, string = item + parts = string.split('\n') + + for part in parts[:-1]: + if part: + line.append((token, part)) + yield line + line = [] + + line.append((token, parts[-1])) + # Note that parts[-1] can be empty, and that's fine. It happens + # in the case of [(Token.SetCursorPosition, '')]. + + # For (token, text, mouse_handler) tuples. + # I know, partly copy/paste, but understandable and more efficient + # than many tests. + else: + token, string, mouse_handler = item + parts = string.split('\n') + + for part in parts[:-1]: + if part: + line.append((token, part, mouse_handler)) + yield line + line = [] + + line.append((token, parts[-1], mouse_handler)) + + # Always yield the last line, even when this is an empty line. This ensures + # that when `tokenlist` ends with a newline character, an additional empty + # line is yielded. (Otherwise, there's no way to differentiate between the + # cases where `tokenlist` does and doesn't end with a newline.) + yield line + + +class _ExplodedList(list): + """ + 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. + """ + def __init__(self, *a, **kw): + super(_ExplodedList, self).__init__(*a, **kw) + self.exploded = True + + def append(self, item): + self.extend([item]) + + def extend(self, lst): + super(_ExplodedList, self).extend(explode_tokens(lst)) + + def insert(self, index, item): + raise NotImplementedError # TODO + + # TODO: When creating a copy() or [:], return also an _ExplodedList. + + def __setitem__(self, index, value): + """ + Ensure that when `(Token, 'long string')` is set, the string will be + exploded. + """ + if not isinstance(index, slice): + index = slice(index, index + 1) + value = explode_tokens([value]) + super(_ExplodedList, self).__setitem__(index, value) + + +def explode_tokens(tokenlist): + """ + Turn a list of (token, 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 tokenlist: List of (token, text) tuples. + """ + # When the tokenlist is already exploded, don't explode again. + if getattr(tokenlist, 'exploded', False): + return tokenlist + + result = [] + + for token, string in tokenlist: + for c in string: + result.append((token, c)) + + return _ExplodedList(result) + + +def find_window_for_buffer_name(cli, buffer_name): + """ + Look for a :class:`~libs.prompt_toolkit.layout.containers.Window` in the Layout + that contains the :class:`~libs.prompt_toolkit.layout.controls.BufferControl` + for the given buffer and return it. If no such Window is found, return None. + """ + from libs.prompt_toolkit.interface import CommandLineInterface + assert isinstance(cli, CommandLineInterface) + + from .containers import Window + from .controls import BufferControl + + for l in cli.layout.walk(cli): + if isinstance(l, Window) and isinstance(l.content, BufferControl): + if l.content.buffer_name == buffer_name: + return l diff --git a/src/libs/prompt_toolkit/mouse_events.py b/src/libs/prompt_toolkit/mouse_events.py new file mode 100644 index 0000000..a0a16b9 --- /dev/null +++ b/src/libs/prompt_toolkit/mouse_events.py @@ -0,0 +1,48 @@ +""" +Mouse events. + + +How it works +------------ + +The renderer has a 2 dimensional grid of mouse event handlers. +(`libs.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 unicode_literals + +__all__ = ( + 'MouseEventType', + 'MouseEvent' +) + + +class MouseEventType: + MOUSE_UP = 'MOUSE_UP' + MOUSE_DOWN = 'MOUSE_DOWN' + SCROLL_UP = 'SCROLL_UP' + SCROLL_DOWN = 'SCROLL_DOWN' + + +MouseEventTypes = MouseEventType # Deprecated: plural for backwards compatibility. + + +class MouseEvent(object): + """ + Mouse event, sent to `UIControl.mouse_handler`. + + :param position: `Point` instance. + :param event_type: `MouseEventType`. + """ + def __init__(self, position, event_type): + self.position = position + self.event_type = event_type + + def __repr__(self): + return 'MouseEvent(%r, %r)' % (self.position, self.event_type) diff --git a/src/libs/prompt_toolkit/output.py b/src/libs/prompt_toolkit/output.py new file mode 100644 index 0000000..5442d14 --- /dev/null +++ b/src/libs/prompt_toolkit/output.py @@ -0,0 +1,192 @@ +""" +Interface for an output. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass +from libs.prompt_toolkit.layout.screen import Size + +__all__ = ( + 'Output', +) + + +class Output(with_metaclass(ABCMeta, object)): + """ + Base class defining the output interface for a + :class:`~libs.prompt_toolkit.renderer.Renderer`. + + Actual implementations are + :class:`~libs.prompt_toolkit.terminal.vt100_output.Vt100_Output` and + :class:`~libs.prompt_toolkit.terminal.win32_output.Win32Output`. + """ + @abstractmethod + def fileno(self): + " Return the file descriptor to which we can write for the output. " + + @abstractmethod + def encoding(self): + """ + 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): + " Write text (Terminal escape sequences will be removed/escaped.) " + + @abstractmethod + def write_raw(self, data): + " Write text. " + + @abstractmethod + def set_title(self, title): + " Set terminal title. " + + @abstractmethod + def clear_title(self): + " Clear title again. (or restore previous title.) " + + @abstractmethod + def flush(self): + " Write to output stream and flush. " + + @abstractmethod + def erase_screen(self): + """ + Erases the screen with the background colour and moves the cursor to + home. + """ + + @abstractmethod + def enter_alternate_screen(self): + " Go to the alternate screen buffer. (For full screen applications). " + + @abstractmethod + def quit_alternate_screen(self): + " Leave the alternate screen buffer. " + + @abstractmethod + def enable_mouse_support(self): + " Enable mouse. " + + @abstractmethod + def disable_mouse_support(self): + " Disable mouse. " + + @abstractmethod + def erase_end_of_line(self): + """ + Erases from the current cursor position to the end of the current line. + """ + + @abstractmethod + def erase_down(self): + """ + Erases the screen from the current line down to the bottom of the + screen. + """ + + @abstractmethod + def reset_attributes(self): + " Reset color and styling attributes. " + + @abstractmethod + def set_attributes(self, attrs): + " Set new color and styling attributes. " + + @abstractmethod + def disable_autowrap(self): + " Disable auto line wrapping. " + + @abstractmethod + def enable_autowrap(self): + " Enable auto line wrapping. " + + @abstractmethod + def cursor_goto(self, row=0, column=0): + " Move cursor position. " + + @abstractmethod + def cursor_up(self, amount): + " Move cursor `amount` place up. " + + @abstractmethod + def cursor_down(self, amount): + " Move cursor `amount` place down. " + + @abstractmethod + def cursor_forward(self, amount): + " Move cursor `amount` place forward. " + + @abstractmethod + def cursor_backward(self, amount): + " Move cursor `amount` place backward. " + + @abstractmethod + def hide_cursor(self): + " Hide cursor. " + + @abstractmethod + def show_cursor(self): + " Show cursor. " + + def ask_for_cpr(self): + """ + Asks for a cursor position report (CPR). + (VT100 only.) + """ + + def bell(self): + " Sound bell. " + + def enable_bracketed_paste(self): + " For vt100 only. " + + def disable_bracketed_paste(self): + " For vt100 only. " + + +class DummyOutput(Output): + """ + For testing. An output class that doesn't render anything. + """ + def fileno(self): + " There is no sensible default for fileno(). " + raise NotImplementedError + + def encoding(self): + return 'utf-8' + + def write(self, data): pass + def write_raw(self, data): pass + def set_title(self, title): pass + def clear_title(self): pass + def flush(self): pass + def erase_screen(self): pass + def enter_alternate_screen(self): pass + def quit_alternate_screen(self): pass + def enable_mouse_support(self): pass + def disable_mouse_support(self): pass + def erase_end_of_line(self): pass + def erase_down(self): pass + def reset_attributes(self): pass + def set_attributes(self, attrs): pass + def disable_autowrap(self): pass + def enable_autowrap(self): pass + def cursor_goto(self, row=0, column=0): pass + def cursor_up(self, amount): pass + def cursor_down(self, amount): pass + def cursor_forward(self, amount): pass + def cursor_backward(self, amount): pass + def hide_cursor(self): pass + def show_cursor(self): pass + def ask_for_cpr(self): pass + def bell(self): pass + def enable_bracketed_paste(self): pass + def disable_bracketed_paste(self): pass + + def get_size(self): + return Size(rows=40, columns=80) diff --git a/src/libs/prompt_toolkit/reactive.py b/src/libs/prompt_toolkit/reactive.py new file mode 100644 index 0000000..ec3aa06 --- /dev/null +++ b/src/libs/prompt_toolkit/reactive.py @@ -0,0 +1,56 @@ +""" +Prompt_toolkit is designed a way that the amount of changing state is reduced +to a minimum. Where possible, code is written in a pure functional way. In +general, this results in code where the flow is very easy to follow: the value +of a variable can be deducted from its first assignment. + +However, often, practicality and performance beat purity and some classes still +have a changing state. In order to not having to care too much about +transferring states between several components we use some reactive +programming. Actually some kind of data binding. + +We introduce two types: + +- Filter: for binding a boolean state. They can be chained using & and | + operators. Have a look in the ``filters`` module. Resolving the actual value + of a filter happens by calling it. + +- Integer: for binding integer values. Reactive operations (like addition and + substraction) are not suppported. Resolving the actual value happens by + casting it to int, like ``int(integer)``. This way, it is possible to use + normal integers as well for static values. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + + +class Integer(with_metaclass(ABCMeta, object)): + """ + Reactive integer -- anything that can be resolved to an ``int``. + """ + @abstractmethod + def __int__(self): + return 0 + + @classmethod + def from_callable(cls, func): + """ + Create an Integer-like object that calls the given function when it is + resolved to an int. + """ + return _IntegerFromCallable(func) + + +Integer.register(int) + + +class _IntegerFromCallable(Integer): + def __init__(self, func=0): + self.func = func + + def __repr__(self): + return 'Integer.from_callable(%r)' % self.func + + def __int__(self): + return int(self.func()) diff --git a/src/libs/prompt_toolkit/renderer.py b/src/libs/prompt_toolkit/renderer.py new file mode 100644 index 0000000..7fc295b --- /dev/null +++ b/src/libs/prompt_toolkit/renderer.py @@ -0,0 +1,526 @@ +""" +Renders the command line on the console. +(Redraws parts of the input line that were changed.) +""" +from __future__ import unicode_literals + +from libs.prompt_toolkit.filters import to_cli_filter +from libs.prompt_toolkit.layout.mouse_handlers import MouseHandlers +from libs.prompt_toolkit.layout.screen import Point, Screen, WritePosition +from libs.prompt_toolkit.output import Output +from libs.prompt_toolkit.styles import Style +from libs.prompt_toolkit.token import Token +from libs.prompt_toolkit.utils import is_windows + +from six.moves import range + +__all__ = ( + 'Renderer', + 'print_tokens', +) + + +def _output_screen_diff(output, screen, current_pos, previous_screen=None, last_token=None, + is_done=False, attrs_for_token=None, size=None, previous_width=0): # XXX: drop is_done + """ + 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_token: `Token` instance that represents the output attributes of + the last drawn character. (Color/attributes.) + :param attrs_for_token: :class:`._TokenToAttrsCache` instance. + :param width: The width of the terminal. + :param prevous_width: The width of the terminal during the last rendering. + """ + width, height = size.columns, size.rows + + #: Remember the last printed character. + last_token = [last_token] # nonlocal + + #: 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(): + " Wrapper around Output.reset_attributes. " + _output_reset_attributes() + last_token[0] = None # Forget last char after resetting attributes. + + def move_cursor(new): + " 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 meight 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): + """ + Write the output of this character. + """ + # If the last printed character has the same token, it also has the + # same style, so we don't output it. + the_last_token = last_token[0] + + if the_last_token and the_last_token == char.token: + write(char.char) + else: + _output_set_attributes(attrs_for_token[char.token]) + write(char.char) + last_token[0] = char.token + + # Disable autowrap + if not previous_screen: + output.disable_autowrap() + reset_attributes() + + # When the previous screen has a different size, redraw everything anyway. + # Also when we are done. (We meight 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(0, 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) + c = 0 # Column counter. + + 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, max(new_row.keys()) if new_row else 0) + previous_max_line_len = min(width - 1, max(previous_row.keys()) if previous_row else 0) + + # Loop over the columns. + c = 0 + while c < new_max_line_len + 1: + 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.token != old_char.token: + current_pos = move_cursor(Point(y=y, x=c)) + + # 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 = current_pos._replace(x=current_pos.x + char_width) + + 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(y=y, x=new_max_line_len+1)) + 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 behaviour 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(y=current_height - 1, x=0)) + + # Move cursor: + if is_done: + current_pos = move_cursor(Point(y=current_height, x=0)) + output.erase_down() + else: + current_pos = move_cursor(screen.cursor_position) + + if is_done: + 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 artifacs on resize events.) + reset_attributes() + + if screen.show_cursor or is_done: + output.show_cursor() + + return current_pos, last_token[0] + + +class HeightIsUnknownError(Exception): + " Information unavailable. Did not yet receive the CPR response. " + + +class _TokenToAttrsCache(dict): + """ + A cache structure that maps Pygments Tokens to :class:`.Attr`. + (This is an important speed up.) + """ + def __init__(self, get_style_for_token): + self.get_style_for_token = get_style_for_token + + def __missing__(self, token): + try: + result = self.get_style_for_token(token) + except KeyError: + result = None + + self[token] = result + return result + + +class Renderer(object): + """ + Typical usage: + + :: + + output = Vt100_Output.from_pty(sys.stdout) + r = Renderer(style, output) + r.render(cli, layout=...) + """ + def __init__(self, style, output, use_alternate_screen=False, mouse_support=False): + assert isinstance(style, Style) + assert isinstance(output, Output) + + self.style = style + self.output = output + self.use_alternate_screen = use_alternate_screen + self.mouse_support = to_cli_filter(mouse_support) + + self._in_alternate_screen = False + self._mouse_support_enabled = False + self._bracketed_paste_enabled = False + + # Waiting for CPR flag. True when we send the request, but didn't got a + # response. + self.waiting_for_cpr = False + + self.reset(_scroll=True) + + def reset(self, _scroll=False, leave_alternate_screen=True): + # 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 = None + self._last_size = None + self._last_token = None + + # When the style hash changes, we have to do a full redraw as well as + # clear the `_attrs_for_token` dictionary. + self._last_style_hash = None + self._attrs_for_token = None + + # Default MouseHandlers. (Just empty.) + self.mouse_handlers = MouseHandlers() + + # Remember the last title. Only set the title when it changes. + self._last_title = None + + #: 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 Windown, also make sure to scroll to the current cursor + # position. (Only when rendering the first time.) + if is_windows() and _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 + + # Flush output. `disable_mouse_support` needs to write to stdout. + self.output.flush() + + @property + def height_is_known(self): + """ + 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.) + """ + return self.use_alternate_screen or self._min_available_height > 0 or \ + is_windows() # On Windows, we don't have to wait for a CPR. + + @property + def rows_above_layout(self): + """ + 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): + """ + Get current cursor position. + 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 + + # For Win32, we have an API call to get the number of rows below the + # cursor. + if is_windows(): + self._min_available_height = self.output.get_rows_below_cursor_position() + else: + if self.use_alternate_screen: + self._min_available_height = self.output.get_size().rows + else: + # Asks for a cursor position report (CPR). + self.waiting_for_cpr = True + self.output.ask_for_cpr() + + def report_absolute_cursor_row(self, row): + """ + To be called when we know the absolute cursor position. + (As an answer of a "Cursor Position Request" response.) + """ + # 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 + self._min_available_height = rows_below_cursor + + self.waiting_for_cpr = False + + def render(self, cli, layout, is_done=False): + """ + 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.use_alternate_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 + + # Enable/disable mouse support. + needs_mouse_support = self.mouse_support(cli) + + 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() + + if is_done: + height = 0 # When we are done, we don't necessary want to fill up until the bottom. + else: + height = self._last_screen.height if self._last_screen else 0 + height = max(self._min_available_height, height) + + # When te size changes, don't consider the previous screen. + if self._last_size != size: + self._last_screen = None + + # When we render using another style, 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: + self._last_screen = None + self._attrs_for_token = None + if self._attrs_for_token is None: + self._attrs_for_token = _TokenToAttrsCache(self.style.get_attrs_for_token) + self._last_style_hash = self.style.invalidation_hash() + + layout.write_to_screen(cli, screen, mouse_handlers, WritePosition( + xpos=0, + ypos=0, + width=size.columns, + height=(size.rows if self.use_alternate_screen else height), + extended_height=size.rows, + )) + + # When grayed. Replace all tokens in the new screen. + if cli.is_aborting or cli.is_exiting: + screen.replace_all_tokens(Token.Aborted) + + # Process diff and write to output. + self._cursor_pos, self._last_token = _output_screen_diff( + output, screen, self._cursor_pos, + self._last_screen, self._last_token, is_done, + attrs_for_token=self._attrs_for_token, + 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 + + # Write title if it changed. + new_title = cli.terminal_title + + if new_title != self._last_title: + if new_title is None: + self.output.clear_title() + else: + self.output.set_title(new_title) + self._last_title = new_title + + output.flush() + + def erase(self, leave_alternate_screen=True, erase_title=True): + """ + 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. + :param erase_title: When True, clear the title from the title bar. + """ + output = self.output + + output.cursor_backward(self._cursor_pos.x) + output.cursor_up(self._cursor_pos.y) + output.erase_down() + output.reset_attributes() + output.flush() + + # Erase title. + if self._last_title and erase_title: + output.clear_title() + + self.reset(leave_alternate_screen=leave_alternate_screen) + + def clear(self): + """ + 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_tokens(output, tokens, style): + """ + Print a list of (Token, text) tuples in the given style to the output. + """ + assert isinstance(output, Output) + assert isinstance(style, Style) + + # Reset first. + output.reset_attributes() + output.enable_autowrap() + + # Print all (token, text) tuples. + attrs_for_token = _TokenToAttrsCache(style.get_attrs_for_token) + + for token, text in tokens: + attrs = attrs_for_token[token] + + if attrs: + output.set_attributes(attrs) + else: + output.reset_attributes() + + output.write(text) + + # Reset again. + output.reset_attributes() + output.flush() diff --git a/src/libs/prompt_toolkit/search_state.py b/src/libs/prompt_toolkit/search_state.py new file mode 100644 index 0000000..3c494ea --- /dev/null +++ b/src/libs/prompt_toolkit/search_state.py @@ -0,0 +1,36 @@ +from .enums import IncrementalSearchDirection +from .filters import to_simple_filter + +__all__ = ( + 'SearchState', +) + + +class SearchState(object): + """ + A search 'query'. + """ + __slots__ = ('text', 'direction', 'ignore_case') + + def __init__(self, text='', direction=IncrementalSearchDirection.FORWARD, ignore_case=False): + ignore_case = to_simple_filter(ignore_case) + + self.text = text + self.direction = direction + self.ignore_case = ignore_case + + def __repr__(self): + return '%s(%r, direction=%r, ignore_case=%r)' % ( + self.__class__.__name__, self.text, self.direction, self.ignore_case) + + def __invert__(self): + """ + Create a new SearchState where backwards becomes forwards and the other + way around. + """ + if self.direction == IncrementalSearchDirection.BACKWARD: + direction = IncrementalSearchDirection.FORWARD + else: + direction = IncrementalSearchDirection.BACKWARD + + return SearchState(text=self.text, direction=direction, ignore_case=self.ignore_case) diff --git a/src/libs/prompt_toolkit/selection.py b/src/libs/prompt_toolkit/selection.py new file mode 100644 index 0000000..6582921 --- /dev/null +++ b/src/libs/prompt_toolkit/selection.py @@ -0,0 +1,47 @@ +""" +Data structures for the selection. +""" +from __future__ import unicode_literals + +__all__ = ( + 'SelectionType', + 'PasteMode', + 'SelectionState', +) + + +class SelectionType(object): + """ + 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(object): + 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(object): + """ + State of the current selection. + + :param original_cursor_position: int + :param type: :class:`~.SelectionType` + """ + def __init__(self, original_cursor_position=0, type=SelectionType.CHARACTERS): + self.original_cursor_position = original_cursor_position + self.type = type + + def __repr__(self): + return '%s(original_cursor_position=%r, type=%r)' % ( + self.__class__.__name__, + self.original_cursor_position, self.type) diff --git a/src/libs/prompt_toolkit/shortcuts.py b/src/libs/prompt_toolkit/shortcuts.py new file mode 100644 index 0000000..4e5d0ea --- /dev/null +++ b/src/libs/prompt_toolkit/shortcuts.py @@ -0,0 +1,717 @@ +""" +Shortcuts for retrieving input from the user. + +If you are using this library for retrieving some input from the user (as a +pure Python replacement for GNU readline), probably for 90% of the use cases, +the :func:`.prompt` function is all you need. It's the easiest shortcut which +does a lot of the underlying work like creating a +:class:`~libs.prompt_toolkit.interface.CommandLineInterface` instance for you. + +When is this not sufficient: + - When you want to have more complicated layouts (maybe with sidebars or + multiple toolbars. Or visibility of certain user interface controls + according to some conditions.) + - When you wish to have multiple input buffers. (If you would create an + editor like a Vi clone.) + - Something else that requires more customization than what is possible + with the parameters of `prompt`. + +In that case, study the code in this file and build your own +`CommandLineInterface` instance. It's not too complicated. +""" +from __future__ import unicode_literals + +from .buffer import Buffer, AcceptAction +from .document import Document +from .enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode +from .filters import IsDone, HasFocus, RendererHeightIsKnown, to_simple_filter, to_cli_filter, Condition +from .history import InMemoryHistory +from .interface import CommandLineInterface, Application, AbortAction +from .key_binding.defaults import load_key_bindings_for_prompt +from .key_binding.registry import Registry +from .keys import Keys +from .layout import Window, HSplit, FloatContainer, Float +from .layout.containers import ConditionalContainer +from .layout.controls import BufferControl, TokenListControl +from .layout.dimension import LayoutDimension +from .layout.lexers import PygmentsLexer +from .layout.margins import PromptMargin, ConditionalMargin +from .layout.menus import CompletionsMenu, MultiColumnCompletionsMenu +from .layout.processors import PasswordProcessor, ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, DisplayMultipleCursors +from .layout.prompt import DefaultPrompt +from .layout.screen import Char +from .layout.toolbars import ValidationToolbar, SystemToolbar, ArgToolbar, SearchToolbar +from .layout.utils import explode_tokens +from .renderer import print_tokens as renderer_print_tokens +from .styles import DEFAULT_STYLE, Style, style_from_dict +from .token import Token +from .utils import is_conemu_ansi, is_windows, DummyContext + +from six import text_type, exec_, PY2 + +import os +import sys +import textwrap +import threading +import time + +try: + from pygments.lexer import Lexer as pygments_Lexer + from pygments.style import Style as pygments_Style +except ImportError: + pygments_Lexer = None + pygments_Style = None + +if is_windows(): + from .terminal.win32_output import Win32Output + from .terminal.conemu_output import ConEmuOutput +else: + from .terminal.vt100_output import Vt100_Output + + +__all__ = ( + 'create_eventloop', + 'create_output', + 'create_prompt_layout', + 'create_prompt_application', + 'prompt', + 'prompt_async', + 'create_confirm_application', + 'run_application', + 'confirm', + 'print_tokens', + 'clear', +) + + +def create_eventloop(inputhook=None, recognize_win32_paste=True): + """ + Create and return an + :class:`~libs.prompt_toolkit.eventloop.base.EventLoop` instance for a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface`. + """ + if is_windows(): + from libs.prompt_toolkit.eventloop.win32 import Win32EventLoop as Loop + return Loop(inputhook=inputhook, recognize_paste=recognize_win32_paste) + else: + from libs.prompt_toolkit.eventloop.posix import PosixEventLoop as Loop + return Loop(inputhook=inputhook) + + +def create_output(stdout=None, true_color=False, ansi_colors_only=None): + """ + Return an :class:`~libs.prompt_toolkit.output.Output` instance for the command + line. + + :param true_color: When True, use 24bit colors instead of 256 colors. + (`bool` or :class:`~libs.prompt_toolkit.filters.SimpleFilter`.) + :param ansi_colors_only: When True, restrict to 16 ANSI colors only. + (`bool` or :class:`~libs.prompt_toolkit.filters.SimpleFilter`.) + """ + stdout = stdout or sys.__stdout__ + true_color = to_simple_filter(true_color) + + if is_windows(): + if is_conemu_ansi(): + return ConEmuOutput(stdout) + else: + return Win32Output(stdout) + else: + term = os.environ.get('TERM', '') + if PY2: + term = term.decode('utf-8') + + return Vt100_Output.from_pty( + stdout, true_color=true_color, + ansi_colors_only=ansi_colors_only, term=term) + + +def create_asyncio_eventloop(loop=None): + """ + Returns an asyncio :class:`~libs.prompt_toolkit.eventloop.EventLoop` instance + for usage in a :class:`~libs.prompt_toolkit.interface.CommandLineInterface`. It + is a wrapper around an asyncio loop. + + :param loop: The asyncio eventloop (or `None` if the default asyncioloop + should be used.) + """ + # Inline import, to make sure the rest doesn't break on Python 2. (Where + # asyncio is not available.) + if is_windows(): + from libs.prompt_toolkit.eventloop.asyncio_win32 import Win32AsyncioEventLoop as AsyncioEventLoop + else: + from libs.prompt_toolkit.eventloop.asyncio_posix import PosixAsyncioEventLoop as AsyncioEventLoop + + return AsyncioEventLoop(loop) + + +def _split_multiline_prompt(get_prompt_tokens): + """ + Take a `get_prompt_tokens` function and return three new functions instead. + One that tells whether this prompt consists of multiple lines; one that + returns the tokens to be shown on the lines above the input; and another + one with the tokens to be shown at the first line of the input. + """ + def has_before_tokens(cli): + for token, char in get_prompt_tokens(cli): + if '\n' in char: + return True + return False + + def before(cli): + result = [] + found_nl = False + for token, char in reversed(explode_tokens(get_prompt_tokens(cli))): + if found_nl: + result.insert(0, (token, char)) + elif char == '\n': + found_nl = True + return result + + def first_input_line(cli): + result = [] + for token, char in reversed(explode_tokens(get_prompt_tokens(cli))): + if char == '\n': + break + else: + result.insert(0, (token, char)) + return result + + return has_before_tokens, before, first_input_line + + +class _RPrompt(Window): + " The prompt that is displayed on the right side of the Window. " + def __init__(self, get_tokens=None): + get_tokens = get_tokens or (lambda cli: []) + + super(_RPrompt, self).__init__( + TokenListControl(get_tokens, align_right=True)) + + +def create_prompt_layout(message='', lexer=None, is_password=False, + reserve_space_for_menu=8, + get_prompt_tokens=None, get_continuation_tokens=None, + get_rprompt_tokens=None, + get_bottom_toolbar_tokens=None, + display_completions_in_columns=False, + extra_input_processors=None, multiline=False, + wrap_lines=True): + """ + Create a :class:`.Container` instance for a prompt. + + :param message: Text to be used as prompt. + :param lexer: :class:`~libs.prompt_toolkit.layout.lexers.Lexer` to be used for + the highlighting. + :param is_password: `bool` or :class:`~libs.prompt_toolkit.filters.CLIFilter`. + When True, display input as '*'. + :param reserve_space_for_menu: Space to be reserved for the menu. When >0, + make sure that a minimal height is allocated in the terminal, in order + to display the completion menu. + :param get_prompt_tokens: An optional callable that returns the tokens to be + shown in the menu. (To be used instead of a `message`.) + :param get_continuation_tokens: An optional callable that takes a + CommandLineInterface and width as input and returns a list of (Token, + text) tuples to be used for the continuation. + :param get_bottom_toolbar_tokens: An optional callable that returns the + tokens for a toolbar at the bottom. + :param display_completions_in_columns: `bool` or + :class:`~libs.prompt_toolkit.filters.CLIFilter`. Display the completions in + multiple columns. + :param multiline: `bool` or :class:`~libs.prompt_toolkit.filters.CLIFilter`. + 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:`~libs.prompt_toolkit.filters.CLIFilter`. + When True (the default), automatically wrap long lines instead of + scrolling horizontally. + """ + assert isinstance(message, text_type), 'Please provide a unicode string.' + assert get_bottom_toolbar_tokens is None or callable(get_bottom_toolbar_tokens) + assert get_prompt_tokens is None or callable(get_prompt_tokens) + assert get_rprompt_tokens is None or callable(get_rprompt_tokens) + assert not (message and get_prompt_tokens) + + display_completions_in_columns = to_cli_filter(display_completions_in_columns) + multiline = to_cli_filter(multiline) + + if get_prompt_tokens is None: + get_prompt_tokens = lambda _: [(Token.Prompt, message)] + + has_before_tokens, get_prompt_tokens_1, get_prompt_tokens_2 = \ + _split_multiline_prompt(get_prompt_tokens) + + # `lexer` is supposed to be a `Lexer` instance. But if a Pygments lexer + # class is given, turn it into a PygmentsLexer. (Important for + # backwards-compatibility.) + try: + if pygments_Lexer and issubclass(lexer, pygments_Lexer): + lexer = PygmentsLexer(lexer, sync_from_start=True) + except TypeError: # Happens when lexer is `None` or an instance of something else. + pass + + # Create processors list. + input_processors = [ + ConditionalProcessor( + # By default, only highlight search when the search + # input has the focus. (Note that this doesn't mean + # there is no search: the Vi 'n' binding for instance + # still allows to jump to the next match in + # navigation mode.) + HighlightSearchProcessor(preview_search=True), + HasFocus(SEARCH_BUFFER)), + HighlightSelectionProcessor(), + ConditionalProcessor(AppendAutoSuggestion(), HasFocus(DEFAULT_BUFFER) & ~IsDone()), + ConditionalProcessor(PasswordProcessor(), is_password), + DisplayMultipleCursors(DEFAULT_BUFFER), + ] + + if extra_input_processors: + input_processors.extend(extra_input_processors) + + # Show the prompt before the input (using the DefaultPrompt processor. + # This also replaces it with reverse-i-search and 'arg' when required. + # (Only for single line mode.) + # (DefaultPrompt should always be at the end of the processors.) + input_processors.append(ConditionalProcessor( + DefaultPrompt(get_prompt_tokens_2), ~multiline)) + + # Create bottom toolbar. + if get_bottom_toolbar_tokens: + toolbars = [ConditionalContainer( + Window(TokenListControl(get_bottom_toolbar_tokens, + default_char=Char(' ', Token.Toolbar)), + height=LayoutDimension.exact(1)), + filter=~IsDone() & RendererHeightIsKnown())] + else: + toolbars = [] + + def get_height(cli): + # 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 reserve_space_for_menu and not cli.is_done: + buff = cli.current_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 LayoutDimension(min=reserve_space_for_menu) + + return LayoutDimension() + + # Create and return Container instance. + return HSplit([ + # The main input, with completion menus floating on top of it. + FloatContainer( + HSplit([ + ConditionalContainer( + Window( + TokenListControl(get_prompt_tokens_1), + dont_extend_height=True), + Condition(has_before_tokens) + ), + Window( + BufferControl( + input_processors=input_processors, + lexer=lexer, + # Enable preview_search, we want to have immediate feedback + # in reverse-i-search mode. + preview_search=True), + get_height=get_height, + left_margins=[ + # In multiline mode, use the window margin to display + # the prompt and continuation tokens. + ConditionalMargin( + PromptMargin(get_prompt_tokens_2, get_continuation_tokens), + filter=multiline + ) + ], + wrap_lines=wrap_lines, + ), + ]), + [ + # Completion menus. + Float(xcursor=True, + ycursor=True, + content=CompletionsMenu( + max_height=16, + scroll_offset=1, + extra_filter=HasFocus(DEFAULT_BUFFER) & + ~display_completions_in_columns)), + Float(xcursor=True, + ycursor=True, + content=MultiColumnCompletionsMenu( + extra_filter=HasFocus(DEFAULT_BUFFER) & + display_completions_in_columns, + show_meta=True)), + + # The right prompt. + Float(right=0, top=0, hide_when_covering_content=True, + content=_RPrompt(get_rprompt_tokens)), + ] + ), + ValidationToolbar(), + SystemToolbar(), + + # In multiline mode, we use two toolbars for 'arg' and 'search'. + ConditionalContainer(ArgToolbar(), multiline), + ConditionalContainer(SearchToolbar(), multiline), + ] + toolbars) + + +def create_prompt_application( + message='', + multiline=False, + wrap_lines=True, + is_password=False, + vi_mode=False, + editing_mode=EditingMode.EMACS, + complete_while_typing=True, + enable_history_search=False, + lexer=None, + enable_system_bindings=False, + enable_open_in_editor=False, + validator=None, + completer=None, + reserve_space_for_menu=8, + auto_suggest=None, + style=None, + history=None, + clipboard=None, + get_prompt_tokens=None, + get_continuation_tokens=None, + get_rprompt_tokens=None, + get_bottom_toolbar_tokens=None, + display_completions_in_columns=False, + get_title=None, + mouse_support=False, + extra_input_processors=None, + key_bindings_registry=None, + on_abort=AbortAction.RAISE_EXCEPTION, + on_exit=AbortAction.RAISE_EXCEPTION, + accept_action=AcceptAction.RETURN_DOCUMENT, + erase_when_done=False, + default=''): + """ + Create an :class:`~Application` instance for a prompt. + + (It is meant to cover 90% of the prompt use cases, where no extreme + customization is required. For more complex input, it is required to create + a custom :class:`~Application` instance.) + + :param message: Text to be shown before the prompt. + :param mulitiline: Allow multiline input. Pressing enter will insert a + newline. (This requires Meta+Enter to accept the input.) + :param wrap_lines: `bool` or :class:`~libs.prompt_toolkit.filters.CLIFilter`. + 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:`~libs.prompt_toolkit.filters.SimpleFilter`. Enable autocompletion + while typing. + :param enable_history_search: `bool` or + :class:`~libs.prompt_toolkit.filters.SimpleFilter`. Enable up-arrow parting + string matching. + :param lexer: :class:`~libs.prompt_toolkit.layout.lexers.Lexer` to be used for + the syntax highlighting. + :param validator: :class:`~libs.prompt_toolkit.validation.Validator` instance + for input validation. + :param completer: :class:`~libs.prompt_toolkit.completion.Completer` instance + for input completion. + :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:`~libs.prompt_toolkit.auto_suggest.AutoSuggest` + instance for input suggestions. + :param style: :class:`.Style` instance for the color scheme. + :param enable_system_bindings: `bool` or + :class:`~libs.prompt_toolkit.filters.CLIFilter`. Pressing Meta+'!' will show + a system prompt. + :param enable_open_in_editor: `bool` or + :class:`~libs.prompt_toolkit.filters.CLIFilter`. Pressing 'v' in Vi mode or + C-X C-E in emacs mode will open an external editor. + :param history: :class:`~libs.prompt_toolkit.history.History` instance. + :param clipboard: :class:`~libs.prompt_toolkit.clipboard.base.Clipboard` instance. + (e.g. :class:`~libs.prompt_toolkit.clipboard.in_memory.InMemoryClipboard`) + :param get_bottom_toolbar_tokens: Optional callable which takes a + :class:`~libs.prompt_toolkit.interface.CommandLineInterface` and returns a + list of tokens for the bottom toolbar. + :param display_completions_in_columns: `bool` or + :class:`~libs.prompt_toolkit.filters.CLIFilter`. Display the completions in + multiple columns. + :param get_title: Callable that returns the title to be displayed in the + terminal. + :param mouse_support: `bool` or :class:`~libs.prompt_toolkit.filters.CLIFilter` + to enable mouse support. + :param default: The default text to be shown in the input buffer. (This can + be edited by the user.) + """ + if key_bindings_registry is None: + key_bindings_registry = load_key_bindings_for_prompt( + enable_system_bindings=enable_system_bindings, + enable_open_in_editor=enable_open_in_editor) + + # Ensure backwards-compatibility, when `vi_mode` is passed. + if vi_mode: + editing_mode = EditingMode.VI + + # Make sure that complete_while_typing is disabled when enable_history_search + # is enabled. (First convert to SimpleFilter, to avoid doing bitwise operations + # on bool objects.) + complete_while_typing = to_simple_filter(complete_while_typing) + enable_history_search = to_simple_filter(enable_history_search) + multiline = to_simple_filter(multiline) + + complete_while_typing = complete_while_typing & ~enable_history_search + + # Accept Pygments styles as well for backwards compatibility. + try: + if pygments_Style and issubclass(style, pygments_Style): + style = style_from_dict(style.styles) + except TypeError: # Happens when style is `None` or an instance of something else. + pass + + # Create application + return Application( + layout=create_prompt_layout( + message=message, + lexer=lexer, + is_password=is_password, + reserve_space_for_menu=(reserve_space_for_menu if completer is not None else 0), + multiline=Condition(lambda cli: multiline()), + get_prompt_tokens=get_prompt_tokens, + get_continuation_tokens=get_continuation_tokens, + get_rprompt_tokens=get_rprompt_tokens, + get_bottom_toolbar_tokens=get_bottom_toolbar_tokens, + display_completions_in_columns=display_completions_in_columns, + extra_input_processors=extra_input_processors, + wrap_lines=wrap_lines), + buffer=Buffer( + enable_history_search=enable_history_search, + complete_while_typing=complete_while_typing, + is_multiline=multiline, + history=(history or InMemoryHistory()), + validator=validator, + completer=completer, + auto_suggest=auto_suggest, + accept_action=accept_action, + initial_document=Document(default), + ), + style=style or DEFAULT_STYLE, + clipboard=clipboard, + key_bindings_registry=key_bindings_registry, + get_title=get_title, + mouse_support=mouse_support, + editing_mode=editing_mode, + erase_when_done=erase_when_done, + reverse_vi_search_direction=True, + on_abort=on_abort, + on_exit=on_exit) + + +def prompt(message='', **kwargs): + """ + Get input from the user and return it. + + This is a wrapper around a lot of ``libs.prompt_toolkit`` functionality and can + be a replacement for `raw_input`. (or GNU readline.) + + If you want to keep your history across several calls, create one + :class:`~libs.prompt_toolkit.history.History` instance and pass it every time. + + This function accepts many keyword arguments. Except for the following, + they are a proxy to the arguments of :func:`.create_prompt_application`. + + :param patch_stdout: Replace ``sys.stdout`` by a proxy that ensures that + print statements from other threads won't destroy the prompt. (They + will be printed above the prompt instead.) + :param return_asyncio_coroutine: When True, return a asyncio coroutine. (Python >3.3) + :param true_color: When True, use 24bit colors instead of 256 colors. + :param refresh_interval: (number; in seconds) When given, refresh the UI + every so many seconds. + """ + patch_stdout = kwargs.pop('patch_stdout', False) + return_asyncio_coroutine = kwargs.pop('return_asyncio_coroutine', False) + true_color = kwargs.pop('true_color', False) + refresh_interval = kwargs.pop('refresh_interval', 0) + eventloop = kwargs.pop('eventloop', None) + + application = create_prompt_application(message, **kwargs) + + return run_application(application, + patch_stdout=patch_stdout, + return_asyncio_coroutine=return_asyncio_coroutine, + true_color=true_color, + refresh_interval=refresh_interval, + eventloop=eventloop) + + +def run_application( + application, patch_stdout=False, return_asyncio_coroutine=False, + true_color=False, refresh_interval=0, eventloop=None): + """ + Run a prompt toolkit application. + + :param patch_stdout: Replace ``sys.stdout`` by a proxy that ensures that + print statements from other threads won't destroy the prompt. (They + will be printed above the prompt instead.) + :param return_asyncio_coroutine: When True, return a asyncio coroutine. (Python >3.3) + :param true_color: When True, use 24bit colors instead of 256 colors. + :param refresh_interval: (number; in seconds) When given, refresh the UI + every so many seconds. + """ + assert isinstance(application, Application) + + if return_asyncio_coroutine: + eventloop = create_asyncio_eventloop() + else: + eventloop = eventloop or create_eventloop() + + # Create CommandLineInterface. + cli = CommandLineInterface( + application=application, + eventloop=eventloop, + output=create_output(true_color=true_color)) + + # Set up refresh interval. + if refresh_interval: + done = [False] + def start_refresh_loop(cli): + def run(): + while not done[0]: + time.sleep(refresh_interval) + cli.request_redraw() + t = threading.Thread(target=run) + t.daemon = True + t.start() + + def stop_refresh_loop(cli): + done[0] = True + + cli.on_start += start_refresh_loop + cli.on_stop += stop_refresh_loop + + # Replace stdout. + patch_context = cli.patch_stdout_context(raw=True) if patch_stdout else DummyContext() + + # Read input and return it. + if return_asyncio_coroutine: + # Create an asyncio coroutine and call it. + exec_context = {'patch_context': patch_context, 'cli': cli, + 'Document': Document} + exec_(textwrap.dedent(''' + def prompt_coro(): + # Inline import, because it slows down startup when asyncio is not + # needed. + import asyncio + + @asyncio.coroutine + def run(): + with patch_context: + result = yield from cli.run_async() + + if isinstance(result, Document): # Backwards-compatibility. + return result.text + return result + return run() + '''), exec_context) + + return exec_context['prompt_coro']() + else: + try: + with patch_context: + result = cli.run() + + if isinstance(result, Document): # Backwards-compatibility. + return result.text + return result + finally: + eventloop.close() + + +def prompt_async(message='', **kwargs): + """ + Similar to :func:`.prompt`, but return an asyncio coroutine instead. + """ + kwargs['return_asyncio_coroutine'] = True + return prompt(message, **kwargs) + + +def create_confirm_application(message): + """ + Create a confirmation `Application` that returns True/False. + """ + registry = Registry() + + @registry.add_binding('y') + @registry.add_binding('Y') + def _(event): + event.cli.buffers[DEFAULT_BUFFER].text = 'y' + event.cli.set_return_value(True) + + @registry.add_binding('n') + @registry.add_binding('N') + @registry.add_binding(Keys.ControlC) + def _(event): + event.cli.buffers[DEFAULT_BUFFER].text = 'n' + event.cli.set_return_value(False) + + return create_prompt_application(message, key_bindings_registry=registry) + + +def confirm(message='Confirm (y or n) '): + """ + Display a confirmation prompt. + """ + assert isinstance(message, text_type) + + app = create_confirm_application(message) + return run_application(app) + + +def print_tokens(tokens, style=None, true_color=False, file=None): + """ + Print a list of (Token, text) tuples in the given style to the output. + E.g.:: + + style = style_from_dict({ + Token.Hello: '#ff0066', + Token.World: '#884444 italic', + }) + tokens = [ + (Token.Hello, 'Hello'), + (Token.World, 'World'), + ] + print_tokens(tokens, style=style) + + :param tokens: List of ``(Token, text)`` tuples. + :param style: :class:`.Style` instance for the color scheme. + :param true_color: When True, use 24bit colors instead of 256 colors. + :param file: The output file. This can be `sys.stdout` or `sys.stderr`. + """ + if style is None: + style = DEFAULT_STYLE + assert isinstance(style, Style) + + output = create_output(true_color=true_color, stdout=file) + renderer_print_tokens(output, tokens, style) + + +def clear(): + """ + Clear the screen. + """ + out = create_output() + out.erase_screen() + out.cursor_goto(0, 0) + out.flush() + + +# Deprecated alias for `prompt`. +get_input = prompt +# Deprecated alias for create_prompt_layout +create_default_layout = create_prompt_layout +# Deprecated alias for create_prompt_application +create_default_application = create_prompt_application diff --git a/src/libs/prompt_toolkit/styles/__init__.py b/src/libs/prompt_toolkit/styles/__init__.py new file mode 100644 index 0000000..5e10f34 --- /dev/null +++ b/src/libs/prompt_toolkit/styles/__init__.py @@ -0,0 +1,21 @@ +""" +Styling for libs.prompt_toolkit applications. +""" +from __future__ import unicode_literals + +from .base import * +from .defaults import * +from .from_dict import * +from .from_pygments import * +from .utils import * + + +#: The default built-in style. +#: (For backwards compatibility, when Pygments is installed, this includes the +#: default Pygments style.) +try: + import pygments +except ImportError: + DEFAULT_STYLE = style_from_dict(DEFAULT_STYLE_EXTENSIONS) +else: + DEFAULT_STYLE = style_from_pygments() diff --git a/src/libs/prompt_toolkit/styles/base.py b/src/libs/prompt_toolkit/styles/base.py new file mode 100644 index 0000000..031e113 --- /dev/null +++ b/src/libs/prompt_toolkit/styles/base.py @@ -0,0 +1,86 @@ +""" +The base classes for the styling. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from collections import namedtuple +from six import with_metaclass + +__all__ = ( + 'Attrs', + 'DEFAULT_ATTRS', + 'ANSI_COLOR_NAMES', + 'Style', + 'DynamicStyle', +) + + +#: Style attributes. +Attrs = namedtuple('Attrs', 'color bgcolor bold underline italic blink reverse') +""" +: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 italic: Boolean +:param blink: Boolean +:param reverse: Boolean +""" + +#: The default `Attrs`. +DEFAULT_ATTRS = Attrs(color=None, bgcolor=None, bold=False, underline=False, + italic=False, blink=False, reverse=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. +ANSI_COLOR_NAMES = [ + 'ansiblack', 'ansiwhite', 'ansidefault', + + # Low intensity. + 'ansired', 'ansigreen', 'ansiyellow', 'ansiblue', 'ansifuchsia', 'ansiturquoise', 'ansilightgray', + + # High intensity. (Not supported everywhere.) + 'ansidarkgray', 'ansidarkred', 'ansidarkgreen', 'ansibrown', 'ansidarkblue', + 'ansipurple', 'ansiteal', +] + + +class Style(with_metaclass(ABCMeta, object)): + """ + Abstract base class for libs.prompt_toolkit styles. + """ + @abstractmethod + def get_attrs_for_token(self, token): + """ + Return :class:`.Attrs` for the given token. + """ + + @abstractmethod + def invalidation_hash(self): + """ + 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 DynamicStyle(Style): + """ + Style class that can dynamically returns an other Style. + + :param get_style: Callable that returns a :class:`.Style` instance. + """ + def __init__(self, get_style): + self.get_style = get_style + + def get_attrs_for_token(self, token): + style = self.get_style() + assert isinstance(style, Style) + + return style.get_attrs_for_token(token) + + def invalidation_hash(self): + return self.get_style().invalidation_hash() diff --git a/src/libs/prompt_toolkit/styles/defaults.py b/src/libs/prompt_toolkit/styles/defaults.py new file mode 100644 index 0000000..c1a299e --- /dev/null +++ b/src/libs/prompt_toolkit/styles/defaults.py @@ -0,0 +1,95 @@ +""" +The default styling. +""" +from __future__ import unicode_literals + +from libs.prompt_toolkit.token import Token + +__all__ = ( + 'DEFAULT_STYLE_EXTENSIONS', + 'default_style_extensions', +) + + +#: Styling of prompt-toolkit specific tokens, that are not know by the default +#: Pygments style. +DEFAULT_STYLE_EXTENSIONS = { + # Highlighting of search matches in document. + Token.SearchMatch: 'noinherit reverse', + Token.SearchMatch.Current: 'noinherit #ffffff bg:#448844 underline', + + # Highlighting of select text in document. + Token.SelectedText: 'reverse', + + Token.CursorColumn: 'bg:#dddddd', + Token.CursorLine: 'underline', + Token.ColorColumn: 'bg:#ccaacc', + + # Highlighting of matching brackets. + Token.MatchingBracket: '', + Token.MatchingBracket.Other: '#000000 bg:#aacccc', + Token.MatchingBracket.Cursor: '#ff8888 bg:#880000', + + Token.MultipleCursors.Cursor: '#000000 bg:#ccccaa', + + # Line numbers. + Token.LineNumber: '#888888', + Token.LineNumber.Current: 'bold', + Token.Tilde: '#8888ff', + + # Default prompt. + Token.Prompt: '', + Token.Prompt.Arg: 'noinherit', + Token.Prompt.Search: 'noinherit', + Token.Prompt.Search.Text: '', + + # Search toolbar. + Token.Toolbar.Search: 'bold', + Token.Toolbar.Search.Text: 'nobold', + + # System toolbar + Token.Toolbar.System: 'bold', + Token.Toolbar.System.Text: 'nobold', + + # "arg" toolbar. + Token.Toolbar.Arg: 'bold', + Token.Toolbar.Arg.Text: 'nobold', + + # Validation toolbar. + Token.Toolbar.Validation: 'bg:#550000 #ffffff', + Token.WindowTooSmall: 'bg:#550000 #ffffff', + + # Completions toolbar. + Token.Toolbar.Completions: 'bg:#bbbbbb #000000', + Token.Toolbar.Completions.Arrow: 'bg:#bbbbbb #000000 bold', + Token.Toolbar.Completions.Completion: 'bg:#bbbbbb #000000', + Token.Toolbar.Completions.Completion.Current: 'bg:#444444 #ffffff', + + # Completions menu. + Token.Menu.Completions: 'bg:#bbbbbb #000000', + Token.Menu.Completions.Completion: '', + Token.Menu.Completions.Completion.Current: 'bg:#888888 #ffffff', + Token.Menu.Completions.Meta: 'bg:#999999 #000000', + Token.Menu.Completions.Meta.Current: 'bg:#aaaaaa #000000', + Token.Menu.Completions.MultiColumnMeta: 'bg:#aaaaaa #000000', + + # Scrollbars. + Token.Scrollbar: 'bg:#888888', + Token.Scrollbar.Button: 'bg:#444444', + Token.Scrollbar.Arrow: 'bg:#222222 #888888 bold', + + # Auto suggestion text. + Token.AutoSuggestion: '#666666', + + # Trailing whitespace and tabs. + Token.TrailingWhiteSpace: '#999999', + Token.Tab: '#999999', + + # When Control-C has been pressed. Grayed. + Token.Aborted: '#888888', + + # Entering a Vi digraph. + Token.Digraph: '#4444ff', +} + +default_style_extensions = DEFAULT_STYLE_EXTENSIONS # Old name. diff --git a/src/libs/prompt_toolkit/styles/from_dict.py b/src/libs/prompt_toolkit/styles/from_dict.py new file mode 100644 index 0000000..8dcd13e --- /dev/null +++ b/src/libs/prompt_toolkit/styles/from_dict.py @@ -0,0 +1,148 @@ +""" +Tool for creating styles from a dictionary. + +This is very similar to the Pygments style dictionary, with some additions: +- Support for reverse and blink. +- Support for ANSI color names. (These will map directly to the 16 terminal + colors.) +""" +from collections.abc import Mapping + +from .base import Style, DEFAULT_ATTRS, ANSI_COLOR_NAMES +from .defaults import DEFAULT_STYLE_EXTENSIONS +from .utils import merge_attrs, split_token_in_parts +from six.moves import range + +__all__ = ( + 'style_from_dict', +) + + +def _colorformat(text): + """ + 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.) + """ + if text[0:1] == '#': + col = text[1:] + if col in ANSI_COLOR_NAMES: + return col + elif len(col) == 6: + return col + elif len(col) == 3: + return col[0]*2 + col[1]*2 + col[2]*2 + elif text == '': + return text + + raise ValueError('Wrong color format %r' % text) + + +def style_from_dict(style_dict, include_defaults=True): + """ + Create a ``Style`` instance from a dictionary or other mapping. + + The dictionary is equivalent to the ``Style.styles`` dictionary from + pygments, with a few additions: it supports 'reverse' and 'blink'. + + Usage:: + + style_from_dict({ + Token: '#ff0000 bold underline', + Token.Title: 'blink', + Token.SomethingElse: 'reverse', + }) + + :param include_defaults: Include the defaults (built-in) styling for + selected text, etc...) + """ + assert isinstance(style_dict, Mapping) + + if include_defaults: + s2 = {} + s2.update(DEFAULT_STYLE_EXTENSIONS) + s2.update(style_dict) + style_dict = s2 + + # Expand token inheritance and turn style description into Attrs. + token_to_attrs = {} + + # (Loop through the tokens in order. Sorting makes sure that + # we process the parent first.) + for ttype, styledef in sorted(style_dict.items()): + # Start from parent Attrs or default Attrs. + attrs = DEFAULT_ATTRS + + if 'noinherit' not in styledef: + for i in range(1, len(ttype) + 1): + try: + attrs = token_to_attrs[ttype[:-i]] + except KeyError: + pass + else: + break + + # Now update with the given attributes. + for part in styledef.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) + + # libs.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) + + # Pygments properties that we ignore. + elif part in ('roman', 'sans', 'mono'): + pass + elif part.startswith('border:'): + pass + + # Colors. + + elif part.startswith('bg:'): + attrs = attrs._replace(bgcolor=_colorformat(part[3:])) + else: + attrs = attrs._replace(color=_colorformat(part)) + + token_to_attrs[ttype] = attrs + + return _StyleFromDict(token_to_attrs) + + +class _StyleFromDict(Style): + """ + Turn a dictionary that maps `Token` to `Attrs` into a style class. + + :param token_to_attrs: Dictionary that maps `Token` to `Attrs`. + """ + def __init__(self, token_to_attrs): + self.token_to_attrs = token_to_attrs + + def get_attrs_for_token(self, token): + # Split Token. + list_of_attrs = [] + for token in split_token_in_parts(token): + list_of_attrs.append(self.token_to_attrs.get(token, DEFAULT_ATTRS)) + return merge_attrs(list_of_attrs) + + def invalidation_hash(self): + return id(self.token_to_attrs) diff --git a/src/libs/prompt_toolkit/styles/from_pygments.py b/src/libs/prompt_toolkit/styles/from_pygments.py new file mode 100644 index 0000000..4abe75f --- /dev/null +++ b/src/libs/prompt_toolkit/styles/from_pygments.py @@ -0,0 +1,77 @@ +""" +Adaptor for building libs.prompt_toolkit styles, starting from a Pygments style. + +Usage:: + + from pygments.styles.tango import TangoStyle + style = style_from_pygments(pygments_style_cls=TangoStyle) +""" +from __future__ import unicode_literals + +from .base import Style +from .from_dict import style_from_dict + +__all__ = ( + 'PygmentsStyle', + 'style_from_pygments', +) + + +# Following imports are only needed when a ``PygmentsStyle`` class is used. +try: + from pygments.style import Style as pygments_Style + from pygments.styles.default import DefaultStyle as pygments_DefaultStyle +except ImportError: + pygments_Style = None + pygments_DefaultStyle = None + + +def style_from_pygments(style_cls=pygments_DefaultStyle, + style_dict=None, + include_defaults=True): + """ + Shortcut to create a :class:`.Style` instance from a Pygments style class + and a style dictionary. + + Example:: + + from libs.prompt_toolkit.styles.from_pygments import style_from_pygments + from pygments.styles import get_style_by_name + style = style_from_pygments(get_style_by_name('monokai')) + + :param style_cls: Pygments style class to start from. + :param style_dict: Dictionary for this style. `{Token: style}`. + :param include_defaults: (`bool`) Include libs.prompt_toolkit extensions. + """ + assert style_dict is None or isinstance(style_dict, dict) + assert style_cls is None or issubclass(style_cls, pygments_Style) + + styles_dict = {} + + if style_cls is not None: + styles_dict.update(style_cls.styles) + + if style_dict is not None: + styles_dict.update(style_dict) + + return style_from_dict(styles_dict, include_defaults=include_defaults) + + +class PygmentsStyle(Style): + " Deprecated. " + def __new__(cls, pygments_style_cls): + assert issubclass(pygments_style_cls, pygments_Style) + return style_from_dict(pygments_style_cls.styles) + + def invalidation_hash(self): + pass + + @classmethod + def from_defaults(cls, style_dict=None, + pygments_style_cls=pygments_DefaultStyle, + include_extensions=True): + " Deprecated. " + return style_from_pygments( + style_cls=pygments_style_cls, + style_dict=style_dict, + include_defaults=include_extensions) diff --git a/src/libs/prompt_toolkit/styles/utils.py b/src/libs/prompt_toolkit/styles/utils.py new file mode 100644 index 0000000..6087e76 --- /dev/null +++ b/src/libs/prompt_toolkit/styles/utils.py @@ -0,0 +1,45 @@ +from __future__ import unicode_literals +from .base import DEFAULT_ATTRS, Attrs + +__all__ = ( + 'split_token_in_parts', + 'merge_attrs', +) + + +def split_token_in_parts(token): + """ + Take a Token, and turn it in a list of tokens, by splitting + it on ':' (taking that as a separator.) + """ + result = [] + current = [] + for part in token + (':', ): + if part == ':': + if current: + result.append(tuple(current)) + current = [] + else: + current.append(part) + + return result + + +def merge_attrs(list_of_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. + """ + result = DEFAULT_ATTRS + + for attr in list_of_attrs: + result = Attrs( + color=attr.color or result.color, + bgcolor=attr.bgcolor or result.bgcolor, + bold=attr.bold or result.bold, + underline=attr.underline or result.underline, + italic=attr.italic or result.italic, + blink=attr.blink or result.blink, + reverse=attr.reverse or result.reverse) + + return result diff --git a/src/libs/prompt_toolkit/terminal/__init__.py b/src/libs/prompt_toolkit/terminal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/libs/prompt_toolkit/terminal/conemu_output.py b/src/libs/prompt_toolkit/terminal/conemu_output.py new file mode 100644 index 0000000..033aef2 --- /dev/null +++ b/src/libs/prompt_toolkit/terminal/conemu_output.py @@ -0,0 +1,42 @@ +from __future__ import unicode_literals + +from libs.prompt_toolkit.renderer import Output + +from .win32_output import Win32Output +from .vt100_output import Vt100_Output + +__all__ = ( + 'ConEmuOutput', +) + + +class ConEmuOutput(object): + """ + 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): + self.win32_output = Win32Output(stdout) + self.vt100_output = Vt100_Output(stdout, lambda: None) + + def __getattr__(self, name): + 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/libs/prompt_toolkit/terminal/vt100_input.py b/src/libs/prompt_toolkit/terminal/vt100_input.py new file mode 100644 index 0000000..6a56890 --- /dev/null +++ b/src/libs/prompt_toolkit/terminal/vt100_input.py @@ -0,0 +1,520 @@ +""" +Parser for VT100 input stream. +""" +from __future__ import unicode_literals + +import os +import re +import six +import termios +import tty + +from six.moves import range + +from ..keys import Keys +from ..key_binding.input_processor import KeyPress + +__all__ = ( + 'InputStream', + 'raw_mode', + 'cooked_mode', +) + +_DEBUG_RENDERER_INPUT = False +_DEBUG_RENDERER_INPUT_FILENAME = 'prompt-toolkit-render-input.log' + + +# 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'( 30: + exclude += ('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 + + +class _16ColorCache(dict): + """ + Cache which maps (r, g, b) tuples to 16 ansi colors. + + :param bg: Cache for background colors, instead of foreground. + """ + def __init__(self, bg=False): + assert isinstance(bg, bool) + self.bg = bg + + def get_code(self, value, exclude=()): + """ + Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for + a given (r,g,b) value. + """ + key = (value, exclude) + if key not in self: + self[key] = self._get(value, exclude) + return self[key] + + def _get(self, value, exclude=()): + 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] + + self[value] = code + return code, match + + +class _256ColorCache(dict): + """ + Cach which maps (r, g, b) tuples to 256 colors. + """ + def __init__(self): + # Build color table. + colors = [] + + # 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): + 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): + 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): + """ + Cache for VT100 escape codes. It maps + (fgcolor, bgcolor, bold, underline, reverse) tuples to VT100 escape sequences. + + :param true_color: When True, use 24bit colors instead of 256 colors. + """ + def __init__(self, true_color=False, ansi_colors_only=False): + assert isinstance(true_color, bool) + self.true_color = true_color + self.ansi_colors_only = to_simple_filter(ansi_colors_only) + + def __missing__(self, attrs): + fgcolor, bgcolor, bold, underline, italic, blink, reverse = attrs + parts = [] + + parts.extend(self._colors_to_code(fgcolor, bgcolor)) + + 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 parts: + result = '\x1b[0;' + ';'.join(parts) + 'm' + else: + result = '\x1b[0m' + + self[attrs] = result + return result + + def _color_name_to_rgb(self, color): + " 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, bg_color): + " 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 explicitely defined to be the same color.) + fg_ansi = [()] + + def get(color, bg): + table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS + + if color is None: + 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.ansi_colors_only(): + if bg: # Background. + if fg_color != bg_color: + exclude = (fg_ansi[0], ) + 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[0] = name + return (code, ) + + # True colors. (Only when this feature is enabled.) + elif self.true_color: + 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 = [] + result.extend(get(fg_color, False)) + result.extend(get(bg_color, True)) + + return map(six.text_type, result) + + +def _get_size(fileno): + # Thanks to fabric (fabfile.org), and + # http://sqizit.bartletts.id.au/2011/02/14/pseudo-terminals-in-python/ + """ + Get the size of this pseudo terminal. + + :param fileno: stdout.fileno() + :returns: A (rows, cols) tuple. + """ + # Inline imports, because these modules are not available on Windows. + # (This file is used by ConEmuOutput, which is used on Windows.) + import fcntl + import termios + + # Buffer for the C call + buf = array.array(b'h' if six.PY2 else u'h', [0, 0, 0, 0]) + + # Do TIOCGWINSZ (Get) + # Note: We should not pass 'True' as a fourth parameter to 'ioctl'. (True + # is the default.) This causes segmentation faults on some systems. + # See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/364 + fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf) + + # Return rows, cols + return buf[0], buf[1] + + +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 true_color: Use 24bit color instead of 256 colors. (Can be a :class:`SimpleFilter`.) + When `ansi_colors_only` is set, only 16 colors are used. + :param ansi_colors_only: Restrict to 16 ANSI colors only. + :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...) + :param write_binary: Encode the output before writing it. If `True` (the + default), the `stdout` object is supposed to expose an `encoding` attribute. + """ + def __init__(self, stdout, get_size, true_color=False, + ansi_colors_only=None, term=None, write_binary=True): + assert callable(get_size) + assert term is None or isinstance(term, six.text_type) + assert all(hasattr(stdout, a) for a in ('write', 'flush')) + + if write_binary: + assert hasattr(stdout, 'encoding') + + self._buffer = [] + self.stdout = stdout + self.write_binary = write_binary + self.get_size = get_size + self.true_color = to_simple_filter(true_color) + self.term = term or 'xterm' + + # ANSI colors only? + if ansi_colors_only is None: + # When not given, use the following default. + ANSI_COLORS_ONLY = bool(os.environ.get( + 'PROMPT_TOOLKIT_ANSI_COLORS_ONLY', False)) + + @Condition + def ansi_colors_only(): + return ANSI_COLORS_ONLY or term in ('linux', 'eterm-color') + else: + ansi_colors_only = to_simple_filter(ansi_colors_only) + + self.ansi_colors_only = ansi_colors_only + + # Cache for escape codes. + self._escape_code_cache = _EscapeCodeCache(ansi_colors_only=ansi_colors_only) + self._escape_code_cache_true_color = _EscapeCodeCache( + true_color=True, ansi_colors_only=ansi_colors_only) + + @classmethod + def from_pty(cls, stdout, true_color=False, ansi_colors_only=None, term=None): + """ + Create an Output class from a pseudo terminal. + (This will take the dimensions by reading the pseudo + terminal attributes.) + """ + assert stdout.isatty() + def get_size(): + rows, columns = _get_size(stdout.fileno()) + # If terminal (incorrectly) reports its size as 0, pick a reasonable default. + # See https://github.com/ipython/ipython/issues/10071 + return Size(rows=(rows or 24), columns=(columns or 80)) + + return cls(stdout, get_size, true_color=true_color, + ansi_colors_only=ansi_colors_only, term=term) + + def fileno(self): + " Return file descriptor. " + return self.stdout.fileno() + + def encoding(self): + " Return encoding used for stdout. " + return self.stdout.encoding + + def write_raw(self, data): + """ + Write raw data to output. + """ + self._buffer.append(data) + + def write(self, data): + """ + Write text to output. + (Removes vt100 escape codes. -- used for safely writing text.) + """ + self._buffer.append(data.replace('\x1b', '?')) + + def set_title(self, title): + """ + 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): + self.set_title('') + + def erase_screen(self): + """ + Erases the screen with the background colour and moves the cursor to + home. + """ + self.write_raw('\x1b[2J') + + def enter_alternate_screen(self): + self.write_raw('\x1b[?1049h\x1b[H') + + def quit_alternate_screen(self): + self.write_raw('\x1b[?1049l') + + def enable_mouse_support(self): + self.write_raw('\x1b[?1000h') + + # 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): + self.write_raw('\x1b[?1000l') + self.write_raw('\x1b[?1015l') + self.write_raw('\x1b[?1006l') + + def erase_end_of_line(self): + """ + Erases from the current cursor position to the end of the current line. + """ + self.write_raw('\x1b[K') + + def erase_down(self): + """ + Erases the screen from the current line down to the bottom of the + screen. + """ + self.write_raw('\x1b[J') + + def reset_attributes(self): + self.write_raw('\x1b[0m') + + def set_attributes(self, attrs): + """ + Create new style and output. + + :param attrs: `Attrs` instance. + """ + if self.true_color() and not self.ansi_colors_only(): + self.write_raw(self._escape_code_cache_true_color[attrs]) + else: + self.write_raw(self._escape_code_cache[attrs]) + + def disable_autowrap(self): + self.write_raw('\x1b[?7l') + + def enable_autowrap(self): + self.write_raw('\x1b[?7h') + + def enable_bracketed_paste(self): + self.write_raw('\x1b[?2004h') + + def disable_bracketed_paste(self): + self.write_raw('\x1b[?2004l') + + def cursor_goto(self, row=0, column=0): + """ Move cursor position. """ + self.write_raw('\x1b[%i;%iH' % (row, column)) + + def cursor_up(self, amount): + if amount == 0: + pass + elif amount == 1: + self.write_raw('\x1b[A') + else: + self.write_raw('\x1b[%iA' % amount) + + def cursor_down(self, amount): + 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): + if amount == 0: + pass + elif amount == 1: + self.write_raw('\x1b[C') + else: + self.write_raw('\x1b[%iC' % amount) + + def cursor_backward(self, amount): + if amount == 0: + pass + elif amount == 1: + self.write_raw('\b') # '\x1b[D' + else: + self.write_raw('\x1b[%iD' % amount) + + def hide_cursor(self): + self.write_raw('\x1b[?25l') + + def show_cursor(self): + self.write_raw('\x1b[?12l\x1b[?25h') # Stop blinking cursor and show. + + def flush(self): + """ + Write to output stream and flush. + """ + if not self._buffer: + return + + data = ''.join(self._buffer) + + try: + # (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 self.write_binary: + if hasattr(self.stdout, 'buffer'): + out = self.stdout.buffer # Py3. + else: + out = self.stdout + out.write(data.encode(self.stdout.encoding or 'utf-8', 'replace')) + else: + self.stdout.write(data) + + self.stdout.flush() + except IOError as e: + if e.args and e.args[0] == errno.EINTR: + # Interrupted system call. Can happpen 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 + + self._buffer = [] + + def ask_for_cpr(self): + """ + Asks for a cursor position report (CPR). + """ + self.write_raw('\x1b[6n') + self.flush() + + def bell(self): + " Sound bell. " + self.write_raw('\a') + self.flush() diff --git a/src/libs/prompt_toolkit/terminal/win32_input.py b/src/libs/prompt_toolkit/terminal/win32_input.py new file mode 100644 index 0000000..3bcc12b --- /dev/null +++ b/src/libs/prompt_toolkit/terminal/win32_input.py @@ -0,0 +1,364 @@ +from __future__ import unicode_literals +from ctypes import windll, pointer +from ctypes.wintypes import DWORD +from six.moves import range + +from libs.prompt_toolkit.key_binding.input_processor import KeyPress +from libs.prompt_toolkit.keys import Keys +from libs.prompt_toolkit.mouse_events import MouseEventType +from libs.prompt_toolkit.win32_types import EventTypes, KEY_EVENT_RECORD, MOUSE_EVENT_RECORD, INPUT_RECORD, STD_INPUT_HANDLE + +import msvcrt +import os +import sys +import six + +__all__ = ( + 'ConsoleInputReader', + 'raw_mode', + 'cooked_mode' +) + + +class ConsoleInputReader(object): + """ + :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, # Contrel-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.ControlJ, # Control-J NOTE: Windows sends \r instead of + # \n when pressing enter. We turn it into \n + # to be compatible with other platforms. + 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-hypen.) + b'\x7f': Keys.Backspace, # (127) Backspace + } + + # 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=True): + self._fdcon = None + self.recognize_paste = recognize_paste + + # When stdin is a tty, use that handle, otherwise, create a handle from + # CONIN$. + if sys.stdin.isatty(): + self.handle = windll.kernel32.GetStdHandle(STD_INPUT_HANDLE) + else: + self._fdcon = os.open('CONIN$', os.O_RDWR | os.O_BINARY) + self.handle = msvcrt.get_osfhandle(self._fdcon) + + def close(self): + " Close fdcon. " + if self._fdcon is not None: + os.close(self._fdcon) + + def read(self): + """ + 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() + + # 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)) + + if self.recognize_paste and self._is_paste(all_keys): + gen = iter(all_keys) + for k in gen: + # Pasting: if the current key consists of text or \n, turn it + # into a BracketedPaste. + data = [] + while k and (isinstance(k.key, six.text_type) or + k.key == Keys.ControlJ): + 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: + for k in all_keys: + yield k + + def _get_keys(self, read, input_records): + """ + 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 type(ev) == KEY_EVENT_RECORD and ev.KeyDown: + for key_press in self._event_to_key_presses(ev): + yield key_press + + elif type(ev) == MOUSE_EVENT_RECORD: + for key_press in self._handle_mouse(ev): + yield key_press + + @staticmethod + def _is_paste(keys): + """ + 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 isinstance(k.key, six.text_type): + text_count += 1 + if k.key == Keys.ControlJ: + newline_count += 1 + + return newline_count >= 1 and text_count > 1 + + def _event_to_key_presses(self, ev): + """ + For this `KEY_EVENT_RECORD`, return a list of `KeyPress` instances. + """ + assert type(ev) == KEY_EVENT_RECORD and ev.KeyDown + + result = None + + u_char = ev.uChar.UnicodeChar + ascii_char = u_char.encode('utf-8') + + # NOTE: We don't use `ev.uChar.AsciiChar`. That appears to be latin-1 + # encoded. 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) + + # Correctly handle Control-Arrow keys. + if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or + ev.ControlKeyState & self.RIGHT_CTRL_PRESSED) and result: + if result.key == Keys.Left: + result.key = Keys.ControlLeft + + if result.key == Keys.Right: + result.key = Keys.ControlRight + + if result.key == Keys.Up: + result.key = Keys.ControlUp + + if result.key == Keys.Down: + result.key = Keys.ControlDown + + # Turn 'Tab' into 'BackTab' when shift was pressed. + if ev.ControlKeyState & self.SHIFT_PRESSED and result: + if result.key == Keys.Tab: + result.key = Keys.BackTab + + # Turn 'Space' into 'ControlSpace' when control was pressed. + if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or + ev.ControlKeyState & 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 (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or + ev.ControlKeyState & 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 = ev.ControlKeyState & self.LEFT_ALT_PRESSED + + if meta_pressed: + return [KeyPress(Keys.Escape, ''), result] + else: + return [result] + + else: + return [] + + def _handle_mouse(self, ev): + """ + Handle mouse events. Return a list of KeyPress instances. + """ + FROM_LEFT_1ST_BUTTON_PRESSED = 0x1 + + result = [] + + # Check event type. + if ev.ButtonState == FROM_LEFT_1ST_BUTTON_PRESSED: + # On a key press, generate both the mouse down and up event. + for event_type in [MouseEventType.MOUSE_DOWN, MouseEventType.MOUSE_UP]: + data = ';'.join([ + event_type, + str(ev.MousePosition.X), + str(ev.MousePosition.Y) + ]) + result.append(KeyPress(Keys.WindowsMouseEvent, data)) + + return result + + +class raw_mode(object): + """ + :: + + with raw_mode(stdin): + ''' the windows terminal is now in 'raw' mode. ''' + + The ``fileno`` attribute is ignored. This is to be compatble with the + `raw_input` method of `.vt100_input`. + """ + def __init__(self, fileno=None): + self.handle = windll.kernel32.GetStdHandle(STD_INPUT_HANDLE) + + def __enter__(self): + # Remember original mode. + original_mode = DWORD() + windll.kernel32.GetConsoleMode(self.handle, pointer(original_mode)) + self.original_mode = original_mode + + self._patch() + + def _patch(self): + # 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, **kw): + # 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 raw mode ''' + """ + def _patch(self): + # 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/libs/prompt_toolkit/terminal/win32_output.py b/src/libs/prompt_toolkit/terminal/win32_output.py new file mode 100644 index 0000000..fde4967 --- /dev/null +++ b/src/libs/prompt_toolkit/terminal/win32_output.py @@ -0,0 +1,556 @@ +from __future__ import unicode_literals + +from ctypes import windll, byref, ArgumentError, c_char, c_long, c_ulong, c_uint, pointer +from ctypes.wintypes import DWORD + +from libs.prompt_toolkit.renderer import Output +from libs.prompt_toolkit.styles import ANSI_COLOR_NAMES +from libs.prompt_toolkit.win32_types import CONSOLE_SCREEN_BUFFER_INFO, STD_OUTPUT_HANDLE, STD_INPUT_HANDLE, COORD, SMALL_RECT + +import os +import six + +__all__ = ( + 'Win32Output', +) + + +def _coord_byval(coord): + """ + 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 runing ``ptipython`` is run (only with IPython), we often got the following error:: + + Error in 'SetConsoleCursorPosition'. + ArgumentError("argument 2: : wrong type",) + argument 2: : 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): + # 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(NoConsoleScreenBufferError, self).__init__(message) + + +class Win32Output(Output): + """ + I/O abstraction for rendering to Windows consoles. + (cmd.exe and similar.) + """ + def __init__(self, stdout, use_complete_width=False): + self.use_complete_width = use_complete_width + + self._buffer = [] + self.stdout = stdout + self.hconsole = windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + + self._in_alternate_screen = 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): + " Return file descriptor. " + return self.stdout.fileno() + + def encoding(self): + " Return encoding used for stdout. " + return self.stdout.encoding + + def write(self, data): + self._buffer.append(data) + + def write_raw(self, data): + " For win32, there is no difference between write and write_raw. " + self.write(data) + + def get_size(self): + from libs.prompt_toolkit.layout.screen import 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, *a, **kw): + """ + 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((' Error in %r %r %s\n' % (func.__name__, e, e)).encode('utf-8')) + + def get_win32_screen_buffer_info(self): + """ + 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): + """ + Set terminal title. + """ + assert isinstance(title, six.text_type) + self._winapi(windll.kernel32.SetConsoleTitleW, title) + + def clear_title(self): + self._winapi(windll.kernel32.SetConsoleTitleW, '') + + def erase_screen(self): + 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): + 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): + """ + """ + 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, length): + 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): + " Reset the console foreground/background color. " + self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, + self.default_attrs) + + def set_attributes(self, attrs): + fgcolor, bgcolor, bold, underline, italic, blink, reverse = attrs + + # Start from the default attributes. + attrs = self.default_attrs + + # Override the last four bits: foreground color. + if fgcolor is not None: + attrs = attrs & ~0xf + attrs |= self.color_lookup_table.lookup_fg_color(fgcolor) + + # Override the next four bits: background color. + if bgcolor is not None: + attrs = attrs & ~0xf0 + attrs |= self.color_lookup_table.lookup_bg_color(bgcolor) + + # Reverse: swap these four bits groups. + if reverse: + attrs = (attrs & ~0xff) | ((attrs & 0xf) << 4) | ((attrs & 0xf0) >> 4) + + self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, attrs) + + def disable_autowrap(self): + # Not supported by Windows. + pass + + def enable_autowrap(self): + # Not supported by Windows. + pass + + def cursor_goto(self, row=0, column=0): + pos = COORD(x=column, y=row) + self._winapi(windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)) + + def cursor_up(self, amount): + sr = self.get_win32_screen_buffer_info().dwCursorPosition + pos = COORD(sr.X, sr.Y - amount) + self._winapi(windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)) + + def cursor_down(self, amount): + self.cursor_up(-amount) + + def cursor_forward(self, amount): + 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(max(0, sr.X + amount), sr.Y) + self._winapi(windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)) + + def cursor_backward(self, amount): + self.cursor_forward(-amount) + + def flush(self): + """ + 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 soluton + # in oder 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): + info = self.get_win32_screen_buffer_info() + return info.srWindow.Bottom - info.dwCursorPosition.Y + 1 + + def scroll_buffer_to_prompt(self): + """ + 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): + """ + 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 = 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): + """ + Make stdout again the active buffer. + """ + if self._in_alternate_screen: + stdout = 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): + ENABLE_MOUSE_INPUT = 0x10 + 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 disable_mouse_support(self): + ENABLE_MOUSE_INPUT = 0x10 + 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): + pass + + def show_cursor(self): + pass + + @classmethod + def win32_refresh_window(cls): + """ + 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 = windll.kernel32.GetConsoleWindow() + + RDW_INVALIDATE = 0x0001 + windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE)) + + +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 BACKROUND_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): + " Create a table that maps the 16 named ansi colors to their Windows code. " + return { + 'ansidefault': color_cls.BLACK, + 'ansiblack': color_cls.BLACK, + 'ansidarkgray': color_cls.BLACK | color_cls.INTENSITY, + 'ansilightgray': color_cls.GRAY, + 'ansiwhite': color_cls.GRAY | color_cls.INTENSITY, + + # Low intensity. + 'ansidarkred': color_cls.RED, + 'ansidarkgreen': color_cls.GREEN, + 'ansibrown': color_cls.YELLOW, + 'ansidarkblue': color_cls.BLUE, + 'ansipurple': color_cls.MAGENTA, + 'ansiteal': color_cls.CYAN, + + # High intensity. + 'ansired': color_cls.RED | color_cls.INTENSITY, + 'ansigreen': color_cls.GREEN | color_cls.INTENSITY, + 'ansiyellow': color_cls.YELLOW | color_cls.INTENSITY, + 'ansiblue': color_cls.BLUE | color_cls.INTENSITY, + 'ansifuchsia': color_cls.MAGENTA | color_cls.INTENSITY, + 'ansiturquoise': color_cls.CYAN | color_cls.INTENSITY, + } + +FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR) +BG_ANSI_COLORS = _create_ansi_color_dict(BACKROUND_COLOR) + +assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) +assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) + + +class ColorLookupTable(object): + """ + Inspired by pygments/formatters/terminal256.py + """ + def __init__(self): + self._win32_colors = self._build_color_table() + self.best_match = {} # Cache + + @staticmethod + def _build_color_table(): + """ + Build an RGB-to-256 color conversion table + """ + FG = FOREGROUND_COLOR + BG = BACKROUND_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, g, b): + 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): + 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): + """ + 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): + """ + 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/libs/prompt_toolkit/token.py b/src/libs/prompt_toolkit/token.py new file mode 100644 index 0000000..86caaeb --- /dev/null +++ b/src/libs/prompt_toolkit/token.py @@ -0,0 +1,47 @@ +""" +The Token class, interchangeable with ``pygments.token``. + +A `Token` has some semantics for a piece of text that is given a style through +a :class:`~libs.prompt_toolkit.styles.Style` class. A pygments lexer for instance, +returns a list of (Token, text) tuples. Each fragment of text has a token +assigned, which when combined with a style sheet, will determine the fine +style. +""" + +# If we don't need any lexers or style classes from Pygments, we don't want +# Pygments to be installed for only the following 10 lines of code. So, there +# is some duplication, but this should stay compatible with Pygments. + +__all__ = ( + 'Token', + 'ZeroWidthEscape', +) + + +class _TokenType(tuple): + def __getattr__(self, val): + if not val or not val[0].isupper(): + return tuple.__getattribute__(self, val) + + new = _TokenType(self + (val,)) + setattr(self, val, new) + return new + + def __repr__(self): + return 'Token' + (self and '.' or '') + '.'.join(self) + + +# Prefer the Token class from Pygments. If Pygments is not installed, use our +# minimalistic Token class. +try: + from pygments.token import Token +except ImportError: + Token = _TokenType() + + +# Built-in tokens: + +#: `ZeroWidthEscape` can be used for raw VT escape sequences that don't +#: cause the cursor position to move. (E.g. FinalTerm's escape sequences +#: for shell integration.) +ZeroWidthEscape = Token.ZeroWidthEscape diff --git a/src/libs/prompt_toolkit/utils.py b/src/libs/prompt_toolkit/utils.py new file mode 100644 index 0000000..3cd9318 --- /dev/null +++ b/src/libs/prompt_toolkit/utils.py @@ -0,0 +1,240 @@ +from __future__ import unicode_literals +import inspect +import os +import signal +import sys +import threading +import weakref + +from wcwidth import wcwidth +from six.moves import range + + +__all__ = ( + 'Event', + 'DummyContext', + 'get_cwidth', + 'suspend_to_background_supported', + 'is_conemu_ansi', + 'is_windows', + 'in_main_thread', + 'take_using_weights', + 'test_callable_args', +) + + +class Event(object): + """ + 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, handler=None): + self.sender = sender + self._handlers = [] + + if handler is not None: + self += handler + + def __call__(self): + " Fire event. " + for handler in self._handlers: + handler(self.sender) + + def fire(self): + " Alias for just calling the event. " + self() + + def __iadd__(self, handler): + """ + Add another handler to this callback. + (Handler should be a callable that takes exactly one parameter: the + sender object.) + """ + # Test handler. + assert callable(handler) + if not test_callable_args(handler, [None]): + raise TypeError("%r doesn't take exactly one argument." % handler) + + # Add to list of event handlers. + self._handlers.append(handler) + return self + + def __isub__(self, handler): + """ + Remove a handler from this callback. + """ + self._handlers.remove(handler) + return self + + +# Cache of signatures. Improves the performance of `test_callable_args`. +_signatures_cache = weakref.WeakKeyDictionary() + + +def test_callable_args(func, args): + """ + Return True when this function can be called with the given arguments. + """ + assert isinstance(args, (list, tuple)) + signature = getattr(inspect, 'signature', None) + + if signature is not None: + # For Python 3, use inspect.signature. + try: + sig = _signatures_cache[func] + except KeyError: + sig = signature(func) + _signatures_cache[func] = sig + + try: + sig.bind(*args) + except TypeError: + return False + else: + return True + else: + # For older Python versions, fall back to using getargspec. + spec = inspect.getargspec(func) + + # Drop the 'self' + def drop_self(spec): + args, varargs, varkw, defaults = spec + if args[0:1] == ['self']: + args = args[1:] + return inspect.ArgSpec(args, varargs, varkw, defaults) + + spec = drop_self(spec) + + # When taking *args, always return True. + if spec.varargs is not None: + return True + + # Test whether the given amount of args is between the min and max + # accepted argument counts. + return len(spec.args) - len(spec.defaults or []) <= len(args) <= len(spec.args) + + +class DummyContext(object): + """ + (contextlib.nested is not available on Py3) + """ + def __enter__(self): + pass + + def __exit__(self, *a): + pass + + +class _CharSizesCache(dict): + """ + Cache for wcwidth sizes. + """ + def __missing__(self, string): + # 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. + if len(string) == 1: + result = max(0, wcwidth(string)) + else: + result = sum(max(0, wcwidth(c)) for c in string) + + # Cache for short strings. + # (It's hard to tell what we can consider short...) + if len(string) < 256: + self[string] = result + + return result + + +_CHAR_SIZES_CACHE = _CharSizesCache() + + +def get_cwidth(string): + """ + Return width of a string. Wrapper around ``wcwidth``. + """ + return _CHAR_SIZES_CACHE[string] + + +def suspend_to_background_supported(): + """ + Returns `True` when the Python implementation supports + suspend-to-background. This is typically `False' on Windows systems. + """ + return hasattr(signal, 'SIGTSTP') + + +def is_windows(): + """ + True when we are using Windows. + """ + return sys.platform.startswith('win') # E.g. 'win32', not 'darwin' or 'linux2' + + +def is_conemu_ansi(): + """ + True when the ConEmu Windows console is used. + """ + return is_windows() and os.environ.get('ConEmuANSI', 'OFF') == 'ON' + + +def in_main_thread(): + """ + True when the current thread is the main thread. + """ + return threading.current_thread().__class__.__name__ == '_MainThread' + + +def take_using_weights(items, weights): + """ + 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 isinstance(items, list) + assert isinstance(weights, list) + assert all(isinstance(i, int) for i in weights) + assert len(items) == len(weights) + assert len(items) > 0 + + 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 diff --git a/src/libs/prompt_toolkit/validation.py b/src/libs/prompt_toolkit/validation.py new file mode 100644 index 0000000..39a27a4 --- /dev/null +++ b/src/libs/prompt_toolkit/validation.py @@ -0,0 +1,64 @@ +""" +Input validation for a `Buffer`. +(Validators will be called before accepting input.) +""" +from __future__ import unicode_literals +from .filters import to_simple_filter + +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +__all__ = ( + 'ConditionalValidator', + 'ValidationError', + 'Validator', +) + + +class ValidationError(Exception): + """ + Error raised by :meth:`.Validator.validate`. + + :param cursor_position: The cursor position where the error occured. + :param message: Text. + """ + def __init__(self, cursor_position=0, message=''): + super(ValidationError, self).__init__(message) + self.cursor_position = cursor_position + self.message = message + + def __repr__(self): + return '%s(cursor_position=%r, message=%r)' % ( + self.__class__.__name__, self.cursor_position, self.message) + + +class Validator(with_metaclass(ABCMeta, object)): + """ + Abstract base class for an input validator. + """ + @abstractmethod + def validate(self, document): + """ + Validate the input. + If invalid, this should raise a :class:`.ValidationError`. + + :param document: :class:`~libs.prompt_toolkit.document.Document` instance. + """ + pass + + +class ConditionalValidator(Validator): + """ + Validator that can be switched on/off according to + a filter. (This wraps around another validator.) + """ + def __init__(self, validator, filter): + assert isinstance(validator, Validator) + + self.validator = validator + self.filter = to_simple_filter(filter) + + def validate(self, document): + # Call the validator only if the filter is active. + if self.filter(): + self.validator.validate(document) diff --git a/src/libs/prompt_toolkit/win32_types.py b/src/libs/prompt_toolkit/win32_types.py new file mode 100644 index 0000000..ba2d90d --- /dev/null +++ b/src/libs/prompt_toolkit/win32_types.py @@ -0,0 +1,155 @@ +from ctypes import Union, Structure, c_char, c_short, c_long, c_ulong +from ctypes.wintypes import DWORD, BOOL, LPVOID, WORD, WCHAR + + +# 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 + """ + _fields_ = [ + ('X', c_short), # Short + ('Y', c_short), # Short + ] + + def __repr__(self): + return '%s(X=%r, Y=%r, type_x=%r, type_y=%r)' % ( + self.__class__.__name__, self.X, self.Y, type(self.X), type(self.Y)) + + +class UNICODE_OR_ASCII(Union): + _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 + """ + _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 + """ + _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 + """ + _fields_ = [ + ('Size', COORD) + ] + + +class MENU_EVENT_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms684213(v=vs.85).aspx + """ + _fields_ = [ + ('CommandId', c_long) # uint + ] + + +class FOCUS_EVENT_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms683149(v=vs.85).aspx + """ + _fields_ = [ + ('SetFocus', c_long) # bool + ] + + +class EVENT_RECORD(Union): + _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 + """ + _fields_ = [ + ('EventType', c_short), # word + ('Event', EVENT_RECORD) # Union. + ] + + +EventTypes = { + 1: 'KeyEvent', + 2: 'MouseEvent', + 4: 'WindowBufferSizeEvent', + 8: 'MenuEvent', + 16: 'FocusEvent' +} + + +class SMALL_RECT(Structure): + """struct in wincon.h.""" + _fields_ = [ + ("Left", c_short), + ("Top", c_short), + ("Right", c_short), + ("Bottom", c_short), + ] + + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + """struct in wincon.h.""" + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", WORD), + ("srWindow", SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + + def __str__(self): + return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( + 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 + """ + _fields_ = [ + ('nLength', DWORD), + ('lpSecurityDescriptor', LPVOID), + ('bInheritHandle', BOOL), + ] diff --git a/src/shellmen b/src/shellmen deleted file mode 100755 index 507bcae..0000000 --- a/src/shellmen +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# set -o xtrace ## To debug scripts -# set -o errexit ## To exit on error -# set -o errunset ## To exit if a variable is referenced but not set - - -function main() { - SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" - cd "${SCRIPTPATH}" - python3 . -} -main $@; diff --git a/src/signal_classes/Controller.py b/src/signal_classes/Controller.py deleted file mode 100644 index c447326..0000000 --- a/src/signal_classes/Controller.py +++ /dev/null @@ -1,185 +0,0 @@ -# Python imports -import threading, subprocess, traceback -from os.path import isfile -from os import listdir - - -# Gtk imports -from xdg.DesktopEntry import DesktopEntry - -# Application imports -from .mixins import * -from . import Menu, Controller_Data - - - -def threaded(fn): - def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs).start() - - return wrapper - - -class Controller(ProcessorMixin, Menu, Controller_Data): - def __init__(self, _settings, args, unknownargs): - super().__init__(_settings, args) - - self.setup_controller_data(_settings) - - base_options = ["[ TO MAIN MENU ]", "Favorites"] - self.menu_data = self.get_desktop_files_info(self.app_paths) - query = "" - - while True: - try: - self.clear_console() - group = self.call_method("main_menu")["group"] - self.clear_console() - - if "Search..." in group: - query = self.call_method("search_menu")["query"] - if "[ Set Favorites ]" in group: - programs_list = self.get_sub_group("Search...", "") - fixed_programs_list = [] - - for program in programs_list: - fixed_programs_list.append({'name': program}) - - self.favorites = self.call_method("set_favorites_menu", [fixed_programs_list])["set_faves"] - self.settings.save_faves(self.favorites) - continue - if "[ Exit ]" in group: - break - - programs_list = ["[ TO MAIN MENU ]"] - programs_list += self.get_sub_group(group, query) - entry = self.call_method("sub_menu", [group, programs_list])["prog"] - - if entry not in base_options: - self.execute_program(self.flat_menu_data, entry) - except Exception as e: - self.logger.debug(f"Traceback: {traceback.print_exc()}") - self.logger.debug(f"Exception: {e}") - - - - def get_desktop_files_info(self, paths): - menu_objects = { - "Accessories": {}, - "Multimedia": {}, - "Graphics": {}, - "Game": {}, - "Office": {}, - "Development": {}, - "Internet": {}, - "Settings": {}, - "System": {}, - "Wine": {}, - "Other": {} - } - - for path in paths: - self.list_and_update_desktop_iles(path, menu_objects); - - return menu_objects - - def list_and_update_desktop_iles(self, path, menu_objects): - try: - for f in listdir(path): - full_path = f"{path}/{f}" - if isfile(full_path) and f.endswith(".desktop"): - xdg_object = DesktopEntry(full_path) - hidden = xdg_object.getHidden() - nodisplay = xdg_object.getNoDisplay() - type = xdg_object.getType() - groups = xdg_object.getCategories() - # Do not show those marked as hidden or not to display - if hidden or nodisplay: - continue - - if type == "Application" and groups != "": - title = xdg_object.getName() - comment = xdg_object.getComment() - # icon = xdg_object.getIcon() - main_exec = xdg_object.getExec() - try_exec = xdg_object.getTryExec() - - group = "" - if "Accessories" in groups or "Utility" in groups: - group = "Accessories" - elif "Multimedia" in groups or "Video" in groups or "Audio" in groups: - group = "Multimedia" - elif "Development" in groups: - group = "Development" - elif "Game" in groups: - group = "Game" - elif "Internet" in groups or "Network" in groups: - group = "Internet" - elif "Graphics" in groups: - group = "Graphics" - elif "Office" in groups: - group = "Office" - elif "System" in groups: - group = "System" - elif "Settings" in groups: - group = "Settings" - elif "Wine" in groups: - group = "Wine" - else: - group = "Other" - - chunk_data = { - "groups": groups, "comment": comment, - "exec": main_exec, "try_exec": try_exec, - "fileName": f - } - - menu_objects[group][title] = chunk_data - self.flat_menu_data[title] = chunk_data - except Exception as e: - self.logger.debug(e) - - - def get_sub_group(self, group, query = ""): - desktop_objects = [] - if "Search..." in group: - for key in self.flat_menu_data.keys(): - option = self.flat_menu_data[key] - keys = option.keys() - - if "comment" in keys and len(option["comment"]) > 0: - if query.lower() in option["comment"].lower(): - desktop_objects.append( f"{key} || {option['comment']}" ) - elif query.lower() in key.lower() or query.lower() in option["fileName"].lower(): - desktop_objects.append( f"{key} || {option['fileName'].replace('.desktop', '')}" ) - elif "Favorites" in group: - desktop_objects = self.favorites - else: - for key in self.menu_data[group]: - option = self.flat_menu_data[key] - keys = option.keys() - if "comment" in keys and len(option["comment"]) > 0: - if query.lower() in option["comment"].lower(): - desktop_objects.append( f"{key} || {option['comment']}" ) - elif query.lower() in key.lower() or query.lower() in option["fileName"].lower(): - desktop_objects.append( f"{key} || {option['fileName'].replace('.desktop', '')}" ) - - return desktop_objects - - - - - def tear_down(self, widget=None, eve=None): - quit() - - def get_clipboard_data(self): - proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE) - retcode = proc.wait() - data = proc.stdout.read() - return data.decode("utf-8").strip() - - def set_clipboard_data(self, data): - proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE) - proc.stdin.write(data) - proc.stdin.close() - retcode = proc.wait() diff --git a/src/signal_classes/Controller_Data.py b/src/signal_classes/Controller_Data.py deleted file mode 100644 index 13b2122..0000000 --- a/src/signal_classes/Controller_Data.py +++ /dev/null @@ -1,32 +0,0 @@ -# Python imports -import os, signal - -# Lib imports -from gi.repository import GLib - -# Application imports - - - -class Controller_Data: - def clear_console(self): - os.system('cls' if os.name == 'nt' else 'clear') - - def call_method(self, _method_name, data = None): - method_name = str(_method_name) - method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}") - return method(data) if data else method() - - def has_method(self, obj, name): - return callable(getattr(obj, name, None)) - - def setup_controller_data(self, _settings): - self.settings = _settings - self.logger = self.settings.get_logger() - self.app_paths = self.settings.get_app_paths() - self.favorites_path = self.settings.get_favorites_path() - self.favorites = self.settings.get_favorites() - self.menu_data = None - self.flat_menu_data = {} - - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down) diff --git a/src/signal_classes/Menu.py b/src/signal_classes/Menu.py deleted file mode 100644 index 4516b22..0000000 --- a/src/signal_classes/Menu.py +++ /dev/null @@ -1,88 +0,0 @@ -# Python imports - -from __future__ import print_function, unicode_literals -# from pprint import pprint -import json - - -# Lib imports -from PyInquirer import style_from_dict, Token, prompt, Separator - -# Application imports -from .mixins import StylesMixin - - - - -GROUPS = [ "Search...", "Favorites", "Accessories", "Multimedia", "Graphics", "Office", - "Development", "Internet", "Settings", "System", "Game", "Wine", - "Other", "[ Set Favorites ]", "[ Exit ]" - ] - - -class Menu(StylesMixin): - """ - The menu class has sub methods that are called per run. - """ - def __init__(self, settings, args): - """ - Construct a new 'Menu' object which pulls in mixins. - :param args: The terminal passed arguments - - :return: returns nothing - """ - self.logger = settings.get_logger() - self.theme = self.call_method(args.theme) - - - def main_menu(self, _group_list = None): - """ - Displays the main menu using the defined GROUPS list... - """ - group_list = GROUPS if not _group_list else _group_list - menu = { - 'type': 'list', - 'name': 'group', - 'message': '[ MAIN MENU ]', - 'choices': group_list - } - - return prompt(menu, style=self.theme) - - - def set_favorites_menu(self, _group_list = None): - GROUPS = [{'name': '[ TO MAIN MENU ]'}, {'name': 'This is a stub method for Favorites...'}] - group_list = GROUPS if not _group_list[0] else _group_list[0] - menu = { - 'type': 'checkbox', - 'qmark': '>', - 'message': 'Select Favorites', - 'name': 'set_faves', - 'choices': group_list - } - - return prompt(menu, style=self.theme) - - - def sub_menu(self, data = ["NO GROUP NAME", "NO PROGRAMS PASSED IN"]): - group = data[0] - prog_list = data[1] - - menu = { - 'type': 'list', - 'name': 'prog', - 'message': f'[ {group} ]', - 'choices': prog_list - } - - return prompt(menu, style=self.theme) - - - def search_menu(self): - menu = { - 'type': 'input', - 'name': 'query', - 'message': 'Program you\'re looking for: ', - } - - return prompt(menu, style=self.theme) diff --git a/src/signal_classes/__init__.py b/src/signal_classes/__init__.py deleted file mode 100644 index b212e68..0000000 --- a/src/signal_classes/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" - Gtk Bound Signal Module -""" -from .mixins import * -from .Menu import Menu -from .Controller_Data import Controller_Data -from .Controller import Controller diff --git a/src/signal_classes/mixins/ProcessorMixin.py b/src/signal_classes/mixins/ProcessorMixin.py deleted file mode 100644 index 8f12231..0000000 --- a/src/signal_classes/mixins/ProcessorMixin.py +++ /dev/null @@ -1,36 +0,0 @@ -# Python imports -import os, subprocess - -# Lib imports - -# Application imports - - - -class ProcessorMixin: - def execute_program(self, data, entry): - parts = entry.split("||") - title = parts[0].strip() - comment = parts[1].strip() - chunk_data = data[title.strip()] - - self.logger.info(f"[Executing Program]\n\t\tEntry: {entry}\n\t\tChunk Data: {chunk_data}") - self.pre_execute(chunk_data) - - def pre_execute(self, option): - try: - self.execute(option["tryExec"]) - except Exception as e: - self.logger.info(f"[Executing Program]\n\t\t Try exec failed!\n{e}") - try: - if option["exec"] and len(option["exec"]) > 0: - self.execute(option["exec"]) - except Exception as e: - self.logger.debug(e) - - - def execute(self, option): - DEVNULL = open(os.devnull, 'w') - command = option.split("%")[0] - self.logger.debug(command) - subprocess.Popen(command.split(), cwd=os.getenv("HOME"), start_new_session=True, stdout=DEVNULL, stderr=DEVNULL) diff --git a/src/signal_classes/mixins/__init__.py b/src/signal_classes/mixins/__init__.py deleted file mode 100644 index 2ff067c..0000000 --- a/src/signal_classes/mixins/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" - Mixins module -""" -from .StylesMixin import StylesMixin -from .ProcessorMixin import ProcessorMixin diff --git a/src/utils/Logger.py b/src/utils/Logger.py deleted file mode 100644 index 06eed47..0000000 --- a/src/utils/Logger.py +++ /dev/null @@ -1,56 +0,0 @@ -# Python imports -import os, logging - -# Application imports - - -class Logger: - def __init__(self, config_path): - self._CONFIG_PATH = config_path - - def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True): - """ - Create a new logging object and return it. - :note: - NOSET # Don't know the actual log level of this... (defaulting or literally none?) - Log Levels (From least to most) - Type Value - CRITICAL 50 - ERROR 40 - WARNING 30 - INFO 20 - DEBUG 10 - :param loggerName: Sets the name of the logger object. (Used in log lines) - :param createFile: Whether we create a log file or just pump to terminal - - :return: the logging object we created - """ - - globalLogLvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels - chLogLevel = logging.CRITICAL # Prety musch the only one we change ever - fhLogLevel = logging.DEBUG - log = logging.getLogger(loggerName) - log.setLevel(globalLogLvl) - - # Set our log output styles - fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S') - cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s') - - ch = logging.StreamHandler() - ch.setLevel(level=chLogLevel) - ch.setFormatter(cFormatter) - log.addHandler(ch) - - if createFile: - folder = self._CONFIG_PATH - file = f"{folder}/application.log" - - if not os.path.exists(folder): - os.mkdir(folder) - - fh = logging.FileHandler(file) - fh.setLevel(level=fhLogLevel) - fh.setFormatter(fFormatter) - log.addHandler(fh) - - return log diff --git a/src/utils/Settings.py b/src/utils/Settings.py deleted file mode 100644 index ce6eedb..0000000 --- a/src/utils/Settings.py +++ /dev/null @@ -1,51 +0,0 @@ -# Python imports -import os, json - -# Gtk imports - -# Application imports -from . import Logger - - - -class Settings: - def __init__(self): - self._SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) - self._USER_HOME = os.path.expanduser('~') - self._CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}" - self._FAVORITES_FILE = f"{self._CONFIG_PATH}/favorites.json" - self._HOME_APPS = f"{self._USER_HOME}/.local/share/applications" - self._APP_PATHS = ["/usr/share/applications", self._HOME_APPS] - - self._logger = Logger(self._CONFIG_PATH).get_logger() - self._faves = [] - - if not os.path.exists(self._CONFIG_PATH): - os.mkdir(self._CONFIG_PATH) - self._logger = Logger(self._CONFIG_PATH).get_logger() - - if not os.path.exists(self._FAVORITES_FILE): - open(self._FAVORITES_FILE, 'a').close() - - - with open(self._FAVORITES_FILE) as f: - try: - self._faves = json.load(f) - except Exception as e: - pass - - f.close() - - - - def save_faves(self, data = None): - with open(self._FAVORITES_FILE, 'w') as f: - json.dump(data, f, separators=(',', ':'), indent=4) - f.close() - - - - def get_logger(self): return self._logger - def get_favorites_path(self): return self._FAVORITES_FILE - def get_app_paths(self): return self._APP_PATHS - def get_favorites(self): return self._faves diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 415301e..a8e5edd 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,6 +1,3 @@ """ Utils module """ - -from .Logger import Logger -from .Settings import Settings diff --git a/src/utils/debugging.py b/src/utils/debugging.py new file mode 100644 index 0000000..b84193a --- /dev/null +++ b/src/utils/debugging.py @@ -0,0 +1,52 @@ +# Python imports + +# Lib imports + +# Application imports + + + +# Break into a Python console upon SIGUSR1 (Linux) or SIGBREAK (Windows: +# CTRL+Pause/Break). To be included in all production code, just in case. +def debug_signal_handler(signal, frame): + del signal + del frame + + try: + import rpdb2 + logger.debug("\n\nStarting embedded RPDB2 debugger. Password is 'foobar'\n\n") + rpdb2.start_embedded_debugger("foobar", True, True) + rpdb2.setbreak(depth=1) + return + except StandardError: + ... + + try: + from rfoo.utils import rconsole + logger.debug("\n\nStarting embedded rconsole debugger...\n\n") + rconsole.spawn_server() + return + except StandardError as ex: + ... + + try: + from pudb import set_trace + logger.debug("\n\nStarting PuDB debugger...\n\n") + set_trace(paused = True) + return + except StandardError as ex: + ... + + try: + import pdb + logger.debug("\n\nStarting embedded PDB debugger...\n\n") + pdb.Pdb(skip=['gi.*']).set_trace() + return + except StandardError as ex: + ... + + try: + import code + code.interact() + except StandardError as ex: + logger.debug(f"{ex}, returning to normal program flow...") diff --git a/src/utils/event_system.py b/src/utils/event_system.py new file mode 100644 index 0000000..9d876cf --- /dev/null +++ b/src/utils/event_system.py @@ -0,0 +1,54 @@ +# Python imports +from collections import defaultdict + +# Lib imports + +# Application imports +from .singleton import Singleton + + + +class EventSystem(Singleton): + """ Create event system. """ + + def __init__(self): + self.subscribers = defaultdict(list) + + + def subscribe(self, event_type, fn): + self.subscribers[event_type].append(fn) + + def unsubscribe(self, event_type, fn): + self.subscribers[event_type].remove(fn) + + def unsubscribe_all(self, event_type): + self.subscribers.pop(event_type, None) + + def emit(self, event_type, data = None): + if event_type in self.subscribers: + for fn in self.subscribers[event_type]: + if data: + if hasattr(data, '__iter__') and not type(data) is str: + fn(*data) + else: + fn(data) + else: + fn() + + def emit_and_await(self, event_type, data = None): + """ NOTE: Should be used when signal has only one listener and vis-a-vis """ + if event_type in self.subscribers: + response = None + for fn in self.subscribers[event_type]: + if data: + if hasattr(data, '__iter__') and not type(data) is str: + response = fn(*data) + else: + response = fn(data) + else: + response = fn() + + if not response in (None, ''): + break + + return response diff --git a/src/utils/ipc_server.py b/src/utils/ipc_server.py new file mode 100644 index 0000000..5d19ccc --- /dev/null +++ b/src/utils/ipc_server.py @@ -0,0 +1,114 @@ +# Python imports +import os +import threading +import time +from multiprocessing.connection import Client +from multiprocessing.connection import Listener + +# Lib imports + +# Application imports +from .singleton import Singleton + + + +class IPCServer(Singleton): + """ Create a listener so that other {app_name} instances send requests back to existing instance. """ + def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "socket"): + self.is_ipc_alive = False + self._ipc_port = 4848 + self._ipc_address = ipc_address + self._conn_type = conn_type + self._ipc_authkey = b'' + bytes(f'{app_name}-ipc', 'utf-8') + self._ipc_timeout = 15.0 + + if conn_type == "socket": + self._ipc_address = f'/tmp/{app_name}-ipc.sock' + elif conn_type == "full_network": + self._ipc_address = '0.0.0.0' + elif conn_type == "full_network_unsecured": + self._ipc_authkey = None + self._ipc_address = '0.0.0.0' + elif conn_type == "local_network_unsecured": + self._ipc_authkey = None + + self._subscribe_to_events() + + def _subscribe_to_events(self): + event_system.subscribe("post_file_to_ipc", self.send_ipc_message) + + + def create_ipc_listener(self) -> None: + if self._conn_type == "socket": + if os.path.exists(self._ipc_address) and settings_manager.is_dirty_start(): + os.unlink(self._ipc_address) + + listener = Listener(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey) + elif "unsecured" not in self._conn_type: + listener = Listener((self._ipc_address, self._ipc_port), authkey=self._ipc_authkey) + else: + listener = Listener((self._ipc_address, self._ipc_port)) + + + self.is_ipc_alive = True + self._run_ipc_loop(listener) + + @daemon_threaded + def _run_ipc_loop(self, listener) -> None: + # NOTE: Not thread safe if using with Gtk. Need to import GLib and use idle_add + while True: + try: + conn = listener.accept() + start_time = time.perf_counter() + self._handle_ipc_message(conn, start_time) + except Exception as e: + ... + + listener.close() + + def _handle_ipc_message(self, conn, start_time) -> None: + while True: + msg = conn.recv() + if settings_manager.is_debug(): + print(msg) + + if "FILE|" in msg: + file = msg.split("FILE|")[1].strip() + if file: + event_system.emit("handle_file_from_ipc", file) + + if "DIR|" in msg: + file = msg.split("DIR|")[1].strip() + if file: + event_system.emit("handle_dir_from_ipc", file) + + conn.close() + break + + + if msg in ['close connection', 'close server']: + conn.close() + break + + # NOTE: Not perfect but insures we don't lock up the connection for too long. + end_time = time.perf_counter() + if (end_time - start_time) > self._ipc_timeout: + conn.close() + break + + + def send_ipc_message(self, message: str = "Empty Data...") -> None: + try: + if self._conn_type == "socket": + conn = Client(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey) + elif "unsecured" not in self._conn_type: + conn = Client((self._ipc_address, self._ipc_port), authkey=self._ipc_authkey) + else: + conn = Client((self._ipc_address, self._ipc_port)) + + conn.send(message) + conn.close() + except ConnectionRefusedError as e: + print("Connection refused...") + except Exception as e: + print(repr(e)) diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..10e93c4 --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,61 @@ +# Python imports +import os +import logging + +# Lib imports + +# Application imports +from .singleton import Singleton + + + +class Logger(Singleton): + """ + Create a new logging object and return it. + :note: + NOSET # Don't know the actual log level of this... (defaulting or literally none?) + Log Levels (From least to most) + Type Value + CRITICAL 50 + ERROR 40 + WARNING 30 + INFO 20 + DEBUG 10 + :param loggerName: Sets the name of the logger object. (Used in log lines) + :param createFile: Whether we create a log file or just pump to terminal + + :return: the logging object we created + """ + + def __init__(self, config_path: str, _ch_log_lvl = logging.CRITICAL, _fh_log_lvl = logging.INFO): + self._CONFIG_PATH = config_path + self.global_lvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels + self.ch_log_lvl = _ch_log_lvl # Prety much the only one we ever change + self.fh_log_lvl = _fh_log_lvl + + def get_logger(self, loggerName: str = "NO_LOGGER_NAME_PASSED", createFile: bool = True) -> logging.Logger: + log = logging.getLogger(loggerName) + log.setLevel(self.global_lvl) + + # Set our log output styles + fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S') + cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s') + + ch = logging.StreamHandler() + ch.setLevel(level=self.ch_log_lvl) + ch.setFormatter(cFormatter) + log.addHandler(ch) + + if createFile: + folder = self._CONFIG_PATH + file = f"{folder}/application.log" + + if not os.path.exists(folder): + os.mkdir(folder) + + fh = logging.FileHandler(file) + fh.setLevel(level=self.fh_log_lvl) + fh.setFormatter(fFormatter) + log.addHandler(fh) + + return log diff --git a/src/utils/settings_manager/__init__.py b/src/utils/settings_manager/__init__.py new file mode 100644 index 0000000..a0b3452 --- /dev/null +++ b/src/utils/settings_manager/__init__.py @@ -0,0 +1,4 @@ +""" + Settings module +""" +from .manager import SettingsManager diff --git a/src/utils/settings_manager/manager.py b/src/utils/settings_manager/manager.py new file mode 100644 index 0000000..d6972a3 --- /dev/null +++ b/src/utils/settings_manager/manager.py @@ -0,0 +1,74 @@ +# Python imports +import signal +import json +from os import path +from os import mkdir + +# Gtk imports + +# Application imports +from ..singleton import Singleton +from ..styles import Styles +from .options.settings import Settings + + +class MissingConfigError(Exception): + pass + + +class SettingsManager(Singleton): + def __init__(self): + self._SCRIPT_PTH = path.dirname(path.realpath(__file__)) + self._USER_HOME = path.expanduser('~') + self._USR_PATH = f"/usr/share/{app_name.lower()}" + + self._USR_CONFIG_FILE = f"{self._USR_PATH}/settings.json" + self._HOME_CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}" + self._CONFIG_FILE = f"{self._HOME_CONFIG_PATH}/settings.json" + + if not path.exists(self._HOME_CONFIG_PATH): + mkdir(self._HOME_CONFIG_PATH) + + + self.settings: Settings = None + self._main_window = None + + self._trace_debug = False + self._debug = False + self._styles = Styles() + + def set_main_window(self, window): self._main_window = window + + def get_home_path(self) -> str: return self._USER_HOME + def get_home_config_path(self) -> str: return self._HOME_CONFIG_PATH + def get_main_window(self): return self._main_window + def get_styles(self): return self._styles + def get_style(self): return self._styles + + def is_trace_debug(self) -> bool: return self._trace_debug + def is_debug(self) -> bool: return self._debug + + def set_trace_debug(self, trace_debug: bool): + self._trace_debug = trace_debug + + def set_debug(self, debug: bool): + self._debug = debug + + def call_method(self, target_class = None, _method_name = None, data = None): + method_name = str(_method_name) + method = getattr(target_class, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}") + return method(data) if data else method() + + def load_settings(self): + if not path.exists(self._CONFIG_FILE): + self.settings = Settings() + return + + with open(self._CONFIG_FILE) as file: + data = json.load(file) + data["load_defaults"] = False + self.settings = Settings(**data) + + def save_settings(self): + with open(self._CONFIG_FILE, 'w') as outfile: + json.dump(self.settings.as_dict(), outfile, separators=(',', ':'), indent=4) diff --git a/src/utils/settings_manager/options/__init__.py b/src/utils/settings_manager/options/__init__.py new file mode 100644 index 0000000..0046efd --- /dev/null +++ b/src/utils/settings_manager/options/__init__.py @@ -0,0 +1,8 @@ +""" + Options module +""" +from .settings import Settings +from .config import Config +from .filters import Filters +from .theming import Theming +from .debugging import Debugging diff --git a/src/utils/settings_manager/options/config.py b/src/utils/settings_manager/options/config.py new file mode 100644 index 0000000..f65d7d0 --- /dev/null +++ b/src/utils/settings_manager/options/config.py @@ -0,0 +1,31 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + +@dataclass +class Config: + thumbnailer_path: str = "ffmpegthumbnailer" + blender_thumbnailer_path: str = "" + mplayer_options: str = "-quiet -really-quiet -xy 1600 -geometry 50%:50%" + music_app: str = "deadbeef" + media_app: str = "mpv" + image_app: str = "mirage" + office_app: str = "libreoffice" + pdf_app: str = "evince" + code_app: str = "atom" + text_app: str = "mousepad" + terminal_app: str = "terminator" + file_manager_app: str = "solarfm" + container_icon_wh: list = field(default_factory=lambda: [128, 128]) + video_icon_wh: list = field(default_factory=lambda: [128, 64]) + sys_icon_wh: list = field(default_factory=lambda: [56, 56]) + steam_cdn_url: str = "https://steamcdn-a.akamaihd.net/steam/apps/" + remux_folder_max_disk_usage: str = "8589934592" + application_dirs: list = field(default_factory=lambda: [ + "/usr/share/applications", + f"{settings_manager.get_home_path()}/.local/share/applications" + ]) diff --git a/src/utils/settings_manager/options/debugging.py b/src/utils/settings_manager/options/debugging.py new file mode 100644 index 0000000..3fc605d --- /dev/null +++ b/src/utils/settings_manager/options/debugging.py @@ -0,0 +1,12 @@ +# Python imports +from dataclasses import dataclass + +# Lib imports + +# Application imports + + +@dataclass +class Debugging: + ch_log_lvl: int = 10 + fh_log_lvl: int = 20 diff --git a/src/utils/settings_manager/options/favorites.py b/src/utils/settings_manager/options/favorites.py new file mode 100644 index 0000000..f15418a --- /dev/null +++ b/src/utils/settings_manager/options/favorites.py @@ -0,0 +1,11 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + +@dataclass +class Favorites: + apps: list = field(default_factory=lambda: []) diff --git a/src/utils/settings_manager/options/filters.py b/src/utils/settings_manager/options/filters.py new file mode 100644 index 0000000..3fe8a7f --- /dev/null +++ b/src/utils/settings_manager/options/filters.py @@ -0,0 +1,36 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + +@dataclass(slots = True) +class Filters: + accessories: list = field(default_factory=lambda: [ + "Utility" + ]) + multimedia: list = field(default_factory=lambda: [ + "Video", + "Audio" + ]) + graphics: list = field(default_factory=lambda: [ + ]) + game: list = field(default_factory=lambda: [ + ]) + office: list = field(default_factory=lambda: [ + ]) + development: list = field(default_factory=lambda:[ + ]) + internet: list = field(default_factory=lambda: [ + "Network" + ]) + settings: list = field(default_factory=lambda: [ + ]) + system: list = field(default_factory=lambda: [ + ]) + wine: list = field(default_factory=lambda: [ + ]) + other: list = field(default_factory=lambda: [ + ]) diff --git a/src/utils/settings_manager/options/settings.py b/src/utils/settings_manager/options/settings.py new file mode 100644 index 0000000..5ec4b1f --- /dev/null +++ b/src/utils/settings_manager/options/settings.py @@ -0,0 +1,33 @@ +# Python imports +from dataclasses import dataclass, field +from dataclasses import asdict + +# Gtk imports + +# Application imports +from .config import Config +from .favorites import Favorites +from .filters import Filters +from .theming import Theming +from .debugging import Debugging + + +@dataclass +class Settings: + load_defaults: bool = True + config: Config = field(default_factory=lambda: Config()) + favorites: Favorites = field(default_factory=lambda: Favorites()) + filters: Filters = field(default_factory=lambda: Filters()) + theming: Theming = field(default_factory=lambda: Theming()) + debugging: Debugging = field(default_factory=lambda: Debugging()) + + def __post_init__(self): + if not self.load_defaults: + self.load_defaults = False + self.config = Config(**self.config) + self.filters = Filters(**self.filters) + self.theming = Theming(**self.theming) + self.debugging = Debugging(**self.debugging) + + def as_dict(self): + return asdict(self) diff --git a/src/utils/settings_manager/options/theming.py b/src/utils/settings_manager/options/theming.py new file mode 100644 index 0000000..034f7bd --- /dev/null +++ b/src/utils/settings_manager/options/theming.py @@ -0,0 +1,13 @@ +# Python imports +from dataclasses import dataclass + +# Lib imports + +# Application imports + + +@dataclass +class Theming: + success_color: str = "#88cc27" + warning_color: str = "#ffa800" + error_color: str = "#ff0000" diff --git a/src/utils/settings_manager/start_check_mixin.py b/src/utils/settings_manager/start_check_mixin.py new file mode 100644 index 0000000..688da36 --- /dev/null +++ b/src/utils/settings_manager/start_check_mixin.py @@ -0,0 +1,51 @@ +# Python imports +import os +import json +import inspect + +# Lib imports + +# Application imports + + + + +class StartCheckMixin: + def is_dirty_start(self) -> bool: return self._dirty_start + def clear_pid(self): self._clean_pid() + + def do_dirty_start_check(self): + if not os.path.exists(self._PID_FILE): + self._write_new_pid() + else: + with open(self._PID_FILE, "r") as _pid: + pid = _pid.readline().strip() + if pid not in ("", None): + self._check_alive_status(int(pid)) + else: + self._write_new_pid() + + """ Check For the existence of a unix pid. """ + def _check_alive_status(self, pid): + print(f"PID Found: {pid}") + try: + os.kill(pid, 0) + except OSError: + print(f"{app_name} is starting dirty...") + self._dirty_start = True + self._write_new_pid() + return + + print("PID is alive... Let downstream errors (sans debug args) handle app closure propigation.") + + def _write_new_pid(self): + pid = os.getpid() + self._write_pid(pid) + print(f"{app_name} PID: {pid}") + + def _clean_pid(self): + os.unlink(self._PID_FILE) + + def _write_pid(self, pid): + with open(self._PID_FILE, "w") as _pid: + _pid.write(f"{pid}") diff --git a/src/utils/singleton.py b/src/utils/singleton.py new file mode 100644 index 0000000..23b7191 --- /dev/null +++ b/src/utils/singleton.py @@ -0,0 +1,24 @@ +# Python imports + +# Lib imports + +# Application imports + + + +class SingletonError(Exception): + pass + + + +class Singleton: + ccount = 0 + + def __new__(cls, *args, **kwargs): + obj = super(Singleton, cls).__new__(cls) + cls.ccount += 1 + + if cls.ccount == 2: + raise SingletonError(f"Exceeded {cls.__name__} instantiation limit...") + + return obj diff --git a/src/signal_classes/mixins/StylesMixin.py b/src/utils/styles.py similarity index 90% rename from src/signal_classes/mixins/StylesMixin.py rename to src/utils/styles.py index 8f8d576..b592f7c 100644 --- a/src/signal_classes/mixins/StylesMixin.py +++ b/src/utils/styles.py @@ -1,15 +1,19 @@ # Python imports # Lib imports -from PyInquirer import style_from_dict, Token +from libs.PyInquirer import style_from_dict, Token # Application imports +from .singleton import Singleton -class StylesMixin: +class Styles(Singleton): + def __init__(self): + ... + """ - The StylesMixin has style methods that get called and + The Styles class has style methods that get called and return their respective objects. """