Complete rewrite and removed legacy bash version
This commit is contained in:
parent
a7da5d9ed8
commit
a7c8e630fa
@ -1,340 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
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.
|
||||
|
@ -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.
|
||||
<br/>
|
||||
To Install manually:
|
||||
<pre>
|
||||
sudo cp shellMen.sh /bin/
|
||||
sudo chown root:root /bin/shellMen
|
||||
sudo chmod 744 /bin/shellMen
|
||||
</pre>
|
||||
# To Uninstall
|
||||
To uninstall automatically please run the install.sh file and select option 2.
|
||||
<br/>
|
||||
To Uninstall manually:
|
||||
<pre>
|
||||
sudo rm /bin/shellMen
|
||||
</pre>
|
||||
# License
|
||||
You should have received a copy of the GNU General Public License along with this program.
|
||||
<br/>If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Images
|
||||
![1 Root Menu View](images/pic1.png)
|
||||
![2 Sub Menu View](images/pic2.png)
|
Binary file not shown.
Before Width: | Height: | Size: 213 KiB |
Binary file not shown.
Before Width: | Height: | Size: 158 KiB |
@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
main()
|
||||
{
|
||||
clear
|
||||
|
||||
read -p "Please Press 1 to Install or 2 to Uninstall --> : " 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
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#---------------------------------------------------------------------------------------#
|
||||
|
||||
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;
|
@ -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
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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()
|
||||
|
48
src/app.py
Normal file
48
src/app.py
Normal file
@ -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 <pid> 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)
|
3
src/core/__init__.py
Normal file
3
src/core/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Gtk Bound Signal Module
|
||||
"""
|
35
src/core/controller.py
Normal file
35
src/core/controller.py
Normal file
@ -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)
|
51
src/core/controller_data.py
Normal file
51
src/core/controller_data.py
Normal file
@ -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()
|
3
src/core/mixins/__init__.py
Normal file
3
src/core/mixins/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Generic Mixins Module
|
||||
"""
|
35
src/core/mixins/processor_mixin.py
Normal file
35
src/core/mixins/processor_mixin.py
Normal file
@ -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)
|
3
src/core/widgets/__init__.py
Normal file
3
src/core/widgets/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Widgets Module
|
||||
"""
|
146
src/core/widgets/desktop_files.py
Normal file
146
src/core/widgets/desktop_files.py
Normal file
@ -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
|
134
src/core/widgets/menu.py
Normal file
134
src/core/widgets/menu.py
Normal file
@ -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)
|
51
src/core/window.py
Normal file
51
src/core/window.py
Normal file
@ -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()
|
29
src/libs/PyInquirer/__init__.py
Normal file
29
src/libs/PyInquirer/__init__.py
Normal file
@ -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
|
41
src/libs/PyInquirer/color_print.py
Normal file
41
src/libs/PyInquirer/color_print.py
Normal file
@ -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
|
98
src/libs/PyInquirer/prompt.py
Normal file
98
src/libs/PyInquirer/prompt.py
Normal file
@ -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
|
0
src/libs/PyInquirer/prompts/__init__.py
Normal file
0
src/libs/PyInquirer/prompts/__init__.py
Normal file
233
src/libs/PyInquirer/prompts/checkbox.py
Normal file
233
src/libs/PyInquirer/prompts/checkbox.py
Normal file
@ -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,
|
||||
' (<up>, <down> to move, <space> to select, <a> '
|
||||
'to toggle, <i> 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
|
||||
)
|
92
src/libs/PyInquirer/prompts/common.py
Normal file
92
src/libs/PyInquirer/prompts/common.py
Normal file
@ -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',
|
||||
})
|
82
src/libs/PyInquirer/prompts/confirm.py
Normal file
82
src/libs/PyInquirer/prompts/confirm.py
Normal file
@ -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,
|
||||
)
|
197
src/libs/PyInquirer/prompts/editor.py
Normal file
197
src/libs/PyInquirer/prompts/editor.py
Normal file
@ -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
|
||||
)
|
195
src/libs/PyInquirer/prompts/expand.py
Normal file
195
src/libs/PyInquirer/prompts/expand.py
Normal file
@ -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
|
||||
)
|
51
src/libs/PyInquirer/prompts/input.py
Normal file
51
src/libs/PyInquirer/prompts/input.py
Normal file
@ -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
|
||||
)
|
184
src/libs/PyInquirer/prompts/list.py
Normal file
184
src/libs/PyInquirer/prompts/list.py
Normal file
@ -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
|
||||
)
|
14
src/libs/PyInquirer/prompts/password.py
Normal file
14
src/libs/PyInquirer/prompts/password.py
Normal file
@ -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)
|
166
src/libs/PyInquirer/prompts/rawlist.py
Normal file
166
src/libs/PyInquirer/prompts/rawlist.py
Normal file
@ -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
|
||||
)
|
15
src/libs/PyInquirer/separator.py
Normal file
15
src/libs/PyInquirer/separator.py
Normal file
@ -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
|
35
src/libs/PyInquirer/utils.py
Normal file
35
src/libs/PyInquirer/utils.py
Normal file
@ -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)))
|
22
src/libs/prompt_toolkit/__init__.py
Normal file
22
src/libs/prompt_toolkit/__init__.py
Normal file
@ -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'
|
192
src/libs/prompt_toolkit/application.py
Normal file
192
src/libs/prompt_toolkit/application.py
Normal file
@ -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 = []
|
88
src/libs/prompt_toolkit/auto_suggest.py
Normal file
88
src/libs/prompt_toolkit/auto_suggest.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""
|
||||
`Fish-style <http://fishshell.com/>`_ like auto-suggestion.
|
||||
|
||||
While a user types input in a certain buffer, suggestions are generated
|
||||
(asynchronously.) Usually, they are displayed after the input. When the cursor
|
||||
presses the right arrow and the cursor is at the end of the input, the
|
||||
suggestion will be inserted.
|
||||
"""
|
||||
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)
|
1415
src/libs/prompt_toolkit/buffer.py
Normal file
1415
src/libs/prompt_toolkit/buffer.py
Normal file
File diff suppressed because it is too large
Load Diff
92
src/libs/prompt_toolkit/buffer_mapping.py
Normal file
92
src/libs/prompt_toolkit/buffer_mapping.py
Normal file
@ -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.')
|
111
src/libs/prompt_toolkit/cache.py
Normal file
111
src/libs/prompt_toolkit/cache.py
Normal file
@ -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
|
8
src/libs/prompt_toolkit/clipboard/__init__.py
Normal file
8
src/libs/prompt_toolkit/clipboard/__init__.py
Normal file
@ -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
|
62
src/libs/prompt_toolkit/clipboard/base.py
Normal file
62
src/libs/prompt_toolkit/clipboard/base.py
Normal file
@ -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.
|
||||
"""
|
42
src/libs/prompt_toolkit/clipboard/in_memory.py
Normal file
42
src/libs/prompt_toolkit/clipboard/in_memory.py
Normal file
@ -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())
|
39
src/libs/prompt_toolkit/clipboard/pyperclip.py
Normal file
39
src/libs/prompt_toolkit/clipboard/pyperclip.py
Normal file
@ -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)
|
170
src/libs/prompt_toolkit/completion.py
Normal file
170
src/libs/prompt_toolkit/completion.py
Normal file
@ -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
|
0
src/libs/prompt_toolkit/contrib/__init__.py
Normal file
0
src/libs/prompt_toolkit/contrib/__init__.py
Normal file
5
src/libs/prompt_toolkit/contrib/completers/__init__.py
Normal file
5
src/libs/prompt_toolkit/contrib/completers/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .filesystem import PathCompleter
|
||||
from .base import WordCompleter
|
||||
from .system import SystemCompleter
|
61
src/libs/prompt_toolkit/contrib/completers/base.py
Normal file
61
src/libs/prompt_toolkit/contrib/completers/base.py
Normal file
@ -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)
|
105
src/libs/prompt_toolkit/contrib/completers/filesystem.py
Normal file
105
src/libs/prompt_toolkit/contrib/completers/filesystem.py
Normal file
@ -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),
|
56
src/libs/prompt_toolkit/contrib/completers/system.py
Normal file
56
src/libs/prompt_toolkit/contrib/completers/system.py
Normal file
@ -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<executable>[^\s]+)
|
||||
|
||||
# Ignore literals in between.
|
||||
(
|
||||
\s+
|
||||
("[^"]*" | '[^']*' | [^'"]+ )
|
||||
)*
|
||||
|
||||
\s+
|
||||
|
||||
# Filename as parameters.
|
||||
(
|
||||
(?P<filename>[^\s]+) |
|
||||
"(?P<double_quoted_filename>[^\s]+)" |
|
||||
'(?P<single_quoted_filename>[^\s]+)'
|
||||
)
|
||||
""",
|
||||
escape_funcs={
|
||||
'double_quoted_filename': (lambda string: string.replace('"', '\\"')),
|
||||
'single_quoted_filename': (lambda string: string.replace("'", "\\'")),
|
||||
},
|
||||
unescape_funcs={
|
||||
'double_quoted_filename': (lambda string: string.replace('\\"', '"')), # XXX: not 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),
|
||||
})
|
@ -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<varname>...)`` for instance.
|
||||
|
||||
When the input is processed for validation (for instance), the regex will
|
||||
execute, the named group is captured, and the validator associated with this
|
||||
named group will test the captured string.
|
||||
|
||||
There is one tricky bit:
|
||||
|
||||
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
|
408
src/libs/prompt_toolkit/contrib/regular_languages/compiler.py
Normal file
408
src/libs/prompt_toolkit/contrib/regular_languages/compiler.py
Normal file
@ -0,0 +1,408 @@
|
||||
r"""
|
||||
Compiler for a regular grammar.
|
||||
|
||||
Example usage::
|
||||
|
||||
# Create and compile grammar.
|
||||
p = compile('add \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)')
|
||||
|
||||
# Match input string.
|
||||
m = p.match('add 23 432')
|
||||
|
||||
# Get variables.
|
||||
m.variables().get('var1') # Returns "23"
|
||||
m.variables().get('var2') # Returns "432"
|
||||
|
||||
|
||||
Partial matches are possible::
|
||||
|
||||
# Create and compile grammar.
|
||||
p = compile('''
|
||||
# Operators with two arguments.
|
||||
((?P<operator1>[^\s]+) \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)) |
|
||||
|
||||
# Operators with only one arguments.
|
||||
((?P<operator2>[^\s]+) \s+ (?P<var1>[^\s]+))
|
||||
''')
|
||||
|
||||
# Match partial input string.
|
||||
m = p.match_prefix('add 23')
|
||||
|
||||
# Get variables. (Notice that both operator1 and operator2 contain the
|
||||
# value "add".) This is because our input is incomplete, and we don't know
|
||||
# yet in which rule of the regex we we'll end up. It could also be that
|
||||
# `operator1` and `operator2` have a different autocompleter and we want to
|
||||
# call all possible autocompleters that would result in valid input.)
|
||||
m.variables().get('var1') # Returns "23"
|
||||
m.variables().get('operator1') # Returns "add"
|
||||
m.variables().get('operator2') # Returns "add"
|
||||
|
||||
"""
|
||||
from __future__ import 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('<trailing_input>', 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)
|
@ -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
|
90
src/libs/prompt_toolkit/contrib/regular_languages/lexer.py
Normal file
90
src/libs/prompt_toolkit/contrib/regular_languages/lexer.py
Normal file
@ -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
|
@ -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
|
@ -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')
|
2
src/libs/prompt_toolkit/contrib/telnet/__init__.py
Normal file
2
src/libs/prompt_toolkit/contrib/telnet/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .server import *
|
||||
from .application import *
|
32
src/libs/prompt_toolkit/contrib/telnet/application.py
Normal file
32
src/libs/prompt_toolkit/contrib/telnet/application.py
Normal file
@ -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.
|
||||
"""
|
11
src/libs/prompt_toolkit/contrib/telnet/log.py
Normal file
11
src/libs/prompt_toolkit/contrib/telnet/log.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""
|
||||
Python logger for the telnet server.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__package__)
|
||||
|
||||
__all__ = (
|
||||
'logger',
|
||||
)
|
181
src/libs/prompt_toolkit/contrib/telnet/protocol.py
Normal file
181
src/libs/prompt_toolkit/contrib/telnet/protocol.py
Normal file
@ -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))
|
407
src/libs/prompt_toolkit/contrib/telnet/server.py
Normal file
407
src/libs/prompt_toolkit/contrib/telnet/server.py
Normal file
@ -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)
|
34
src/libs/prompt_toolkit/contrib/validators/base.py
Normal file
34
src/libs/prompt_toolkit/contrib/validators/base.py
Normal file
@ -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)
|
1001
src/libs/prompt_toolkit/document.py
Normal file
1001
src/libs/prompt_toolkit/document.py
Normal file
File diff suppressed because it is too large
Load Diff
29
src/libs/prompt_toolkit/enums.py
Normal file
29
src/libs/prompt_toolkit/enums.py
Normal file
@ -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'
|
0
src/libs/prompt_toolkit/eventloop/__init__.py
Normal file
0
src/libs/prompt_toolkit/eventloop/__init__.py
Normal file
46
src/libs/prompt_toolkit/eventloop/asyncio_base.py
Normal file
46
src/libs/prompt_toolkit/eventloop/asyncio_base.py
Normal file
@ -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
|
113
src/libs/prompt_toolkit/eventloop/asyncio_posix.py
Normal file
113
src/libs/prompt_toolkit/eventloop/asyncio_posix.py
Normal file
@ -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)
|
83
src/libs/prompt_toolkit/eventloop/asyncio_win32.py
Normal file
83
src/libs/prompt_toolkit/eventloop/asyncio_win32.py
Normal file
@ -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)
|
85
src/libs/prompt_toolkit/eventloop/base.py
Normal file
85
src/libs/prompt_toolkit/eventloop/base.py
Normal file
@ -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.)
|
||||
"""
|
29
src/libs/prompt_toolkit/eventloop/callbacks.py
Normal file
29
src/libs/prompt_toolkit/eventloop/callbacks.py
Normal file
@ -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
|
107
src/libs/prompt_toolkit/eventloop/inputhook.py
Normal file
107
src/libs/prompt_toolkit/eventloop/inputhook.py
Normal file
@ -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
|
311
src/libs/prompt_toolkit/eventloop/posix.py
Normal file
311
src/libs/prompt_toolkit/eventloop/posix.py
Normal file
@ -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)
|
82
src/libs/prompt_toolkit/eventloop/posix_utils.py
Normal file
82
src/libs/prompt_toolkit/eventloop/posix_utils.py
Normal file
@ -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)
|
216
src/libs/prompt_toolkit/eventloop/select.py
Normal file
216
src/libs/prompt_toolkit/eventloop/select.py
Normal file
@ -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()
|
23
src/libs/prompt_toolkit/eventloop/utils.py
Normal file
23
src/libs/prompt_toolkit/eventloop/utils.py
Normal file
@ -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
|
187
src/libs/prompt_toolkit/eventloop/win32.py
Normal file
187
src/libs/prompt_toolkit/eventloop/win32.py
Normal file
@ -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)
|
36
src/libs/prompt_toolkit/filters/__init__.py
Normal file
36
src/libs/prompt_toolkit/filters/__init__.py
Normal file
@ -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 *
|
234
src/libs/prompt_toolkit/filters/base.py
Normal file
234
src/libs/prompt_toolkit/filters/base.py
Normal file
@ -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)
|
395
src/libs/prompt_toolkit/filters/cli.py
Normal file
395
src/libs/prompt_toolkit/filters/cli.py
Normal file
@ -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()'
|
55
src/libs/prompt_toolkit/filters/types.py
Normal file
55
src/libs/prompt_toolkit/filters/types.py
Normal file
@ -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 = []
|
39
src/libs/prompt_toolkit/filters/utils.py
Normal file
39
src/libs/prompt_toolkit/filters/utils.py
Normal file
@ -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)
|
120
src/libs/prompt_toolkit/history.py
Normal file
120
src/libs/prompt_toolkit/history.py
Normal file
@ -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)
|
135
src/libs/prompt_toolkit/input.py
Normal file
135
src/libs/prompt_toolkit/input.py
Normal file
@ -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
|
1185
src/libs/prompt_toolkit/interface.py
Normal file
1185
src/libs/prompt_toolkit/interface.py
Normal file
File diff suppressed because it is too large
Load Diff
1
src/libs/prompt_toolkit/key_binding/__init__.py
Normal file
1
src/libs/prompt_toolkit/key_binding/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from __future__ import unicode_literals
|
407
src/libs/prompt_toolkit/key_binding/bindings/basic.py
Normal file
407
src/libs/prompt_toolkit/key_binding/bindings/basic.py
Normal file
@ -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
|
161
src/libs/prompt_toolkit/key_binding/bindings/completion.py
Normal file
161
src/libs/prompt_toolkit/key_binding/bindings/completion.py
Normal file
@ -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)
|
451
src/libs/prompt_toolkit/key_binding/bindings/emacs.py
Normal file
451
src/libs/prompt_toolkit/key_binding/bindings/emacs.py
Normal file
@ -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
|
578
src/libs/prompt_toolkit/key_binding/bindings/named_commands.py
Normal file
578
src/libs/prompt_toolkit/key_binding/bindings/named_commands.py
Normal file
@ -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)
|
185
src/libs/prompt_toolkit/key_binding/bindings/scroll.py
Normal file
185
src/libs/prompt_toolkit/key_binding/bindings/scroll.py
Normal file
@ -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
|
25
src/libs/prompt_toolkit/key_binding/bindings/utils.py
Normal file
25
src/libs/prompt_toolkit/key_binding/bindings/utils.py
Normal file
@ -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
|
1903
src/libs/prompt_toolkit/key_binding/bindings/vi.py
Normal file
1903
src/libs/prompt_toolkit/key_binding/bindings/vi.py
Normal file
File diff suppressed because it is too large
Load Diff
119
src/libs/prompt_toolkit/key_binding/defaults.py
Normal file
119
src/libs/prompt_toolkit/key_binding/defaults.py
Normal file
@ -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)
|
1378
src/libs/prompt_toolkit/key_binding/digraphs.py
Normal file
1378
src/libs/prompt_toolkit/key_binding/digraphs.py
Normal file
File diff suppressed because it is too large
Load Diff
372
src/libs/prompt_toolkit/key_binding/input_processor.py
Normal file
372
src/libs/prompt_toolkit/key_binding/input_processor.py
Normal file
@ -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
|
96
src/libs/prompt_toolkit/key_binding/manager.py
Normal file
96
src/libs/prompt_toolkit/key_binding/manager.py
Normal file
@ -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
|
350
src/libs/prompt_toolkit/key_binding/registry.py
Normal file
350
src/libs/prompt_toolkit/key_binding/registry.py
Normal file
@ -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
|
61
src/libs/prompt_toolkit/key_binding/vi_state.py
Normal file
61
src/libs/prompt_toolkit/key_binding/vi_state.py
Normal file
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user