Complete rewrite and removed legacy bash version

This commit is contained in:
itdominator 2023-09-17 20:26:11 -05:00
parent a7da5d9ed8
commit a7c8e630fa
163 changed files with 27028 additions and 1105 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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.
"""

View File

@ -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
View 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
View File

@ -0,0 +1,3 @@
"""
Gtk Bound Signal Module
"""

35
src/core/controller.py Normal file
View 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)

View 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()

View File

@ -0,0 +1,3 @@
"""
Generic Mixins Module
"""

View 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)

View File

@ -0,0 +1,3 @@
"""
Widgets Module
"""

View 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
View 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
View 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()

View 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

View 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

View 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

View File

View 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
)

View 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',
})

View 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,
)

View 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
)

View 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
)

View 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
)

View 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
)

View 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)

View 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
)

View 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

View 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)))

View 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'

View 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 = []

View 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)

File diff suppressed because it is too large Load Diff

View 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.')

View 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

View 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

View 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.
"""

View 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())

View 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)

View 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

View File

@ -0,0 +1,5 @@
from __future__ import unicode_literals
from .filesystem import PathCompleter
from .base import WordCompleter
from .system import SystemCompleter

View 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)

View 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),

View 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),
})

View File

@ -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

View 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)

View File

@ -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

View 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

View File

@ -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

View File

@ -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')

View File

@ -0,0 +1,2 @@
from .server import *
from .application import *

View 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.
"""

View File

@ -0,0 +1,11 @@
"""
Python logger for the telnet server.
"""
from __future__ import unicode_literals
import logging
logger = logging.getLogger(__package__)
__all__ = (
'logger',
)

View 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))

View 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)

View 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)

File diff suppressed because it is too large Load Diff

View 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'

View 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

View 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)

View 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)

View 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.)
"""

View 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

View 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

View 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)

View 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)

View 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()

View 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

View 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)

View 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 *

View 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)

View 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()'

View 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 = []

View 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)

View 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)

View 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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View 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

View 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)

View 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

View 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)

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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)

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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