mirror of https://github.com/seebye/ueberzug
archiving
parent
52855b2493
commit
5995be053c
@ -1,64 +0,0 @@
|
||||
name: Wheel builder
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published,edited]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.release.body, '<!--build-->')
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Creating environment variables
|
||||
run: |
|
||||
github_tag="${GITHUB_REF#refs/tags/}"
|
||||
echo "GITHUB_TAG=${github_tag}" >> "$GITHUB_ENV"
|
||||
echo "GITHUB_RELEASE_API_URL=${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${github_tag}" >> "$GITHUB_ENV"
|
||||
echo "GITHUB_TAG_FOLDER=tag_data" >> "$GITHUB_ENV"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt install -y libx11-dev libxext-dev libxres-dev jq
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade wheel
|
||||
- name: Download release and extracting
|
||||
run: |
|
||||
code_archive_url="$(curl --header "Accept: application/vnd.github.v3+json" "${GITHUB_RELEASE_API_URL}" | jq --raw-output .zipball_url)"
|
||||
echo downloading "${code_archive_url}"
|
||||
wget --output-document="release.zip" "${code_archive_url}"
|
||||
# the folder name in the archive may change
|
||||
# so create a new folder so we can use globs
|
||||
# without having to fear the existence of other folders
|
||||
mkdir "${GITHUB_TAG_FOLDER}"
|
||||
cd "${GITHUB_TAG_FOLDER}"
|
||||
unzip ../release.zip
|
||||
- name: building
|
||||
run: |
|
||||
cd "${GITHUB_TAG_FOLDER}"/*/
|
||||
python setup.py bdist_wheel
|
||||
- name: uploading
|
||||
run: |
|
||||
upload_url="$(curl --header "Accept: application/vnd.github.v3+json" "${GITHUB_RELEASE_API_URL}" | jq --raw-output .upload_url)"
|
||||
upload_url="${upload_url%{*}"
|
||||
|
||||
cd "${GITHUB_TAG_FOLDER}"/*/
|
||||
for filename in dist/*; do
|
||||
echo uploading "${filename}"
|
||||
filename_encoded="$(printf '%s' "${filename#*/}" | jq --slurp --raw-input --raw-output @uri)"
|
||||
echo encoded filename "${filename_encoded}"
|
||||
curl --request POST \
|
||||
--header "Accept: application/vnd.github.v3+json" \
|
||||
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
|
||||
--header "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${filename}" \
|
||||
"${upload_url}?name=${filename_encoded}"
|
||||
done
|
@ -1,20 +0,0 @@
|
||||
name: Pull request closer
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened,reopened]
|
||||
jobs:
|
||||
close-fork-pull-request:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: close pull request
|
||||
run: |
|
||||
pull_request_id='${{ github.event.pull_request.number }}'
|
||||
echo closing pull request "${pull_request_id}" in repository "${GITHUB_REPOSITORY}"
|
||||
curl \
|
||||
--request PATCH \
|
||||
--header "Accept: application/vnd.github.v3+json" \
|
||||
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${pull_request_id}" \
|
||||
--data '{"state":"closed"}'
|
@ -1,111 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# other
|
||||
il.tar
|
||||
test.sh
|
||||
test.sh.bak
|
||||
|
||||
!ueberzug/lib/
|
@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. 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
|
||||
them 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 prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. 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.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey 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;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If 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 convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU 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 that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
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.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
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.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
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
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 3 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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program 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, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU 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. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
@ -1,3 +0,0 @@
|
||||
include ueberzug/lib/lib.sh
|
||||
include ueberzug/X/*.h
|
||||
include ueberzug/X/*.c
|
@ -1,313 +1,14 @@
|
||||
# Überzug
|
||||
## :warning: **seebye/ueberzug is not maintained**
|
||||
|
||||
Überzug is a command line util
|
||||
which allows to draw images on terminals by using child windows.
|
||||
First of all: I decided not to work on this project anymore.
|
||||
|
||||
Advantages to w3mimgdisplay:
|
||||
- no race conditions as a new window is created to display images
|
||||
- expose events will be processed,
|
||||
so images will be redrawn on switch workspaces
|
||||
- tmux support (excluding multi pane windows)
|
||||
- terminals without the WINDOWID environment variable are supported
|
||||
- chars are used as position - and size unit
|
||||
- no memory leak (/ unlimited cache)
|
||||
ueberzug was a tool which allowed to hack image support into terminal emulators if they fulfilled certain properties.
|
||||
So it was not made to compete with projects which try to establish a standard for images in terminals like SIXEL, kittys image protocol or similar,
|
||||
but more it was thought to help during the time in which these protocols are supported to rarely.
|
||||
It will stop working at the latest if you are forced to switch to wayland.
|
||||
Such an tool may also increase the adoption time of these image protocols which is why I suggest not to fork this project,
|
||||
but to move to one of the terminal emulators which have image support.
|
||||
E.g. SIXEL is already supported by the following terminal emulators:
|
||||
foot, konsole, wezterm, xterm, Yakuake, Zellij and so on (Source: [https://www.arewesixelyet.com/](https://www.arewesixelyet.com/)).
|
||||
|
||||
## Overview
|
||||
|
||||
- [Dependencies](#dependencies)
|
||||
- [Installation](#installation)
|
||||
- [Communication](#communication)
|
||||
* [Command formats](#command-formats)
|
||||
* [Actions](#actions)
|
||||
+ [Add](#add)
|
||||
+ [Remove](#remove)
|
||||
* [Libraries](#libraries)
|
||||
+ [~~Bash~~ (deprecated)](#bash)
|
||||
+ [Python](#python)
|
||||
* [Examples](#examples)
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
Libraries used in the c extension:
|
||||
|
||||
- python
|
||||
- X11
|
||||
- Xext
|
||||
- XRes
|
||||
|
||||
There are also other direct dependencies,
|
||||
but they will be installed by pip.
|
||||
|
||||
## Installation
|
||||
|
||||
- Install by using pip:
|
||||
The package is named `ueberzug`
|
||||
- Packages of linux distributions:
|
||||
At least one packager applies patches to my code.
|
||||
So if there are issues uninstall it and install it via pip.
|
||||
Actually I think it's not fine that they call it ueberzug after changing the code.
|
||||
As bugs introduced by them look like they are part of my work.
|
||||
|
||||
Note: You can improve the performance of image manipulation functions
|
||||
by using [pillow-simd](https://github.com/uploadcare/pillow-simd) instead of pillow.
|
||||
|
||||
## Communication
|
||||
|
||||
The communication is realised via stdin.
|
||||
A command is a request to execute a specific action with the passed arguments.
|
||||
(Therefore a command has to contain a key value pair "action": action_name)
|
||||
Commands are separated with a line break.
|
||||
|
||||
### Command formats
|
||||
|
||||
- json: Command as json object
|
||||
- simple:
|
||||
Key-value pairs seperated by a tab,
|
||||
pairs are also seperated by a tab
|
||||
**:warning: ONLY FOR TESTING!**
|
||||
Simple was never intended for the usage in production!
|
||||
It doesn't support paths containing tabs or line breaks
|
||||
which makes it error prone.
|
||||
- bash: dump of an associative array (`declare -p variable_name`)
|
||||
|
||||
### Actions
|
||||
|
||||
#### Add
|
||||
|
||||
Name: add
|
||||
Description:
|
||||
Adds an image to the screen.
|
||||
If there's already an image with the same identifier
|
||||
it will be replaced.
|
||||
|
||||
| Key | Type | Description | Optional |
|
||||
|---------------|--------------|--------------------------------------------------------------------|----------|
|
||||
| identifier | String | a freely choosen identifier of the image | No |
|
||||
| x | Integer | x position | No |
|
||||
| y | Integer | y position | No |
|
||||
| path | String | path to the image | No |
|
||||
| width | Integer | desired width; original width will be used if not set | Yes |
|
||||
| height | Integer | desired height; original width will be used if not set | Yes |
|
||||
| ~~max_width~~ | Integer | **Deprecated: replaced by scalers (this behavior is implemented by the default scaler contain)**<br>image will be resized (while keeping it's aspect ratio) if it's width is bigger than max width | Yes | image width |
|
||||
| ~~max_height~~| Integer | **Deprecated: replaced by scalers (this behavior is implemented by the default scaler contain)**<br>image will be resized (while keeping it's aspect ratio) if it's height is bigger than max height | Yes | image height |
|
||||
| draw | Boolean | redraw window after adding the image, default True | Yes | True |
|
||||
| synchronously_draw | Boolean | redraw window immediately | Yes | False |
|
||||
| scaler | String | name of the image scaler<br>(algorithm which resizes the image to fit into the placement) | Yes | contain |
|
||||
| scaling_position_x | Float | the centered position, if possible<br>Specified as factor of the image size,<br>so it should be an element of [0, 1]. | Yes | 0 |
|
||||
| scaling_position_y | Float | analogous to scaling_position_x | Yes | 0 |
|
||||
|
||||
|
||||
ImageScalers:
|
||||
|
||||
| Name | Description |
|
||||
|---------------|----------------------------------------------------------------------------------|
|
||||
| crop | Crops out an area of the size of the placement size. |
|
||||
| distort | Distorts the image to the placement size. |
|
||||
| fit_contain | Resizes the image that either the width matches the maximum width or the height matches the maximum height while keeping the image ratio. |
|
||||
| contain | Resizes the image to a size <= the placement size while keeping the image ratio. |
|
||||
| forced_cover | Resizes the image to cover the entire area which should be filled<br>while keeping the image ratio.<br>If the image is smaller than the desired size<br>it will be stretched to reach the desired size.<br>If the ratio of the area differs<br>from the image ratio the edges will be cut off. |
|
||||
| cover | The same as forced_cover but images won't be stretched<br>if they are smaller than the area which should be filled. |
|
||||
|
||||
#### Remove
|
||||
|
||||
Name: remove
|
||||
Description:
|
||||
Removes an image from the screen.
|
||||
|
||||
| Key | Type | Description | Optional |
|
||||
|---------------|--------------|--------------------------------------------------------------------|----------|
|
||||
| identifier | String | a previously used identifier | No |
|
||||
| draw | Boolean | redraw window after removing the image, default True | Yes |
|
||||
|
||||
|
||||
### Libraries
|
||||
|
||||
Just a reminder: This is a GPLv3 licensed project, so if you use any of these libraries you also need to license it with a GPLv3 compatible license.
|
||||
|
||||
#### Bash
|
||||
|
||||
The library is deprecated.
|
||||
Dump associative arrays if you want to use ueberzug with bash.
|
||||
|
||||
~~First of all the library doesn't follow the posix standard,
|
||||
so you can't use it in any other shell than bash.~~
|
||||
|
||||
~~Executing `ueberzug library` will print the path to the library to stdout.~~
|
||||
|
||||
~~**Functions**~~:
|
||||
|
||||
- ~~`ImageLayer` starts the ueberzug process and uses bashs associative arrays to transfer commands.~~
|
||||
- ~~Also there will be a function named `ImageLayer::{action_name}` declared for each action.~~
|
||||
~~Each of this function takes the key values pairs of the respective action as arguments.~~
|
||||
~~Every argument of these functions has to be an associative key value pair.~~
|
||||
~~`ImageLayer::{action_name} [{key0}]="{value0}" [{key1}]="{value1}" ...`~~
|
||||
~~Executing such a function builds the desired command string according to the passed arguments and prints it to stdout.~~
|
||||
|
||||
#### Python
|
||||
|
||||
First of all everything which isn't mentioned here isn't safe to use and
|
||||
won't necessarily shipped with new coming versions.
|
||||
|
||||
The library is included in ueberzug's package.
|
||||
```python
|
||||
import ueberzug.lib.v0 as ueberzug
|
||||
```
|
||||
|
||||
**Classes**:
|
||||
|
||||
1. Visibility:
|
||||
An enum which contains the visibility states of a placement.
|
||||
|
||||
- VISIBLE
|
||||
- INVISIBLE
|
||||
2. Placement:
|
||||
A placement to put images on.
|
||||
|
||||
Every key value pair of the add action is an attribute (except identifier).
|
||||
Changing one of it will lead to building and transmitting an add command *if the placement is visible*.
|
||||
The identifier key value pair is implemented as a property and not changeable.
|
||||
|
||||
Constructor:
|
||||
|
||||
| Name | Type | Optional | Description |
|
||||
|---------------|--------------|----------|------------------------------------------------|
|
||||
| canvas | Canvas | No | the canvas where images should be drawn on |
|
||||
| identifier | String | No | a unique string used to address this placement |
|
||||
| visibility | Visibility | Yes | the initial visibility state<br>(if it's VISIBLE every attribute without a default value needs to be set) |
|
||||
| \*\*kwargs | dict | Yes | key value pairs of the add action |
|
||||
|
||||
Properties:
|
||||
|
||||
| Name | Type | Setter | Description |
|
||||
|---------------|--------------|--------|--------------------------------------|
|
||||
| identifier | String | No | the identifier of this placement |
|
||||
| canvas | Canvas | No | the canvas this placement belongs to |
|
||||
| visibility | Visibility | Yes | the visibility state of this placement<br>- setting it to VISIBLE leads to the transmission of an add command<br>- setting it to INVISIBLE leads to the transmission of a remove command |
|
||||
|
||||
**Warning**:
|
||||
The transmission of a command can lead to an IOError.
|
||||
(A transmission happens on assign a new value to an attribute of a visible Placement.
|
||||
The transmission is delayed till leaving a with-statement if lazy_drawing is used.)
|
||||
3. ScalerOption:
|
||||
Enum which contains the useable scaler names.
|
||||
4. Canvas:
|
||||
Should either be used with a with-statement or with a decorated function.
|
||||
(Starts and stops the ueberzug process)
|
||||
|
||||
Constructor:
|
||||
|
||||
| Name | Type | default | Description |
|
||||
|---------------|--------------|----------|------------------------------------------------|
|
||||
| debug | bool | False | suppresses printing stderr if it's false |
|
||||
|
||||
Methods:
|
||||
|
||||
| Name | Returns | Description |
|
||||
|----------------------|--------------|--------------------------------------|
|
||||
| create_placement | Placement | prevents the use of the same identifier multiple times,<br>takes the same arguments as the Placement constructor (excluding canvas parameter) |
|
||||
| \_\_call\_\_ | Function | Decorator which returns a function which calls the decorated function with the keyword parameter canvas=this_canvas_object.<br>Of course other arguments are also passed through. |
|
||||
| request_transmission | - | Transmits queued commands if automatic\_transmission is enabled or force=True is passed as keyword argument. |
|
||||
|
||||
Properties / Attributes:
|
||||
|
||||
| Name | Type | Setter | Description |
|
||||
|---------------|-------------------------|--------|--------------------------------------|
|
||||
| lazy_drawing | context manager factory | No | prevents the transmission of commands till the with-statement was left<br>`with canvas.lazy_drawing: pass`|
|
||||
| synchronous_lazy_drawing | context manager factory | No | Does the same as lazy_drawing. Additionally forces the redrawing of the windows to happen immediately. |
|
||||
| automatic\_transmission | bool | Yes | Transmit commands instantly on changing a placement. If it's disabled commands won't be transmitted till a lazy_drawing or synchronous_lazy_drawing with-statement was left or request_transmission(force=True) was called. Default: True |
|
||||
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
Command formats:
|
||||
|
||||
- Json command format: `{"action": "add", "x": 0, "y": 0, "path": "/some/path/some_image.jpg"}`
|
||||
- Simple command format: `action add x 0 y 0 path /some/path/some_image.jpg`
|
||||
- Bash command format: `declare -A command=([path]="/some/path/some_image.jpg" [action]="add" [x]="0" [y]="0" )`
|
||||
|
||||
Bash:
|
||||
|
||||
```bash
|
||||
# process substitution example:
|
||||
ueberzug layer --parser bash 0< <(
|
||||
declare -Ap add_command=([action]="add" [identifier]="example0" [x]="0" [y]="0" [path]="/some/path/some_image0.jpg")
|
||||
declare -Ap add_command=([action]="add" [identifier]="example1" [x]="10" [y]="0" [path]="/some/path/some_image1.jpg")
|
||||
sleep 5
|
||||
declare -Ap remove_command=([action]="remove" [identifier]="example0")
|
||||
sleep 5
|
||||
)
|
||||
|
||||
# group commands example:
|
||||
{
|
||||
declare -Ap add_command=([action]="add" [identifier]="example0" [x]="0" [y]="0" [path]="/some/path/some_image0.jpg")
|
||||
declare -Ap add_command=([action]="add" [identifier]="example1" [x]="10" [y]="0" [path]="/some/path/some_image1.jpg")
|
||||
read
|
||||
declare -Ap remove_command=([action]="remove" [identifier]="example0")
|
||||
read
|
||||
} | ueberzug layer --parser bash
|
||||
```
|
||||
|
||||
Python library:
|
||||
|
||||
- curses (based on https://docs.python.org/3/howto/curses.html#user-input):
|
||||
```python
|
||||
import curses
|
||||
import time
|
||||
from curses.textpad import Textbox, rectangle
|
||||
import ueberzug.lib.v0 as ueberzug
|
||||
|
||||
|
||||
@ueberzug.Canvas()
|
||||
def main(stdscr, canvas):
|
||||
demo = canvas.create_placement('demo', x=10, y=0)
|
||||
stdscr.addstr(0, 0, "Enter IM message: (hit Ctrl-G to send)")
|
||||
|
||||
editwin = curses.newwin(5, 30, 3, 1)
|
||||
rectangle(stdscr, 2, 0, 2+5+1, 2+30+1)
|
||||
stdscr.refresh()
|
||||
|
||||
box = Textbox(editwin)
|
||||
|
||||
# Let the user edit until Ctrl-G is struck.
|
||||
box.edit()
|
||||
|
||||
# Get resulting contents
|
||||
message = box.gather()
|
||||
demo.path = ''.join(message.split())
|
||||
demo.visibility = ueberzug.Visibility.VISIBLE
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
curses.wrapper(main)
|
||||
```
|
||||
|
||||
- general example:
|
||||
```python
|
||||
import ueberzug.lib.v0 as ueberzug
|
||||
import time
|
||||
|
||||
if __name__ == '__main__':
|
||||
with ueberzug.Canvas() as c:
|
||||
paths = ['/some/path/some_image.png', '/some/path/another_image.png']
|
||||
demo = c.create_placement('demo', x=0, y=0, scaler=ueberzug.ScalerOption.COVER.value)
|
||||
demo.path = paths[0]
|
||||
demo.visibility = ueberzug.Visibility.VISIBLE
|
||||
|
||||
for i in range(30):
|
||||
with c.lazy_drawing:
|
||||
demo.x = i * 3
|
||||
demo.y = i * 3
|
||||
demo.path = paths[i % 2]
|
||||
time.sleep(1/30)
|
||||
|
||||
time.sleep(2)
|
||||
```
|
||||
|
||||
Scripts:
|
||||
|
||||
- fzf with image preview: see examples/fzfimg.sh
|
||||
- Mastodon viewer: see examples/mastodon.sh
|
||||
**If you decide to fork the project** none the less it would be nice from you to change or remove the author name in all files.
|
||||
|
@ -1,146 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# This is just an example how ueberzug can be used with fzf.
|
||||
|
||||
# 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 3 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 <https://www.gnu.org/licenses/>.
|
||||
readonly BASH_BINARY="$(which bash)"
|
||||
readonly REDRAW_COMMAND="toggle-preview+toggle-preview"
|
||||
readonly REDRAW_KEY="µ"
|
||||
declare -r -x DEFAULT_PREVIEW_POSITION="right"
|
||||
declare -r -x UEBERZUG_FIFO="$(mktemp --dry-run --suffix "fzf-$$-ueberzug")"
|
||||
declare -r -x PREVIEW_ID="preview"
|
||||
|
||||
|
||||
function is_option_key [[ "${@}" =~ ^(\-.*|\+.*) ]]
|
||||
function is_key_value [[ "${@}" == *=* ]]
|
||||
|
||||
|
||||
function map_options {
|
||||
local -n options="${1}"
|
||||
local -n options_map="${2}"
|
||||
|
||||
for ((i=0; i < ${#options[@]}; i++)); do
|
||||
local key="${options[$i]}" next_key="${options[$((i + 1))]:---}"
|
||||
local value=true
|
||||
is_option_key "${key}" || \
|
||||
continue
|
||||
if is_key_value "${key}"; then
|
||||
<<<"${key}" \
|
||||
IFS='=' read key value
|
||||
elif ! is_option_key "${next_key}"; then
|
||||
value="${next_key}"
|
||||
fi
|
||||
options_map["${key}"]="${value}"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
function parse_options {
|
||||
declare -g -a script_options=("${@}")
|
||||
declare -g -A mapped_options
|
||||
map_options script_options mapped_options
|
||||
declare -g -r -x PREVIEW_POSITION="${mapped_options[--preview-window]%%:[^:]*}"
|
||||
}
|
||||
|
||||
|
||||
function start_ueberzug {
|
||||
mkfifo "${UEBERZUG_FIFO}"
|
||||
<"${UEBERZUG_FIFO}" \
|
||||
ueberzug layer --parser bash --silent &
|
||||
# prevent EOF
|
||||
3>"${UEBERZUG_FIFO}" \
|
||||
exec
|
||||
}
|
||||
|
||||
|
||||
function finalise {
|
||||
3>&- \
|
||||
exec
|
||||
&>/dev/null \
|
||||
rm "${UEBERZUG_FIFO}"
|
||||
&>/dev/null \
|
||||
kill $(jobs -p)
|
||||
}
|
||||
|
||||
|
||||
function calculate_position {
|
||||
# TODO costs: creating processes > reading files
|
||||
# so.. maybe we should store the terminal size in a temporary file
|
||||
# on receiving SIGWINCH
|
||||
# (in this case we will also need to use perl or something else
|
||||
# as bash won't execute traps if a command is running)
|
||||
< <(</dev/tty stty size) \
|
||||
read TERMINAL_LINES TERMINAL_COLUMNS
|
||||
|
||||
case "${PREVIEW_POSITION:-${DEFAULT_PREVIEW_POSITION}}" in
|
||||
left|up|top)
|
||||
X=1
|
||||
Y=1
|
||||
;;
|
||||
right)
|
||||
X=$((TERMINAL_COLUMNS - COLUMNS - 2))
|
||||
Y=1
|
||||
;;
|
||||
down|bottom)
|
||||
X=1
|
||||
Y=$((TERMINAL_LINES - LINES - 1))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
function draw_preview {
|
||||
calculate_position
|
||||
|
||||
>"${UEBERZUG_FIFO}" declare -A -p cmd=( \
|
||||
[action]=add [identifier]="${PREVIEW_ID}" \
|
||||
[x]="${X}" [y]="${Y}" \
|
||||
[width]="${COLUMNS}" [height]="${LINES}" \
|
||||
[scaler]=forced_cover [scaling_position_x]=0.5 [scaling_position_y]=0.5 \
|
||||
[path]="${@}")
|
||||
# add [synchronously_draw]=True if you want to see each change
|
||||
}
|
||||
|
||||
|
||||
function print_on_winch {
|
||||
# print "$@" to stdin on receiving SIGWINCH
|
||||
# use exec as we will only kill direct childs on exiting,
|
||||
# also the additional bash process isn't needed
|
||||
</dev/tty \
|
||||
exec perl -e '
|
||||
require "sys/ioctl.ph";
|
||||
while (1) {
|
||||
local $SIG{WINCH} = sub {
|
||||
ioctl(STDIN, &TIOCSTI, $_) for split "", join " ", @ARGV;
|
||||
};
|
||||
sleep;
|
||||
}' \
|
||||
"${@}" &
|
||||
}
|
||||
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
trap finalise EXIT
|
||||
parse_options "${@}"
|
||||
# print the redraw key twice as there's a run condition we can't circumvent
|
||||
# (we can't know the time fzf finished redrawing it's layout)
|
||||
print_on_winch "${REDRAW_KEY}${REDRAW_KEY}"
|
||||
start_ueberzug
|
||||
|
||||
export -f draw_preview calculate_position
|
||||
SHELL="${BASH_BINARY}" \
|
||||
fzf --preview "draw_preview {}" \
|
||||
--preview-window "${DEFAULT_PREVIEW_POSITION}" \
|
||||
--bind "${REDRAW_KEY}:${REDRAW_COMMAND}" \
|
||||
"${@}"
|
||||
fi
|
@ -1,233 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# This is just an example how ueberzug can be used.
|
||||
|
||||
# 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 3 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 <https://www.gnu.org/licenses/>.
|
||||
source "`ueberzug library`"
|
||||
|
||||
readonly USER_AGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
|
||||
declare -g target_host
|
||||
|
||||
function unpack {
|
||||
local tmp="${@/[^(]*\(}"
|
||||
echo -n "${tmp%)}"
|
||||
}
|
||||
|
||||
function Api::discover {
|
||||
local -A params="( `unpack "$@"` )"
|
||||
local -a hosts=( "${params[host]}" )
|
||||
local -A added
|
||||
|
||||
declare -p params
|
||||
declare -p hosts
|
||||
|
||||
for ((i=0; i < ${#hosts[@]} && ${#hosts[@]} < ${params[max]}; i++)); do
|
||||
#echo
|
||||
#echo [i=$i, ${#hosts[@]}/${params[max]}] Querying ${hosts[$i]}
|
||||
local -a results=(`curl --user-agent "${USER_AGENT}" "${hosts[$i]}/api/v1/timelines/public?limit=40" 2>/dev/null | \
|
||||
jq -r '.[].url' 2>/dev/null | \
|
||||
grep -oP 'https?://[^.]+\.[^/]+'`)
|
||||
|
||||
for host in "${results[@]}"; do
|
||||
if ! [ ${added["$host"]+exists} ]; then
|
||||
added["$host"]=True
|
||||
hosts+=( "$host" )
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
declare -p hosts
|
||||
}
|
||||
|
||||
function Api::get_local_timeline {
|
||||
local host="$1"
|
||||
local -a urls="( `{ curl --fail --user-agent "${USER_AGENT}" "${host}/api/v1/timelines/public?local=true&only_media=true&limit=40" 2>/dev/null ||
|
||||
curl --user-agent "${USER_AGENT}" "${host}/api/v1/timelines/public?local=true&limit=40" 2>/dev/null ; } | \
|
||||
jq -r '.[].media_attachments[].preview_url' 2>/dev/null | \
|
||||
sort -u` )"
|
||||
declare -p urls
|
||||
}
|
||||
|
||||
function Array::max_length {
|
||||
local -a items=( "$@" )
|
||||
local max=0
|
||||
|
||||
for i in "${items[@]}"; do
|
||||
if (( ${#i} > max )); then
|
||||
max=${#i}
|
||||
fi
|
||||
done
|
||||
|
||||
echo $max
|
||||
}
|
||||
|
||||
declare -g screen_counter=0
|
||||
|
||||
function Screen::new {
|
||||
let screen_counter+=1
|
||||
tput smcup 1>&2
|
||||
}
|
||||
|
||||
function Screen::pop {
|
||||
tput rmcup 1>&2
|
||||
let screen_counter-=1
|
||||
}
|
||||
|
||||
function Screen::move_cursor {
|
||||
tput cup "$1" "$2" 1>&2
|
||||
}
|
||||
|
||||
function Screen::hide_cursor {
|
||||
tput civis 1>&2
|
||||
}
|
||||
|
||||
function Screen::show_cursor {
|
||||
tput cnorm 1>&2
|
||||
}
|
||||
|
||||
function Screen::width {
|
||||
tput cols
|
||||
}
|
||||
|
||||
function Screen::height {
|
||||
tput lines
|
||||
}
|
||||
|
||||
function Screen::cleanup {
|
||||
while ((0 < screen_counter)); do
|
||||
Screen::pop
|
||||
done
|
||||
Screen::show_cursor
|
||||
}
|
||||
|
||||
function Screen::popup {
|
||||
local -a message=( "$@" )
|
||||
local line_length="`Array::max_length "${message[@]}"`"
|
||||
local line_count="${#message[@]}"
|
||||
local offset_x="$(( `Screen::width` / 2 - line_length / 2 ))"
|
||||
local offset_y="$(( `Screen::height` / 2 - ( line_count + 1 ) / 2 ))"
|
||||
|
||||
Screen::new
|
||||
|
||||
for ((i=0; i < ${#message[@]}; i++)); do
|
||||
Screen::move_cursor $(( offset_y + i )) $offset_x
|
||||
echo "${message[$i]}" 1>&2
|
||||
Screen::move_cursor $(( offset_y + 1 + i )) $offset_x
|
||||
done
|
||||
}
|
||||
|
||||
function Screen::dropdown {
|
||||
local offset_y="$(( `Screen::height` / 2 - 1 ))"
|
||||
local title="$1"
|
||||
shift
|
||||
|
||||
Screen::new
|
||||
Screen::hide_cursor
|
||||
Screen::move_cursor $offset_y 1
|
||||
smenu -m "$title" -M -W' '$'\n' -t 1 -l <<<"${@}"
|
||||
}
|
||||
|
||||
function Screen::select_host {
|
||||
local -A params="( `unpack "$@"` )"
|
||||
Screen::popup 'Searching hosts' \
|
||||
"Searching up to ${params[max]} mastodon instances," \
|
||||
"using ${params[host]} as entry point."
|
||||
Screen::hide_cursor
|
||||
local -A hosts="( $(unpack "`Api::discover "$@"`") )"
|
||||
Screen::pop
|
||||
|
||||
echo -n "$(Screen::dropdown "Select host:" "${hosts[@]}")"
|
||||
Screen::pop
|
||||
}
|
||||
|
||||
function Screen::display_media {
|
||||
local -a urls=( "$@" )
|
||||
|
||||
ImageLayer 0< <(
|
||||
function cleanup {
|
||||
if [[ "$tmpfolder" == "/tmp/"* ]]; then
|
||||
rm "${tmpfolder}/"*
|
||||
rmdir "${tmpfolder}"
|
||||
fi
|
||||
}
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
padding=3
|
||||
width=40
|
||||
height=14
|
||||
page_width=`Screen::width`
|
||||
page_height=`Screen::height`
|
||||
full_width=$(( width + 2 * padding ))
|
||||
full_height=$(( height + 2 * padding ))
|
||||
cols=$(( page_width / full_width ))
|
||||
rows=$(( page_height / full_height ))
|
||||
page_media_count=$(( rows * cols ))
|
||||
offset_x=$(( (page_width - cols * full_width) / 2 ))
|
||||
offset_y=$(( (page_height - rows * full_height) / 2 ))
|
||||
iterations=$(( ${#urls[@]} / ( cols * rows ) ))
|
||||
tmpfolder=`mktemp --directory`
|
||||
|
||||
for ((i=0; i < iterations; i++)); do
|
||||
for ((r=0; r < rows; r++)); do
|
||||
for ((c=0; c < cols; c++)); do
|
||||
index=$(( i * page_media_count + r * rows + c ))
|
||||
url="${urls[$index]}"
|
||||
name="`basename "${url}"`"
|
||||
path="${tmpfolder}/$name"
|
||||
curl --user-agent "${USER_AGENT}" "${url}" 2>/dev/null > "${path}"
|
||||
ImageLayer::add [identifier]="${r},${c}" \
|
||||
[x]="$((offset_x + c * full_width))" [y]="$((offset_y + r * full_height))" \
|
||||
[max_width]="$width" [max_height]="$height" \
|
||||
[path]="$path"
|
||||
Screen::move_cursor "$((offset_y + (r + 1) * full_height - padding + 1))" "$((offset_x + c * full_width))"
|
||||
echo -n "$name" 1>&2
|
||||
done
|
||||
done
|
||||
|
||||
read
|
||||
|
||||
for ((r=0; r < rows; r++)); do
|
||||
for ((c=0; c < cols; c++)); do
|
||||
ImageLayer::remove [identifier]="${r},${c}"
|
||||
done
|
||||
done
|
||||
|
||||
clear 1>&2
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
function Screen::display_timeline {
|
||||
local -A params="( `unpack "$@"` )"
|
||||
local -A urls="( $(unpack "`Api::get_local_timeline "${params[host]}"`") )"
|
||||
Screen::new
|
||||
Screen::hide_cursor
|
||||
|
||||
if (( ${#urls[@]} == 0 )); then
|
||||
Screen::pop
|
||||
Screen::popup "There was no image in the current feed."
|
||||
read
|
||||
Screen::pop
|
||||
return
|
||||
fi
|
||||
|
||||
Screen::display_media "${urls[@]}"
|
||||
Screen::pop
|
||||
}
|
||||
|
||||
trap 'Screen::cleanup' EXIT
|
||||
target_host="$(Screen::select_host [max]=30 [host]="https://mastodon.social")"
|
||||
|
||||
if [ -n "$target_host" ]; then
|
||||
Screen::display_timeline [host]="$target_host"
|
||||
fi
|
@ -1 +0,0 @@
|
||||
I don't accept pull requests.
|
@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import distutils.core
|
||||
import setuptools
|
||||
import glob
|
||||
|
||||
import ueberzug
|
||||
# To use a consistent encoding
|
||||
|
||||
# Arguments marked as "Required" below must be included for upload to PyPI.
|
||||
# Fields marked as "Optional" may be commented out.
|
||||
|
||||
setuptools.setup(
|
||||
# This is the name of your project. The first time you publish this
|
||||
# package, this name will be registered for you. It will determine how
|
||||
# users can install this project, e.g.:
|
||||
#
|
||||
# $ pip install sampleproject
|
||||
#
|
||||
# And where it will live on PyPI: https://pypi.org/project/sampleproject/
|
||||
#
|
||||
# There are some restrictions on what makes a valid project name
|
||||
# specification here:
|
||||
# https://packaging.python.org/specifications/core-metadata/#name
|
||||
name='ueberzug', # Required
|
||||
license=ueberzug.__license__,
|
||||
|
||||
include_package_data=True,
|
||||
package_data={
|
||||
'': ['*.sh'],
|
||||
},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'ueberzug=ueberzug.__main__:main'
|
||||
]
|
||||
},
|
||||
ext_modules=[
|
||||
distutils.core.Extension(
|
||||
"ueberzug.X",
|
||||
glob.glob("ueberzug/X/*.c"),
|
||||
libraries=["X11", "Xext", "XRes"],
|
||||
include_dirs=["ueberzug/X"]),
|
||||
],
|
||||
|
||||
# Versions should comply with PEP 440:
|
||||
# https://www.python.org/dev/peps/pep-0440/
|
||||
#
|
||||
# For a discussion on single-sourcing the version across setup.py and the
|
||||
# project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version=ueberzug.__version__, # Required
|
||||
|
||||
# This is a one-line description or tagline of what your project does. This
|
||||
# corresponds to the "Summary" metadata field:
|
||||
# https://packaging.python.org/specifications/core-metadata/#summary
|
||||
description=ueberzug.__description__, # Required
|
||||
|
||||
# This should be a valid link to your project's main homepage.
|
||||
#
|
||||
# This field corresponds to the "Home-Page" metadata field:
|
||||
# https://packaging.python.org/specifications/core-metadata/#home-page-optional
|
||||
url=ueberzug.__url_project__, # Optional
|
||||
|
||||
# This should be your name or the name of the organization which owns the
|
||||
# project.
|
||||
author=ueberzug.__author__, # Optional
|
||||
|
||||
# Classifiers help users find your project by categorizing it.
|
||||
#
|
||||
# For a list of valid classifiers, see
|
||||
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[ # Optional
|
||||
'Environment :: Console',
|
||||
'Environment :: X11 Applications',
|
||||
'Intended Audience :: Developers',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Topic :: Software Development :: User Interfaces',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
# This field adds keywords for your project which will appear on the
|
||||
# project page. What does your project relate to?
|
||||
#
|
||||
# Note that this is a string of words separated by whitespace, not a list.
|
||||
keywords='image media terminal ui gui ', # Optional
|
||||
|
||||
# You can just specify package directories manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
#
|
||||
# Alternatively, if you just want to distribute a single Python file, use
|
||||
# the `py_modules` argument instead as follows, which will expect a file
|
||||
# called `my_module.py` to exist:
|
||||
#
|
||||
# py_modules=["my_module"],
|
||||
#
|
||||
packages=setuptools.find_packages(), # Required
|
||||
|
||||
# This field lists other packages that your project depends on to run.
|
||||
# Any package you put here will be installed by pip when your project is
|
||||
# installed, so they must be valid existing projects.
|
||||
#
|
||||
# For an analysis of "install_requires" vs pip's requirements files see:
|
||||
# https://packaging.python.org/en/latest/requirements.html
|
||||
install_requires=['pillow', 'docopt', 'attrs>=18.2.0'], # Optional
|
||||
python_requires='>=3.6',
|
||||
|
||||
# List additional URLs that are relevant to your project as a dict.
|
||||
#
|
||||
# This field corresponds to the "Project-URL" metadata fields:
|
||||
# https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use
|
||||
#
|
||||
# Examples listed include a pattern for specifying where the package tracks
|
||||
# issues, where the source is hosted, where to say thanks to the package
|
||||
# maintainers, and where to support the project financially. The key is
|
||||
# what's used to render the link text on PyPI.
|
||||
project_urls={ # Optional
|
||||
'Bug Reports': ueberzug.__url_bug_reports__,
|
||||
'Source': ueberzug.__url_repository__,
|
||||
},
|
||||
)
|
@ -1,58 +0,0 @@
|
||||
#include "python.h"
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
#include "util.h"
|
||||
#include "display.h"
|
||||
#include "window.h"
|
||||
#include "Xshm.h"
|
||||
|
||||
|
||||
static PyObject *
|
||||
X_init_threads(PyObject *self) {
|
||||
if (XInitThreads() == 0) {
|
||||
raise(OSError, "Xlib concurrent threads initialization failed.");
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
||||
static PyMethodDef X_methods[] = {
|
||||
{"init_threads", (PyCFunction)X_init_threads,
|
||||
METH_NOARGS,
|
||||
"Initializes Xlib support for concurrent threads."},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
static PyModuleDef module = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
.m_name = "ueberzug.X",
|
||||
.m_doc = "Modul which implements the interaction with the Xshm extension.",
|
||||
.m_size = -1,
|
||||
.m_methods = X_methods,
|
||||
};
|
||||
|
||||
|
||||
PyMODINIT_FUNC
|
||||
PyInit_X(void) {
|
||||
PyObject *module_instance;
|
||||
if (PyType_Ready(&DisplayType) < 0 ||
|
||||
PyType_Ready(&WindowType) < 0 ||
|
||||
PyType_Ready(&ImageType) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
module_instance = PyModule_Create(&module);
|
||||
if (module_instance == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_INCREF(&DisplayType);
|
||||
Py_INCREF(&WindowType);
|
||||
Py_INCREF(&ImageType);
|
||||
PyModule_AddObject(module_instance, "Display", (PyObject*)&DisplayType);
|
||||
PyModule_AddObject(module_instance, "OverlayWindow", (PyObject*)&WindowType);
|
||||
PyModule_AddObject(module_instance, "Image", (PyObject*)&ImageType);
|
||||
return module_instance;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
#ifndef __X_H__
|
||||
#define __X_H__
|
||||
#include "python.h"
|
||||
|
||||
|
||||
PyModuleDef module;
|
||||
#endif
|
@ -1,296 +0,0 @@
|
||||
#include "python.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/shm.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/XShm.h>
|
||||
|
||||
#include "math.h"
|
||||
#include "util.h"
|
||||
#include "display.h"
|
||||
|
||||
#define INVALID_SHM_ID -1
|
||||
#define INVALID_SHM_ADDRESS (char*)-1
|
||||
#define BYTES_PER_PIXEL 4
|
||||
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
int width;
|
||||
int height;
|
||||
int buffer_size;
|
||||
DisplayObject *display_pyobject;
|
||||
XShmSegmentInfo segmentInfo;
|
||||
XImage *image;
|
||||
} ImageObject;
|
||||
|
||||
|
||||
static inline Display *
|
||||
get_display(ImageObject *self) {
|
||||
return self->display_pyobject->event_display;
|
||||
}
|
||||
|
||||
static bool
|
||||
Image_init_shared_memory(ImageObject *self) {
|
||||
self->segmentInfo.shmid = shmget(
|
||||
IPC_PRIVATE,
|
||||
self->buffer_size,
|
||||
IPC_CREAT | 0600);
|
||||
return self->segmentInfo.shmid != INVALID_SHM_ID;
|
||||
}
|
||||
|
||||
static bool
|
||||
Image_map_shared_memory(ImageObject *self) {
|
||||
// Map the shared memory segment into the address space of this process
|
||||
self->segmentInfo.shmaddr = (char*)shmat(self->segmentInfo.shmid, 0, 0);
|
||||
|
||||
if (self->segmentInfo.shmaddr != INVALID_SHM_ADDRESS) {
|
||||
self->segmentInfo.readOnly = true;
|
||||
// Mark the shared memory segment for removal
|
||||
// It will be removed even if this program crashes
|
||||
shmctl(self->segmentInfo.shmid, IPC_RMID, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
Image_create_shared_image(ImageObject *self) {
|
||||
Display *display = get_display(self);
|
||||
int screen = XDefaultScreen(display);
|
||||
// Allocate the memory needed for the XImage structure
|
||||
self->image = XShmCreateImage(
|
||||
display, XDefaultVisual(display, screen),
|
||||
DefaultDepth(display, screen), ZPixmap, 0,
|
||||
&self->segmentInfo, 0, 0);
|
||||
|
||||
if (self->image) {
|
||||
self->image->data = (char*)self->segmentInfo.shmaddr;
|
||||
self->image->width = self->width;
|
||||
self->image->height = self->height;
|
||||
|
||||
// Ask the X server to attach the shared memory segment and sync
|
||||
XShmAttach(display, &self->segmentInfo);
|
||||
XFlush(display);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
Image_destroy_shared_image(ImageObject *self) {
|
||||
if (self->image) {
|
||||
XShmDetach(get_display(self), &self->segmentInfo);
|
||||
XDestroyImage(self->image);
|
||||
self->image = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
Image_free_shared_memory(ImageObject *self) {
|
||||
if(self->segmentInfo.shmaddr != INVALID_SHM_ADDRESS) {
|
||||
shmdt(self->segmentInfo.shmaddr);
|
||||
self->segmentInfo.shmaddr = INVALID_SHM_ADDRESS;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
Image_finalise(ImageObject *self) {
|
||||
Image_destroy_shared_image(self);
|
||||
Image_free_shared_memory(self);
|
||||
Py_CLEAR(self->display_pyobject);
|
||||
}
|
||||
|
||||
static int
|
||||
Image_init(ImageObject *self, PyObject *args, PyObject *kwds) {
|
||||
static char *kwlist[] = {"display", "width", "height", NULL};
|
||||
PyObject *display_pyobject;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "O!ii", kwlist,
|
||||
&DisplayType, &display_pyobject,
|
||||
&self->width, &self->height)) {
|
||||
Py_INIT_RETURN_ERROR;
|
||||
}
|
||||
|
||||
if (self->display_pyobject) {
|
||||
Image_finalise(self);
|
||||
}
|
||||
|
||||
Py_INCREF(display_pyobject);
|
||||
self->display_pyobject = (DisplayObject*)display_pyobject;
|
||||
self->buffer_size = self->width * self->height * BYTES_PER_PIXEL;
|
||||
|
||||
if (!Image_init_shared_memory(self)) {
|
||||
raiseInit(OSError, "could not init shared memory");
|
||||
}
|
||||
|
||||
if (!Image_map_shared_memory(self)) {
|
||||
raiseInit(OSError, "could not map shared memory");
|
||||
}
|
||||
|
||||
if (!Image_create_shared_image(self)) {
|
||||
Image_free_shared_memory(self);
|
||||
raiseInit(OSError, "could not allocate the XImage structure");
|
||||
}
|
||||
|
||||
Py_INIT_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
static void
|
||||
Image_dealloc(ImageObject *self) {
|
||||
Image_finalise(self);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Image_copy_to(ImageObject *self, PyObject *args, PyObject *kwds) {
|
||||
// draws the image on the surface at x, y
|
||||
static char *kwlist[] = {"drawable", "x", "y", "width", "height", NULL};
|
||||
Drawable surface;
|
||||
GC gc;
|
||||
int x, y;
|
||||
unsigned int width, height;
|
||||
Display *display = get_display(self);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "kiiII", kwlist,
|
||||
&surface, &x, &y, &width, &height)) {
|
||||
Py_RETURN_ERROR;
|
||||
}
|
||||
|
||||
gc = XCreateGC(display, surface, 0, NULL);
|
||||
XShmPutImage(display, surface, gc,
|
||||
self->image, 0, 0,
|
||||
x, y, width, height, false);
|
||||
XFreeGC(display, gc);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Image_draw(ImageObject *self, PyObject *args, PyObject *kwds) {
|
||||
// puts the pixels on the image at x, y
|
||||
static char *kwlist[] = {"x", "y", "width", "height", "pixels", NULL};
|
||||
int offset_x, offset_y;
|
||||
int width, height;
|
||||
int pixels_per_row;
|
||||
int source_pixels_per_row;
|
||||
int destination_pixels_per_row;
|
||||
int destination_offset_x_bytes;
|
||||
char *pixels;
|
||||
Py_ssize_t pixels_size;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "iiiis#", kwlist,
|
||||
&offset_x, &offset_y, &width, &height,
|
||||
&pixels, &pixels_size)) {
|
||||
Py_RETURN_ERROR;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
destination_offset_x_bytes = max(0, offset_x) * BYTES_PER_PIXEL;
|
||||
source_pixels_per_row = width * BYTES_PER_PIXEL;
|
||||
destination_pixels_per_row = self->width * BYTES_PER_PIXEL;
|
||||
pixels_per_row = min(width + min(offset_x, 0), self->width - max(offset_x, 0)) * BYTES_PER_PIXEL;
|
||||
|
||||
if (offset_x + width > 0 && offset_x < self->width) {
|
||||
// < 0 -> start y = 0, min(surface.height, height - abs(offset))
|
||||
// > 0 -> start y = offset, height = min(surface.height, height + offset)
|
||||
for (int y = max(0, offset_y); y < min(self->height, height + offset_y); y++) {
|
||||
// < 0 -> first row = abs(offset) => n row = y + abs(offset)
|
||||
// > 0 -> first row = - offset => n row = y - offset
|
||||
// => n row = y - offset
|
||||
int pixels_y = y - offset_y;
|
||||
void *destination =
|
||||
self->image->data + y * destination_pixels_per_row
|
||||
+ destination_offset_x_bytes;
|
||||
void *source = pixels + pixels_y * source_pixels_per_row;
|
||||
|
||||
if (! ((uintptr_t)self->image->data <= (uintptr_t)destination)) {
|
||||
raise(AssertionError,
|
||||
"The destination start address calculation went wrong.\n"
|
||||
"It points to an address which is before the start address of the buffer.\n"
|
||||
"%p not smaller than %p",
|
||||
self->image->data, destination);
|
||||
}
|
||||
if (! ((uintptr_t)destination + pixels_per_row
|
||||
<= (uintptr_t)self->image->data + self->buffer_size)) {
|
||||
raise(AssertionError,
|
||||
"The destination end address calculation went wrong.\n"
|
||||
"It points to an address which is after the end address of the buffer.\n"
|
||||
"%p not smaller than %p",
|
||||
destination + pixels_per_row,
|
||||
self->image->data + self->buffer_size);
|
||||
}
|
||||
if (! ((uintptr_t)pixels <= (uintptr_t)source)) {
|
||||
raise(AssertionError,
|
||||
"The source start address calculation went wrong.\n"
|
||||
"It points to an address which is before the start address of the buffer.\n"
|
||||
"%p not smaller than %p",
|
||||
pixels, source);
|
||||
}
|
||||
if (! ((uintptr_t)source + pixels_per_row
|
||||
<= (uintptr_t)pixels + pixels_size)) {
|
||||
raise(AssertionError,
|
||||
"The source end address calculation went wrong.\n"
|
||||
"It points to an address which is after the end address of the buffer."
|
||||
"%p not smaller than %p",
|
||||
source + pixels_per_row,
|
||||
pixels + pixels_size);
|
||||
}
|
||||
|
||||
memcpy(destination, source, pixels_per_row);
|
||||
}
|
||||
}
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyMethodDef Image_methods[] = {
|
||||
{"copy_to", (PyCFunction)Image_copy_to,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"Draws the image on the surface at the passed coordinate.\n"
|
||||
"\n"
|
||||
"Args:\n"
|
||||
" drawable (int): the surface to draw on\n"
|
||||
" x (int): the x position where this image should be placed\n"
|
||||
" y (int): the y position where this image should be placed\n"
|
||||
" width (int): the width of the area\n"
|
||||
" which should be copied to the drawable\n"
|
||||
" height (int): the height of the area\n"
|
||||
" which should be copied to the drawable"},
|
||||
{"draw", (PyCFunction)Image_draw,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"Places the pixels on the image at the passed coordinate.\n"
|
||||
"\n"
|
||||
"Args:\n"
|
||||
" x (int): the x position where the pixels should be placed\n"
|
||||
" y (int): the y position where the pixels should be placed\n"
|
||||
" width (int): amount of pixels per row in the passed data\n"
|
||||
" height (int): amount of pixels per column in the passed data\n"
|
||||
" pixels (bytes): the pixels to place on the image"},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
PyTypeObject ImageType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "ueberzug.X.Image",
|
||||
.tp_doc =
|
||||
"An shared memory X11 Image\n"
|
||||
"\n"
|
||||
"Args:\n"
|
||||
" display (ueberzug.X.Display): the X11 display\n"
|
||||
" width (int): the width of this image\n"
|
||||
" height (int): the height of this image",
|
||||
.tp_basicsize = sizeof(ImageObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_new = PyType_GenericNew,
|
||||
.tp_init = (initproc)Image_init,
|
||||
.tp_dealloc = (destructor) Image_dealloc,
|
||||
.tp_methods = Image_methods,
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
#ifndef __XSHM_H__
|
||||
#define __XSHM_H__
|
||||
#include "python.h"
|
||||
|
||||
|
||||
extern PyTypeObject ImageType;
|
||||
#endif
|
@ -1,276 +0,0 @@
|
||||
#include "python.h"
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XRes.h>
|
||||
#include <X11/extensions/XShm.h>
|
||||
|
||||
#include "util.h"
|
||||
#include "display.h"
|
||||
|
||||
|
||||
#define INVALID_PID (pid_t)-1
|
||||
|
||||
|
||||
#define REOPEN_DISPLAY(display) \
|
||||
if (display != NULL) { \
|
||||
XCloseDisplay(display); \
|
||||
} \
|
||||
display = XOpenDisplay(NULL);
|
||||
|
||||
#define CLOSE_DISPLAY(display) \
|
||||
if (display != NULL) { \
|
||||
XCloseDisplay(display); \
|
||||
display = NULL; \
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
Display_init(DisplayObject *self, PyObject *args, PyObject *kwds) {
|
||||
// Two connections are opened as
|
||||
// a death lock can occur
|
||||
// if you listen for events
|
||||
// (this will happen in parallel in asyncio worker threads)
|
||||
// and request information (e.g. XGetGeometry)
|
||||
// simultaneously.
|
||||
REOPEN_DISPLAY(self->event_display);
|
||||
REOPEN_DISPLAY(self->info_display);
|
||||
|
||||
if (self->event_display == NULL ||
|
||||
self->info_display == NULL) {
|
||||
raiseInit(OSError, "could not open a connection to the X server");
|
||||
}
|
||||
|
||||
int _;
|
||||
if (!XResQueryExtension(self->info_display, &_, &_)) {
|
||||
raiseInit(OSError, "the extension XRes is required");
|
||||
}
|
||||
|
||||
if (!XShmQueryExtension(self->event_display)) {
|
||||
raiseInit(OSError, "the extension Xext is required");
|
||||
}
|
||||
|
||||
int screen = XDefaultScreen(self->info_display);
|
||||
self->screen_width = XDisplayWidth(self->info_display, screen);
|
||||
self->screen_height = XDisplayHeight(self->info_display, screen);
|
||||
self->bitmap_pad = XBitmapPad(self->info_display);
|
||||
self->bitmap_unit = XBitmapUnit(self->info_display);
|
||||
|
||||
self->wm_class = XInternAtom(self->info_display, "WM_CLASS", False);
|
||||
self->wm_name = XInternAtom(self->info_display, "WM_NAME", False);
|
||||
self->wm_locale_name = XInternAtom(self->info_display, "WM_LOCALE_NAME", False);
|
||||
self->wm_normal_hints = XInternAtom(self->info_display, "WM_NORMAL_HINTS", False);
|
||||
|
||||
Py_INIT_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
static void
|
||||
Display_dealloc(DisplayObject *self) {
|
||||
CLOSE_DISPLAY(self->event_display);
|
||||
CLOSE_DISPLAY(self->info_display);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
has_property(DisplayObject *self, Window window, Atom property) {
|
||||
Atom actual_type_return;
|
||||
int actual_format_return;
|
||||
unsigned long bytes_after_return;
|
||||
unsigned char* prop_to_return = NULL;
|
||||
unsigned long nitems_return;
|
||||
|
||||
int status = XGetWindowProperty(
|
||||
self->info_display, window, property, 0,
|
||||
0L, False,
|
||||
AnyPropertyType,
|
||||
&actual_type_return,
|
||||
&actual_format_return,
|
||||
&nitems_return, &bytes_after_return, &prop_to_return);
|
||||
if (status == Success && prop_to_return) {
|
||||
XFree(prop_to_return);
|
||||
}
|
||||
return status == Success && !(actual_type_return == None && actual_format_return == 0);
|
||||
}
|
||||
|
||||
|
||||
static PyObject *
|
||||
Display_get_child_window_ids(DisplayObject *self, PyObject *args, PyObject *kwds) {
|
||||
static char *kwlist[] = {"parent_id", NULL};
|
||||
Window parent = XDefaultRootWindow(self->info_display);
|
||||
Window _, *children;
|
||||
unsigned int children_count;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "|k", kwlist,
|
||||
&parent)) {
|
||||
Py_RETURN_ERROR;
|
||||
}
|
||||
|
||||
if (!XQueryTree(
|
||||
self->info_display, parent,
|
||||
&_, &_, &children, &children_count)) {
|
||||
raise(OSError, "failed to query child windows of %lu", parent);
|
||||
}
|
||||
|
||||
PyObject *child_ids = PyList_New(0);
|
||||
if (children) {
|
||||
for (unsigned int i = 0; i < children_count; i++) {
|
||||
// assume that windows without essential properties
|
||||
// like the window title aren't shown to the user
|
||||
int is_helper_window = (
|
||||
!has_property(self, children[i], self->wm_class) &&
|
||||
!has_property(self, children[i], self->wm_name) &&
|
||||
!has_property(self, children[i], self->wm_locale_name) &&
|
||||
!has_property(self, children[i], self->wm_normal_hints));
|
||||
if (is_helper_window) {
|
||||
continue;
|
||||
}
|
||||
PyObject *py_window_id = Py_BuildValue("k", children[i]);
|
||||
PyList_Append(child_ids, py_window_id);
|
||||
Py_XDECREF(py_window_id);
|
||||
}
|
||||
XFree(children);
|
||||
}
|
||||
|
||||
return child_ids;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Display_get_window_pid(DisplayObject *self, PyObject *args, PyObject *kwds) {
|
||||
static char *kwlist[] = {"window_id", NULL};
|
||||
Window window;
|
||||
long num_ids;
|
||||
int num_specs = 1;
|
||||
XResClientIdValue *client_ids;
|
||||
XResClientIdSpec client_specs[1];
|
||||
pid_t window_creator_pid = INVALID_PID;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "k", kwlist,
|
||||
&window)) {
|
||||
Py_RETURN_ERROR;
|
||||
}
|
||||
|
||||
client_specs[0].client = window;
|
||||
client_specs[0].mask = XRES_CLIENT_ID_PID_MASK;
|
||||
if (Success != XResQueryClientIds(
|
||||
self->info_display, num_specs, client_specs,
|
||||
&num_ids, &client_ids)) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
for(int i = 0; i < num_ids; i++) {
|
||||
XResClientIdValue *value = client_ids + i;
|
||||
XResClientIdType type = XResGetClientIdType(value);
|
||||
|
||||
if (type == XRES_CLIENT_ID_PID) {
|
||||
window_creator_pid = XResGetClientPid(value);
|
||||
}
|
||||
}
|
||||
|
||||
XFree(client_ids);
|
||||
|
||||
if (window_creator_pid != INVALID_PID) {
|
||||
return Py_BuildValue("i", window_creator_pid);
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Display_wait_for_event(DisplayObject *self) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
XEvent event;
|
||||
XPeekEvent(self->event_display, &event);
|
||||
Py_END_ALLOW_THREADS
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Display_discard_event(DisplayObject *self) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
XEvent event;
|
||||
XNextEvent(self->event_display, &event);
|
||||
Py_END_ALLOW_THREADS
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Display_get_bitmap_format_scanline_pad(DisplayObject *self, void *closure) {
|
||||
return Py_BuildValue("i", self->bitmap_pad);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Display_get_bitmap_format_scanline_unit(DisplayObject *self, void *closure) {
|
||||
return Py_BuildValue("i", self->bitmap_unit);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Display_get_screen_width(DisplayObject *self, void *closure) {
|
||||
return Py_BuildValue("i", self->screen_width);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Display_get_screen_height(DisplayObject *self, void *closure) {
|
||||
return Py_BuildValue("i", self->screen_height);
|
||||
}
|
||||
|
||||
|
||||
static PyGetSetDef Display_properties[] = {
|
||||
{"bitmap_format_scanline_pad", (getter)Display_get_bitmap_format_scanline_pad,
|
||||
.doc = "int: Each scanline must be padded to a multiple of bits of this value."},
|
||||
{"bitmap_format_scanline_unit", (getter)Display_get_bitmap_format_scanline_unit,
|
||||
.doc = "int:\n"
|
||||
" The size of a bitmap's scanline unit in bits.\n"
|
||||
" The scanline is calculated in multiples of this value."},
|
||||
{"screen_width", (getter)Display_get_screen_width,
|
||||
.doc = "int: The width of the default screen at the time the connection to X11 was opened."},
|
||||
{"screen_height", (getter)Display_get_screen_height,
|
||||
.doc = "int: The height of the default screen at the time the connection to X11 was opened."},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
static PyMethodDef Display_methods[] = {
|
||||
{"wait_for_event", (PyCFunction)Display_wait_for_event,
|
||||
METH_NOARGS,
|
||||
"Waits for an event to occur. till an event occur."},
|
||||
{"discard_event", (PyCFunction)Display_discard_event,
|
||||
METH_NOARGS,
|
||||
"Discards the first event from the event queue."},
|
||||
{"get_child_window_ids", (PyCFunction)Display_get_child_window_ids,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"Queries for the ids of the children of the window with the passed identifier.\n"
|
||||
"\n"
|
||||
"Args:\n"
|
||||
" parent_id (int): optional\n"
|
||||
" the id of the window for which to query for the ids of its children\n"
|
||||
" if it's not specified the id of the default root window will be used\n"
|
||||
"\n"
|
||||
"Returns:\n"
|
||||
" list of ints: the ids of the child windows"},
|
||||
{"get_window_pid", (PyCFunction)Display_get_window_pid,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"Tries to figure out the pid of the process which created the window with the passed id.\n"
|
||||
"\n"
|
||||
"Args:\n"
|
||||
" window_id (int): the window id for which to retrieve information\n"
|
||||
"\n"
|
||||
"Returns:\n"
|
||||
" int or None: the pid of the creator of the window"},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
PyTypeObject DisplayType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "ueberzug.X.Display",
|
||||
.tp_doc = "X11 display\n",
|
||||
.tp_basicsize = sizeof(DisplayObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_new = PyType_GenericNew,
|
||||
.tp_init = (initproc)Display_init,
|
||||
.tp_dealloc = (destructor)Display_dealloc,
|
||||
.tp_getset = Display_properties,
|
||||
.tp_methods = Display_methods,
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
#ifndef __DISPLAY_H__
|
||||
#define __DISPLAY_H__
|
||||
|
||||
#include "python.h"
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
// Always use the event_display
|
||||
// except for functions which return information
|
||||
// e.g. XGetGeometry.
|
||||
Display *event_display;
|
||||
Display *info_display;
|
||||
int bitmap_pad;
|
||||
int bitmap_unit;
|
||||
int screen;
|
||||
int screen_width;
|
||||
int screen_height;
|
||||
|
||||
Atom wm_class;
|
||||
Atom wm_name;
|
||||
Atom wm_locale_name;
|
||||
Atom wm_normal_hints;
|
||||
} DisplayObject;
|
||||
extern PyTypeObject DisplayType;
|
||||
|
||||
#endif
|
@ -1,7 +0,0 @@
|
||||
#ifndef __MATH_H__
|
||||
#define __MATH_H__
|
||||
|
||||
#define min(a,b) (((a) < (b)) ? (a) : (b))
|
||||
#define max(a,b) (((a) > (b)) ? (a) : (b))
|
||||
|
||||
#endif
|
@ -1,11 +0,0 @@
|
||||
#ifndef __PYTHON_H__
|
||||
#define __PYTHON_H__
|
||||
|
||||
#ifndef __linux__
|
||||
#error OS unsupported
|
||||
#endif
|
||||
|
||||
#define PY_SSIZE_T_CLEAN // Make "s#" use Py_ssize_t rather than int.
|
||||
#include <Python.h>
|
||||
|
||||
#endif
|
@ -1,28 +0,0 @@
|
||||
#ifndef __UTIL_H__
|
||||
#define __UTIL_H__
|
||||
|
||||
#define Py_INIT_ERROR -1
|
||||
#define Py_INIT_SUCCESS 0
|
||||
#define Py_ERROR NULL
|
||||
#define Py_RETURN_ERROR return Py_ERROR
|
||||
#define Py_INIT_RETURN_ERROR return Py_INIT_ERROR
|
||||
#define Py_INIT_RETURN_SUCCESS return Py_INIT_SUCCESS
|
||||
|
||||
#define __raise(return_value, Exception, message...) { \
|
||||
char errorMessage[500]; \
|
||||
snprintf(errorMessage, 500, message); \
|
||||
PyErr_SetString( \
|
||||
PyExc_##Exception, \
|
||||
errorMessage); \
|
||||
return return_value; \
|
||||
}
|
||||
#define raise(Exception, message...) __raise(Py_ERROR, Exception, message)
|
||||
#define raiseInit(Exception, message...) __raise(Py_INIT_ERROR, Exception, message)
|
||||
|
||||
#define ARRAY_LENGTH(stack_array) \
|
||||
(sizeof stack_array \
|
||||
? sizeof stack_array / sizeof *stack_array \
|
||||
: 0)
|
||||
|
||||
|
||||
#endif
|
@ -1,316 +0,0 @@
|
||||
#include "python.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/shape.h>
|
||||
|
||||
#include "util.h"
|
||||
#include "display.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
DisplayObject *display_pyobject;
|
||||
Window parent;
|
||||
Window window;
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
} WindowObject;
|
||||
|
||||
|
||||
static inline Display *
|
||||
get_event_display(WindowObject *self) {
|
||||
return self->display_pyobject->event_display;
|
||||
}
|
||||
|
||||
static inline Display *
|
||||
get_info_display(WindowObject *self) {
|
||||
return self->display_pyobject->info_display;
|
||||
}
|
||||
|
||||
static void
|
||||
Window_create(WindowObject *self) {
|
||||
Window _0; int _1; unsigned int _2;
|
||||
XGetGeometry(
|
||||
get_info_display(self),
|
||||
self->parent,
|
||||
&_0, &_1, &_1,
|
||||
&self->width, &self->height,
|
||||
&_2, &_2);
|
||||
|
||||
Display *display = get_event_display(self);
|
||||
int screen = XDefaultScreen(display);
|
||||
Visual *visual = XDefaultVisual(display, screen);
|
||||
unsigned long attributes_mask =
|
||||
CWEventMask | CWBackPixel | CWColormap | CWBorderPixel;
|
||||
XSetWindowAttributes attributes;
|
||||
attributes.event_mask = ExposureMask;
|
||||
attributes.colormap = XCreateColormap(
|
||||
display, XDefaultRootWindow(display),
|
||||
visual, AllocNone);
|
||||
attributes.background_pixel = 0;
|
||||
attributes.border_pixel = 0;
|
||||
|
||||
self->window = XCreateWindow(
|
||||
display, self->parent,
|
||||
0, 0, self->width, self->height, 0,
|
||||
DefaultDepth(display, screen),
|
||||
InputOutput, visual,
|
||||
attributes_mask, &attributes);
|
||||
}
|
||||
|
||||
static void
|
||||
set_subscribed_events(Display *display, Window window, long event_mask) {
|
||||
XSetWindowAttributes attributes;
|
||||
attributes.event_mask = event_mask;
|
||||
XChangeWindowAttributes(
|
||||
display, window,
|
||||
CWEventMask , &attributes);
|
||||
}
|
||||
|
||||
static void
|
||||
Window_finalise(WindowObject *self) {
|
||||
if (self->window) {
|
||||
Display *display = get_event_display(self);
|
||||
set_subscribed_events(
|
||||
display, self->parent, NoEventMask);
|
||||
XDestroyWindow(display, self->window);
|
||||
XFlush(display);
|
||||
}
|
||||
|
||||
Py_CLEAR(self->display_pyobject);
|
||||
self->window = 0;
|
||||
}
|
||||
|
||||
static inline void
|
||||
set_xshape_mask(Display *display, Window window, int kind, XRectangle area[], int area_length) {
|
||||
XShapeCombineRectangles(
|
||||
display, window,
|
||||
kind,
|
||||
0, 0, area, area_length,
|
||||
ShapeSet, 0);
|
||||
}
|
||||
|
||||
static inline void
|
||||
set_input_mask(Display *display, Window window, XRectangle area[], int area_length) {
|
||||
set_xshape_mask(
|
||||
display, window, ShapeInput, area, area_length);
|
||||
}
|
||||
|
||||
static inline void
|
||||
set_visibility_mask(Display *display, Window window, XRectangle area[], int area_length) {
|
||||
set_xshape_mask(
|
||||
display, window, ShapeBounding, area, area_length);
|
||||
}
|
||||
|
||||
static int
|
||||
Window_init(WindowObject *self, PyObject *args, PyObject *kwds) {
|
||||
static XRectangle empty_area[0] = {};
|
||||
static char *kwlist[] = {"display", "parent", NULL};
|
||||
PyObject *display_pyobject;
|
||||
Window parent;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "O!k", kwlist,
|
||||
&DisplayType, &display_pyobject, &parent)) {
|
||||
Py_INIT_RETURN_ERROR;
|
||||
}
|
||||
|
||||
if (self->display_pyobject) {
|
||||
Window_finalise(self);
|
||||
}
|
||||
|
||||
Py_INCREF(display_pyobject);
|
||||
self->display_pyobject = (DisplayObject*)display_pyobject;
|
||||
Display *display = get_event_display(self);
|
||||
self->parent = parent;
|
||||
Window_create(self);
|
||||
set_subscribed_events(
|
||||
display, self->parent, StructureNotifyMask);
|
||||
set_input_mask(
|
||||
display, self->window,
|
||||
empty_area, ARRAY_LENGTH(empty_area));
|
||||
set_visibility_mask(
|
||||
display, self->window,
|
||||
empty_area, ARRAY_LENGTH(empty_area));
|
||||
XMapWindow(display, self->window);
|
||||
|
||||
Py_INIT_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
static void
|
||||
Window_dealloc(WindowObject *self) {
|
||||
Window_finalise(self);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Window_set_visibility_mask(WindowObject *self, PyObject *args, PyObject *kwds) {
|
||||
static char *kwlist[] = {"area", NULL};
|
||||
PyObject *py_area;
|
||||
Py_ssize_t area_length;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "O!", kwlist,
|
||||
&PyList_Type, &py_area)) {
|
||||
Py_RETURN_ERROR;
|
||||
}
|
||||
|
||||
area_length = PyList_Size(py_area);
|
||||
XRectangle area[area_length];
|
||||
|
||||
for (Py_ssize_t i = 0; i < area_length; i++) {
|
||||
short x, y;
|
||||
unsigned short width, height;
|
||||
PyObject *py_rectangle = PyList_GetItem(py_area, i);
|
||||
|
||||
if (!PyObject_TypeCheck(py_rectangle, &PyTuple_Type)) {
|
||||
raise(ValueError, "Expected a list of a tuple of ints!");
|
||||
}
|
||||
if (!PyArg_ParseTuple(
|
||||
py_rectangle, "hhHH",
|
||||
&x, &y, &width, &height)) {
|
||||
raise(ValueError,
|
||||
"Expected a rectangle to be a "
|
||||
"tuple of (x: int, y: int, width: int, height: int)!");
|
||||
}
|
||||
|
||||
area[i].x = x;
|
||||
area[i].y = y;
|
||||
area[i].width = width;
|
||||
area[i].height = height;
|
||||
}
|
||||
|
||||
set_visibility_mask(
|
||||
get_event_display(self),
|
||||
self->window,
|
||||
area, area_length);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Window_draw(WindowObject *self) {
|
||||
XFlush(get_event_display(self));
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Window_get_id(WindowObject *self, void *closure) {
|
||||
return Py_BuildValue("k", self->window);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Window_get_parent_id(WindowObject *self, void *closure) {
|
||||
return Py_BuildValue("k", self->parent);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Window_get_width(WindowObject *self, void *closure) {
|
||||
return Py_BuildValue("I", self->width);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Window_get_height(WindowObject *self, void *closure) {
|
||||
return Py_BuildValue("I", self->height);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
Window_process_event(WindowObject *self) {
|
||||
XEvent event;
|
||||
XAnyEvent *metadata = &event.xany;
|
||||
Display *display = get_event_display(self);
|
||||
|
||||
if (!XPending(display)) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
XPeekEvent(display, &event);
|
||||
|
||||
if (! (event.type == Expose && metadata->window == self->window) &&
|
||||
! (event.type == ConfigureNotify && metadata->window == self->parent)) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
XNextEvent(display, &event);
|
||||
|
||||
switch (event.type) {
|
||||
case Expose:
|
||||
if(event.xexpose.count == 0) {
|
||||
Py_XDECREF(PyObject_CallMethod(
|
||||
(PyObject*)self, "draw", NULL));
|
||||
}
|
||||
break;
|
||||
case ConfigureNotify: {
|
||||
unsigned int delta_width =
|
||||
((unsigned int)event.xconfigure.width) - self->width;
|
||||
unsigned int delta_height =
|
||||
((unsigned int)event.xconfigure.height) - self->height;
|
||||
|
||||
if (delta_width != 0 || delta_height != 0) {
|
||||
self->width = (unsigned int)event.xconfigure.width;
|
||||
self->height = (unsigned int)event.xconfigure.height;
|
||||
XResizeWindow(display, self->window, self->width, self->height);
|
||||
}
|
||||
|
||||
if (delta_width > 0 || delta_height > 0) {
|
||||
Py_XDECREF(PyObject_CallMethod(
|
||||
(PyObject*)self, "draw", NULL));
|
||||
}
|
||||
else {
|
||||
XFlush(display);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
static PyGetSetDef Window_properties[] = {
|
||||
{"id", (getter)Window_get_id, .doc = "int: the X11 id of this window."},
|
||||
{"parent_id", (getter)Window_get_parent_id, .doc = "int: the X11 id of the parent window."},
|
||||
{"width", (getter)Window_get_width, .doc = "int: the width of this window."},
|
||||
{"height", (getter)Window_get_height, .doc = "int: the height of this window."},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static PyMethodDef Window_methods[] = {
|
||||
{"draw", (PyCFunction)Window_draw,
|
||||
METH_NOARGS,
|
||||
"Redraws the window."},
|
||||
{"set_visibility_mask", (PyCFunction)Window_set_visibility_mask,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"Specifies the part of the window which should be visible.\n"
|
||||
"\n"
|
||||
"Args:\n"
|
||||
" area (tuple of (tuple of (x: int, y: int, width: int, height: int))):\n"
|
||||
" the visible area specified by rectangles"},
|
||||
{"process_event", (PyCFunction)Window_process_event,
|
||||
METH_NOARGS,
|
||||
"Processes the next X11 event if it targets this window.\n"
|
||||
"\n"
|
||||
"Returns:\n"
|
||||
" bool: True if an event was processed"},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
PyTypeObject WindowType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "ueberzug.X.OverlayWindow",
|
||||
.tp_doc =
|
||||
"Basic implementation of an overlay window\n"
|
||||
"\n"
|
||||
"Args:\n"
|
||||
" display (ueberzug.X.Display): the X11 display\n"
|
||||
" parent (int): the parent window of this window",
|
||||
.tp_basicsize = sizeof(WindowObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_new = PyType_GenericNew,
|
||||
.tp_init = (initproc)Window_init,
|
||||
.tp_dealloc = (destructor)Window_dealloc,
|
||||
.tp_getset = Window_properties,
|
||||
.tp_methods = Window_methods,
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
#ifndef __WINDOW_H__
|
||||
#define __WINDOW_H__
|
||||
#include "python.h"
|
||||
|
||||
|
||||
extern PyTypeObject WindowType;
|
||||
#endif
|
@ -1,7 +0,0 @@
|
||||
__version__ = '18.1.9'
|
||||
__license__ = 'GPLv3'
|
||||
__description__ ='ueberzug is a command line util which allows to display images in combination with X11'
|
||||
__url_repository__ = ''
|
||||
__url_bug_reports__ = ''
|
||||
__url_project__ = __url_repository__
|
||||
__author__ = ''
|
@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Usage:
|
||||
ueberzug layer [options]
|
||||
ueberzug library
|
||||
ueberzug version
|
||||
ueberzug query_windows PIDS ...
|
||||
|
||||
Routines:
|
||||
layer Display images
|
||||
library Prints the path to the bash library
|
||||
version Prints the project version
|
||||
query_windows Orders ueberzug to search for windows.
|
||||
Only for internal use.
|
||||
|
||||
Layer options:
|
||||
-p, --parser <parser> one of json, simple, bash
|
||||
json: Json-Object per line
|
||||
simple: Key-Values separated by a tab
|
||||
bash: associative array dumped via `declare -p`
|
||||
[default: json]
|
||||
-l, --loader <loader> one of synchronous, thread, process
|
||||
synchronous: load images right away
|
||||
thread: load images in threads
|
||||
process: load images in additional processes
|
||||
[default: thread]
|
||||
-s, --silent print stderr to /dev/null
|
||||
|
||||
|
||||
License:
|
||||
This program comes with ABSOLUTELY NO WARRANTY.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions.
|
||||
"""
|
||||
import docopt
|
||||
|
||||
|
||||
def main():
|
||||
options = docopt.docopt(__doc__)
|
||||
module = None
|
||||
|
||||
if options['layer']:
|
||||
import ueberzug.layer as layer
|
||||
module = layer
|
||||
elif options['library']:
|
||||
import ueberzug.library as library
|
||||
module = library
|
||||
elif options['query_windows']:
|
||||
import ueberzug.query_windows as query_windows
|
||||
module = query_windows
|
||||
elif options['version']:
|
||||
import ueberzug.version as version
|
||||
module = version
|
||||
|
||||
module.main(options)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,294 +0,0 @@
|
||||
import abc
|
||||
import enum
|
||||
import os.path
|
||||
|
||||
import attr
|
||||
|
||||
import ueberzug.geometry as geometry
|
||||
import ueberzug.scaling as scaling
|
||||
import ueberzug.conversion as conversion
|
||||
|
||||
|
||||
@attr.s
|
||||
class Action(metaclass=abc.ABCMeta):
|
||||
"""Describes the structure used to define actions classes.
|
||||
|
||||
Defines a general interface used to implement the building of commands
|
||||
and their execution.
|
||||
"""
|
||||
action = attr.ib(type=str, default=attr.Factory(
|
||||
lambda self: self.get_action_name(), takes_self=True))
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def get_action_name():
|
||||
"""Returns the constant name which is associated to this action."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def apply(self, windows, view, tools):
|
||||
"""Executes the action on the passed view and windows."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class Drawable:
|
||||
"""Defines the attributes of drawable actions."""
|
||||
draw = attr.ib(default=True, converter=conversion.to_bool)
|
||||
synchronously_draw = attr.ib(default=False, converter=conversion.to_bool)
|
||||
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class Identifiable:
|
||||
"""Defines the attributes of actions
|
||||
which are associated to an identifier.
|
||||
"""
|
||||
identifier = attr.ib(type=str)
|
||||
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class DrawAction(Action, Drawable, metaclass=abc.ABCMeta):
|
||||
"""Defines actions which redraws all windows."""
|
||||
# pylint: disable=abstract-method
|
||||
__redraw_scheduled = False
|
||||
|
||||
@staticmethod
|
||||
def schedule_redraw(windows):
|
||||
"""Creates a async function which redraws every window
|
||||
if there is no unexecuted function
|
||||
(returned by this function)
|
||||
which does the same.
|
||||
|
||||
Args:
|
||||
windows (batch.BatchList of ui.CanvasWindow):
|
||||
the windows to be redrawn
|
||||
|
||||
Returns:
|
||||
function: the redraw function or None
|
||||
"""
|
||||
if not DrawAction.__redraw_scheduled:
|
||||
DrawAction.__redraw_scheduled = True
|
||||
|
||||
async def redraw():
|
||||
windows.draw()
|
||||
DrawAction.__redraw_scheduled = False
|
||||
return redraw()
|
||||
return None
|
||||
|
||||
async def apply(self, windows, view, tools):
|
||||
if self.draw:
|
||||
import asyncio
|
||||
if self.synchronously_draw:
|
||||
windows.draw()
|
||||
# force coroutine switch
|
||||
await asyncio.sleep(0)
|
||||
return
|
||||
|
||||
function = self.schedule_redraw(windows)
|
||||
if function:
|
||||
asyncio.ensure_future(function)
|
||||
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class ImageAction(DrawAction, Identifiable, metaclass=abc.ABCMeta):
|
||||
"""Defines actions which are related to images."""
|
||||
# pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class AddImageAction(ImageAction):
|
||||
"""Displays the image according to the passed option.
|
||||
If there's already an image with the given identifier
|
||||
it's going to be replaced.
|
||||
"""
|
||||
|
||||
x = attr.ib(type=int, converter=int)
|
||||
y = attr.ib(type=int, converter=int)
|
||||
path = attr.ib(type=str)
|
||||
width = attr.ib(type=int, converter=int, default=0)
|
||||
height = attr.ib(type=int, converter=int, default=0)
|
||||
scaling_position_x = attr.ib(type=float, converter=float, default=0)
|
||||
scaling_position_y = attr.ib(type=float, converter=float, default=0)
|
||||
scaler = attr.ib(
|
||||
type=str, default=scaling.ContainImageScaler.get_scaler_name())
|
||||
# deprecated
|
||||
max_width = attr.ib(type=int, converter=int, default=0)
|
||||
max_height = attr.ib(type=int, converter=int, default=0)
|
||||
|
||||
@staticmethod
|
||||
def get_action_name():
|
||||
return 'add'
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
self.width = self.max_width or self.width
|
||||
self.height = self.max_height or self.height
|
||||
# attrs doesn't support overriding the init method
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.__scaler_class = None
|
||||
self.__last_modified = None
|
||||
|
||||
@property
|
||||
def scaler_class(self):
|
||||
"""scaling.ImageScaler: the used scaler class of this placement"""
|
||||
if self.__scaler_class is None:
|
||||
self.__scaler_class = \
|
||||
scaling.ScalerOption(self.scaler).scaler_class
|
||||
return self.__scaler_class
|
||||
|
||||
@property
|
||||
def last_modified(self):
|
||||
"""float: the last modified time of the image"""
|
||||
if self.__last_modified is None:
|
||||
self.__last_modified = os.path.getmtime(self.path)
|
||||
return self.__last_modified
|
||||
|
||||
def is_same_image(self, old_placement):
|
||||
"""Determines whether the placement contains the same image
|
||||
after applying the changes of this command.
|
||||
|
||||
Args:
|
||||
old_placement (ui.CanvasWindow.Placement):
|
||||
the old data of the placement
|
||||
|
||||
Returns:
|
||||
bool: True if it's the same file
|
||||
"""
|
||||
return old_placement and not (
|
||||
old_placement.last_modified < self.last_modified
|
||||
or self.path != old_placement.path)
|
||||
|
||||
def is_full_reload_required(self, old_placement,
|
||||
screen_columns, screen_rows):
|
||||
"""Determines whether it's required to fully reload
|
||||
the image of the placement to properly render the placement.
|
||||
|
||||
Args:
|
||||
old_placement (ui.CanvasWindow.Placement):
|
||||
the old data of the placement
|
||||
screen_columns (float):
|
||||
the maximum amount of columns the screen can display
|
||||
screen_rows (float):
|
||||
the maximum amount of rows the screen can display
|
||||
|
||||
Returns:
|
||||
bool: True if the image should be reloaded
|
||||
"""
|
||||
return old_placement and (
|
||||
(not self.scaler_class.is_indulgent_resizing()
|
||||
and old_placement.scaler.is_indulgent_resizing())
|
||||
or (old_placement.width <= screen_columns < self.width)
|
||||
or (old_placement.height <= screen_rows < self.height))
|
||||
|
||||
def is_partly_reload_required(self, old_placement,
|
||||
screen_columns, screen_rows):
|
||||
"""Determines whether it's required to partly reload
|
||||
the image of the placement to render the placement more quickly.
|
||||
|
||||
Args:
|
||||
old_placement (ui.CanvasWindow.Placement):
|
||||
the old data of the placement
|
||||
screen_columns (float):
|
||||
the maximum amount of columns the screen can display
|
||||
screen_rows (float):
|
||||
the maximum amount of rows the screen can display
|
||||
|
||||
Returns:
|
||||
bool: True if the image should be reloaded
|
||||
"""
|
||||
return old_placement and (
|
||||
(self.scaler_class.is_indulgent_resizing()
|
||||
and not old_placement.scaler.is_indulgent_resizing())
|
||||
or (self.width <= screen_columns < old_placement.width)
|
||||
or (self.height <= screen_rows < old_placement.height))
|
||||
|
||||
async def apply(self, windows, view, tools):
|
||||
try:
|
||||
import ueberzug.ui as ui
|
||||
import ueberzug.loading as loading
|
||||
old_placement = view.media.pop(self.identifier, None)
|
||||
cache = old_placement and old_placement.cache
|
||||
image = old_placement and old_placement.image
|
||||
|
||||
max_font_width = max(map(
|
||||
lambda i: i or 0, windows.parent_info.font_width or [0]))
|
||||
max_font_height = max(map(
|
||||
lambda i: i or 0, windows.parent_info.font_height or [0]))
|
||||
font_size_available = max_font_width and max_font_height
|
||||
screen_columns = (font_size_available and
|
||||
view.screen_width / max_font_width)
|
||||
screen_rows = (font_size_available and
|
||||
view.screen_height / max_font_height)
|
||||
|
||||
# By default images are only stored up to a resolution which
|
||||
# is about as big as the screen resolution.
|
||||
# (loading.CoverPostLoadImageProcessor)
|
||||
# The principle of spatial locality does not apply to
|
||||
# resize operations of images with big resolutions
|
||||
# which is why those operations should be applied
|
||||
# to a resized version of those images.
|
||||
# Sometimes we still need all pixels e.g.
|
||||
# if the image scaler crop is used.
|
||||
# So sometimes it's required to fully load them
|
||||
# and sometimes it's not required anymore which is
|
||||
# why they should be partly reloaded
|
||||
# (to speed up the resize operations again).
|
||||
if (not self.is_same_image(old_placement)
|
||||
or (font_size_available and self.is_full_reload_required(
|
||||
old_placement, screen_columns, screen_rows))
|
||||
or (font_size_available and self.is_partly_reload_required(
|
||||
old_placement, screen_columns, screen_rows))):
|
||||
upper_bound_size = None
|
||||
image_post_load_processor = None
|
||||
if (self.scaler_class != scaling.CropImageScaler and
|
||||
font_size_available):
|
||||
upper_bound_size = (
|
||||
max_font_width * self.width,
|
||||
max_font_height * self.height)
|
||||
if (self.scaler_class != scaling.CropImageScaler
|
||||
and font_size_available
|
||||
and self.width <= screen_columns
|
||||
and self.height <= screen_rows):
|
||||
image_post_load_processor = \
|
||||
loading.CoverPostLoadImageProcessor(
|
||||
view.screen_width, view.screen_height)
|
||||
image = tools.loader.load(
|
||||
self.path, upper_bound_size, image_post_load_processor)
|
||||
cache = None
|
||||
|
||||
view.media[self.identifier] = ui.CanvasWindow.Placement(
|
||||
self.x, self.y, self.width, self.height,
|
||||
geometry.Point(self.scaling_position_x,
|
||||
self.scaling_position_y),
|
||||
self.scaler_class(),
|
||||
self.path, image, self.last_modified, cache)
|
||||
finally:
|
||||
await super().apply(windows, view, tools)
|
||||
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class RemoveImageAction(ImageAction):
|
||||
"""Removes the image with the passed identifier."""
|
||||
|
||||
@staticmethod
|
||||
def get_action_name():
|
||||
return 'remove'
|
||||
|
||||
async def apply(self, windows, view, tools):
|
||||
try:
|
||||
if self.identifier in view.media:
|
||||
del view.media[self.identifier]
|
||||
finally:
|
||||
await super().apply(windows, view, tools)
|
||||
|
||||
|
||||
@enum.unique
|
||||
class Command(str, enum.Enum):
|
||||
ADD = AddImageAction
|
||||
REMOVE = RemoveImageAction
|
||||
|
||||
def __new__(cls, action_class):
|
||||
inst = str.__new__(cls)
|
||||
inst._value_ = action_class.get_action_name()
|
||||
inst.action_class = action_class
|
||||
return inst
|
@ -1,267 +0,0 @@
|
||||
"""This module defines util classes
|
||||
which allow to execute operations
|
||||
for each element of a list of objects of the same class.
|
||||
"""
|
||||
import abc
|
||||
import collections.abc
|
||||
import functools
|
||||
|
||||
|
||||
class SubclassingMeta(abc.ABCMeta):
|
||||
"""Metaclass which creates a subclass for each instance.
|
||||
|
||||
As decorators only work
|
||||
if the class object contains the declarations,
|
||||
we need to create a subclass for each different type
|
||||
if we want to dynamically use them.
|
||||
"""
|
||||
SUBCLASS_IDENTIFIER = '__subclassed__'
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if hasattr(cls, SubclassingMeta.SUBCLASS_IDENTIFIER):
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
subclass = type(cls.__name__, (cls,), {
|
||||
SubclassingMeta.SUBCLASS_IDENTIFIER:
|
||||
SubclassingMeta.SUBCLASS_IDENTIFIER})
|
||||
return subclass(*args, **kwargs)
|
||||
|
||||
|
||||
class BatchList(collections.abc.MutableSequence, metaclass=SubclassingMeta):
|
||||
"""BatchList provides the execution of methods and field access
|
||||
for each element of a list of instances of the same class
|
||||
in a similar way to one of these instances it would.
|
||||
"""
|
||||
__attributes_declared = False
|
||||
|
||||
class BatchMember:
|
||||
def __init__(self, outer, name):
|
||||
"""
|
||||
Args:
|
||||
outer (BatchList): Outer class instance
|
||||
"""
|
||||
self.outer = outer
|
||||
self.name = name
|
||||
|
||||
class BatchField(BatchMember):
|
||||
def __get__(self, owner_instance, owner_class):
|
||||
return BatchList([instance.__getattribute__(self.name)
|
||||
for instance in self.outer])
|
||||
|
||||
def __set__(self, owner_instance, value):
|
||||
for instance in self.outer:
|
||||
instance.__setattr__(self.name, value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
for instance in self.outer:
|
||||
instance.__delattr__(self.name)
|
||||
|
||||
class BatchMethod(BatchMember):
|
||||
def __call__(self, *args, **kwargs):
|
||||
return BatchList(
|
||||
[instance.__getattribute__(self.name)(*args, **kwargs)
|
||||
for instance in self.outer])
|
||||
|
||||
def __init__(self, collection: list):
|
||||
"""
|
||||
Args:
|
||||
collection (List): List of target instances
|
||||
"""
|
||||
self.__collection = collection.copy()
|
||||
self.__initialized = False
|
||||
self.__type = None
|
||||
self.entered = False
|
||||
self.__attributes_declared = True
|
||||
self.__init_members__()
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if self.__initialized:
|
||||
raise TypeError("'%s' object is not callable" % self.__type)
|
||||
return BatchList([])
|
||||
|
||||
def __getattr__(self, name):
|
||||
if self.__initialized:
|
||||
return AttributeError("'%s' object has no attribute '%s'"
|
||||
% (self.__type, name))
|
||||
return BatchList([])
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if (not self.__attributes_declared or
|
||||
self.__initialized or
|
||||
not isinstance(getattr(self, name), BatchList)):
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def __init_members__(self):
|
||||
if self.__collection and not self.__initialized:
|
||||
# Note: We can't simply use the class,
|
||||
# as the attributes exists only after the instantiation
|
||||
self.__initialized = True
|
||||
instance = self.__collection[0]
|
||||
self.__type = type(instance)
|
||||
self.__init_attributes__(instance)
|
||||
self.__init_methods__(instance)
|
||||
|
||||
def __declare_decorator__(self, name, decorator):
|
||||
setattr(type(self), name, decorator)
|
||||
|
||||
def __init_attributes__(self, target_instance):
|
||||
for name in self.__get_public_attributes(target_instance):
|
||||
self.__declare_decorator__(name, BatchList.BatchField(self, name))
|
||||
|
||||
@staticmethod
|
||||
def __get_public_attributes(target_instance):
|
||||
attributes = (vars(target_instance)
|
||||
if hasattr(target_instance, '__dict__')
|
||||
else [])
|
||||
return (name for name in attributes
|
||||
if not name.startswith('_'))
|
||||
|
||||
@staticmethod
|
||||
@functools.lru_cache()
|
||||
def __get_public_members(target_type):
|
||||
members = {
|
||||
name: member
|
||||
for type_members in
|
||||
map(vars, reversed(target_type.mro()))
|
||||
for name, member in type_members.items()
|
||||
}
|
||||
return {
|
||||
name: member
|
||||
for name, member in members.items()
|
||||
if not name.startswith('_')
|
||||
}
|
||||
|
||||
def __init_methods__(self, target_instance):
|
||||
public_members = self.__get_public_members(type(target_instance))
|
||||
for name, value in public_members.items():
|
||||
if callable(value):
|
||||
self.__declare_decorator__(
|
||||
name, BatchList.BatchMethod(self, name))
|
||||
else:
|
||||
# should be an decorator
|
||||
self.__declare_decorator__(
|
||||
name, BatchList.BatchField(self, name))
|
||||
|
||||
def __enter__(self):
|
||||
self.entered = True
|
||||
return BatchList([instance.__enter__() for instance in self])
|
||||
|
||||
def __exit__(self, *args):
|
||||
for instance in self:
|
||||
instance.__exit__(*args)
|
||||
|
||||
def __iadd__(self, other):
|
||||
if self.entered:
|
||||
for i in other:
|
||||
i.__enter__()
|
||||
self.__collection.__iadd__(other)
|
||||
self.__init_members__()
|
||||
return self
|
||||
|
||||
def append(self, item):
|
||||
if self.entered:
|
||||
item.__enter__()
|
||||
self.__collection.append(item)
|
||||
self.__init_members__()
|
||||
|
||||
def insert(self, index, item):
|
||||
if self.entered:
|
||||
item.__enter__()
|
||||
self.__collection.insert(index, item)
|
||||
self.__init_members__()
|
||||
|
||||
def extend(self, iterable):
|
||||
for item in iterable:
|
||||
self.append(item)
|
||||
|
||||
def __add__(self, other):
|
||||
return BatchList(self.__collection.__add__(other))
|
||||
|
||||
def reverse(self):
|
||||
self.__collection.reverse()
|
||||
|
||||
def clear(self):
|
||||
if self.entered:
|
||||
for i in self.__collection:
|
||||
i.__exit__(None, None, None)
|
||||
self.__collection.clear()
|
||||
|
||||
def copy(self):
|
||||
return BatchList(self.__collection.copy())
|
||||
|
||||
def pop(self, *args):
|
||||
result = self.__collection.pop(*args)
|
||||
|
||||
if self.entered:
|
||||
result.__exit__(None, None, None)
|
||||
|
||||
return result
|
||||
|
||||
def remove(self, value):
|
||||
if self.entered:
|
||||
value.__exit__(None, None, None)
|
||||
return self.__collection.remove(value)
|
||||
|
||||
def __isub__(self, other):
|
||||
for i in other:
|
||||
self.remove(i)
|
||||
return self
|
||||
|
||||
def __sub__(self, other):
|
||||
copied = self.copy()
|
||||
copied -= other
|
||||
return copied
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__collection)
|
||||
|
||||
def __delitem__(self, key):
|
||||
return self.pop(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.pop(key)
|
||||
self.insert(key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__collection[key]
|
||||
|
||||
def count(self, *args, **kwargs):
|
||||
return self.__collection.count(*args, **kwargs)
|
||||
|
||||
def index(self, *args, **kwargs):
|
||||
return self.__collection.index(*args, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__collection)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.__collection
|
||||
|
||||
def __reversed__(self):
|
||||
return reversed(self.__collection)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
class FooBar:
|
||||
def __init__(self, a, b, c):
|
||||
self.mhm = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
|
||||
def ok(self):
|
||||
return self.b
|
||||
|
||||
@property
|
||||
def prop(self):
|
||||
return self.c
|
||||
|
||||
# print attributes
|
||||
# print(vars(FooBar()))
|
||||
# print properties and methods
|
||||
# print(vars(FooBar).keys())
|
||||
blist = BatchList([FooBar('foo', 'bar', 'yay')])
|
||||
blist += [FooBar('foobar', 'barfoo', 'yay foobar')]
|
||||
print('mhm', blist.mhm)
|
||||
print('prop', blist.prop)
|
||||
# print('ok', blist.ok)
|
||||
print('ok call', blist.ok())
|
@ -1,14 +0,0 @@
|
||||
|
||||
|
||||
def to_bool(value):
|
||||
"""Converts a String to a Boolean.
|
||||
|
||||
Args:
|
||||
value (str or bool): a boolean or a string representing a boolean
|
||||
|
||||
Returns:
|
||||
bool: the evaluated boolean
|
||||
"""
|
||||
import distutils.util
|
||||
return (value if isinstance(value, bool)
|
||||
else bool(distutils.util.strtobool(value)))
|
@ -1,47 +0,0 @@
|
||||
import select
|
||||
import fcntl
|
||||
import contextlib
|
||||
import pathlib
|
||||
|
||||
|
||||
class LineReader:
|
||||
"""Async iterator class used to read lines"""
|
||||
|
||||
def __init__(self, loop, file):
|
||||
self._loop = loop
|
||||
self._file = file
|
||||
|
||||
@staticmethod
|
||||
async def read_line(loop, file):
|
||||
"""Waits asynchronously for a line and returns it"""
|
||||
return await loop.run_in_executor(None, file.readline)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if select.select([self._file], [], [], 0)[0]:
|
||||
return self._file.readline()
|
||||
return await LineReader.read_line(self._loop, self._file)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def lock(path: pathlib.PosixPath):
|
||||
"""Creates a lock file,
|
||||
a file protected from beeing used by other processes.
|
||||
(The lock file isn't the same as the file of the passed path.)
|
||||
|
||||
Args:
|
||||
path (pathlib.PosixPath): path to the file
|
||||
"""
|
||||
path = path.with_suffix('.lock')
|
||||
|
||||
if not path.exists():
|
||||
path.touch()
|
||||
|
||||
with path.open("r+") as lock_file:
|
||||
try:
|
||||
fcntl.lockf(lock_file.fileno(), fcntl.LOCK_EX)
|
||||
yield lock_file
|
||||
finally:
|
||||
fcntl.lockf(lock_file.fileno(), fcntl.LOCK_UN)
|
@ -1,20 +0,0 @@
|
||||
"""Module which defines classes all about geometry"""
|
||||
|
||||
|
||||
class Point:
|
||||
"""Data class which holds a coordinate."""
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.x, self.y) == (other.x, other.y)
|
||||
|
||||
|
||||
class Distance:
|
||||
"""Data class which holds the distance values in all directions."""
|
||||
def __init__(self, top=0, left=0, bottom=0, right=0):
|
||||
self.top = top
|
||||
self.left = left
|
||||
self.bottom = bottom
|
||||
self.right = right
|
@ -1,254 +0,0 @@
|
||||
import atexit
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import signal
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
import ueberzug.thread as thread
|
||||
import ueberzug.files as files
|
||||
import ueberzug.xutil as xutil
|
||||
import ueberzug.parser as parser
|
||||
import ueberzug.ui as ui
|
||||
import ueberzug.batch as batch
|
||||
import ueberzug.action as action
|
||||
import ueberzug.tmux_util as tmux_util
|
||||
import ueberzug.geometry as geometry
|
||||
import ueberzug.loading as loading
|
||||
import ueberzug.X as X
|
||||
|
||||
|
||||
async def process_xevents(loop, display, windows):
|
||||
"""Coroutine which processes X11 events"""
|
||||
async for _ in xutil.Events(loop, display):
|
||||
if not any(windows.process_event()):
|
||||
display.discard_event()
|
||||
|
||||
|
||||
async def process_commands(loop, shutdown_routine_factory,
|
||||
windows, view, tools):
|
||||
"""Coroutine which processes the input of stdin"""
|
||||
try:
|
||||
async for line in files.LineReader(loop, sys.stdin):
|
||||
if not line:
|
||||
break
|
||||
|
||||
try:
|
||||
data = tools.parser.parse(line[:-1])
|
||||
command = action.Command(data['action'])
|
||||
await command.action_class(**data) \
|
||||
.apply(windows, view, tools)
|
||||
except (OSError, KeyError, ValueError, TypeError) as error:
|
||||
tools.error_handler(error)
|
||||
finally:
|
||||
asyncio.ensure_future(shutdown_routine_factory())
|
||||
|
||||
|
||||
async def query_windows(display: X.Display, window_factory, windows, view):
|
||||
"""Signal handler for SIGUSR1.
|
||||
Searches for added and removed tmux clients.
|
||||
Added clients: additional windows will be mapped
|
||||
Removed clients: existing windows will be destroyed
|
||||
"""
|
||||
parent_window_infos = xutil.get_parent_window_infos(display)
|
||||
view.offset = tmux_util.get_offset()
|
||||
map_parent_window_id_info = {info.window_id: info
|
||||
for info in parent_window_infos}
|
||||
parent_window_ids = map_parent_window_id_info.keys()
|
||||
map_current_windows = {window.parent_id: window
|
||||
for window in windows}
|
||||
current_window_ids = map_current_windows.keys()
|
||||
diff_window_ids = parent_window_ids ^ current_window_ids
|
||||
added_window_ids = diff_window_ids & parent_window_ids
|
||||
removed_window_ids = diff_window_ids & current_window_ids
|
||||
draw = added_window_ids or removed_window_ids
|
||||
|
||||
if added_window_ids:
|
||||
windows += window_factory.create(*[
|
||||
map_parent_window_id_info.get(wid)
|
||||
for wid in added_window_ids
|
||||
])
|
||||
|
||||
if removed_window_ids:
|
||||
windows -= [
|
||||
map_current_windows.get(wid)
|
||||
for wid in removed_window_ids
|
||||
]
|
||||
|
||||
if draw:
|
||||
windows.draw()
|
||||
|
||||
|
||||
async def reset_terminal_info(windows):
|
||||
"""Signal handler for SIGWINCH.
|
||||
Resets the terminal information of all windows.
|
||||
"""
|
||||
windows.reset_terminal_info()
|
||||
|
||||
|
||||
async def shutdown(loop):
|
||||
try:
|
||||
all_tasks = asyncio.all_tasks()
|
||||
current_task = asyncio.current_task()
|
||||
except AttributeError:
|
||||
all_tasks = asyncio.Task.all_tasks()
|
||||
current_task = asyncio.tasks.Task.current_task()
|
||||
|
||||
tasks = [task for task in all_tasks
|
||||
if task is not current_task]
|
||||
list(map(lambda task: task.cancel(), tasks))
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
loop.stop()
|
||||
|
||||
|
||||
def shutdown_factory(loop):
|
||||
return lambda: asyncio.ensure_future(shutdown(loop))
|
||||
|
||||
|
||||
def setup_tmux_hooks():
|
||||
"""Registers tmux hooks which are
|
||||
required to notice a change in the visibility
|
||||
of the pane this program runs in.
|
||||
Also it's required to notice new tmux clients
|
||||
displaying our pane.
|
||||
|
||||
Returns:
|
||||
function which unregisters the registered hooks
|
||||
"""
|
||||
events = (
|
||||
'client-session-changed',
|
||||
'session-window-changed',
|
||||
'pane-mode-changed',
|
||||
'client-detached'
|
||||
)
|
||||
lock_directory_path = pathlib.PosixPath(tempfile.gettempdir()) / 'ueberzug'
|
||||
lock_file_path = lock_directory_path / tmux_util.get_session_id()
|
||||
own_pid = str(os.getpid())
|
||||
command_template = 'ueberzug query_windows '
|
||||
|
||||
try:
|
||||
lock_directory_path.mkdir()
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
def update_hooks(pid_file, pids):
|
||||
pids = ' '.join(pids)
|
||||
command = command_template + pids
|
||||
|
||||
pid_file.seek(0)
|
||||
pid_file.truncate()
|
||||
pid_file.write(pids)
|
||||
pid_file.flush()
|
||||
|
||||
for event in events:
|
||||
if pids:
|
||||
tmux_util.register_hook(event, command)
|
||||
else:
|
||||
tmux_util.unregister_hook(event)
|
||||
|
||||
def remove_hooks():
|
||||
"""Removes the hooks registered by the outer function."""
|
||||
with files.lock(lock_file_path) as lock_file:
|
||||
pids = set(lock_file.read().split())
|
||||
pids.discard(own_pid)
|
||||
update_hooks(lock_file, pids)
|
||||
|
||||
with files.lock(lock_file_path) as lock_file:
|
||||
pids = set(lock_file.read().split())
|
||||
pids.add(own_pid)
|
||||
update_hooks(lock_file, pids)
|
||||
|
||||
return remove_hooks
|
||||
|
||||
|
||||
def error_processor_factory(parser):
|
||||
def wrapper(exception):
|
||||
return process_error(parser, exception)
|
||||
return wrapper
|
||||
|
||||
|
||||
def process_error(parser, exception):
|
||||
print(parser.unparse({
|
||||
'type': 'error',
|
||||
'name': type(exception).__name__,
|
||||
'message': str(exception),
|
||||
# 'stack': traceback.format_exc()
|
||||
}), file=sys.stderr)
|
||||
|
||||
|
||||
class View:
|
||||
"""Data class which holds meta data about the screen"""
|
||||
def __init__(self):
|
||||
self.offset = geometry.Distance()
|
||||
self.media = {}
|
||||
self.screen_width = 0
|
||||
self.screen_height = 0
|
||||
|
||||
|
||||
class Tools:
|
||||
"""Data class which holds helper functions, ..."""
|
||||
def __init__(self, loader, parser, error_handler):
|
||||
self.loader = loader
|
||||
self.parser = parser
|
||||
self.error_handler = error_handler
|
||||
|
||||
|
||||
def main(options):
|
||||
if options['--silent']:
|
||||
try:
|
||||
outfile = os.open(os.devnull, os.O_WRONLY)
|
||||
os.close(sys.stderr.fileno())
|
||||
os.dup2(outfile, sys.stderr.fileno())
|
||||
finally:
|
||||
os.close(outfile)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
executor = thread.DaemonThreadPoolExecutor(max_workers=2)
|
||||
parser_object = (parser.ParserOption(options['--parser'])
|
||||
.parser_class())
|
||||
image_loader = (loading.ImageLoaderOption(options['--loader'])
|
||||
.loader_class())
|
||||
error_handler = error_processor_factory(parser_object)
|
||||
view = View()
|
||||
tools = Tools(image_loader, parser_object, error_handler)
|
||||
X.init_threads()
|
||||
display = X.Display()
|
||||
window_factory = ui.CanvasWindow.Factory(display, view)
|
||||
window_infos = xutil.get_parent_window_infos(display)
|
||||
windows = batch.BatchList(window_factory.create(*window_infos))
|
||||
image_loader.register_error_handler(error_handler)
|
||||
view.screen_width = display.screen_width
|
||||
view.screen_height = display.screen_height
|
||||
|
||||
if tmux_util.is_used():
|
||||
atexit.register(setup_tmux_hooks())
|
||||
view.offset = tmux_util.get_offset()
|
||||
|
||||
with windows, image_loader:
|
||||
loop.set_default_executor(executor)
|
||||
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
loop.add_signal_handler(
|
||||
sig, shutdown_factory(loop))
|
||||
|
||||
loop.add_signal_handler(
|
||||
signal.SIGUSR1,
|
||||
lambda: asyncio.ensure_future(query_windows(
|
||||
display, window_factory, windows, view)))
|
||||
|
||||
loop.add_signal_handler(
|
||||
signal.SIGWINCH,
|
||||
lambda: asyncio.ensure_future(
|
||||
reset_terminal_info(windows)))
|
||||
|
||||
asyncio.ensure_future(process_xevents(loop, display, windows))
|
||||
asyncio.ensure_future(process_commands(
|
||||
loop, shutdown_factory(loop),
|
||||
windows, view, tools))
|
||||
|
||||
try:
|
||||
loop.run_forever()
|
||||
finally:
|
||||
loop.close()
|
||||
executor.shutdown(wait=False)
|
@ -1,71 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
function String::trim {
|
||||
while read line; do
|
||||
printf %s\\n "$line"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
function Error::raise {
|
||||
local -a stack=()
|
||||
local stack_size=${#FUNCNAME[@]}
|
||||
|
||||
for ((i = 1; i < $stack_size; i++)); do
|
||||
local caller="${FUNCNAME[$i]}"
|
||||
local line_number="${BASH_LINENO[$(( i - 1 ))]}"
|
||||
local file="${BASH_SOURCE[$i]}"
|
||||
[ -z "$caller" ] && caller=main
|
||||
|
||||
stack+=(
|
||||
# note: lines ending with a backslash are counted as a single line
|
||||
$'\t'"File ${file}, line ${line_number}, in ${caller}"
|
||||
$'\t\t'"`String::trim < "${file}" | head --lines "${line_number}" | tail --lines 1`"
|
||||
)
|
||||
done
|
||||
|
||||
printf '%s\n' "${@}" "${stack[@]}" 1>&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
function Map::escape_items {
|
||||
while (( "${#@}" > 0 )); do
|
||||
local key="${1%%=[^=]*}"
|
||||
local value="${1#[^=]*=}"
|
||||
printf "%s=%q " "$key" "$value"
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
function ImageLayer {
|
||||
ueberzug layer -p bash "$@"
|
||||
}
|
||||
|
||||
function ImageLayer::__build_command {
|
||||
local -a required_keys=( $1 ); shift
|
||||
local -A data="( `Map::escape_items "$@"` )"
|
||||
|
||||
for key in "${required_keys[@]}"; do
|
||||
# see: https://stackoverflow.com/a/13221491
|
||||
if ! [ ${data["$key"]+exists} ]; then
|
||||
Error::raise "Key '$key' missing!"
|
||||
fi
|
||||
done
|
||||
|
||||
declare -p data
|
||||
}
|
||||
|
||||
function ImageLayer::build_command {
|
||||
local action="$1"; shift
|
||||
local required_keys="$1"; shift
|
||||
ImageLayer::__build_command "action $required_keys" [action]="$action" "$@"
|
||||
}
|
||||
|
||||
function ImageLayer::add {
|
||||
ImageLayer::build_command add "identifier x y path" "$@"
|
||||
}
|
||||
|
||||
function ImageLayer::remove {
|
||||
ImageLayer::build_command remove "identifier" "$@"
|
||||
}
|
@ -1,411 +0,0 @@
|
||||
import abc
|
||||
import enum
|
||||
import subprocess
|
||||
import threading
|
||||
import json
|
||||
import collections
|
||||
import contextlib
|
||||
import os
|
||||
import signal
|
||||
|
||||
import attr
|
||||
|
||||
import ueberzug.action as _action
|
||||
from ueberzug.scaling import ScalerOption
|
||||
from ueberzug.loading import ImageLoaderOption
|
||||
|
||||
|
||||
class Visibility(enum.Enum):
|
||||
"""Enum which defines the different visibility states."""
|
||||
VISIBLE = enum.auto()
|
||||
INVISIBLE = enum.auto()
|
||||
|
||||
|
||||
class Placement:
|
||||
"""The class which represent a (image) placement on the canvas.
|
||||
|
||||
Attributes:
|
||||
Every parameter defined by the add action is an attribute.
|
||||
|
||||
Raises:
|
||||
IOError: on assign a new value to an attribute
|
||||
if stdin of the ueberzug process was closed
|
||||
during an attempt of writing to it.
|
||||
"""
|
||||
|
||||
__initialised = False
|
||||
__DEFAULT_VALUES = {str: '', int: 0}
|
||||
__ATTRIBUTES = {attribute.name: attribute
|
||||
for attribute in attr.fields(_action.AddImageAction)}
|
||||
__EMPTY_BASE_PAIRS = (
|
||||
lambda attributes, default_values:
|
||||
{attribute.name: default_values[attribute.type]
|
||||
for attribute in attributes.values()
|
||||
if (attribute.default == attr.NOTHING
|
||||
and attribute.init)}
|
||||
)(__ATTRIBUTES, __DEFAULT_VALUES)
|
||||
|
||||
def __init__(self, canvas, identifier,
|
||||
visibility: Visibility = Visibility.INVISIBLE,
|
||||
**kwargs):
|
||||
"""
|
||||
Args:
|
||||
canvas (Canvas): the canvas this placement belongs to
|
||||
identifier (str): a string which uniquely identifies this placement
|
||||
visibility (Visibility): the initial visibility of this placement
|
||||
(all required parameters need to be set
|
||||
if it's visible)
|
||||
kwargs: parameters of the add action
|
||||
"""
|
||||
self.__canvas = canvas
|
||||
self.__identifier = identifier
|
||||
self.__visibility = False
|
||||
self.__data = {}
|
||||
self.__initialised = True
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
self.visibility = visibility
|
||||
|
||||
@property
|
||||
def canvas(self):
|
||||
"""Canvas: the canvas this placement belongs to"""
|
||||
return self.__canvas
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""str: the identifier of this placement"""
|
||||
return self.__identifier
|
||||
|
||||
@property
|
||||
def visibility(self):
|
||||
"""Visibility: the visibility of this placement"""
|
||||
return self.__visibility
|
||||
|
||||
@visibility.setter
|
||||
def visibility(self, value):
|
||||
if self.__visibility != value:
|
||||
if value is Visibility.INVISIBLE:
|
||||
self.__remove()
|
||||
elif value is Visibility.VISIBLE:
|
||||
self.__update()
|
||||
else:
|
||||
raise TypeError("expected an instance of Visibility")
|
||||
self.__visibility = value
|
||||
|
||||
def __remove(self):
|
||||
self.__canvas.enqueue(
|
||||
_action.RemoveImageAction(identifier=self.identifier))
|
||||
self.__canvas.request_transmission()
|
||||
|
||||
def __update(self):
|
||||
self.__canvas.enqueue(_action.AddImageAction(**{
|
||||
**self.__data,
|
||||
**attr.asdict(_action.Identifiable(identifier=self.identifier))
|
||||
}))
|
||||
self.__canvas.request_transmission()
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self.__ATTRIBUTES:
|
||||
raise AttributeError("There is no attribute named %s" % name)
|
||||
|
||||
attribute = self.__ATTRIBUTES[name]
|
||||
|
||||
if name in self.__data:
|
||||
return self.__data[name]
|
||||
if attribute.default != attr.NOTHING:
|
||||
return attribute.default
|
||||
return None
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if not self.__initialised:
|
||||
super().__setattr__(name, value)
|
||||
return
|
||||
if name not in self.__ATTRIBUTES:
|
||||
if hasattr(self, name):
|
||||
super().__setattr__(name, value)
|
||||
return
|
||||
raise AttributeError("There is no attribute named %s" % name)
|
||||
|
||||
data = dict(self.__data)
|
||||
self.__data.update(attr.asdict(_action.AddImageAction(**{
|
||||
**self.__EMPTY_BASE_PAIRS,
|
||||
**self.__data,
|
||||
**attr.asdict(_action.Identifiable(identifier=self.identifier)),
|
||||
name: value
|
||||
})))
|
||||
|
||||
# remove the key's of the empty base pairs
|
||||
# so the developer is forced to set them by himself
|
||||
for key in self.__EMPTY_BASE_PAIRS:
|
||||
if key not in data and key != name:
|
||||
del self.__data[key]
|
||||
|
||||
if self.visibility is Visibility.VISIBLE:
|
||||
self.__update()
|
||||
|
||||
|
||||
class UeberzugProcess:
|
||||
"""Class which handles the creation and
|
||||
destructions of ueberzug processes.
|
||||
"""
|
||||
__KILL_TIMEOUT_SECONDS = 1
|
||||
__BUFFER_SIZE_BYTES = 50 * 1024
|
||||
|
||||
def __init__(self, options):
|
||||
"""
|
||||
Args:
|
||||
options (list of str): additional command line arguments
|
||||
"""
|
||||
self.__start_options = options
|
||||
self.__process = None
|
||||
|
||||
@property
|
||||
def stdin(self):
|
||||
"""_io.TextIOWrapper: stdin of the ueberzug process"""
|
||||
return self.__process.stdin
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
"""bool: ueberzug process is still running"""
|
||||
return (self.__process is not None
|
||||
and self.__process.poll() is None)
|
||||
|
||||
@property
|
||||
def responsive(self):
|
||||
"""bool: ueberzug process is able to receive instructions"""
|
||||
return self.running and not self.__process.stdin.closed
|
||||
|
||||
def start(self):
|
||||
"""Starts a new ueberzug process
|
||||
if there's none or it's not responsive.
|
||||
"""
|
||||
if self.responsive:
|
||||
return
|
||||
if self.running:
|
||||
self.stop()
|
||||
|
||||
self.__process = subprocess.Popen(
|
||||
['ueberzug', 'layer'] + self.__start_options,
|
||||
stdin=subprocess.PIPE,
|
||||
bufsize=self.__BUFFER_SIZE_BYTES,
|
||||
universal_newlines=True,
|
||||
start_new_session=True)
|
||||
|
||||
def stop(self):
|
||||
"""Sends SIGTERM to the running ueberzug process
|
||||
and waits for it to exit.
|
||||
If the process won't end after a specific timeout
|
||||
SIGKILL will also be send.
|
||||
"""
|
||||
if self.running:
|
||||
timer_kill = None
|
||||
|
||||
try:
|
||||
ueberzug_pgid = os.getpgid(self.__process.pid)
|
||||
own_pgid = os.getpgid(0)
|
||||
assert ueberzug_pgid != own_pgid
|
||||
timer_kill = threading.Timer(
|
||||
self.__KILL_TIMEOUT_SECONDS,
|
||||
os.killpg,
|
||||
[ueberzug_pgid, signal.SIGKILL])
|
||||
|
||||
self.__process.terminate()
|
||||
timer_kill.start()
|
||||
self.__process.communicate()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
finally:
|
||||
if timer_kill is not None:
|
||||
timer_kill.cancel()
|
||||
|
||||
|
||||
class CommandTransmitter:
|
||||
"""Describes the structure used to define command transmitter classes.
|
||||
|
||||
Defines a general interface used to implement different ways
|
||||
of storing and transmitting commands to ueberzug processes.
|
||||
"""
|
||||
|
||||
def __init__(self, process):
|
||||
self._process = process
|
||||
|
||||
@abc.abstractproperty
|
||||
def synchronously_draw(self):
|
||||
"""bool: execute draw operations of ImageActions synchrously"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def enqueue(self, action: _action.Action):
|
||||
"""Enqueues a command.
|
||||
|
||||
Args:
|
||||
action (action.Action): the command which should be executed
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def transmit(self):
|
||||
"""Transmits every command in the queue."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class DequeCommandTransmitter(CommandTransmitter):
|
||||
"""Implements the command transmitter with a dequeue."""
|
||||
|
||||
def __init__(self, process):
|
||||
super().__init__(process)
|
||||
self.__queue_commands = collections.deque()
|
||||
self.__synchronously_draw = False
|
||||
|
||||
@property
|
||||
def synchronously_draw(self):
|
||||
return self.__synchronously_draw
|
||||
|
||||
@synchronously_draw.setter
|
||||
def synchronously_draw(self, value):
|
||||
self.__synchronously_draw = value
|
||||
|
||||
def enqueue(self, action: _action.Action):
|
||||
self.__queue_commands.append(action)
|
||||
|
||||
def transmit(self):
|
||||
while self.__queue_commands:
|
||||
command = self.__queue_commands.popleft()
|
||||
self._process.stdin.write(json.dumps({
|
||||
**attr.asdict(command),
|
||||
**attr.asdict(_action.Drawable(
|
||||
synchronously_draw=self.__synchronously_draw,
|
||||
draw=not self.__queue_commands))
|
||||
}))
|
||||
self._process.stdin.write('\n')
|
||||
self._process.stdin.flush()
|
||||
|
||||
|
||||
class LazyCommandTransmitter(CommandTransmitter):
|
||||
"""Implements lazily transmitting commands as decorator class.
|
||||
|
||||
Ignores calls of the transmit method.
|
||||
"""
|
||||
def __init__(self, transmitter):
|
||||
super().__init__(None)
|
||||
self.transmitter = transmitter
|
||||
|
||||
@property
|
||||
def synchronously_draw(self):
|
||||
return self.transmitter.synchronously_draw
|
||||
|
||||
@synchronously_draw.setter
|
||||
def synchronously_draw(self, value):
|
||||
self.transmitter.synchronously_draw = value
|
||||
|
||||
def enqueue(self, action: _action.Action):
|
||||
self.transmitter.enqueue(action)
|
||||
|
||||
def transmit(self):
|
||||
pass
|
||||
|
||||
def force_transmit(self):
|
||||
"""Executes the transmit method of the decorated CommandTransmitter."""
|
||||
self.transmitter.transmit()
|
||||
|
||||
|
||||
class Canvas:
|
||||
"""The class which represents the drawing area."""
|
||||
|
||||
def __init__(self, debug=False):
|
||||
self.__process_arguments = (
|
||||
['--loader', ImageLoaderOption.SYNCHRONOUS.value]
|
||||
if debug else
|
||||
['--silent'])
|
||||
self.__process = None
|
||||
self.__transmitter = None
|
||||
self.__used_identifiers = set()
|
||||
self.automatic_transmission = True
|
||||
|
||||
def create_placement(self, identifier, *args, **kwargs):
|
||||
"""Creates a placement associated with this canvas.
|
||||
|
||||
Args:
|
||||
the same as the constructor of Placement
|
||||
"""
|
||||
if identifier in self.__used_identifiers:
|
||||
raise ValueError("Identifier '%s' is already taken." % identifier)
|
||||
self.__used_identifiers.add(identifier)
|
||||
return Placement(self, identifier, *args, **kwargs)
|
||||
|
||||
@property
|
||||
@contextlib.contextmanager
|
||||
def lazy_drawing(self):
|
||||
"""Context manager factory function which
|
||||
prevents transmitting commands till the with-statement ends.
|
||||
|
||||
Raises:
|
||||
IOError: on transmitting commands
|
||||
if stdin of the ueberzug process was closed
|
||||
during an attempt of writing to it.
|
||||
"""
|
||||
try:
|
||||
self.__transmitter.transmit()
|
||||
self.__transmitter = LazyCommandTransmitter(self.__transmitter)
|
||||
yield
|
||||
self.__transmitter.force_transmit()
|
||||
finally:
|
||||
self.__transmitter = self.__transmitter.transmitter
|
||||
|
||||
@property
|
||||
@contextlib.contextmanager
|
||||
def synchronous_lazy_drawing(self):
|
||||
"""Context manager factory function which
|
||||
prevents transmitting commands till the with-statement ends.
|
||||
Also enforces to execute the draw operation synchronously
|
||||
right after the last command.
|
||||
|
||||
Raises:
|
||||
IOError: on transmitting commands
|
||||
if stdin of the ueberzug process was closed
|
||||
during an attempt of writing to it.
|
||||
"""
|
||||
try:
|
||||
self.__transmitter.synchronously_draw = True
|
||||
with self.lazy_drawing:
|
||||
yield
|
||||
finally:
|
||||
self.__transmitter.synchronously_draw = False
|
||||
|
||||
def __call__(self, function):
|
||||
def decorator(*args, **kwargs):
|
||||
with self:
|
||||
return function(*args, canvas=self, **kwargs)
|
||||
return decorator
|
||||
|
||||
def __enter__(self):
|
||||
self.__process = UeberzugProcess(self.__process_arguments)
|
||||
self.__transmitter = DequeCommandTransmitter(self.__process)
|
||||
self.__process.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
try:
|
||||
self.__process.stop()
|
||||
finally:
|
||||
self.__process = None
|
||||
self.__transmitter = None
|
||||
|
||||
def enqueue(self, command: _action.Action):
|
||||
"""Enqueues a command.
|
||||
|
||||
Args:
|
||||
action (action.Action): the command which should be executed
|
||||
"""
|
||||
if not self.__process.responsive:
|
||||
self.__process.start()
|
||||
|
||||
self.__transmitter.enqueue(command)
|
||||
|
||||
def request_transmission(self, *, force=False):
|
||||
"""Requests the transmission of every command in the queue."""
|
||||
if not self.__process.responsive:
|
||||
self.__process.start()
|
||||
|
||||
if self.automatic_transmission or force:
|
||||
self.__transmitter.transmit()
|
@ -1,8 +0,0 @@
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
|
||||
def main(options):
|
||||
directory = \
|
||||
pathlib.PosixPath(os.path.abspath(os.path.dirname(__file__))) / 'lib'
|
||||
print((directory / 'lib.sh').as_posix())
|
@ -1,494 +0,0 @@
|
||||
import abc
|
||||
import queue
|
||||
import weakref
|
||||
import os
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import enum
|
||||
|
||||
import ueberzug.thread as thread
|
||||
import ueberzug.pattern as pattern
|
||||
|
||||
|
||||
INDEX_ALPHA_CHANNEL = 3
|
||||
|
||||
|
||||
def load_image(path, upper_bound_size):
|
||||
"""Loads the image and converts it
|
||||
if it doesn't use the RGB or RGBX mode.
|
||||
|
||||
Args:
|
||||
path (str): the path of the image file
|
||||
upper_bound_size (tuple of (width: int, height: int)):
|
||||
the maximal size to load data for
|
||||
|
||||
Returns:
|
||||
tuple of (PIL.Image, bool): rgb image, downscaled
|
||||
|
||||
Raises:
|
||||
OSError: for unsupported formats
|
||||
"""
|
||||
import PIL.Image
|
||||
image = PIL.Image.open(path)
|
||||
original_size = image.width, image.height
|
||||
downscaled = False
|
||||
mask = None
|
||||
|
||||
if upper_bound_size:
|
||||
upper_bound_size = tuple(
|
||||
min(size for size in size_pair if size > 0)
|
||||
for size_pair in zip(upper_bound_size, original_size))
|
||||
image.draft(None, upper_bound_size)
|
||||
downscaled = (image.width, image.height) < original_size
|
||||
|
||||
image.load()
|
||||
|
||||
if (image.format == 'PNG'
|
||||
and image.mode in ('L', 'P')
|
||||
and 'transparency' in image.info):
|
||||
# Prevent pillow to print the warning
|
||||
# 'Palette images with Transparency expressed in bytes should be
|
||||
# converted to RGBA images'
|
||||
image = image.convert('RGBA')
|
||||
|
||||
if image.mode == 'RGBA':
|
||||
mask = image.split()[INDEX_ALPHA_CHANNEL]
|
||||
|
||||
if image.mode not in ('RGB', 'RGBX'):
|
||||
image_rgb = PIL.Image.new(
|
||||
"RGB", image.size, color=(255, 255, 255))
|
||||
image_rgb.paste(image, mask=mask)
|
||||
image = image_rgb
|
||||
|
||||
return image, downscaled
|
||||
|
||||
|
||||
class ImageHolder:
|
||||
"""Holds the reference of an image.
|
||||
It serves as bridge between image loader and image user.
|
||||
"""
|
||||
def __init__(self, path, image=None):
|
||||
self.path = path
|
||||
self.image = image
|
||||
self.waiter = threading.Condition()
|
||||
|
||||
def reveal_image(self, image):
|
||||
"""Assigns an image to this holder and
|
||||
notifies waiting image users about it.
|
||||
|
||||
Args:
|
||||
image (PIL.Image): the loaded image
|
||||
"""
|
||||
with self.waiter:
|
||||
self.image = image
|
||||
self.waiter.notify_all()
|
||||
|
||||
def await_image(self):
|
||||
"""Waits till an image loader assigns the image
|
||||
if it's not already happened.
|
||||
|
||||
Returns:
|
||||
PIL.Image: the image assigned to this holder
|
||||
"""
|
||||
if self.image is None:
|
||||
with self.waiter:
|
||||
if self.image is None:
|
||||
self.waiter.wait()
|
||||
return self.image
|
||||
|
||||
|
||||
class PostLoadImageProcessor(metaclass=abc.ABCMeta):
|
||||
"""Describes the structure used to define callbacks which
|
||||
will be invoked after loading an image.
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def on_loaded(self, image):
|
||||
"""Postprocessor of an loaded image.
|
||||
The returned image will be assigned to the image holder.
|
||||
|
||||
Args:
|
||||
image (PIL.Image): the loaded image
|
||||
|
||||
Returns:
|
||||
PIL.Image:
|
||||
the image which will be assigned
|
||||
to the image holder of this loading process
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CoverPostLoadImageProcessor(PostLoadImageProcessor):
|
||||
"""Implementation of PostLoadImageProcessor
|
||||
which resizes an image (if possible -> needs to be bigger)
|
||||
such that it covers only just a given resolution.
|
||||
"""
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def on_loaded(self, image):
|
||||
import PIL.Image
|
||||
resize_ratio = max(min(1, self.width / image.width),
|
||||
min(1, self.height / image.height))
|
||||
|
||||
if resize_ratio != 1:
|
||||
image = image.resize(
|
||||
(int(resize_ratio * image.width),
|
||||
int(resize_ratio * image.height)),
|
||||
PIL.Image.ANTIALIAS)
|
||||
|
||||
return image
|
||||
|
||||
|
||||
class ImageLoader(metaclass=abc.ABCMeta):
|
||||
"""Describes the structure used to define image loading strategies.
|
||||
|
||||
Defines a general interface used to implement different ways
|
||||
of loading images.
|
||||
E.g. loading images asynchron
|
||||
"""
|
||||
@pattern.LazyConstant
|
||||
def PLACEHOLDER():
|
||||
"""PIL.Image: fallback image for occuring errors"""
|
||||
# pylint: disable=no-method-argument,invalid-name
|
||||
import PIL.Image
|
||||
return PIL.Image.new('RGB', (1, 1))
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def get_loader_name():
|
||||
"""Returns the constant name which is associated to this loader."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __init__(self):
|
||||
self.error_handler = None
|
||||
|
||||
@abc.abstractmethod
|
||||
def load(self, path, upper_bound_size, post_load_processor=None):
|
||||
"""Starts the image loading procedure for the passed path.
|
||||
How and when an image get's loaded depends on the implementation
|
||||
of the used ImageLoader class.
|
||||
|
||||
Args:
|
||||
path (str): the path to the image which should be loaded
|
||||
upper_bound_size (tuple of (width: int, height: int)):
|
||||
the maximal size to load data for
|
||||
post_load_processor (PostLoadImageProcessor):
|
||||
allows to apply changes to the recently loaded image
|
||||
|
||||
Returns:
|
||||
ImageHolder: which the image will be assigned to
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def register_error_handler(self, error_handler):
|
||||
"""Set's the error handler to the passed function.
|
||||
An error handler will be called with exceptions which were
|
||||
raised during loading an image.
|
||||
|
||||
Args:
|
||||
error_handler (Function(Exception)):
|
||||
the function which should be called
|
||||
to handle an error
|
||||
"""
|
||||
self.error_handler = error_handler
|
||||
|
||||
def process_error(self, exception):
|
||||
"""Processes an exception.
|
||||
Calls the error_handler with the exception
|
||||
if there's any.
|
||||
|
||||
Args:
|
||||
exception (Exception): the occurred error
|
||||
"""
|
||||
if (self.error_handler is not None and
|
||||
exception is not None):
|
||||
self.error_handler(exception)
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""Finalises the image loader."""
|
||||
pass
|
||||
|
||||
|
||||
class SynchronousImageLoader(ImageLoader):
|
||||
"""Implementation of ImageLoader
|
||||
which loads images right away in the same thread
|
||||
it was requested to load the image.
|
||||
"""
|
||||
@staticmethod
|
||||
def get_loader_name():
|
||||
return "synchronous"
|
||||
|
||||
def load(self, path, upper_bound_size, post_load_processor=None):
|
||||
image = None
|
||||
|
||||
try:
|
||||
image, _ = load_image(path, None)
|
||||
except OSError as exception:
|
||||
self.process_error(exception)
|
||||
|
||||
if image and post_load_processor:
|
||||
image = post_load_processor.on_loaded(image)
|
||||
|
||||
return ImageHolder(path, image or self.PLACEHOLDER)
|
||||
|
||||
|
||||
class AsynchronousImageLoader(ImageLoader):
|
||||
"""Extension of ImageLoader
|
||||
which adds basic functionality
|
||||
needed to implement asynchron image loading.
|
||||
"""
|
||||
@enum.unique
|
||||
class Priority(enum.Enum):
|
||||
"""Enum which defines the possible priorities
|
||||
of queue entries.
|
||||
"""
|
||||
HIGH = enum.auto()
|
||||
LOW = enum.auto()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__queue = queue.Queue()
|
||||
self.__queue_low_priority = queue.Queue()
|
||||
self.__waiter_low_priority = threading.Condition()
|
||||
|
||||
def _enqueue(self, queue, image_holder, upper_bound_size, post_load_processor):
|
||||
"""Enqueues the image holder weakly referenced.
|
||||
|
||||
Args:
|
||||
queue (queue.Queue): the queue to operate on
|
||||
image_holder (ImageHolder):
|
||||
the image holder for which an image should be loaded
|
||||
upper_bound_size (tuple of (width: int, height: int)):
|
||||
the maximal size to load data for
|
||||
post_load_processor (PostLoadImageProcessor):
|
||||
allows to apply changes to the recently loaded image
|
||||
"""
|
||||
queue.put((
|
||||
weakref.ref(image_holder), upper_bound_size, post_load_processor))
|
||||
|
||||
def _dequeue(self, queue):
|
||||
"""Removes queue entries till an alive reference was found.
|
||||
The referenced image holder will be returned in this case.
|
||||
Otherwise if there wasn't found any alive reference
|
||||
None will be returned.
|
||||
|
||||
Args:
|
||||
queue (queue.Queue): the queue to operate on
|
||||
|
||||
Returns:
|
||||
tuple of (ImageHolder, tuple of (width: int, height: int),
|
||||
PostLoadImageProcessor):
|
||||
an queued image holder or None, upper bound size or None,
|
||||
the post load image processor or None
|
||||
"""
|
||||
holder_reference = None
|
||||
image_holder = None
|
||||
upper_bound_size = None
|
||||
post_load_processor = None
|
||||
|
||||
while not queue.empty():
|
||||
holder_reference, upper_bound_size, post_load_processor = \
|
||||
queue.get_nowait()
|
||||
image_holder = holder_reference and holder_reference()
|
||||
if (holder_reference is None or
|
||||
image_holder is not None):
|
||||
break
|
||||
|
||||
return image_holder, upper_bound_size, post_load_processor
|
||||
|
||||
@abc.abstractmethod
|
||||
def _schedule(self, function, priority):
|
||||
"""Schedules the execution of a function.
|
||||
Functions should be executed in different thread pools
|
||||
based on their priority otherwise you can wait for a death lock.
|
||||
|
||||
Args:
|
||||
function (Function): the function which should be executed
|
||||
priority (AsynchronImageLoader.Priority):
|
||||
the priority of the execution of this function
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _load_image(self, path, upper_bound_size, post_load_processor):
|
||||
"""Wrapper for calling load_image.
|
||||
Behaves like calling it directly,
|
||||
but allows e.g. executing the function in other processes.
|
||||
"""
|
||||
image, *other_data = load_image(path, upper_bound_size)
|
||||
|
||||
if image and post_load_processor:
|
||||
image = post_load_processor.on_loaded(image)
|
||||
|
||||
return (image, *other_data)
|
||||
|
||||
def load(self, path, upper_bound_size, post_load_processor=None):
|
||||
holder = ImageHolder(path)
|
||||
self._enqueue(
|
||||
self.__queue, holder, upper_bound_size, post_load_processor)
|
||||
self._schedule(self.__process_high_priority_entry,
|
||||
self.Priority.HIGH)
|
||||
return holder
|
||||
|
||||
def __wait_for_main_work(self):
|
||||
"""Waits till all queued high priority entries were processed."""
|
||||
if not self.__queue.empty():
|
||||
with self.__waiter_low_priority:
|
||||
if not self.__queue.empty():
|
||||
self.__waiter_low_priority.wait()
|
||||
|
||||
def __notify_main_work_done(self):
|
||||
"""Notifies waiting threads that
|
||||
all queued high priority entries were processed.
|
||||
"""
|
||||
if self.__queue.empty():
|
||||
with self.__waiter_low_priority:
|
||||
if self.__queue.empty():
|
||||
self.__waiter_low_priority.notify_all()
|
||||
|
||||
def __process_high_priority_entry(self):
|
||||
"""Processes a single queued high priority entry."""
|
||||
self.__process_queue(self.__queue)
|
||||
self.__notify_main_work_done()
|
||||
|
||||
def __process_low_priority_entry(self):
|
||||
"""Processes a single queued low priority entry."""
|
||||
self.__wait_for_main_work()
|
||||
self.__process_queue(self.__queue_low_priority)
|
||||
|
||||
def __process_queue(self, queue):
|
||||
"""Processes a single queued entry.
|
||||
|
||||
Args:
|
||||
queue (queue.Queue): the queue to operate on
|
||||
"""
|
||||
image = None
|
||||
image_holder, upper_bound_size, post_load_processor = \
|
||||
self._dequeue(queue)
|
||||
if image_holder is None:
|
||||
return
|
||||
|
||||
try:
|
||||
image, downscaled = self._load_image(
|
||||
image_holder.path, upper_bound_size, post_load_processor)
|
||||
if upper_bound_size and downscaled:
|
||||
self._enqueue(
|
||||
self.__queue_low_priority,
|
||||
image_holder, None, post_load_processor)
|
||||
self._schedule(self.__process_low_priority_entry,
|
||||
self.Priority.LOW)
|
||||
except OSError as exception:
|
||||
self.process_error(exception)
|
||||
finally:
|
||||
image_holder.reveal_image(image or self.PLACEHOLDER)
|
||||
|
||||
|
||||
# * Pythons GIL limits the usefulness of threads.
|
||||
# So in order to use all cpu cores (assumed GIL isn't released)
|
||||
# you need to use multiple processes.
|
||||
# * Pillows load method will read & decode the image.
|
||||
# So it does the I/O and CPU work.
|
||||
# Decoding seems to be the bottleneck for large images.
|
||||
# * Using multiple processes comes with it's own bottleneck
|
||||
# (transfering the data between the processes).
|
||||
#
|
||||
# => Using multiple processes seems to be faster for small images.
|
||||
# Using threads seems to be faster for large images.
|
||||
class ThreadImageLoader(AsynchronousImageLoader):
|
||||
"""Implementation of AsynchronImageLoader
|
||||
which loads images in multiple threads.
|
||||
"""
|
||||
@staticmethod
|
||||
def get_loader_name():
|
||||
return "thread"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
threads = os.cpu_count()
|
||||
threads_low_priority = max(1, threads // 2)
|
||||
self.__executor = thread.DaemonThreadPoolExecutor(
|
||||
max_workers=threads)
|
||||
self.__executor_low_priority = thread.DaemonThreadPoolExecutor(
|
||||
max_workers=threads_low_priority)
|
||||
self.threads = threads + threads_low_priority
|
||||
|
||||
def __exit__(self, *_):
|
||||
self.__executor_low_priority.shutdown()
|
||||
self.__executor.shutdown()
|
||||
|
||||
def _schedule(self, function, priority):
|
||||
executor = self.__executor
|
||||
if priority == self.Priority.LOW:
|
||||
executor = self.__executor_low_priority
|
||||
executor.submit(function) \
|
||||
.add_done_callback(
|
||||
lambda future: self.process_error(future.exception()))
|
||||
|
||||
|
||||
class ProcessImageLoader(ThreadImageLoader):
|
||||
"""Implementation of AsynchronImageLoader
|
||||
which loads images in multiple processes.
|
||||
Therefore it allows to utilise all cpu cores
|
||||
for decoding an image.
|
||||
"""
|
||||
@staticmethod
|
||||
def get_loader_name():
|
||||
return "process"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__executor_loader = concurrent.futures.ProcessPoolExecutor(
|
||||
max_workers=self.threads)
|
||||
# ProcessPoolExecutor won't work
|
||||
# when used first in ThreadPoolExecutor
|
||||
self.__executor_loader \
|
||||
.submit(id, id) \
|
||||
.result()
|
||||
|
||||
def __exit__(self, *args):
|
||||
super().__exit__(*args)
|
||||
self.__executor_loader.shutdown()
|
||||
|
||||
@staticmethod
|
||||
def _load_image_extern(path, upper_bound_size, post_load_processor):
|
||||
"""This function is a wrapper for the image loading function
|
||||
as sometimes pillow restores decoded images
|
||||
received from other processes wrongly.
|
||||
E.g. a PNG is reported as webp (-> crash on using an image function)
|
||||
So this function is a workaround which prevents these crashs to happen.
|
||||
"""
|
||||
image, *other_data = load_image(path, upper_bound_size)
|
||||
|
||||
if image and post_load_processor:
|
||||
image = post_load_processor.on_loaded(image)
|
||||
|
||||
return (image.mode, image.size, image.tobytes(), *other_data)
|
||||
|
||||
def _load_image(self, path, upper_bound_size, post_load_processor=None):
|
||||
import PIL.Image
|
||||
future = self.__executor_loader.submit(
|
||||
ProcessImageLoader._load_image_extern,
|
||||
path, upper_bound_size, post_load_processor)
|
||||
mode, size, data, downscaled = future.result()
|
||||
return PIL.Image.frombytes(mode, size, data), downscaled
|
||||
|
||||
|
||||
@enum.unique
|
||||
class ImageLoaderOption(str, enum.Enum):
|
||||
"""Enum which lists the useable ImageLoader classes."""
|
||||
SYNCHRONOUS = SynchronousImageLoader
|
||||
THREAD = ThreadImageLoader
|
||||
PROCESS = ProcessImageLoader
|
||||
|
||||
def __new__(cls, loader_class):
|
||||
inst = str.__new__(cls)
|
||||
# Based on an official example
|
||||
# https://docs.python.org/3/library/enum.html#using-a-custom-new
|
||||
# So.. stfu pylint
|
||||
# pylint: disable=protected-access
|
||||
inst._value_ = loader_class.get_loader_name()
|
||||
inst.loader_class = loader_class
|
||||
return inst
|
@ -1,145 +0,0 @@
|
||||
"""This module defines data parser
|
||||
which can be used to exchange information between processes.
|
||||
"""
|
||||
import json
|
||||
import abc
|
||||
import shlex
|
||||
import itertools
|
||||
import enum
|
||||
|
||||
|
||||
class Parser:
|
||||
"""Subclasses of this abstract class define
|
||||
how to input will be parsed.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def get_name():
|
||||
"""Returns the constant name which is associated to this parser."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def parse(self, line):
|
||||
"""Parses a line.
|
||||
|
||||
Args:
|
||||
line (str): read line
|
||||
|
||||
Returns:
|
||||
dict containing the key value pairs of the line
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def unparse(self, data):
|
||||
"""Composes the data to a string
|
||||
whichs follows the syntax of the parser.
|
||||
|
||||
Args:
|
||||
data (dict): data as key value pairs
|
||||
|
||||
Returns:
|
||||
string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class JsonParser(Parser):
|
||||
"""Parses json input"""
|
||||
|
||||
@staticmethod
|
||||
def get_name():
|
||||
return 'json'
|
||||
|
||||
def parse(self, line):
|
||||
try:
|
||||
data = json.loads(line)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(
|
||||
'Expected to parse an json object, got ' + line)
|
||||
return data
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(error)
|
||||
|
||||
def unparse(self, data):
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
class SimpleParser(Parser):
|
||||
"""Parses key value pairs separated by a tab.
|
||||
Does not support escaping spaces.
|
||||
"""
|
||||
SEPARATOR = '\t'
|
||||
|
||||
@staticmethod
|
||||
def get_name():
|
||||
return 'simple'
|
||||
|
||||
def parse(self, line):
|
||||
components = line.split(SimpleParser.SEPARATOR)
|
||||
|
||||
if len(components) % 2 != 0:
|
||||
raise ValueError(
|
||||
'Expected key value pairs, ' +
|
||||
'but at least one key has no value: ' +
|
||||
line)
|
||||
|
||||
return {
|
||||
key: value
|
||||
for key, value in itertools.zip_longest(
|
||||
components[::2], components[1::2])
|
||||
}
|
||||
|
||||
def unparse(self, data):
|
||||
return SimpleParser.SEPARATOR.join(
|
||||
str(key) + SimpleParser.SEPARATOR + str(value.replace('\n', ''))
|
||||
for key, value in data.items())
|
||||
|
||||
|
||||
class BashParser(Parser):
|
||||
"""Parses input generated
|
||||
by dumping associative arrays with `declare -p`.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_name():
|
||||
return 'bash'
|
||||
|
||||
def parse(self, line):
|
||||
# remove 'typeset -A varname=( ' and ')'
|
||||
start = line.find('(')
|
||||
end = line.rfind(')')
|
||||
|
||||
if not 0 <= start < end:
|
||||
raise ValueError(
|
||||
"Expected input to be formatted like "
|
||||
"the output of bashs `declare -p` function. "
|
||||
"Got: " + line)
|
||||
|
||||
components = itertools.dropwhile(
|
||||
lambda text: not text or text[0] != '[',
|
||||
shlex.split(line[start + 1:end]))
|
||||
return {
|
||||
key[1:-1]: value
|
||||
for pair in components
|
||||
for key, value in (pair.split('=', maxsplit=1),)
|
||||
}
|
||||
|
||||
def unparse(self, data):
|
||||
return ' '.join(
|
||||
'[' + str(key) + ']=' + shlex.quote(value)
|
||||
for key, value in data.items())
|
||||
|
||||
|
||||
@enum.unique
|
||||
class ParserOption(str, enum.Enum):
|
||||
JSON = JsonParser
|
||||
SIMPLE = SimpleParser
|
||||
BASH = BashParser
|
||||
|
||||
def __new__(cls, parser_class):
|
||||
inst = str.__new__(cls)
|
||||
inst._value_ = parser_class.get_name()
|
||||
inst.parser_class = parser_class
|
||||
return inst
|
@ -1,12 +0,0 @@
|
||||
class LazyConstant:
|
||||
def __init__(self, function):
|
||||
self.value = None
|
||||
self.function = function
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if self.value is None:
|
||||
self.value = self.function()
|
||||
return self.value
|
||||
|
||||
def __set__(self, instance, value):
|
||||
raise AttributeError("can't set attribute")
|
@ -1,159 +0,0 @@
|
||||
import re
|
||||
import os
|
||||
import functools
|
||||
|
||||
|
||||
MAX_PROCESS_NAME_LENGTH = 15
|
||||
|
||||
|
||||
@functools.wraps(os.getpid)
|
||||
def get_own_pid(*args, **kwargs):
|
||||
# pylint: disable=missing-docstring
|
||||
return os.getpid(*args, **kwargs)
|
||||
|
||||
|
||||
def get_info(pid: int):
|
||||
"""Determines information about the process with the given pid.
|
||||
|
||||
Determines
|
||||
- the process id (pid)
|
||||
- the command name (comm)
|
||||
- the state (state)
|
||||
- the process id of the parent process (ppid)
|
||||
- the process group id (pgrp)
|
||||
- the session id (session)
|
||||
- the controlling terminal (tty_nr)
|
||||
of the process with the given pid.
|
||||
|
||||
Args:
|
||||
pid (int or str):
|
||||
the associated pid of the process
|
||||
for which to retrieve the information for
|
||||
|
||||
Returns:
|
||||
dict of str: bytes:
|
||||
containing the listed information.
|
||||
The term in the brackets is used as key.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: if there is no process with the given pid
|
||||
"""
|
||||
with open(f'/proc/{pid}/stat', 'rb') as proc_file:
|
||||
data = proc_file.read()
|
||||
return (
|
||||
re.search(
|
||||
rb'^(?P<pid>[-+]?\d+) '
|
||||
rb'\((?P<comm>.{0,' +
|
||||
str(MAX_PROCESS_NAME_LENGTH).encode() + rb'})\) '
|
||||
rb'(?P<state>.) '
|
||||
rb'(?P<ppid>[-+]?\d+) '
|
||||
rb'(?P<pgrp>[-+]?\d+) '
|
||||
rb'(?P<session>[-+]?\d+) '
|
||||
rb'(?P<tty_nr>[-+]?\d+)', data)
|
||||
.groupdict())
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_pty_slave_folders():
|
||||
"""Determines the folders in which linux
|
||||
creates the control device files of the pty slaves.
|
||||
|
||||
Returns:
|
||||
list of str: containing the paths to these folders
|
||||
"""
|
||||
paths = []
|
||||
|
||||
with open('/proc/tty/drivers', 'rb') as drivers_file:
|
||||
for line in drivers_file:
|
||||
# The documentation about /proc/tty/drivers
|
||||
# is a little bit short (man proc):
|
||||
# /proc/tty
|
||||
# Subdirectory containing the pseudo-files and
|
||||
# subdirectories for tty drivers and line disciplines.
|
||||
# So.. see the source code:
|
||||
# https://github.com/torvalds/linux/blob/8653b778e454a7708847aeafe689bce07aeeb94e/fs/proc/proc_tty.c#L28-L67
|
||||
driver = (
|
||||
re.search(
|
||||
rb'^(?P<name>(\S| )+?) +'
|
||||
rb'(?P<path>/dev/\S+) ',
|
||||
line)
|
||||
.groupdict())
|
||||
if driver['name'] == b'pty_slave':
|
||||
paths += [driver['path'].decode()]
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def get_parent_pid(pid: int):
|
||||
"""Determines pid of the parent process of the process with the given pid.
|
||||
|
||||
Args:
|
||||
pid (int or str):
|
||||
the associated pid of the process
|
||||
for which to retrieve the information for
|
||||
|
||||
Returns:
|
||||
int: the pid of the parent process
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: if there is no process with the given pid
|
||||
"""
|
||||
process_info = get_info(pid)
|
||||
return int(process_info['ppid'])
|
||||
|
||||
|
||||
def calculate_minor_device_number(tty_nr: int):
|
||||
"""Calculates the minor device number contained
|
||||
in a tty_nr of a stat file of the procfs.
|
||||
|
||||
Args:
|
||||
tty_nr (int):
|
||||
a tty_nr of a stat file of the procfs
|
||||
|
||||
Returns:
|
||||
int: the minor device number contained in the tty_nr
|
||||
"""
|
||||
TTY_NR_BITS = 32
|
||||
FIRST_BITS = 8
|
||||
SHIFTED_BITS = 12
|
||||
FIRST_BITS_MASK = 0xFF
|
||||
SHIFTED_BITS_MASK = 0xFFF00000
|
||||
minor_device_number = (
|
||||
(tty_nr & FIRST_BITS_MASK) +
|
||||
((tty_nr & SHIFTED_BITS_MASK)
|
||||
>> (TTY_NR_BITS - SHIFTED_BITS - FIRST_BITS)))
|
||||
return minor_device_number
|
||||
|
||||
|
||||
def get_pty_slave(pid: int):
|
||||
"""Determines control device file
|
||||
of the pty slave of the process with the given pid.
|
||||
|
||||
Args:
|
||||
pid (int or str):
|
||||
the associated pid of the process
|
||||
for which to retrieve the information for
|
||||
|
||||
Returns:
|
||||
str or None:
|
||||
the path to the control device file
|
||||
or None if no path was found
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: if there is no process with the given pid
|
||||
"""
|
||||
pty_slave_folders = get_pty_slave_folders()
|
||||
process_info = get_info(pid)
|
||||
tty_nr = int(process_info['tty_nr'])
|
||||
minor_device_number = calculate_minor_device_number(tty_nr)
|
||||
|
||||
for folder in pty_slave_folders:
|
||||
device_path = f'{folder}/{minor_device_number}'
|
||||
|
||||
try:
|
||||
if tty_nr == os.stat(device_path).st_rdev:
|
||||
return device_path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return None
|
@ -1,99 +0,0 @@
|
||||
import os
|
||||
import signal
|
||||
import errno
|
||||
|
||||
|
||||
def get_command(pid):
|
||||
"""Figures out the associated command name
|
||||
of a process with the given pid.
|
||||
|
||||
Args:
|
||||
pid (int): the pid of the process of interest
|
||||
|
||||
Returns:
|
||||
str: the associated command name
|
||||
"""
|
||||
with open('/proc/{}/comm'.format(pid), 'r') as commfile:
|
||||
return '\n'.join(commfile.readlines())
|
||||
|
||||
|
||||
def is_same_command(pid0, pid1):
|
||||
"""Checks whether the associated command name
|
||||
of the processes of the given pids equals to each other.
|
||||
|
||||
Args:
|
||||
pid0 (int): the pid of the process of interest
|
||||
pid1 (int): the pid of another process of interest
|
||||
|
||||
Returns:
|
||||
bool: True if both processes have
|
||||
the same associated command name
|
||||
"""
|
||||
return get_command(pid0) == get_command(pid1)
|
||||
|
||||
|
||||
def send_signal_safe(own_pid, target_pid):
|
||||
"""Sends SIGUSR1 to a process if both
|
||||
processes have the same associated command name.
|
||||
(Race condition free)
|
||||
|
||||
Requires:
|
||||
- Python 3.9+
|
||||
- Linux 5.1+
|
||||
|
||||
Args:
|
||||
own_pid (int): the pid of this process
|
||||
target_pid (int):
|
||||
the pid of the process to send the signal to
|
||||
"""
|
||||
pidfile = None
|
||||
try:
|
||||
pidfile = os.open(f'/proc/{target_pid}', os.O_DIRECTORY)
|
||||
if is_same_command(own_pid, target_pid):
|
||||
signal.pidfd_send_signal(pidfile, signal.SIGUSR1)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as error:
|
||||
# not sure if errno is really set..
|
||||
# at least the documentation of the used functions says so..
|
||||
# see e.g.: https://github.com/python/cpython/commit/7483451577916e693af6d20cf520b2cc7e2174d2#diff-99fb04b208835118fdca0d54b76a00c450da3eaff09d2b53e8a03d63bbe88e30R1279-R1281
|
||||
# and https://docs.python.org/3/c-api/exceptions.html#c.PyErr_SetFromErrno
|
||||
|
||||
# caused by either pidfile_open or pidfd_send_signal
|
||||
if error.errno != errno.ESRCH:
|
||||
raise
|
||||
# else: the process is death
|
||||
finally:
|
||||
if pidfile is not None:
|
||||
os.close(pidfile)
|
||||
|
||||
|
||||
def send_signal_unsafe(own_pid, target_pid):
|
||||
"""Sends SIGUSR1 to a process if both
|
||||
processes have the same associated command name.
|
||||
(Race condition if process dies)
|
||||
|
||||
Args:
|
||||
own_pid (int): the pid of this process
|
||||
target_pid (int):
|
||||
the pid of the process to send the signal to
|
||||
"""
|
||||
try:
|
||||
if is_same_command(own_pid, target_pid):
|
||||
os.kill(target_pid, signal.SIGUSR1)
|
||||
except (FileNotFoundError, ProcessLookupError):
|
||||
pass
|
||||
|
||||
|
||||
def main(options):
|
||||
# assumption:
|
||||
# started by calling the programs name
|
||||
# ueberzug layer and
|
||||
# ueberzug query_windows
|
||||
own_pid = os.getpid()
|
||||
|
||||
for pid in options['PIDS']:
|
||||
try:
|
||||
send_signal_safe(own_pid, int(pid))
|
||||
except AttributeError:
|
||||
send_signal_unsafe(own_pid, int(pid))
|
@ -1,264 +0,0 @@
|
||||
"""Modul which implements class and functions
|
||||
all about scaling images.
|
||||
"""
|
||||
import abc
|
||||
import enum
|
||||
|
||||
import ueberzug.geometry as geometry
|
||||
|
||||
|
||||
class ImageScaler(metaclass=abc.ABCMeta):
|
||||
"""Describes the structure used to define image scaler classes.
|
||||
|
||||
Defines a general interface used to implement different ways
|
||||
of scaling images to specific sizes.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def get_scaler_name():
|
||||
"""Returns:
|
||||
str: the constant name which is associated to this scaler.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def is_indulgent_resizing():
|
||||
"""This method specifies whether the
|
||||
algorithm returns noticeable different results for
|
||||
the same image with different sizes (bigger than the
|
||||
maximum size which is passed to the scale method).
|
||||
|
||||
Returns:
|
||||
bool: False if the results differ
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def calculate_resolution(self, image, width: int, height: int):
|
||||
"""Calculates the final resolution of the scaled image.
|
||||
|
||||
Args:
|
||||
image (PIL.Image): the image which should be scaled
|
||||
width (int): maximum width that can be taken
|
||||
height (int): maximum height that can be taken
|
||||
|
||||
Returns:
|
||||
tuple: final width: int, final height: int
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def scale(self, image, position: geometry.Point,
|
||||
width: int, height: int):
|
||||
"""Scales the image according to the respective implementation.
|
||||
|
||||
Args:
|
||||
image (PIL.Image): the image which should be scaled
|
||||
position (geometry.Position): the centered position, if possible
|
||||
Specified as factor of the image size,
|
||||
so it should be an element of [0, 1].
|
||||
width (int): maximum width that can be taken
|
||||
height (int): maximum height that can be taken
|
||||
|
||||
Returns:
|
||||
PIL.Image: the scaled image
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class OffsetImageScaler(ImageScaler, metaclass=abc.ABCMeta):
|
||||
"""Extension of the ImageScaler class by Offset specific functions."""
|
||||
# pylint can't detect abstract subclasses
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
@staticmethod
|
||||
def get_offset(position: float, target_size: float, image_size: float):
|
||||
"""Calculates a offset which contains the position
|
||||
in a range from offset to offset + target_size.
|
||||
|
||||
Args:
|
||||
position (float): the centered position, if possible
|
||||
Specified as factor of the image size,
|
||||
so it should be an element of [0, 1].
|
||||
target_size (int): the image size of the wanted result
|
||||
image_size (int): the image size
|
||||
|
||||
Returns:
|
||||
int: the offset
|
||||
"""
|
||||
return int(min(max(0, position * image_size - target_size / 2),
|
||||
image_size - target_size))
|
||||
|
||||
|
||||
class MinSizeImageScaler(ImageScaler):
|
||||
"""Partial implementation of an ImageScaler.
|
||||
Subclasses calculate the final resolution of the scaled image
|
||||
as the minimum value of the image size and the maximum size.
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def calculate_resolution(self, image, width: int, height: int):
|
||||
return (min(width, image.width),
|
||||
min(height, image.height))
|
||||
|
||||
|
||||
class CropImageScaler(MinSizeImageScaler, OffsetImageScaler):
|
||||
"""Implementation of the ImageScaler
|
||||
which crops out the maximum image size.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_scaler_name():
|
||||
return "crop"
|
||||
|
||||
@staticmethod
|
||||
def is_indulgent_resizing():
|
||||
return False
|
||||
|
||||
def scale(self, image, position: geometry.Point,
|
||||
width: int, height: int):
|
||||
width, height = self.calculate_resolution(image, width, height)
|
||||
image_width, image_height = image.width, image.height
|
||||
offset_x = self.get_offset(position.x, width, image_width)
|
||||
offset_y = self.get_offset(position.y, height, image_height)
|
||||
return image \
|
||||
.crop((offset_x, offset_y,
|
||||
offset_x + width, offset_y + height))
|
||||
|
||||
|
||||
class DistortImageScaler(ImageScaler):
|
||||
"""Implementation of the ImageScaler
|
||||
which distorts the image to the maximum image size.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_scaler_name():
|
||||
return "distort"
|
||||
|
||||
@staticmethod
|
||||
def is_indulgent_resizing():
|
||||
return True
|
||||
|
||||
def calculate_resolution(self, image, width: int, height: int):
|
||||
return width, height
|
||||
|
||||
def scale(self, image, position: geometry.Point,
|
||||
width: int, height: int):
|
||||
import PIL.Image
|
||||
width, height = self.calculate_resolution(image, width, height)
|
||||
return image.resize((width, height), PIL.Image.ANTIALIAS)
|
||||
|
||||
|
||||
class FitContainImageScaler(DistortImageScaler):
|
||||
"""Implementation of the ImageScaler
|
||||
which resizes the image that either
|
||||
the width matches the maximum width
|
||||
or the height matches the maximum height
|
||||
while keeping the image ratio.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_scaler_name():
|
||||
return "fit_contain"
|
||||
|
||||
@staticmethod
|
||||
def is_indulgent_resizing():
|
||||
return True
|
||||
|
||||
def calculate_resolution(self, image, width: int, height: int):
|
||||
factor = min(width / image.width, height / image.height)
|
||||
return int(image.width * factor), int(image.height * factor)
|
||||
|
||||
|
||||
class ContainImageScaler(FitContainImageScaler):
|
||||
"""Implementation of the ImageScaler
|
||||
which resizes the image to a size <= the maximum size
|
||||
while keeping the image ratio.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_scaler_name():
|
||||
return "contain"
|
||||
|
||||
@staticmethod
|
||||
def is_indulgent_resizing():
|
||||
return True
|
||||
|
||||
def calculate_resolution(self, image, width: int, height: int):
|
||||
return super().calculate_resolution(
|
||||
image, min(width, image.width), min(height, image.height))
|
||||
|
||||
|
||||
class ForcedCoverImageScaler(DistortImageScaler, OffsetImageScaler):
|
||||
"""Implementation of the ImageScaler
|
||||
which resizes the image to cover the entire area which should be filled
|
||||
while keeping the image ratio.
|
||||
If the image is smaller than the desired size
|
||||
it will be stretched to reach the desired size.
|
||||
If the ratio of the area differs
|
||||
from the image ratio the edges will be cut off.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_scaler_name():
|
||||
return "forced_cover"
|
||||
|
||||
@staticmethod
|
||||
def is_indulgent_resizing():
|
||||
return True
|
||||
|
||||
def scale(self, image, position: geometry.Point,
|
||||
width: int, height: int):
|
||||
import PIL.Image
|
||||
width, height = self.calculate_resolution(image, width, height)
|
||||
image_width, image_height = image.width, image.height
|
||||
if width / image_width > height / image_height:
|
||||
image_height = int(image_height * width / image_width)
|
||||
image_width = width
|
||||
else:
|
||||
image_width = int(image_width * height / image_height)
|
||||
image_height = height
|
||||
offset_x = self.get_offset(position.x, width, image_width)
|
||||
offset_y = self.get_offset(position.y, height, image_height)
|
||||
|
||||
return image \
|
||||
.resize((image_width, image_height), PIL.Image.ANTIALIAS) \
|
||||
.crop((offset_x, offset_y,
|
||||
offset_x + width, offset_y + height))
|
||||
|
||||
|
||||
class CoverImageScaler(MinSizeImageScaler, ForcedCoverImageScaler):
|
||||
"""The same as ForcedCoverImageScaler but images won't be stretched
|
||||
if they are smaller than the area which should be filled.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_scaler_name():
|
||||
return "cover"
|
||||
|
||||
@staticmethod
|
||||
def is_indulgent_resizing():
|
||||
return True
|
||||
|
||||
|
||||
@enum.unique
|
||||
class ScalerOption(str, enum.Enum):
|
||||
"""Enum which lists the useable ImageScaler classes."""
|
||||
DISTORT = DistortImageScaler
|
||||
CROP = CropImageScaler
|
||||
FIT_CONTAIN = FitContainImageScaler
|
||||
CONTAIN = ContainImageScaler
|
||||
FORCED_COVER = ForcedCoverImageScaler
|
||||
COVER = CoverImageScaler
|
||||
|
||||
def __new__(cls, scaler_class):
|
||||
inst = str.__new__(cls)
|
||||
# Based on an official example
|
||||
# https://docs.python.org/3/library/enum.html#using-a-custom-new
|
||||
# So.. stfu pylint
|
||||
# pylint: disable=protected-access
|
||||
inst._value_ = scaler_class.get_scaler_name()
|
||||
inst.scaler_class = scaler_class
|
||||
return inst
|
@ -1,109 +0,0 @@
|
||||
import sys
|
||||
import struct
|
||||
import fcntl
|
||||
import termios
|
||||
import math
|
||||
|
||||
|
||||
class TerminalInfo:
|
||||
@staticmethod
|
||||
def get_size(fd_pty=None):
|
||||
"""Determines the columns, rows, width (px),
|
||||
height (px) of the terminal.
|
||||
|
||||
Returns:
|
||||
tuple of int: cols, rows, width, height
|
||||
"""
|
||||
fd_pty = fd_pty or sys.stdout.fileno()
|
||||
farg = struct.pack("HHHH", 0, 0, 0, 0)
|
||||
fretint = fcntl.ioctl(fd_pty, termios.TIOCGWINSZ, farg)
|
||||
rows, cols, xpixels, ypixels = struct.unpack("HHHH", fretint)
|
||||
return cols, rows, xpixels, ypixels
|
||||
|
||||
@staticmethod
|
||||
def __guess_padding(chars, pixels):
|
||||
# (this won't work all the time but
|
||||
# it's still better than disrespecting padding all the time)
|
||||
# let's assume the padding is the same on both sides:
|
||||
# let font_width = floor(xpixels / cols)
|
||||
# (xpixels - padding)/cols = font_size
|
||||
# <=> (xpixels - padding) = font_width * cols
|
||||
# <=> - padding = font_width * cols - xpixels
|
||||
# <=> padding = - font_width * cols + xpixels
|
||||
font_size = math.floor(pixels / chars)
|
||||
padding = (- font_size * chars + pixels) / 2
|
||||
return padding
|
||||
|
||||
@staticmethod
|
||||
def __guess_font_size(chars, pixels, padding):
|
||||
return (pixels - 2 * padding) / chars
|
||||
|
||||
def __init__(self, pty=None):
|
||||
self.pty = pty
|
||||
self.font_width = None
|
||||
self.font_height = None
|
||||
self.padding_vertical = None
|
||||
self.padding_horizontal = None
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
"""bool: True if the information
|
||||
of every attribute has been calculated.
|
||||
"""
|
||||
return all((self.font_width, self.font_height,
|
||||
self.padding_vertical, self.padding_horizontal))
|
||||
|
||||
def reset(self):
|
||||
"""Resets the font size and padding."""
|
||||
self.font_width = None
|
||||
self.font_height = None
|
||||
self.padding_vertical = None
|
||||
self.padding_horizontal = None
|
||||
|
||||
def calculate_sizes(self, fallback_width, fallback_height):
|
||||
"""Calculates the values for font_{width,height} and
|
||||
padding_{horizontal,vertical}.
|
||||
"""
|
||||
if isinstance(self.pty, (int, type(None))):
|
||||
self.__calculate_sizes(self.pty, fallback_width, fallback_height)
|
||||
else:
|
||||
with open(self.pty) as fd_pty:
|
||||
self.__calculate_sizes(fd_pty, fallback_width, fallback_height)
|
||||
|
||||
def __calculate_sizes(self, fd_pty, fallback_width, fallback_height):
|
||||
cols, rows, xpixels, ypixels = TerminalInfo.get_size(fd_pty)
|
||||
xpixels = xpixels or fallback_width
|
||||
ypixels = ypixels or fallback_height
|
||||
padding_horizontal = self.__guess_padding(cols, xpixels)
|
||||
padding_vertical = self.__guess_padding(rows, ypixels)
|
||||
self.padding_horizontal = max(padding_horizontal, padding_vertical)
|
||||
self.padding_vertical = self.padding_horizontal
|
||||
self.font_width = self.__guess_font_size(
|
||||
cols, xpixels, self.padding_horizontal)
|
||||
self.font_height = self.__guess_font_size(
|
||||
rows, ypixels, self.padding_vertical)
|
||||
|
||||
if xpixels < fallback_width and ypixels < fallback_height:
|
||||
# some terminal emulators return the size of the text area
|
||||
# instead of the size of the whole window
|
||||
# -----
|
||||
# we're still missing information
|
||||
# e.g.:
|
||||
# we know the size of the text area but
|
||||
# we still don't know the margin of the text area to the edges
|
||||
# (a character has a specific size so:
|
||||
# there's some additional varying space
|
||||
# if the character size isn't a divider of
|
||||
# (window size - padding))
|
||||
# -> it's okay not to know it
|
||||
# if the terminal emulator centers the text area
|
||||
# (kitty seems to do that)
|
||||
# -> it's not okay not to know it
|
||||
# if the terminal emulator just
|
||||
# adds the additional space to the right margin
|
||||
# (which will most likely be done)
|
||||
# (stterm seems to do that)
|
||||
self.padding_horizontal = 1/2 * (fallback_width - xpixels)
|
||||
self.padding_vertical = 1/2 * (fallback_height - ypixels)
|
||||
self.font_width = xpixels / cols
|
||||
self.font_height = ypixels / rows
|
@ -1,48 +0,0 @@
|
||||
"""This module reimplements the ThreadPoolExecutor.
|
||||
https://github.com/python/cpython/blob/master/Lib/concurrent/futures/thread.py
|
||||
|
||||
The only change is the prevention of waiting
|
||||
for each thread to exit on exiting the script.
|
||||
"""
|
||||
import threading
|
||||
import weakref
|
||||
import concurrent.futures as futures
|
||||
|
||||
|
||||
def _worker(executor_reference, work_queue):
|
||||
# pylint: disable=W0212
|
||||
try:
|
||||
while True:
|
||||
work_item = work_queue.get(block=True)
|
||||
if work_item is not None:
|
||||
work_item.run()
|
||||
del work_item
|
||||
continue
|
||||
executor = executor_reference()
|
||||
if executor is None or executor._shutdown:
|
||||
if executor is not None:
|
||||
executor._shutdown = True
|
||||
work_queue.put(None)
|
||||
return
|
||||
del executor
|
||||
except BaseException:
|
||||
futures._base.LOGGER.critical('Exception in worker', exc_info=True)
|
||||
|
||||
|
||||
class DaemonThreadPoolExecutor(futures.ThreadPoolExecutor):
|
||||
"""The concurrent.futures.ThreadPoolExecutor extended by
|
||||
the prevention of waiting for each thread on exiting the script.
|
||||
"""
|
||||
|
||||
def _adjust_thread_count(self):
|
||||
def weakref_cb(_, queue=self._work_queue):
|
||||
queue.put(None)
|
||||
num_threads = len(self._threads)
|
||||
if num_threads < self._max_workers:
|
||||
thread_name = '%s_%d' % (self, num_threads)
|
||||
thread = threading.Thread(name=thread_name, target=_worker,
|
||||
args=(weakref.ref(self, weakref_cb),
|
||||
self._work_queue))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self._threads.add(thread)
|
@ -1,103 +0,0 @@
|
||||
import subprocess
|
||||
import shlex
|
||||
import os
|
||||
|
||||
import ueberzug.geometry as geometry
|
||||
|
||||
|
||||
def is_used():
|
||||
"""Determines whether this program runs in tmux or not."""
|
||||
return get_pane() is not None
|
||||
|
||||
|
||||
def get_pane():
|
||||
"""Determines the pane identifier this process runs in.
|
||||
|
||||
Returns:
|
||||
str or None
|
||||
"""
|
||||
return os.environ.get('TMUX_PANE')
|
||||
|
||||
|
||||
def get_session_id():
|
||||
"""Determines the session identifier this process runs in.
|
||||
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
return subprocess.check_output([
|
||||
'tmux', 'display', '-p',
|
||||
'-F', '#{session_id}',
|
||||
'-t', get_pane()
|
||||
]).decode().strip()
|
||||
|
||||
|
||||
def get_offset():
|
||||
"""Determines the offset
|
||||
of the pane (this process runs in)
|
||||
within it's tmux window.
|
||||
"""
|
||||
result = subprocess.check_output([
|
||||
'tmux', 'display', '-p',
|
||||
'-F', '#{pane_top},#{pane_left},'
|
||||
'#{pane_bottom},#{pane_right},'
|
||||
'#{window_height},#{window_width}',
|
||||
'-t', get_pane()
|
||||
]).decode()
|
||||
top, left, bottom, right, height, width = \
|
||||
(int(i) for i in result.split(','))
|
||||
return geometry.Distance(
|
||||
top, left, height - bottom, width - right)
|
||||
|
||||
|
||||
def is_window_focused():
|
||||
"""Determines whether the window
|
||||
which owns the pane
|
||||
which owns this process is focused.
|
||||
"""
|
||||
result = subprocess.check_output([
|
||||
'tmux', 'display', '-p',
|
||||
'-F', '#{window_active},#{pane_in_mode}',
|
||||
'-t', get_pane()
|
||||
]).decode()
|
||||
return result == "1,0\n"
|
||||
|
||||
|
||||
def get_client_pids():
|
||||
"""Determines the tty for each tmux client
|
||||
displaying the pane this program runs in.
|
||||
"""
|
||||
if not is_window_focused():
|
||||
return {}
|
||||
|
||||
return {int(pid)
|
||||
for pid in
|
||||
subprocess.check_output([
|
||||
'tmux', 'list-clients',
|
||||
'-F', '#{client_pid}',
|
||||
'-t', get_pane()
|
||||
]).decode().splitlines()}
|
||||
|
||||
|
||||
def register_hook(event, command):
|
||||
"""Updates the hook of the passed event
|
||||
for the pane this program runs in
|
||||
to the execution of a program.
|
||||
|
||||
Note: tmux does not support multiple hooks for the same target.
|
||||
So if there's already an hook registered it will be overwritten.
|
||||
"""
|
||||
subprocess.check_call([
|
||||
'tmux', 'set-hook',
|
||||
'-t', get_pane(),
|
||||
event, 'run-shell ' + shlex.quote(command)
|
||||
])
|
||||
|
||||
|
||||
def unregister_hook(event):
|
||||
"""Removes the hook of the passed event
|
||||
for the pane this program runs in.
|
||||
"""
|
||||
subprocess.check_call([
|
||||
'tmux', 'set-hook', '-u', '-t', get_pane(), event
|
||||
])
|
@ -1,177 +0,0 @@
|
||||
"""This module contains user interface related classes and methods.
|
||||
"""
|
||||
import abc
|
||||
import weakref
|
||||
import attr
|
||||
|
||||
import PIL.Image as Image
|
||||
|
||||
import ueberzug.xutil as xutil
|
||||
import ueberzug.geometry as geometry
|
||||
import ueberzug.scaling as scaling
|
||||
import ueberzug.X as X
|
||||
|
||||
|
||||
def roundup(value, unit):
|
||||
return ((value + (unit - 1)) & ~(unit - 1)) >> 3
|
||||
|
||||
|
||||
class WindowFactory:
|
||||
"""Window factory class"""
|
||||
def __init__(self, display):
|
||||
self.display = display
|
||||
|
||||
@abc.abstractmethod
|
||||
def create(self, *window_infos: xutil.TerminalWindowInfo):
|
||||
"""Creates a child window for each window id."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CanvasWindow(X.OverlayWindow):
|
||||
"""Ensures unmapping of windows"""
|
||||
class Factory(WindowFactory):
|
||||
"""CanvasWindows factory class"""
|
||||
def __init__(self, display, view):
|
||||
super().__init__(display)
|
||||
self.view = view
|
||||
|
||||
def create(self, *window_infos: xutil.TerminalWindowInfo):
|
||||
return [CanvasWindow(self.display, self.view, info)
|
||||
for info in window_infos]
|
||||
|
||||
class Placement:
|
||||
@attr.s
|
||||
class TransformedImage:
|
||||
"""Data class which contains the options
|
||||
an image was transformed with
|
||||
and the image data."""
|
||||
options = attr.ib(type=tuple)
|
||||
data = attr.ib(type=bytes)
|
||||
|
||||
def __init__(self, x: int, y: int, width: int, height: int,
|
||||
scaling_position: geometry.Point,
|
||||
scaler: scaling.ImageScaler,
|
||||
path: str, image: Image, last_modified: int,
|
||||
cache: weakref.WeakKeyDictionary = None):
|
||||
# x, y are useful names in this case
|
||||
# pylint: disable=invalid-name
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.scaling_position = scaling_position
|
||||
self.scaler = scaler
|
||||
self.path = path
|
||||
self.image = image
|
||||
self.last_modified = last_modified
|
||||
self.cache = cache or weakref.WeakKeyDictionary()
|
||||
|
||||
def transform_image(self, term_info: xutil.TerminalWindowInfo,
|
||||
width: int, height: int,
|
||||
format_scanline: tuple):
|
||||
"""Scales to image and calculates
|
||||
the width & height needed to display it.
|
||||
|
||||
Returns:
|
||||
tuple of (width: int, height: int, image: bytes)
|
||||
"""
|
||||
image = self.image.await_image()
|
||||
scanline_pad, scanline_unit = format_scanline
|
||||
transformed_image = self.cache.get(term_info)
|
||||
final_size = self.scaler.calculate_resolution(
|
||||
image, width, height)
|
||||
options = (self.scaler.get_scaler_name(),
|
||||
self.scaling_position, final_size)
|
||||
|
||||
if (transformed_image is None
|
||||
or transformed_image.options != options):
|
||||
image = self.scaler.scale(
|
||||
image, self.scaling_position, width, height)
|
||||
stride = roundup(image.width * scanline_unit, scanline_pad)
|
||||
transformed_image = self.TransformedImage(
|
||||
options, image.tobytes("raw", 'BGRX', stride, 0))
|
||||
self.cache[term_info] = transformed_image
|
||||
|
||||
return (*final_size, transformed_image.data)
|
||||
|
||||
def resolve(self, pane_offset: geometry.Distance,
|
||||
term_info: xutil.TerminalWindowInfo,
|
||||
format_scanline):
|
||||
"""Resolves the position and size of the image
|
||||
according to the teminal window information.
|
||||
|
||||
Returns:
|
||||
tuple of (x: int, y: int, width: int, height: int,
|
||||
image: PIL.Image)
|
||||
"""
|
||||
# x, y are useful names in this case
|
||||
# pylint: disable=invalid-name
|
||||
image = self.image.await_image()
|
||||
x = int((self.x + pane_offset.left) * term_info.font_width +
|
||||
term_info.padding_horizontal)
|
||||
y = int((self.y + pane_offset.top) * term_info.font_height +
|
||||
term_info.padding_vertical)
|
||||
width = int((self.width and (self.width * term_info.font_width))
|
||||
or image.width)
|
||||
height = \
|
||||
int((self.height and (self.height * term_info.font_height))
|
||||
or image.height)
|
||||
|
||||
return (x, y, *self.transform_image(
|
||||
term_info, width, height, format_scanline))
|
||||
|
||||
def __init__(self, display: X.Display,
|
||||
view, parent_info: xutil.TerminalWindowInfo):
|
||||
"""Changes the foreground color of the gc object.
|
||||
|
||||
Args:
|
||||
display (X.Display): any created instance
|
||||
parent_id (int): the X11 window id of the parent window
|
||||
"""
|
||||
super().__init__(display, parent_info.window_id)
|
||||
self.parent_info = parent_info
|
||||
self._view = view
|
||||
self.scanline_pad = display.bitmap_format_scanline_pad
|
||||
self.scanline_unit = display.bitmap_format_scanline_unit
|
||||
self.screen_width = display.screen_width
|
||||
self.screen_height = display.screen_height
|
||||
self._image = X.Image(
|
||||
display,
|
||||
self.screen_width,
|
||||
self.screen_height)
|
||||
|
||||
def __enter__(self):
|
||||
self.draw()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
def draw(self):
|
||||
"""Draws the window and updates the visibility mask."""
|
||||
rectangles = []
|
||||
|
||||
if not self.parent_info.ready:
|
||||
self.parent_info.calculate_sizes(
|
||||
self.width, self.height)
|
||||
|
||||
for placement in self._view.media.values():
|
||||
# x, y are useful names in this case
|
||||
# pylint: disable=invalid-name
|
||||
x, y, width, height, image = \
|
||||
placement.resolve(self._view.offset, self.parent_info,
|
||||
(self.scanline_pad, self.scanline_unit))
|
||||
rectangles.append((x, y, width, height))
|
||||
self._image.draw(x, y, width, height, image)
|
||||
|
||||
self._image.copy_to(
|
||||
self.id,
|
||||
0, 0,
|
||||
min(self.width, self.screen_width),
|
||||
min(self.height, self.screen_height))
|
||||
self.set_visibility_mask(rectangles)
|
||||
super().draw()
|
||||
|
||||
def reset_terminal_info(self):
|
||||
"""Resets the terminal information of this window."""
|
||||
self.parent_info.reset()
|
@ -1,5 +0,0 @@
|
||||
import ueberzug
|
||||
|
||||
|
||||
def main(_):
|
||||
print(ueberzug.__version__)
|
@ -1,144 +0,0 @@
|
||||
"""This module contains x11 utils"""
|
||||
import functools
|
||||
import asyncio
|
||||
|
||||
import ueberzug.tmux_util as tmux_util
|
||||
import ueberzug.terminal as terminal
|
||||
import ueberzug.process as process
|
||||
import ueberzug.X as X
|
||||
|
||||
|
||||
PREPARED_DISPLAYS = []
|
||||
DISPLAY_SUPPLIES = 1
|
||||
|
||||
|
||||
class Events:
|
||||
"""Async iterator class for x11 events"""
|
||||
|
||||
def __init__(self, loop, display: X.Display):
|
||||
self._loop = loop
|
||||
self._display = display
|
||||
|
||||
@staticmethod
|
||||
async def wait_for_event(loop, display: X.Display):
|
||||
"""Waits asynchronously for an x11 event and returns it"""
|
||||
return await loop.run_in_executor(None, display.wait_for_event)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
return await Events.wait_for_event(self._loop, self._display)
|
||||
|
||||
|
||||
class TerminalWindowInfo(terminal.TerminalInfo):
|
||||
def __init__(self, window_id, fd_pty=None):
|
||||
super().__init__(fd_pty)
|
||||
self.window_id = window_id
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_parent_pids(pid):
|
||||
"""Determines all parent pids of this process.
|
||||
The list is sorted from youngest parent to oldest parent.
|
||||
"""
|
||||
pids = []
|
||||
next_pid = pid
|
||||
|
||||
while next_pid > 1:
|
||||
pids.append(next_pid)
|
||||
next_pid = process.get_parent_pid(next_pid)
|
||||
|
||||
return pids
|
||||
|
||||
|
||||
def get_pid_window_id_map(display: X.Display):
|
||||
"""Determines the pid of each mapped window.
|
||||
|
||||
Returns:
|
||||
dict of {pid: window_id}
|
||||
"""
|
||||
return {
|
||||
display.get_window_pid(window_id): window_id
|
||||
for window_id in display.get_child_window_ids()}
|
||||
|
||||
|
||||
def sort_by_key_list(mapping: dict, key_list: list):
|
||||
"""Sorts the items of the mapping
|
||||
by the index of the keys in the key list.
|
||||
|
||||
Args:
|
||||
mapping (dict): the mapping to be sorted
|
||||
key_list (list): the list which specifies the order
|
||||
|
||||
Returns:
|
||||
list: which contains the sorted items as tuples
|
||||
"""
|
||||
key_map = {key: index for index, key in enumerate(key_list)}
|
||||
return sorted(
|
||||
mapping.items(),
|
||||
key=lambda item: key_map.get(item[0], float('inf')))
|
||||
|
||||
|
||||
def key_intersection(mapping: dict, key_list: list):
|
||||
"""Creates a new map which only contains the intersection
|
||||
of the keys.
|
||||
|
||||
Args:
|
||||
mapping (dict): the mapping to be filtered
|
||||
key_list (list): the keys to be used as a whitelist
|
||||
|
||||
Returns:
|
||||
dict: which only contains keys which are also in key_list
|
||||
"""
|
||||
key_map = {key: index for index, key in enumerate(key_list)}
|
||||
return {key: value for key, value in mapping.items()
|
||||
if key in key_map}
|
||||
|
||||
|
||||
def get_first_pty(pids: list):
|
||||
"""Determines the pseudo terminal of
|
||||
the first process in the passed list which owns one.
|
||||
"""
|
||||
for pid in pids:
|
||||
pty_slave_file = process.get_pty_slave(pid)
|
||||
if pty_slave_file:
|
||||
return pty_slave_file
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_parent_window_infos(display: X.Display):
|
||||
"""Determines the window id of each
|
||||
terminal which displays the program using
|
||||
this layer.
|
||||
|
||||
Returns:
|
||||
list of TerminalWindowInfo
|
||||
"""
|
||||
window_infos = []
|
||||
client_pids = {}
|
||||
|
||||
if tmux_util.is_used():
|
||||
client_pids = tmux_util.get_client_pids()
|
||||
else:
|
||||
client_pids = {process.get_own_pid()}
|
||||
|
||||
if client_pids:
|
||||
pid_window_id_map = get_pid_window_id_map(display)
|
||||
|
||||
for pid in client_pids:
|
||||
ppids = get_parent_pids(pid)
|
||||
ppid_window_id_map = key_intersection(pid_window_id_map, ppids)
|
||||
try:
|
||||
window_pid, window_id = next(iter(sort_by_key_list(
|
||||
ppid_window_id_map, ppids)))
|
||||
window_children_pids = ppids[:ppids.index(window_pid)][::-1]
|
||||
pty = get_first_pty(window_children_pids)
|
||||
window_infos.append(TerminalWindowInfo(window_id, pty))
|
||||
except StopIteration:
|
||||
# Window needs to be mapped,
|
||||
# otherwise it's not listed in _NET_CLIENT_LIST
|
||||
pass
|
||||
|
||||
return window_infos
|
Loading…
Reference in New Issue