././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9878325 agordejo-0.3.1/.gitignore0000644000175000017500000000257700000000000014005 0ustar00nilsnils# ---> Python # 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/ .nox/ .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 # IPython profile_default/ ipython_config.py # 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/ .dmypy.json dmypy.json # Pyre type checker .pyre/ #build an makefile *.bin *.build build buildnsmdata Makefile engine/compiledprefix.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9878325 agordejo-0.3.1/CHANGELOG0000644000175000017500000000360400000000000013217 0ustar00nilsnils#Changelog Format: Double ## for a version number followed by a space, ISO-Date, Semantic Versioning: ## YYYY-MM-DD major.minor.patch Two empty lines before the next entry. External contributors notice at the end of the line: (LastName, FirstName / nick) ## 2022-01-15 0.3.1 Option in Control menu to split the session view between horizontally and vertically The session list now dynamically expands to the needed width. Better log messages Fix regression and workarounds for crashes introduced by a recent PyQt update. Fix a bug where symlink targets are not available or have no permission but the session list tries to count the symlink and filesizes and crashes. This CHANGELOG is now available through the programs help menu directly. ## 2021-07-08 0.3.0 Remove "Quick" mode. As it turns out "Full" mode is quick enough. Port convenience features to full mode. Add button in session chooser for alternative access to context menu options Add a timeline above running session to show global jack transport position. Also add controls to set the position. Saving the timeline settings per session is done via nsm-data, which increases version from 1.0 to 1.1 Add normal "Save" to tray icon. Add file integrity check after copying a session Add progress updates to copy-session. Submenu in tray icon to toggle visibility of individual clients (if supported) Double click on a crashed clients opens it again. More programs and icons added to the internal database. Fix a rare crash where the hostname must be case sensitive. ## 2021-01-15 0.2.1 Remove Nuitka as dependency. Build commands stay the same. ## 2020-12-05 0.2.0 Fix crash from Qt/PyQt regression with Qt 5.15.2 Install nsm-data manpage ## 2020-10-15 0.1.1 Correct typo in the name of the project :) Sorry. ## 2020-07-15 0.1.0 A lot is working already, single-computer session are working fine. Network sessions of any form are not usable yet. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/COPYING0000644000175000017500000010451500000000000013043 0ustar00nilsnils GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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. Copyright (C) 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 . 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: Copyright (C) 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 . 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 . ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/LICENSE0000644000175000017500000010665000000000000013017 0ustar00nilsnilsNo file of this program is without a specific license. But some files miss a direct header: This program is licensed under the GPLv3, as seen in the original text below. This software release (or source repository) contains files that were either autogenerated from files not contained here or are "save files" of programs such as the Qt Designer. For technical reasons they do not have a license header. They are all licensed GPLv3 as well. All documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. Again, if a file with documentation is not licensed through a header or notice in the file itself it is licensed under CC-By-Sa 4.0 as well. The reasons for not including a header are purely technical. All files and content considered "branding" are licensed as CC-By-ND. Such as image files, logos, icons. These and the name of the program itself cannot be reused when forking and re-releasing. GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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. Copyright (C) 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 . 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: Copyright (C) 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 . 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 . ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9878325 agordejo-0.3.1/Makefile.in0000644000175000017500000000641500000000000014055 0ustar00nilsnils #This is a makefile and needs tabs, not spaces. .PHONY: install uninstall clean gitclean resources all: #Our Program mkdir -p build cd build && printf "prefix = \"$(PREFIX)\"" > compiledprefix.py #Only copy the needed files cp -r "$(PROGRAM)" __main__.py engine qtgui build #Clean all pycache in build cd build && find . -type d -name "__pycache__" -exec rm -r {} + python3 -m zipapp "build" --output="$(PROGRAM).bin" --python="/usr/bin/env python3" rm build/compiledprefix.py #nsm-data mkdir -p buildnsmdata cp tools/nsm-data buildnsmdata/__main__.py cp tools/nsmclient.py buildnsmdata/nsmclient.py cd buildnsmdata && find . -type d -name "__pycache__" -exec rm -r {} + python3 -m zipapp "buildnsmdata" --output="nsm-data.bin" --python="/usr/bin/env python3" clean: rm -rf build/ rm -rf buildnsmdata/ rm -f "$(PROGRAM).bin" rm -f "nsm-data.bin" rm Makefile find . -type d -name "__pycache__" -exec rm -r {} + #Convenience function for developing, not used for the build or install process gitclean: git clean -f -X -d #Convenience function for developing, not used for the build or install process resources: cd documentation && python3 build.py cd documentation && sh build-documentation.sh cd qtgui/resources && sh buildresources.sh install: #nsm-data install -D -m 755 "nsm-data.bin" $(DESTDIR)$(PREFIX)/bin/nsm-data #Agordejo install -D -m 755 $(PROGRAM).bin $(DESTDIR)$(PREFIX)/bin/$(PROGRAM) install -D -m 644 documentation/out/* -t $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM) install -D -m 644 README.md $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM)/README.md install -D -m 644 LICENSE $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM)/LICENSE install -D -m 644 CHANGELOG $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM)/CHANGELOG install -D -m 644 desktop/desktop.desktop $(DESTDIR)$(PREFIX)/share/applications/org.laborejo.agordejo.desktop install -D -m 644 desktop/agordejo-continue.desktop $(DESTDIR)$(PREFIX)/share/applications/org.laborejo.agordejo-continue.desktop install -d $(DESTDIR)$(PREFIX)/share/man/man1 gzip -c documentation/$(PROGRAM).1 > $(DESTDIR)$(PREFIX)/share/man/man1/$(PROGRAM).1.gz gzip -c documentation/nsm-data.1 > $(DESTDIR)$(PREFIX)/share/man/man1/nsm-data.1.gz #Icons for size in 16 32 64 128 256 512 ; do \ install -D -m 644 desktop/images/"$$size"x"$$size".png $(DESTDIR)$(PREFIX)/share/icons/hicolor/"$$size"x"$$size"/apps/$(PROGRAM).png ; \ done install -D -m 644 desktop/images/256x256.png $(DESTDIR)$(PREFIX)/share/pixmaps/$(PROGRAM).png #Engine resources install -d $(DESTDIR)$(PREFIX)/share/$(PROGRAM) cp -r engine/resources/* $(DESTDIR)$(PREFIX)/share/$(PROGRAM)/ uninstall: #Directories rm -rf $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM) rm -rf $(DESTDIR)$(PREFIX)/share/$(PROGRAM) #Files rm -f $(DESTDIR)$(PREFIX)/bin/$(PROGRAM) rm -f $(DESTDIR)$(PREFIX)/bin/nsm-data rm -f $(DESTDIR)$(PREFIX)/share/applications/org.laborejo.agordejo.desktop rm -f $(DESTDIR)$(PREFIX)/share/applications/org.laborejo.agordejo-continue.desktop rm -f $(DESTDIR)$(PREFIX)/share/man/man1/$(PROGRAM).1.gz rm -f $(DESTDIR)$(PREFIX)/share/man/man1/nsm-data.1.gz #Icons for size in 16 32 64 128 256 512 ; do \ rm -f $(DESTDIR)$(PREFIX)/share/icons/hicolor/"$$size"x"$$size"/apps/$(PROGRAM).png ; \ done rm -f $(DESTDIR)$(PREFIX)/share/pixmaps/$(PROGRAM).png ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/README.md0000644000175000017500000000536300000000000013270 0ustar00nilsnils [//]: # (Generated 2022-01-15T19:48:12.408433. Changes belong into template/documentation/readme.template) #Agordejo Program version 0.3.1 ![Screenshot](https://git.laborejo.org/lss/Agordejo/raw/branch/master/documentation/screenshot.png "Screenshot") Agordejo (Esperanto: 'place to set things up') is a music production session manager. It is used to start your programs, remember their (JACK) interconnections and make your life easier in general. Agordejo does not re-invent the wheel but instead uses the New-Session-Manager daemon and enhances it with some tricks of its own, that always remain 100% compatible with the original sessions. This is a proof of concept version. It aims to show that session management with NSM can be quick and convenient and make the user feel in control. Some functionality has not yet been implemented, most prominently anything related to NSM over network. There is always the possibility to break things when trying out corner cases and hacks. That said, for single-computer sessions with just one daemon and one GUI at the same time Agordejo should provide a good user experience. This README is just a short introduction. Consult the manual (see below) for more information. # Contact and Information * Website https://www.laborejo.org * Bugs and Issues: https://www.laborejo.org/bugs * Git Repositories for all programs: https://git.laborejo.org * Documentation and Manual https://www.laborejo.org/documentation/agordejo # Installation and Starting ## Download ### Release Version If the latest release is not available through your package manger you can build it yourself: Download the latest code release on https://www.laborejo.org/downloads and extract it. ### Git Version It is possible to clone a git repository. `git clone https://git.laborejo.org/lss/agordejo.git` ## Dependencies * Glibc * Python 3.6 (maybe earlier) * PyQt5 for Python 3 * DejaVu Sans Sarif TTF (Font) (recommended, but not technically necessary) * nsmd: New Session Manager * grep #### Build Dependencies * Bash * GCC (development is done on 8.2, but most likely you can use a much earlier version) ### Environment: * Jack Audio Connection Kit must be running ## Build and Install ./configure --prefix=/usr/local make sudo make install ## Starting If you installed Agordejo through a package manager or yourself simply use your application launcher or terminal to start the executable `agordejo` You can also run Agordejo after extracting the release archive or cloning from git, without make or installation. If you did so, for additional features please link tools/nsm-data to your executable PATH. Use the manpage `man agordejo` or run `agordejo --help` (or local variant `./agordejo --help` ) to see available command line parameters. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9878325 agordejo-0.3.1/__main__.py0000644000175000017500000000065300000000000014100 0ustar00nilsnils#!/usr/bin/env python3 # encoding: utf-8 # file: __main__.py """ This file is an alternativ to ./agordejo when using zipapp. Any code changes in ./agordejo must be manually recplicated here. """ if __name__ == '__main__': from engine import start from qtgui import mainwindow #which in turn imports the engine and starts the engine mainwindow.MainWindow() #Program is over. Code here does not get executed. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/agordejo0000755000175000017500000000035400000000000013524 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- from engine import start from qtgui import mainwindow #which in turn imports the engine and starts the engine mainwindow.MainWindow() #Program is over. Code here does not get executed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272568.0144994 agordejo-0.3.1/configure0000755000175000017500000000352400000000000013715 0ustar00nilsnils#!/bin/bash program=agordejo version=0.3.1 #debugsym=true prefix=/usr/local required_version_python=3.8 required_version_pyqt=5.0 for arg in "$@"; do case "$arg" in --prefix=*) prefix=`echo $arg | sed 's/--prefix=//'` ;; #--enable-debug) # debugsym=true;; #--disable-debug) # debugsym=false;; --help) echo 'usage: ./configure [options]' echo 'options:' echo ' --prefix=: installation prefix' #echo ' --enable-debug: include debug symbols' #echo ' --disable-debug: do not include debug symbols' echo 'all invalid options are silently ignored' exit 0 ;; esac done echo "PREFIX=$prefix" echo "Checking Dependencies. No output is good" function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } command -v python3 >/dev/null 2>&1 || { echo >&2 "Python3 >= $required_version_python is required but it's not installed. Aborting."; exit 1; } PY3VERSION=$(python3 -c 'import platform; print(platform.python_version())') if version_gt $required_version_python $PY3VERSION; then echo "Python3 must be version >= $required_version_python but is $PY3VERSION. Aborting."; exit 1; fi python3 -c 'import PyQt5' >/dev/null 2>&1 || { echo >&2 "PyQt for Python3 >= $required_version_pyqt is required but it's not installed. Aborting."; exit 1; } PYQTVERSION=$(python3 -c 'from PyQt5.QtCore import QT_VERSION_STR; print(QT_VERSION_STR)') if version_gt $required_version_pyqt $PYQTVERSION; then echo "PyQt must be version >= $required_version_pyqt but is $PYQTVERSION. Aborting."; exit 1; fi echo "generating makefile" printf "PREFIX=$prefix\nPROGRAM=$program\nVERSION=$version\n" >Makefile #if $debugsym; then # echo 'dbg = -g' >>Makefile #fi cat Makefile.in >>Makefile echo 'configuration complete, type make to build.' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/desktop/agordejo-continue.desktop0000644000175000017500000000047500000000000020470 0ustar00nilsnils[Desktop Entry] Type=Application Name=Agordejo Continue Comment=Music and audio production session manager based on NSM. Continue last session, start as TrayIcon. Exec=agordejo --continue --hide Icon=agordejo Terminal=false StartupNotify=false Version=1.0 Categories=AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack; ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9978325 agordejo-0.3.1/desktop/desktop.desktop0000644000175000017500000000037000000000000016517 0ustar00nilsnils[Desktop Entry] Type=Application Name=Agordejo Comment=Music and audio production session manager based on NSM. Exec=agordejo Icon=agordejo Terminal=false StartupNotify=false Version=1.0 Categories=AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack; ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/desktop/images/1248-transparent.png0000644000175000017500000010615400000000000020372 0ustar00nilsnilsPNG  IHDR a pHYsodtEXtSoftwarewww.inkscape.org< IDATxw%E`$$e iUyU\30u]UUYu͂ " (d 90 0s=?/9]]o9 stUo$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I4D IRm8BY Xc , E$XwڟL"$pw B`u`ɿw)ܖ{sdt|Z88:NSmɶ? fJtΝlyYK~/oV~W5Hw5ۿ,'.;fي@BfXFK?et_,$|9: vvVkfy6HIKl< pTKI؎Q;آ~/%% PxdX>Nl~N[&HwfJ-#5Iq9fKp;p!Lt)ԗ:Dz~z#oW2捤Ls޶Fk^AzFKH: =4_FJw_s0t.z~x_>>KJ@>D:2vd/SL蝇 َar.mO?׸gwzǩ{3G6 4v&n3noK_pĮ t_cII_\ҵ{^H9|Fyvw_lvsP"=txCOkgj5S5yb 8dZmDoR7 }Tۡ}(BJGuGFJ8NZEim:)4ӋE3>ަx&v\VAmyJXH;JK2'ImPMCG=k|GKwwڤ' j5L1i? :*D Rә,!.[of?mǀ:`4}\'Ma$=tx)9jwV<ۻ,Ṥ}YviMS?܎l qipK>7+>C[vP:^I1? KOs4y6cHџoHokieՎ$nʏ6|\nG*ߨH'KNix5y>YAmI G(Jfj n&9`.}gmv HS\۵њ}ixm| 5g[G49}Wkm>CNdtV3VGg}F{Š.[!:7غ5 8 x&gs^t6' iщ#\^p_um9/ɸ-l?uq}M{i5X({T= V H *dNXJWeK]y{ϼͮy){t ! H|/Is$篌%m&=ڸϿwvQĺ4꼄 #߮7LQ]LiÙ?O=D;I0IE]Gy)]QJZyDR"Xf3) }Q`GGt҈,W7㴖p״HώztZR2Dw3ݚIj Vu-m>HEŚ׀ư 844:Reؖx/FHIwO҉H~Lz X6Y x,NPԢ=>~NoqۥIw]L*FYFLB]I#W'] %Zk\+_Tw&4Ţ{=VǢ`8B;xXtX{FߴU[vm6H L؏ I xS}^ :iյ~-&%uC&ix3_-c]RYlATmJ"` (½UlG-%݇~mkqHnKH~' }/9w=LmWnƵHߣa>Sxd(ĺ1*Ҕ>_[EXztc)<>7ݯu2G҈ۊ/,큳@g?χM y4:ۑ|݇Z}> aAp1u~g ) i\t#i.[߭S/iR=ŴWn/47FC\}29}(|yM*k}D=Y=/+@e=}HJv"Mh"46qb}**mؘfԶxnK˧$iz*ІԬZ\"_HSˣ4An͵\֧Q9p=GblVޤJ[Bt)}.lFě>6 T_ܹ= I7\?7Pv6o_O#%Jə p{&i~N/sR]~Mel'$zkHz k$L}H@KOx\O3h:KIo?uQ/tX\JOen"Y熌wBL:v%QEzFPUO L7mQ$ QOOAJH0E|TϬ)اZ 8H37r{N V(i1)VWn\Gz/%uR-oVms6W=^O=JF>DfiJSxzWRvk_F6SK $(?ElF( Yܣ>NMom'>9WfmmҪaTﱴfԞTgb-߶%Ufug믕zkYIs+^[yvha7 q%stho{yWd}ؖ{*SIm% (=vv@_H* BÙYؚ7d|L((ҳڜq=e4E-omG|բ}9׫H7v+yW!׫Zph1c[ -wn ޒSL-ʬ=_+XJJPۙcLq5mm/5&OJҙgmKUaK@<_i[a{҂g| 8>{kęS?J<_emBZ 2 hw=rRfߐT -Ք:ΤK~J ib D{[nmH*i3ڟIۯͿF6u.KP>:<7^DYDZ|eQ` *ሪn|BRA(kQnU%)ʀ֨}H eр}NIGغ~./ˀoF1i!GK7u"^G치<5&KtR6_,!-P7OtG=}=`N0-v) DVߋ;j Bu.)ϙAdZ2"۶-zzTmMTmÚ[wsWB*E !=|^OLC9'>>;E}!]YN7#!wB5Y(ӶH1uF=#3Io9mǓgz[kiB`%A5I 6,oi5S9XSN q:Sxr?<ۂ43‘A͕Ŀ\xCp j 8ǑTd}/VbHӤ#|5h&vWYVpFA:xM8q 2l#[Hu#H5ܔH;pSvUtԲkI'-i73+qdCI\B< ~ =K}P^Sj4YJHQ p7қD2} ȕL2~ '86Ok`]s)p*{` GkQs"%ז7>^VNEhk;\Dڷm9T.z@sHGi.6_;^L*N_F0:9]9+À[2ıT-ݏ3]63(zDͨ%#!7^t|H ?;<'pߵ_롎QpO `Ns[yu$8?h-( %=t4"Ei 75,Mpjyvy5"&%k}q nX` t &=8p]I]X6nl7F@wi*N"_'a3Ի`EjHZ<~K 0mIW &?=p:4`848ebNY;:55( с_i߁^>6x'_ aG|~gLGcn %A` א;DZ1HxJk(0;H/qǣoUmHu>Am+>-98{8#p]idU.ܐ\Go):q{et#wD!j(ҋHߥב'P#FHbBy8N~hXJuQFK IDAT;]lj}{^*bQv uJyCpNqʽIS5G0+]SCR|*T)ͭE!6y1YL|):5W[@a"R_+l'm CtȺ$:WÔ~V|1p :d&:+:/p*n҈A7M}8x8:@yl+c'QW?n'&xW4`^A=ZF'Ր(E5'HվK ,&l&4py w&YCn)G'. ܓڟ]JOZ57+:R9TK,:1D6w+Xg/x]C)vVB͙L;E,6ĢMm`x_n(5H7 41Z 9tJx+~:b1/#z!~Quu|o@m9Eںa1%q&4H fl楤p [nu{itSi]RXW0TMV%F_ }&ٰ#MkT zp`N3i_H]_>:S,M,^{s%t"EY]IpSGAr\t#X 2GjCjf2#NOɑp'[3ߝ'& u ﳆ萈QS-#3Tgf[ψt-86:WCn=zLq^\긗Z?Li&>nѿlӠ/t:Yif}}0CW, ~ԲE>]&~:< A8p^gG~&w'uԨZ?Li&4p_?0`5̔lnʣ?kY |N Ęah `9ˁCkv֥z]p_L3FuJR$6̠/@4cJ1 pSftY3DW}(%^AN)׀fWW]Rõyvet!pnT58߹Djjx!ᆮ$5E8[[tԱ2縨adjtT2]S.^kփ/ڳ9HQW N>OK~[^MokA!XհR0j)dۅ6ITN ~E܍Z]LRpzΡBS_T ◤'R ^H8>* G%-6ִnj+j8V]'Y5u^gCJ؁T~_`qiЫ] Ib"Gk=tz_õt^՞Qv'/07O6QQL }=p5ݬaTWWpS^0jx+^˃A[Ϝl]u\XcԐ;L,k=tz_ԌSP5ը%h>ܮQJ9Z5H#* .~0ϴa$mF\jSCRz5ܔ{C^ ?dLpxI5\F]-ǸZ>׵ġ!u_MM? Z5a_8V\4ig "VCBR#non$>,2n ^Peu"F.$^^Oz0}dN 竖Ѓ_Srtc:~sijC75zΡz"nc%iA_2xbF[jl3Y`tiqC|NHz`췎]jAX&Sùu$֋`LzqNAU:AdrWt?!pT}+QT^satjvþWټzT5 sf%7^Ӂ2n@7yރl^\4uMQ(*éZ t.s :Ֆ\v<~s;i*G V^q{fְ[(A`xuLK|p]p_R{{TQ*_c]Aj5L 'Zi$:#."%J"̩;Ib}4K2m7u;t-UqS~]LڑY{i8Jv Khqik Ds0:lդ^$j838e`NQn-ނ ]Cܑ3J2;MfV 66hq綸6C.ݕvOjvѣT5鸨Z-yRB7Ǡ L ;"A>4jva?~:UM16vL(9 k[gN+캖ی4j "-|'ҔI`_Ojfeqp2pep]͎ڵ~lA a9iH E/aCSIJ`w` ۹:Uⶏ| 7H#,{6n6&~ oȡ瀇{{l[N a V{ѣT=8uF:S,2jeNҽ9NHvQWG=p_C\zjwP1vh3wNRV:n^FtKj֝Z">t<:wpFp @ZCсLݼeџ_s&ʸtK6xX\AL2 ˇUׁk OϰgFS/I+=Y<<*o.}?4K[t[iVg*er R ;^vi0݀5'4F 8u˯ &Me~u7un(Br ӏEF:!6zq׻SmYI{7:+x5|"+pXtO- `. % :(.`SH#5\J]]K| 86:iN>I|MAt1s 22s /YHА6v!}V#H~(Y97+Iײ=X oq(h{kHj'׺3`":RyF҇{ȹ4 R} co[x=Ǿk퟇:wu\xY~44Tgkҝgw" [<@VTnӉfT{\lܟ ] x>ppc2VBf t0u<g zyt4g4IKGڐk6F>4|1p5htx.rm53~Hq6b'KG' ?x+޵fHf%L#ZD7LZ]Kn 2lBZj=J6F ~twyB{6BZ翔%fj^ms⮡? w sgGrڌ=ڝT?§;]mx)iNBS5_!$ơVKL{wүӁG1HoDkׅQM9-i>Q;P2Jk׳|jqCiapPk$ [v?m؍ %'G[.!MݍtobPZ}/ՂolԿpĨ'6 -o<^MBGЬ6Y+u&ѮE52s_Ë& F sо'*a$1~GY}32Jz_=T.!AUL/?sb'bz!%[m7Eڍz#+ ?bq& 'p]̣&;gHCo }#pKM;]?\B]#rf %k={%zdn|՜xT8u :9Ԟmb!iVVJ*?]L@w%~d_BQ ǓF Dz!qS=g)]1 _EsS!FĬt!-+}. M1֔ZA =d!q#>ITӰbUۗT."处ŷ}Hmxpۼ(<6o'ݘ埃XJJԐx C7ehp;ж\S&>B ;^ a7ly5ϿʻbL^i;w0ڋ Sۉ5unob^H|BTRfkwަNe4#r'j;' yJ^H*%Po+(م4W^m1p6!g~:fBG-e&uvt{]={vD{]qORQ<Qgj+i }Uc=)s܏*l`}}H2KK臯 y{}鵋(3jiP'Io/ҳ-߶lnfθ$֢܋ڋZQ'Sfd?"z_֮ *No1>ǢQ6y/S[ӄ"vdJם,5->\c-iGQgRn"gZ L}ن ?VbU7(1Gk'gpS~A, >ԇ~y&}#F Usb<%7ω?;iېVX|YH]PW>4;g9[_S.!(M}T4 q7m7gO+O]F+~Nm;p|c3ĝ OxXpi01agc-*_a E~-ҾDCX-7佖A+z ` 6і )"_UI+=op]؛\EkaqM/(ܟہ}[L"V\Odjcttf bl!m…/Aڭ=9 co{^;i>Sţ2IBƟOզϑ-k1S;žř#w{zۀ2m&gU^ۥ>F%Y/h/sٶAE]j!p\ hЉNEH^ W]ܯD$y5_sZW[dvC1\8a7/ۉ&6#~z/hf؏H+EU[Tw([T 0~N˙ܣMmǭ(_ яUgk{ݻ_N 7O cI.п7蛤`M Gܫ~QAmVD9 qXE.ߗ+3!׃!%S/۴Hs gj T_gܯ, MmGe*FVЯ*ּS>|v^hzn^F;6OlWt HȾX ~}#K%_ôo?fڕ+K~lْ=\ IDAT\:hCbOHsڄ4ՠd?܇I*ԷEi3cX?SWЧ ikzUnic?&{jYvj[oXA?zfėngJAmut_07VKntS%'0(c)3äF-ܿ\m%a|~ sois+H3'I{UΥ;t84 p)Rb$l"eMN0jHMIz<1G1}4m<1JAeٴ]BUrXcӴd/zJ_tſP«H4*Iv]YЦSŕtjpLHTήQϥSA: tlhGSva7ie3Ttߒ)v[W}E'6Їv!~>5_4Spm,m3zԴպ*ďฃ2Uvnci KtjoV,p[$& 0ޔ)CU{mɘw Qk+Ia#[]j_->hImSͩ!ItQj0+/I[KˇmjO {1iŹmNZllq*'VmU{F) 8jr:Q;_.vȟf(ڳ4FE:TO-y>;/6"VP+IK1v p)ʙMaU"Sp ՚9:IQj֙u37J"%IJ eoBW(~ }kZUQ/0/ukI/~糼3 fs%JݤED5p?|Q9&Vy$Uy0 * ( ."DfqQ-jbQ}5*.15 HMdUff?ۿ[oթSUwy3wSu`H-n;+k5P{Ƕ0kkfǸՀ!bPIh!GL*0n"~嶴"ĬmE3&uĬw\`-wenDg76~r[4 -X-iఖ=Le U7 @0Wi=8wgg`  36n L<|Z1<~o.#&x)v~^9mߠ^L:嶌 bxT -X_3@-SFQ9vfMs0k_60:oq>O|;w'\nsb.bWO6MvG0]3a6NdL8̀y y-og}@}"X3Π pjFu ˁ2ڮö3pQb\p֛atlK>b ہÁcoښD&D KpG/oP,A !CrNg6is=R? 0n_X7.28%>aO܏I]4W+H"fӁZs>.3'{Zܿa䋔a >FA z؇qqy81ԗm7@#1u{- mx!91v=Ȱzf/͖}O$LW[_&;A']UF"D682ד~~Ey.i}s DdPe--sĵOF >[F|/;앷LmaZ+x1&ܰ!%Dۉ'ۈHgl]εuҬ:sWݕ.s owϖeD݉1 a;rڝt̖݈ٟprWFIt;3 ۉ:\S6הuFfʯtݤ$=vۮ];5DX>I~n;m ^ת ^kf)E_29y;: EeUN D`N8y: ``8a(|uzuumGi79-| }Dlqۣr5e =^g=Ntq}=g8)2]d|a]>Arl'niDv-'{3?08_: D0Z"@p1Py;,'&XOW{7K{q:_;Uu;5j"v5pQZ*q^Jj!F"X硴x}`ۋ?|%s;wV2Yb_A|_A|79$iQ 䕛1اQq'K$I}GFh<`vZ/ .]|%k8p$I(oi3ƈ7y ۞D {`YǍ$ITt@KR07k&ӉA?e$Ib+1[v% X)KQ>R7m-$Ip8LZ8|J2pm pѢ{O1 -f?+$IݳYҔ17n m-; 7-,8I$-Vϵ]ICfn-EA&^qm4?$IZ6gO]I#`n@ O- 8I$-FwַݯjaF`y P@ Kww,F`硷h2yxv#Ih\ǺdCxog/oo~v#4 M7(s %Ms&!DrL݉ ]tAI$ Ƨ{Zb3!ڡ|3?Mpk%p`ۍ3">//*_Gۜ pI 3{bu=["I$uRdC2Zڠf>\v#49 Mm7bJ<8o/#f|pXl[f$IzxFu󻟂qm7Bi'UҚwŶ1BG":Y$Id^/9N`;r}Zh|H긱h^- \q*>ǭ1t;H$iޱ}(K$-8x)ۡ enzM{wCXduRLc .d܏J$izcfnۺ~'4N6h^8SӀ׶܎qw1m7;>Ijv#I!;:37%?`Pwj)n앪8أ톌lݐ 8 8ؽH$itk ! /ڳ2wMpح.4S $IX}rV-~<}m6YĸkEক8 m6ݐo$In |юk2Lmxg?6%17}14XKݐ7݀Ed;kpݐ=lҺSPM72uc<^z>K?Pez $Id1N|&pߑؙA!I$5"x^NIq0Fh U$I>_nQ" N37= h<$I4I~zW2FIZ| M'`(xI$iҼ4s_'M _?He9H$IAV)#Iqg,_;!I$M}xpD$M=h?0cY<0$Idu-E4ae$I$Iٜa:80FəP%I$Ijt0% sn2'I$InR6 }OMJI$IEd p-e&xDzjrK8`$I$IZD̸Gf_6{)I$IҢpx.p/໙u zH;(I$IR&p@B[7'naw&xiS$I$I@|_#f `]7O<w-nG+1uL΃Ð݀fbn=ˈ@mS=6Оa5ql#r5[ng8&.vi`obvpqiI\."l5p !>̶bˈkb XI|6IBKnv6pXۍqO" w!ncR\KJ>:]F TxA^Cd ;_!nv"oMm!G=/^D-Vٵ)O&^׻6\ 7qݻ"s7w0r5[״}?܅(s3;>d <xpfv11fW/œN|J,*/mC*x>gc)P/qUXnSĘ?LHKE;Vn ޱS:׻uk%?kSs^,s:ߵmc_Wmt[F{w=_㣩=t!>y?*`gb_C^ot[ϫ]gYE_!ozXCד/y aU۸8V-[) ~u[9NP~O<|‡fYhl s?$`9q!n{@6˵%čCܧI)U7_Z\!jͲLW6_,/h_e?*OK=k ^F{&7KY=nk{"jO·Ny "93(M";1݈d/9|9u6'nnk;9b<\'6XC3yH6W<6.<@Ա 9̬uXߏܠ+&cZ e?a_{c5ծc9*GC~Ň\Le;!"] S _J_4]|+˾9a;+ DEa#BWev}lݙj=AӈvJX; sLT[X;j[&cNdptN3N}MfeKd9~5W2 @}ETt:ɒv";<+w}jO[5h ┏7Yzڽ\ o%r4s7o{\WʲWWϔ+vJ2S\ݏ4\5uI3Ð}-I?g,Zt l5i`îD&N΍B;Z=ˇPYR3s v6-%IXǿ]ªD8䞳jkUŠ} c7}Un:c+j{p'Ʃ9ASJyl{]22ĖI9О]uT._],i8WGO=YTFd*,/#^+6F^FGlLȼսm*SΏlݳ31t.DyFw7 ;y2?B7Ƞm|x^<*XOPf5s9{$ Y'4z->LcYԻvw&%wދna N&իz(]oV3k޺Kk7I-O[qrZiܑcqBͺAۖ?QS_OSmy"?u;YsWon)7V4hC,B3DN]M J?Jʻ>.Qh߫uP L=|9%tPRX&ޏXl=_>H`w\J-ZGyUwi-qv{e9n_ƲU&Y {_UD ,>Sb9}[ϗΧ&>ݱ'ߏ7xv:^Qw1Z|NYI ] IDATzb3Tw]Iǘ֌K}9r#P68YJle]A4}꺼PJc W{`Uj/ˠ:;Q5'' zŒ REu&ȿܝ]ά[U%m`1({wC3WDr龹Qb?,5gP=-%o~|ז{EMru|]: ~I~ᤳ~J~&)[\oDrOtaK?p[G'S,9;d ,nϪT+̺h y/zu1v~*iDr5iC6Ct0s㭍D;YT+g?zlC.!u ~Y_%+^/{kq+p9e e 6սہ>5(c:/c̩k]I/]jʎ)ؤt\Is>gZ1͔PJ}W&AҜ-,#zz.MltH`tԩO7^TO~P.\~ _=[5Vm,)>,'We1ye)IZMT'Q8n%vJ}$ۙxTYf];N\ au R)WQ~? c;-]Τ4u}5%M|6Ta kBlnC}ȢmgWi`a&w¤k>3+3Izglv:&w -^kˑs=_Ftl52Ucm/J+~Nff˕}l/3i_W*z69 ZN.K}_҈\9It 1XE|b7$hڳ0\N čPEϠA Mޭ;H%^8㹝,ȻiU}gF~pk(Hi@t*f.o> _\}ԩʾܻwkτl;P? q|n(uOwf[j5ţS~Ur',sl9e\S܃oGz2M]}_ܘKퟨLz{vSk0tgwYn]c IRN"olI)73֍-3V3)]G )|41 *Sc 6Q= 3<9쪟6/Q?eK݃,lC`^hw<xc= @RҳB+O$͜Պ\K}W{3T:QSDM;vPS8w1̺eXզy,G1mbWYS?n ]X߯mPxqW:NfLC#=K tJnu(DnM$7PA>)47 %pR˧h݉G݃_?X}˷\*X4C̞6 -!dǢDuyKYF3 nVY9uruʥ56O.5yjRu/qSW.,{E]u]'gƧ Le]ÛZ,&λ_ͣ 9p gg.8q]Vo%o"Npl:[?'W7.j= [CqpLz&"=9B]fϸ? g a:纙 vjj;ϓ{|%q#ז fMD?Fg܃3D`a<2nMƁ1R>0[RnGRL7<#wtۓQ\ϟ=Lj@?*SL Q=29zRO1&ыCc~#F@qxHZ!i` MI|,I Rbr'(=ghz.'!osrzgn %2R3MJꯁ{"fpk;wxI+}kq.@ܝ:;cM5nV_ n;KOh?O!q _vټϦ>0xR_O`{/LYIdAm:.׍>[]D0 mCvG1i'H,`|~tOwmr1W!ƨ"I^w]Dz:怡Bߣ^Jj{-.nZAWժ?U>sf:g.S]5 j˵}=n: ]JwYY;aIs+T^Bx&[º٤kj~ܵq#=׬.Zn} q5u_C{$ T7>8&=d3X$D|P$wm`U'F҃8/:ÙD#s ߓȈ*H/>Pu^gh6A̮eĽ6HsDKiYbɝ]Ng7-Ww ^ hnAq.U [bf:KH;enCY, Vi9Ϧ^m$+u}*'1}y$yeyM:ʴkRb&ι#ʀ[HbMs$Iًƽ\B4ݚs3~ƽn8/&P 1qǾ3n:MW}q6e5*R?[bQ?pe%D^^_+2rʑ=Kzֵ5u{bDd~5k#krRulWu2:W7 !=k&0Y?X}q_z܇̸ױtz=zS|zbr6 xЩ[ #}uР̓ʀ- up&'&;!n5[RK̀Lq G"C5joOO`2'xh3.95j+:71`r2 %z4ԝTzި xbŦnG(mkuN^LXGS, z6v62s3<(hV]ϯ$}^"KVyv4lK%26 ޖ6F-XPRq-s!ND\oG l٫[Ц1׈]_Fu qޛ$iΠ8*;\Gd#so2ѨK]-oe.\LdIgwme^qȀXJd5XӦkc3O'j t8o2QwqÛj}#ҤvKd/17DJpl#5])uos; >'k}x}>iM&8uAbx BL'Դ3Nqf,\ Y}U빸aSӮ^ʧBݜr{>CjKt? EK.K~1a/א&8ک?C$I}Aڤ͝F,R:~%ۉMČW1Uz6S}Xt;uK%qcAbǯS[ASmNMO;fRjw ɯQ.^CzRz3w%fk}\FǓ'js2ҁ'3dZ\ʲR4ˠpU .%u39ݓ]b=[dYRх98~75hSN{`}MJsI5u2I$峙{ BTcKH%y%RpV~ ߬Yv/t'1xtENy@FwYK2QBYlL2sDSQfLz}iK̺:K,5{]^ ڿsV[F% u,~Y,U2*BudjI/"_UwCMu˙!Yh,MDW7*p[?~T5rQ⦭J]σ2q=/d,[63KwKe}&e@ ^Ar5.5VRzXN̎WTw-ۿmJrT $S[NѩT_3XGUu+m̌uAt !&XfU\W1deϤZ[v֟anTB=ʚl*(un: 􉅟WUڷSO%U^EvK:'b ǎh4\Gg0oGOO0w,.pךlGFtḵuX|٘U榻ŤXRXr+[JXROE Qmp_雨?.S,wN?fX89¾oۨ:ˆ(<:s;RKHg~~Q73QEePf`_\fl 1XVf(vpeͺ΢ٸkrgS\,e=kk꽰^7xX6^ՔgeQOdlg]~I:z@6;ۆ}:ϊ|\b=O\*bNh1yCU[g׺$oB$ڗL㾉3w<~_.RIR.Ci!q@Ơ-v@k-TJn:` q6&↩n}Jta"p a3'=r}0w*7#*l˞w;2pw0,| 3@b7Yfyw&S(Yu_.WVrXҋ,UԹ7Llc;1qD V_USHdx}b<8b;;?pL"0gf]@u\ϫ{tTzd: J;kg1hPzR{q©r>16`1hs$qW=l[d ⳰QwNT>qvqZܹL+@;{v$]'^Ͽ'ơw cߙ֖=\)+#O$Oxȸ)QD2Kﰒ@: Sn&>h /ˈ/9 4BMY?%w<M1)7Bx|I%DvKEBl~r%pmAta+v굜GzwR7؏M:zqkZ.$=Y5# KYV݀Iw噭Mҳ>qEެu,3M IDAT`uݞP~p973D,Ս:L,LvKg|eKS3KʝgJtp91/ܗbuH}tnx .>k@J 햕+atAIR1-D&piz 8^`[krBq޾Y\* ~/ftvKwӮ;{ا ˩ kVnDv[/K#ED6V/67uu9 O'(nzn%2Q_ǑS,;fpryf;%b-?'T9@v̟Hf91)ɧilt5m?ۈls?Ew|l[uەch5sך\]N{8qlωgmҫk9sA;L6\bÈ"w*Yg#l#eo ΋?I,ޮk }ԹN&PnF,<0uf8v1/MdM^!ί΅ofpm&3qM,gE FL^Q"hJ"Q̝řZήj3t܌mt\fY.~_ne_4ιsxYMtݙ8xVʵ{QgAMA a cHRMz&u0`:pQ<bKGD̿ pHYs UktIME JnIDATx[[lTE+KbFbkK#^!LH4_$1؄HR&h@%[6ɂxcgCPˑV'Ns7#Lۂ.3Os{|0Y-d< YkIӜC ᆭJ:! 1tV ӏLs$ wILĕrұUj^+IBpB-ٽ)$}?y{r{ʱ++%6"  P[R^eLMl.5 lY݉5"{?C(x<}6?Tit{]^1Cf8LB|#3iSJ]Vv$8G_bw0R[%;5 ڷXiG@mH{ P&¹Jk@: H5P @D_vO ;Lt. NqҭeO;"1mě,z[$U"2o?,Op X[~~@. @_G\\ Zts tڅ };TzTjY}2blo% u. hg,m 0 BۢK u. ¥yiݢgBHb%C Bz% utG ޅQYd`B™%tEXtdate:create2020-07-24T11:10:15+00:00Y*%tEXtdate:modify2020-07-24T11:10:15+00:00(wAOtEXtSoftwarewww.inkscape.org<IENDB`././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/desktop/images/16x16.png0000644000175000017500000000070300000000000016213 0ustar00nilsnilsPNG  IHDR:gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs00FytIME wz8IDATe=jBa+"pMs]B}h*Bpvj8yI~ʺHTؕ`+E}x93dIz*$$22RJ{K9uFxkei'LIr?h6U%tEXtdate:create2020-07-24T11:10:14+00:00]G%tEXtdate:modify2020-07-24T11:10:14+00:00JtEXtSoftwarewww.inkscape.org<IENDB`././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/desktop/images/256x256.png0000644000175000017500000000763200000000000016377 0ustar00nilsnilsPNG  IHDRygAMA a cHRMz&u0`:pQ<bKGD̿ pHYspto:tIME G[hIDATx]kpU^7"`HyQ_Z:Sm>FVuD3h5ֶP)b"VC^" yxszsN.{^k>{0"|?j)I֢k _":6 $jDȌ&  !:H(E@u S#)- j,L! YȒ*`!K8[PX$@I@Nc ?  [x>\|y@gQK.DA2?76h+ n9͈+@RQVi^3j{#7Y5:׳1_ r 39ҷĜ8ZMܬVW=1ohqUٛ6~w"p0\`#Qf'p4Fk8O1†1לGaq g-;٥NK`IMWrOB&up 1 `}<0J H,II7ٌMM"St(S"!`==z O(r*q.4T)O½T&c8&9n'L5^ͧІp#.TAAPK)0Ɓs[|}=!0yw}^q+WF!S|d=&YcG;8\=)Ս $bX\()݃yFw)/yCrxE_XÊx!s5^V2 > (=ùluSsB䞯 G``Xfdz YRF^XD Gt5k~ S-f 6~Z|*Zf NrCA }W%/ZwSV2 oh[(a- E7%e[w8{b"v+@f0G#H-yj D7ZwҝͨZ/cP;iy3TqQ_Ԓi2鞘OyдQ4,%i +3ez]$y ުe )^,{y9t,=e;8rҮR SLeisxzIPu7{>Ti'y O=Tj: |`dkL@qZOpajl(J[dS}o=>|hfm5" oM#&ze?S>lWBo.(dF4zo(J z# [6qa15` rqj 7TpsVz42 9FG'|~ݍ s{ 6u:9`#zPvԀYFpPkJr?2I/h9-W.McRG+m$twgtgpJE,p2" .T9bC`_(4 ]c& P2'aeo 'aek$Y${{үԸU*ߝ1/823jdMo~?tZ^> s=X= `)M 48 A>zS͇3 `ICFM_7-IpG )E,^d# 1@?7O289@2(@f`?T0FRŷ7G^)ԗ97Y\k vCϸeujU1ً6 Ƴ<P0'ys$jmelg%A[ j;0bgNa PNIEΪ\u7\+;k2Y.;oY wԛ SMj9gS1#ذlqTrwm=pCT2D1fxpT0=ַxU@ڸţT/vXs2`TG?Գ>Sگ;G?7slO7}cy]ĹdڕdC}ƽ8tV)S6*tX-cLas Ϸp{cuT. ,$c,ok7H-,oQVq΍&p^1Dde^Drws`) xZ$[/kt1.l:'3نc{),Sf/u1Ƭ`A SigTmq;$>ҽ7Ƙѱ'9?8g$^Ƅ Vx gCFT-N:k1}퓏;ڛBs`z'=KZnV/lHoݮV]yM@d'^RFX2ID{s9pʚJE= [lBx;W`j,id&)@bS'jP_ؓ0o._k'6;XsiU՞(aţx|)% k:EWxNV3QaWz4OV^KF D3 O} M W+ )Wg|ᙗ޽W5Q1^; ϰP#J]@Ԍ@IȌo [&&@PD% 3Dlh+vQ3>%@?b u'@l+@&0jgE[I^ YQWWOdG?1# nXC?ڻe%y]@Ԍρv NBC1B# .P. jDWI^{5s$%])'@lrXCGM`Ԍύr5 UM꥘ 7&z)VP.,j$ F4mWL5b `T fh)V. j$ h*k&@\LQf@QsPT[n@xBO! @ NYYo=E7I <^'ѝԆRL{ _'릠vhaO"*~ =_)ލ௾.*z"U!Wҕ|ʯ<ɃGO+zmwe!Vzѳ:(nCk@[z Z p3Zt歂PgbΝꌚԚ4'%SW-Gϩ#ZJzM`X=v5hA/:$a7\.=NhK-jܓ==h0ciu$ %//^X I3æ)v!iWf?DO-%yzw?VJJZ'>PcW>,w %i5CԱ''%e ܱwQ}ğ54fOWH楠wBO$:MRTx)%@(~:G>I$!V~zCէF>Um\9C砧F6ph [r2ǎ_T)mWG oP(5 ~_S%"%P(}W)6r绍?:vd݉Eӡ$o/y=]RRWIV|,z¤7]/+ƱJ* 0FVvm>PևM B3OK^}* 7ٓbJ h$}I^M`'s!| .}BݦX̹TcOi%E]f|FsQ]$ewwh,bJ {0i,Gn6b~̧ `) G>kE!5qG4-WҢM>W*%-UTL]17) G#H&n NUdnak+@N%yLSL @U3QVLPI^pZ/`bJ@]UQ)9_ &b+:br̋ zyjC |$'& K&PO3~N-*$$/iJyYv,Pk%ܙļ)Z-JC\B̛ɯ \xf܉Z߿Zesv\H,h9ޙW]P&s]%@I3II_SF%p'}Gel<ӃHS,t6l( H3Q HS e#0Y~r5*p RJR (VDȣR <2@a4@!&8.h $,.S}7#+E}Zv-e$oU~ 3~i1J%~-@E3~& i5QD䥶*v"P~DZWՌ:'4Nܨ2L.*2LyW:dWGƧ2MfZ 96uD䅚@5/$/zo=ocSP"$[a705[$H(rxX\w< A mCyTuV;͈v@}NhާWߝmA[uz%<+W~>OogݾOyw #CP##hD+߫9+S %y&OD7hdDƚ%HD @Dt}jmT:ޢqA:}+ޮ4I н&%$"M)[F3ޞ?^˯J@$[F3ޞ\ho\ M}gG?=AS<^h M\ q;JFÜg1is'iԌ2E'1K£$ϵ j[z(9RgJĴDDgN 1x|^ t}?Aʘ 1@""zzi~jzbV j[3rSW¢$59??>)s&E;Ön6l9>OHV @iB0< MMHfx@+@71c'dO8 xkv݁=L, 6+\蹾qa{(M`Zwhfpp^hГq"H`HAL1   Osf5=_撤w[@(ִ;zkp$@BW"Ogϣ 9 = ѭ 6 [wӷ”0wFAY^q@]x.):0 K@Pïp vC{Hpx}+@=C884 M`p񅄿c$ H+@._ Lic#i}; 1<&cH84 XaT_ xn@- tb(p<# 80mdO |wLA/1_ TyHbM:~ WBLL ( | Hf|$ Hf|$q8` 7aX8U]0QJwO <"+@>+@Iӌc<=`=)+@8#b8p߈6;r Z8M+@H8G#&@~H  h 8Gyp_R5W%y]@b5͎_tNk< aXX xf|`8*;f|`*;'H#R PMk_= yqHt̿R}~ڋPGrL=hs9k|=L3lQk=KGN $6PYi֡ hk=Igz_\&iZ^ļ^b`ʫ3ղhzr6[+\>^8 K_muf=Eg~nn,{\Yi: NO#^8=U'>R<klD>_-%=S_2q1xm5;5cГUpj0@}͘QW=qK\ Lq_RYnqqX>N|“>ma pJі3i;M0gRGjLRԑ0fNm)jßJR[swxmNM`Bz|f<˅] hcpK$b8`O]Ck6ؗM7Y;*Ը~ pHh;sr:RTf夿NrHfb"RwMNp̭%tUehc|KΓ۷T]'-J bHZeh#"9kaߑVb>^ŸJRG;|LߪT*9mnlo~;a&?,8܈χZG>B ڠ}ʵDCyJųwCtl}hߙ:ߛmX'n^ *J1FqeX3,n>1m]XLY+['@H=}_3zUn 5"WL^dzD4sh6>ɭg/|1g'Xzz'e淤0Pb&@ iyB+׈입^8"vS{ssNEρȺ MZ a6uULYF6l iՑYus?[Gn6k}꧃=4ծLl;{׌PrSQKkGݐ> lgn_`e6SV.6dPWQB*E/&$K P:'43jcp$F3~2玟-#,_6JT-F,\vJv  SLY8d*Jv }_3fmU R 0mmuP9q<@u 8/\e (Y45~S2 f$o ӌ/*#=٬wwJI*Z8g({&Nv@g=T!S6q [f̨Ց U8X>#qϏ߭+7SM1L|zFW*WYBzJK"ȔEJ5sJMU~ 'ϫ=d"RT|fCc$EүIj|#vi-viZjn$MbM̧a~Uવ!Rۙ"dZDWð9Ke4 $RB8 M/ X0OijJ+h,5.`s^:ƿI+0RkI]J*j_Qq̃N-f)hA*?. i8?w1BU4r qw̬W+~F4ϕ|$EĀ>s&@ra{ x'C ??HKܚCа2e/&0U7աܴV0nw6xMgآ16L|a( e9jJ>r'A`1iJ6s|FLgy'CU$'4SV:KFp$z}") |w{72.`]Kyngb1pi,ɓCЌY!Sf13Lo2O8R %@qJ/CXr *A'Cbf 8ZfsɰP7UfCw w6}x2 0S7K`zM(*yaV1'ًV5kTN),&4 @7<v%tEXtdate:create2020-07-24T11:10:16+00:00hn%tEXtdate:modify2020-07-24T11:10:16+00:00[tEXtSoftwarewww.inkscape.org<IENDB`././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/desktop/images/64x64.png0000644000175000017500000000202300000000000016216 0ustar00nilsnilsPNG  IHDR@@.gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs1StIME JIDATXíVMHTQ>3 /D"4c-jq!"E6*\-$j&0#RqL2t”#fsϹdf;%  0 k V<, (wT@jCu!|Sn&pNP+#T.&N-+з``:knH<8¹MZR9ܪIDJlZ_+xe 0@Y~Dk#x,*nHX[Z(@y` 5Z\#:E`v5_]LN) ěf EFz^823*iLO@)VIL$?LO}ZsP`&[٦ei uʡ@.an%^ٽw/:zm(k܆ [A͂q:) -hp J![`ăGXXʀU̻/0$`]J޽<5,(> Lg\ 4{lcJ2Tbo}k+pLx"~oOH`q\t4x3xMq?Ek \h仫9)fX9f&SV9J񲞰҆7P{f|c@hz4ֈ/pKȶywm_=1aG,o%G[aEO%tEXtdate:create2020-07-24T11:10:15+00:00Y*%tEXtdate:modify2020-07-24T11:10:15+00:00(wAOtEXtSoftwarewww.inkscape.org<IENDB`././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/desktop/images/aboutlogo.png0000644000175000017500000001405000000000000017421 0ustar00nilsnilsPNG  IHDR,;~!gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs'tIME ~86IDATxiŹYAA`(qKFD q\h4?.Qp {5*q@\nT,0034}Μ9ܛzΗS[]U?odeϓ`%,`e+,K$XaY"  VXH²D%,`e+,K$XaY"  VXH²D%,`e+,K$XaY"  VXH²D%,`e+,K$XaY"  VXH²D%,`e+,K$XaY"  VXH²D%,`e+,K$XaY"  VXH²D%,`e+,K$XaY"  VXH²D%,$}9䱤Jgq,9v4bV1̡:O3FK)g3 ( b2,4$J6`k|/qјbB~ im薇jJ\K6M4`,ґ[(FnpZMyف[PZE9Yħ|Ȏ=Putbb==za[i_g3! la `q+ X{!II k74W鳑wy|yec4'Ʒq/ݝ2`YGz<:J^ u{ᑫ+A PIR׻^Kkmr~ L`?SkRU;u2SŞ;_˓<ʈ[*SB#' ۪NCL\Xcاq&УzQ[m8LzIˍ:=U)UzFq{jB_k'$Sasws7y E'=->{#p Zblaq:C&qP?y86ҙ݋_G5CByc|2Iywܼ})d)S*p! ,}N7ք: -98jXd8Uf,fz`SsBV/=GJ"bS@'Qc.~+)Gg=g!rhh|ӸQ9 ҺF8J^X{ IOXaU)W-ÌX|ZrX~J.:|`|f X4C',R?5xjrp^F?gV;SrhRIs.#5iVu-ܑw-W&u_s>3N//wN;7~'ݜ2. 7&]Hf_|:g:-㢤>-DBf^ìPВ_NN]*[;в*lcvudk?nrO_~_\ލouQF3hD'9j.BObInx|º볮tгiE3Eaĸ'PrhJw}x6*.ò\Oa߆ IoқFvT2G ez6ϫDSo,<>8d$dY>oSXcqKuw$ROGV .SLɵB?u0J,{q-s6)tqz 4]O:W%ci!xy٢|dbLq|.6 r¶uLzJ/=:T.y VyIbWߤ3#ߙ  (Ƈܯ/n-GNfь ,VDZ-blP7*ĨV0*3=}Q^jTr%i Mi6 3򃸤|Jwv tOPKz+Y%( O{24R$&yg4"Sa]T70"YLНbrOΔèo`wdI]Xu5<Q*VI:T;0L =UR+&!.=Z Ύ+aDddž``_f'^&mtb[iP?U+OYZBwJ:GjnXiPbQZH;\mV/[5 :UU4L0In$'yhw(q$NEҋ;U)]<|E**P~"[BR:0Os4^\#iv⟚Xs?}!V!J?9b"tW !t_ }]Φکm7Wb\VP;Is]͕X$=B$9j M;txHz](ShzE І|?3/p?oKY` ӓxԾ媒EX•4Ur1,\:Їsw>Wyv\ DkǷsKĹkJ_+/q{l.5r=NԄeJaWql?28?Kb* -Nl~N[&HwfJ-#5Iq9fKp;p!Lt)ԗ:Dz~z#oW2捤Ls޶Fk^AzFKH: =4_FJw_s0t.z~x_>>KJ@>D:2vd/SL蝇 َar.mO?׸gwzǩ{3G6 4v&n3noK_pĮ t_cII_\ҵ{^H9|Fyvw_lvsP"=txCOkgj5S5yb 8dZmDoR7 }Tۡ}(BJGuGFJ8NZEim:)4ӋE3>ަx&v\VAmyJXH;JK2'ImPMCG=k|GKwwڤ' j5L1i? :*D Rә,!.[of?mǀ:`4}\'Ma$=tx)9jwV<ۻ,Ṥ}YviMS?܎l qipK>7+>C[vP:^I1? KOs4y6cHџoHokieՎ$nʏ6|\nG*ߨH'KNix5y>YAmI G(Jfj n&9`.}gmv HS\۵њ}ixm| 5g[G49}Wkm>CNdtV3VGg}F{Š.[!:7غ5 8 x&gs^t6' iщ#\^p_um9/ɸ-l?uq}M{i5X({T= V H *dNXJWeK]y{ϼͮy){t ! H|/Is$篌%m&=ڸϿwvQĺ4꼄 #߮7LQ]LiÙ?O=D;I0IE]Gy)]QJZyDR"Xf3) }Q`GGt҈,W7㴖p״HώztZR2Dw3ݚIj Vu-m>HEŚ׀ư 844:Reؖx/FHIwO҉H~Lz X6Y x,NPԢ=>~NoqۥIw]L*FYFLB]I#W'] %Zk\+_Tw&4Ţ{=VǢ`8B;xXtX{FߴU[vm6H L؏ I xS}^ :iյ~-&%uC&ix3_-c]RYlATmJ"` (½UlG-%݇~mkqHnKH~' }/9w=LmWnƵHߣa>Sxd(ĺ1*Ҕ>_[EXztc)<>7ݯu2G҈ۊ/,큳@g?χM y4:ۑ|݇Z}> aAp1u~g ) i\t#i.[߭S/iR=ŴWn/47FC\}29}(|yM*k}D=Y=/+@e=}HJv"Mh"46qb}**mؘfԶxnK˧$iz*ІԬZ\"_HSˣ4An͵\֧Q9p=GblVޤJ[Bt)}.lFě>6 T_ܹ= I7\?7Pv6o_O#%Jə p{&i~N/sR]~Mel'$zkHz k$L}H@KOx\O3h:KIo?uQ/tX\JOen"Y熌wBL:v%QEzFPUO L7mQ$ QOOAJH0E|TϬ)اZ 8H37r{N V(i1)VWn\Gz/%uR-oVms6W=^O=JF>DfiJSxzWRvk_F6SK $(?ElF( Yܣ>NMom'>9WfmmҪaTﱴfԞTgb-߶%Ufug믕zkYIs+^[yvha7 q%stho{yWd}ؖ{*SIm% (=vv@_H* BÙYؚ7d|L((ҳڜq=e4E-omG|բ}9׫H7v+yW!׫Zph1c[ -wn ޒSL-ʬ=_+XJJPۙcLq5mm/5&OJҙgmKUaK@<_i[a{҂g| 8>{kęS?J<_emBZ 2 hw=rRfߐT -Ք:ΤK~J ib D{[nmH*i3ڟIۯͿF6u.KP>:<7^DYDZ|eQ` *ሪn|BRA(kQnU%)ʀ֨}H eр}NIGغ~./ˀoF1i!GK7u"^G치<5&KtR6_,!-P7OtG=}=`N0-v) DVߋ;j Bu.)ϙAdZ2"۶-zzTmMTmÚ[wsWB*E !=|^OLC9'>>;E}!]YN7#!wB5Y(ӶH1uF=#3Io9mǓgz[kiB`%A5I 6,oi5S9XSN q:Sxr?<ۂ43‘A͕Ŀ\xCp j 8ǑTd}/VbHӤ#|5h&vWYVpFA:xM8q 2l#[Hu#H5ܔH;pSvUtԲkI'-i73+qdCI\B< ~ =K}P^Sj4YJHQ p7қD2} ȕL2~ '86Ok`]s)p*{` GkQs"%ז7>^VNEhk;\Dڷm9T.z@sHGi.6_;^L*N_F0:9]9+À[2ıT-ݏ3]63(zDͨ%#!7^t|H ?;<'pߵ_롎QpO `Ns[yu$8?h-( %=t4"Ei 75,Mpjyvy5"&%k}q nX` t &=8p]I]X6nl7F@wi*N"_'a3Ի`EjHZ<~K 0mIW &?=p:4`848ebNY;:55( с_i߁^>6x'_ aG|~gLGcn %A` א;DZ1HxJk(0;H/qǣoUmHu>Am+>-98{8#p]idU.ܐ\Go):q{et#wD!j(ҋHߥב'P#FHbBy8N~hXJuQFK IDAT;]lj}{^*bQv uJyCpNqʽIS5G0+]SCR|*T)ͭE!6y1YL|):5W[@a"R_+l'm CtȺ$:WÔ~V|1p :d&:+:/p*n҈A7M}8x8:@yl+c'QW?n'&xW4`^A=ZF'Ր(E5'HվK ,&l&4py w&YCn)G'. ܓڟ]JOZ57+:R9TK,:1D6w+Xg/x]C)vVB͙L;E,6ĢMm`x_n(5H7 41Z 9tJx+~:b1/#z!~Quu|o@m9Eںa1%q&4H fl楤p [nu{itSi]RXW0TMV%F_ }&ٰ#MkT zp`N3i_H]_>:S,M,^{s%t"EY]IpSGAr\t#X 2GjCjf2#NOɑp'[3ߝ'& u ﳆ萈QS-#3Tgf[ψt-86:WCn=zLq^\긗Z?Li&>nѿlӠ/t:Yif}}0CW, ~ԲE>]&~:< A8p^gG~&w'uԨZ?Li&4p_?0`5̔lnʣ?kY |N Ęah `9ˁCkv֥z]p_L3FuJR$6̠/@4cJ1 pSftY3DW}(%^AN)׀fWW]Rõyvet!pnT58߹Djjx!ᆮ$5E8[[tԱ2縨adjtT2]S.^kփ/ڳ9HQW N>OK~[^MokA!XհR0j)dۅ6ITN ~E܍Z]LRpzΡBS_T ◤'R ^H8>* G%-6ִnj+j8V]'Y5u^gCJ؁T~_`qiЫ] Ib"Gk=tz_õt^՞Qv'/07O6QQL }=p5ݬaTWWpS^0jx+^˃A[Ϝl]u\XcԐ;L,k=tz_ԌSP5ը%h>ܮQJ9Z5H#* .~0ϴa$mF\jSCRz5ܔ{C^ ?dLpxI5\F]-ǸZ>׵ġ!u_MM? Z5a_8V\4ig "VCBR#non$>,2n ^Peu"F.$^^Oz0}dN 竖Ѓ_Srtc:~sijC75zΡz"nc%iA_2xbF[jl3Y`tiqC|NHz`췎]jAX&Sùu$֋`LzqNAU:AdrWt?!pT}+QT^satjvþWټzT5 sf%7^Ӂ2n@7yރl^\4uMQ(*éZ t.s :Ֆ\v<~s;i*G V^q{fְ[(A`xuLK|p]p_R{{TQ*_c]Aj5L 'Zi$:#."%J"̩;Ib}4K2m7u;t-UqS~]LڑY{i8Jv Khqik Ds0:lդ^$j838e`NQn-ނ ]Cܑ3J2;MfV 66hq綸6C.ݕvOjvѣT5鸨Z-yRB7Ǡ L ;"A>4jva?~:UM16vL(9 k[gN+캖ی4j "-|'ҔI`_Ojfeqp2pep]͎ڵ~lA a9iH E/aCSIJ`w` ۹:Uⶏ| 7H#,{6n6&~ oȡ瀇{{l[N a V{ѣT=8uF:S,2jeNҽ9NHvQWG=p_C\zjwP1vh3wNRV:n^FtKj֝Z">t<:wpFp @ZCсLݼeџ_s&ʸtK6xX\AL2 ˇUׁk OϰgFS/I+=Y<<*o.}?4K[t[iVg*er R ;^vi0݀5'4F 8u˯ &Me~u7un(Br ӏEF:!6zq׻SmYI{7:+x5|"+pXtO- `. % :(.`SH#5\J]]K| 86:iN>I|MAt1s 22s /YHА6v!}V#H~(Y97+Iײ=X oq(h{kHj'׺3`":RyF҇{ȹ4 R} co[x=Ǿk퟇:wu\xY~44Tgkҝgw" [<@VTnӉfT{\lܟ ] x>ppc2VBf t0u<g zyt4g4IKGڐk6F>4|1p5htx.rm53~Hq6b'KG' ?x+޵fHf%L#ZD7LZ]Kn 2lBZj=J6F ~twyB{6BZ翔%fj^ms⮡? w sgGrڌ=ڝT?§;]mx)iNBS5_!$ơVKL{wүӁG1HoDkׅQM9-i>Q;P2Jk׳|jqCiapPk$ [v?m؍ %'G[.!MݍtobPZ}/ՂolԿpĨ'6 -o<^MBGЬ6Y+u&ѮE52s_Ë& F sо'*a$1~GY}32Jz_=T.!AUL/?sb'bz!%[m7Eڍz#+ ?bq& 'p]̣&;gHCo }#pKM;]?\B]#rf %k={%zdn|՜xT8u :9Ԟmb!iVVJ*?]L@w%~d_BQ ǓF Dz!qS=g)]1 _EsS!FĬt!-+}. M1֔ZA =d!q#>ITӰbUۗT."处ŷ}Hmxpۼ(<6o'ݘ埃XJJԐx C7ehp;ж\S&>B ;^ a7ly5ϿʻbL^i;w0ڋ Sۉ5unob^H|BTRfkwަNe4#r'j;' yJ^H*%Po+(م4W^m1p6!g~:fBG-e&uvt{]={vD{]qORQ<Qgj+i }Uc=)s܏*l`}}H2KK臯 y{}鵋(3jiP'Io/ҳ-߶lnfθ$֢܋ڋZQ'Sfd?"z_֮ *No1>ǢQ6y/S[ӄ"vdJם,5->\c-iGQgRn"gZ L}ن ?VbU7(1Gk'gpS~A, >ԇ~y&}#F Usb<%7ω?;iېVX|YH]PW>4;g9[_S.!(M}T4 q7m7gO+O]F+~Nm;p|c3ĝ OxXpi01agc-*_a E~-ҾDCX-7佖A+z ` 6і )"_UI+=op]؛\EkaqM/(ܟہ}[L"V\Odjcttf bl!m…/Aڭ=9 co{^;i>Sţ2IBƟOզϑ-k1S;žř#w{zۀ2m&gU^ۥ>F%Y/h/sٶAE]j!p\ hЉNEH^ W]ܯD$y5_sZW[dvC1\8a7/ۉ&6#~z/hf؏H+EU[Tw([T 0~N˙ܣMmǭ(_ яUgk{ݻ_N 7O cI.п7蛤`M Gܫ~QAmVD9 qXE.ߗ+3!׃!%S/۴Hs gj T_gܯ, MmGe*FVЯ*ּS>|v^hzn^F;6OlWt HȾX ~}#K%_ôo?fڕ+K~lْ=\ IDAT\:hCbOHsڄ4ՠd?܇I*ԷEi3cX?SWЧ ikzUnic?&{jYvj[oXA?zfėngJAmut_07VKntS%'0(c)3äF-ܿ\m%a|~ sois+H3'I{UΥ;t84 p)Rb$l"eMN0jHMIz<1G1}4m<1JAeٴ]BUrXcӴd/zJ_tſP«H4*Iv]YЦSŕtjpLHTήQϥSA: tlhGSva7ie3Ttߒ)v[W}E'6Їv!~>5_4Spm,m3zԴպ*ďฃ2Uvnci KtjoV,p[$& 0ޔ)CU{mɘw Qk+Ia#[]j_->hImSͩ!ItQj0+/I[KˇmjO {1iŹmNZllq*'VmU{F) 8jr:Q;_.vȟf(ڳ4FE:TO-y>;/6"VP+IK1v p)ʙMaU"Sp ՚9:IQj֙u37J"%IJ eoBW(~ }kZUQ/0/ukI/~糼3 fs%JݤED5p?|Q9&Vy$Uy0 * ( ."DfqQ-jbQ}5*.15 HMdUff?ۿ[oթSUwy3wSu`H-n;+k5P{Ƕ0kkfǸՀ!bPIh!GL*0n"~嶴"ĬmE3&uĬw\`-wenDg76~r[4 -X-iఖ=Le U7 @0Wi=8wgg`  36n L<|Z1<~o.#&x)v~^9mߠ^L:嶌 bxT -X_3@-SFQ9vfMs0k_60:oq>O|;w'\nsb.bWO6MvG0]3a6NdL8̀y y-og}@}"X3Π pjFu ˁ2ڮö3pQb\p֛atlK>b ہÁcoښD&D KpG/oP,A !CrNg6is=R? 0n_X7.28%>aO܏I]4W+H"fӁZs>.3'{Zܿa䋔a >FA z؇qqy81ԗm7@#1u{- mx!91v=Ȱzf/͖}O$LW[_&;A']UF"D682ד~~Ey.i}s DdPe--sĵOF >[F|/;앷LmaZ+x1&ܰ!%Dۉ'ۈHgl]εuҬ:sWݕ.s owϖeD݉1 a;rڝt̖݈ٟprWFIt;3 ۉ:\S6הuFfʯtݤ$=vۮ];5DX>I~n;m ^ת ^kf)E_29y;: EeUN D`N8y: ``8a(|uzuumGi79-| }Dlqۣr5e =^g=Ntq}=g8)2]d|a]>Arl'niDv-'{3?08_: D0Z"@p1Py;,'&XOW{7K{q:_;Uu;5j"v5pQZ*q^Jj!F"X硴x}`ۋ?|%s;wV2Yb_A|_A|79$iQ 䕛1اQq'K$I}GFh<`vZ/ .]|%k8p$I(oi3ƈ7y ۞D {`YǍ$ITt@KR07k&ӉA?e$Ib+1[v% X)KQ>R7m-$Ip8LZ8|J2pm pѢ{O1 -f?+$IݳYҔ17n m-; 7-,8I$-Vϵ]ICfn-EA&^qm4?$IZ6gO]I#`n@ O- 8I$-FwַݯjaF`y P@ Kww,F`硷h2yxv#Ih\ǺdCxog/oo~v#4 M7(s %Ms&!DrL݉ ]tAI$ Ƨ{Zb3!ڡ|3?Mpk%p`ۍ3">//*_Gۜ pI 3{bu=["I$uRdC2Zڠf>\v#49 Mm7bJ<8o/#f|pXl[f$IzxFu󻟂qm7Bi'UҚwŶ1BG":Y$Id^/9N`;r}Zh|H긱h^- \q*>ǭ1t;H$iޱ}(K$-8x)ۡ enzM{wCXduRLc .d܏J$izcfnۺ~'4N6h^8SӀ׶܎qw1m7;>Ijv#I!;:37%?`Pwj)n앪8أ톌lݐ 8 8ؽH$itk ! /ڳ2wMpح.4S $IX}rV-~<}m6YĸkEক8 m6ݐo$In |юk2Lmxg?6%17}14XKݐ7݀Ed;kpݐ=lҺSPM72uc<^z>K?Pez $Id1N|&pߑؙA!I$5"x^NIq0Fh U$I>_nQ" N37= h<$I4I~zW2FIZ| M'`(xI$iҼ4s_'M _?He9H$IAV)#Iqg,_;!I$M}xpD$M=h?0cY<0$Idu-E4ae$I$Iٜa:80FəP%I$Ijt0% sn2'I$InR6 }OMJI$IEd p-e&xDzjrK8`$I$IZD̸Gf_6{)I$IҢpx.p/໙u zH;(I$IR&p@B[7'naw&xiS$I$I@|_#f `]7O<w-nG+1uL΃Ð݀fbn=ˈ@mS=6Оa5ql#r5[ng8&.vi`obvpqiI\."l5p !>̶bˈkb XI|6IBKnv6pXۍqO" w!ncR\KJ>:]F TxA^Cd ;_!nv"oMm!G=/^D-Vٵ)O&^׻6\ 7qݻ"s7w0r5[״}?܅(s3;>d <xpfv11fW/œN|J,*/mC*x>gc)P/qUXnSĘ?LHKE;Vn ޱS:׻uk%?kSs^,s:ߵmc_Wmt[F{w=_㣩=t!>y?*`gb_C^ot[ϫ]gYE_!ozXCד/y aU۸8V-[) ~u[9NP~O<|‡fYhl s?$`9q!n{@6˵%čCܧI)U7_Z\!jͲLW6_,/h_e?*OK=k ^F{&7KY=nk{"jO·Ny "93(M";1݈d/9|9u6'nnk;9b<\'6XC3yH6W<6.<@Ա 9̬uXߏܠ+&cZ e?a_{c5ծc9*GC~Ň\Le;!"] S _J_4]|+˾9a;+ DEa#BWev}lݙj=AӈvJX; sLT[X;j[&cNdptN3N}MfeKd9~5W2 @}ETt:ɒv";<+w}jO[5h ┏7Yzڽ\ o%r4s7o{\WʲWWϔ+vJ2S\ݏ4\5uI3Ð}-I?g,Zt l5i`îD&N΍B;Z=ˇPYR3s v6-%IXǿ]ªD8䞳jkUŠ} c7}Un:c+j{p'Ʃ9ASJyl{]22ĖI9О]uT._],i8WGO=YTFd*,/#^+6F^FGlLȼսm*SΏlݳ31t.DyFw7 ;y2?B7Ƞm|x^<*XOPf5s9{$ Y'4z->LcYԻvw&%wދna N&իz(]oV3k޺Kk7I-O[qrZiܑcqBͺAۖ?QS_OSmy"?u;YsWon)7V4hC,B3DN]M J?Jʻ>.Qh߫uP L=|9%tPRX&ޏXl=_>H`w\J-ZGyUwi-qv{e9n_ƲU&Y {_UD ,>Sb9}[ϗΧ&>ݱ'ߏ7xv:^Qw1Z|NYI ] IDATzb3Tw]Iǘ֌K}9r#P68YJle]A4}꺼PJc W{`Uj/ˠ:;Q5'' zŒ REu&ȿܝ]ά[U%m`1({wC3WDr龹Qb?,5gP=-%o~|ז{EMru|]: ~I~ᤳ~J~&)[\oDrOtaK?p[G'S,9;d ,nϪT+̺h y/zu1v~*iDr5iC6Ct0s㭍D;YT+g?zlC.!u ~Y_%+^/{kq+p9e e 6սہ>5(c:/c̩k]I/]jʎ)ؤt\Is>gZ1͔PJ}W&AҜ-,#zz.MltH`tԩO7^TO~P.\~ _=[5Vm,)>,'We1ye)IZMT'Q8n%vJ}$ۙxTYf];N\ au R)WQ~? c;-]Τ4u}5%M|6Ta kBlnC}ȢmgWi`a&w¤k>3+3Izglv:&w -^kˑs=_Ftl52Ucm/J+~Nff˕}l/3i_W*z69 ZN.K}_҈\9It 1XE|b7$hڳ0\N čPEϠA Mޭ;H%^8㹝,ȻiU}gF~pk(Hi@t*f.o> _\}ԩʾܻwkτl;P? q|n(uOwf[j5ţS~Ur',sl9e\S܃oGz2M]}_ܘKퟨLz{vSk0tgwYn]c IRN"olI)73֍-3V3)]G )|41 *Sc 6Q= 3<9쪟6/Q?eK݃,lC`^hw<xc= @RҳB+O$͜Պ\K}W{3T:QSDM;vPS8w1̺eXզy,G1mbWYS?n ]X߯mPxqW:NfLC#=K tJnu(DnM$7PA>)47 %pR˧h݉G݃_?X}˷\*X4C̞6 -!dǢDuyKYF3 nVY9uruʥ56O.5yjRu/qSW.,{E]u]'gƧ Le]ÛZ,&λ_ͣ 9p gg.8q]Vo%o"Npl:[?'W7.j= [CqpLz&"=9B]fϸ? g a:纙 vjj;ϓ{|%q#ז fMD?Fg܃3D`a<2nMƁ1R>0[RnGRL7<#wtۓQ\ϟ=Lj@?*SL Q=29zRO1&ыCc~#F@qxHZ!i` MI|,I Rbr'(=ghz.'!osrzgn %2R3MJꯁ{"fpk;wxI+}kq.@ܝ:;cM5nV_ n;KOh?O!q _vټϦ>0xR_O`{/LYIdAm:.׍>[]D0 mCvG1i'H,`|~tOwmr1W!ƨ"I^w]Dz:怡Bߣ^Jj{-.nZAWժ?U>sf:g.S]5 j˵}=n: ]JwYY;aIs+T^Bx&[º٤kj~ܵq#=׬.Zn} q5u_C{$ T7>8&=d3X$D|P$wm`U'F҃8/:ÙD#s ߓȈ*H/>Pu^gh6A̮eĽ6HsDKiYbɝ]Ng7-Ww ^ hnAq.U [bf:KH;enCY, Vi9Ϧ^m$+u}*'1}y$yeyM:ʴkRb&ι#ʀ[HbMs$Iًƽ\B4ݚs3~ƽn8/&P 1qǾ3n:MW}q6e5*R?[bQ?pe%D^^_+2rʑ=Kzֵ5u{bDd~5k#krRulWu2:W7 !=k&0Y?X}q_z܇̸ױtz=zS|zbr6 xЩ[ #}uР̓ʀ- up&'&;!n5[RK̀Lq G"C5joOO`2'xh3.95j+:71`r2 %z4ԝTzި xbŦnG(mkuN^LXGS, z6v62s3<(hV]ϯ$}^"KVyv4lK%26 ޖ6F-XPRq-s!ND\oG l٫[Ц1׈]_Fu qޛ$iΠ8*;\Gd#so2ѨK]-oe.\LdIgwme^qȀXJd5XӦkc3O'j t8o2QwqÛj}#ҤvKd/17DJpl#5])uos; >'k}x}>iM&8uAbx BL'Դ3Nqf,\ Y}U빸aSӮ^ʧBݜr{>CjKt? EK.K~1a/א&8ک?C$I}Aڤ͝F,R:~%ۉMČW1Uz6S}Xt;uK%qcAbǯS[ASmNMO;fRjw ɯQ.^CzRz3w%fk}\FǓ'js2ҁ'3dZ\ʲR4ˠpU .%u39ݓ]b=[dYRх98~75hSN{`}MJsI5u2I$峙{ BTcKH%y%RpV~ ߬Yv/t'1xtENy@FwYK2QBYlL2sDSQfLz}iK̺:K,5{]^ ڿsV[F% u,~Y,U2*BudjI/"_UwCMu˙!Yh,MDW7*p[?~T5rQ⦭J]σ2q=/d,[63KwKe}&e@ ^Ar5.5VRzXN̎WTw-ۿmJrT $S[NѩT_3XGUu+m̌uAt !&XfU\W1deϤZ[v֟anTB=ʚl*(un: 􉅟WUڷSO%U^EvK:'b ǎh4\Gg0oGOO0w,.pךlGFtḵuX|٘U榻ŤXRXr+[JXROE Qmp_雨?.S,wN?fX89¾oۨ:ˆ(<:s;RKHg~~Q73QEePf`_\fl 1XVf(vpeͺ΢ٸkrgS\,e=kk꽰^7xX6^ՔgeQOdlg]~I:z@6;ۆ}:ϊ|\b=O\*bNh1yCU[g׺$oB$ڗL㾉3w<~_.RIR.Ci!q@Ơ-v@k-TJn:` q6&↩n}Jta"p a3'=r}0w*7#*l˞w;2pw0,| 3@b7Yfyw&S(Yu_.WVrXҋ,UԹ7Llc;1qD V_USHdx}b<8b;;?pL"0gf]@u\ϫ{tTzd: J;kg1hPzR{q©r>16`1hs$qW=l[d ⳰QwNT>qvqZܹL+@;{v$]'^Ͽ'ơw cߙ֖=\)+#O$Oxȸ)QD2Kﰒ@: Sn&>h /ˈ/9 4BMY?%w<M1)7Bx|I%DvKEBl~r%pmAta+v굜GzwR7؏M:zqkZ.$=Y5# KYV݀Iw噭Mҳ>qEެu,3M IDAT`uݞP~p973D,Ս:L,LvKg|eKS3KʝgJtp91/ܗbuH}tnx .>k@J 햕+atAIR1-D&piz 8^`[krBq޾Y\* ~/ftvKwӮ;{ا ˩ kVnDv[/K#ED6V/67uu9 O'(nzn%2Q_ǑS,;fpryf;%b-?'T9@v̟Hf91)ɧilt5m?ۈls?Ew|l[uەch5sך\]N{8qlωgmҫk9sA;L6\bÈ"w*Yg#l#eo ΋?I,ޮk }ԹN&PnF,<0uf8v1/MdM^!ί΅ofpm&3qM,gE FL^Q"hJ"Q̝řZήj3t܌mt\fY.~_ne_4ιsxYMtݙ8xVʵ{Q>>>>>././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/desktop/images/favicon.png0000644000175000017500000000122300000000000017051 0ustar00nilsnilsPNG  IHDR V%(gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs``wtIME waIDAT8ˍ?KQƟij-irj %r5eDP[HK6B" BiQ4sGryqL4*+yrI2Jhk}g\g$QFK):.}sY 4D<'y)~S V6oM$j.I) %CYh~T_,- V WgIj/ 3E!cT"f?5@:Yo)kx7:Y[@-3A^?@̗()xU@S +7$ɻU-4PWB}$s"4? b LL_h%tEXtdate:create2020-07-24T11:10:17+00:00ε%tEXtdate:modify2020-07-24T11:10:17+00:00PftEXtSoftwarewww.inkscape.org<IENDB`././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/LICENSE0000644000175000017500000004735600000000000015677 0ustar00nilsnilsfrom: https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt Attribution-ShareAlike 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-ShareAlike 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. Additional offer from the Licensor -- Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. c. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/agordejo.10000644000175000017500000000574700000000000016544 0ustar00nilsnils.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.48.5. .TH AGORDEJO "1" "January 2022" "Agordejo 0.3.1" "User Commands" .SH NAME Agordejo - Music and audio production session manager based on NSM. .SH DESCRIPTION usage: agordejo [\-h] [\-v] [\-V] [\-u URL] [\-\-nsm\-url URL] [\-l SESSION] [\-c] [\-i] .IP [\-\-session\-root SESSIONROOT] .PP Agordejo \- Version 0.3.1 \- Copyright 2022 by Laborejo Software Suite \- https://www.laborejo.org/agordejo .SS "options:" .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-v\fR, \fB\-\-version\fR show program's version number and exit .TP \fB\-V\fR, \fB\-\-verbose\fR (Development) Switch the logger to INFO and give out all kinds of information to get a high\-level idea of what the program is doing. .TP \fB\-u\fR URL, \fB\-\-url\fR URL Force URL for the session. If there is already a running session we will connect to it. Otherwise we will start one there. Default is local host with random port. Example: osc.udp://myhost.localdomain:14294/ .TP \fB\-\-nsm\-url\fR URL Same as \fB\-\-url\fR. .TP \fB\-l\fR SESSION, \fB\-\-load\-session\fR SESSION Session to open on startup, must exist. Overrides \fB\-\-continue\fR .TP \fB\-c\fR, \fB\-\-continue\fR Autostart last active session. .TP \fB\-i\fR, \fB\-\-hide\fR Start GUI hidden in tray, only if tray available on system. .TP \fB\-\-session\-root\fR SESSIONROOT Root directory of all sessions. Defaults to '$HOME/NSM Sessions' .SH USAGE Agordejo (Esperanto: 'place to set things up') is a music production session manager. It is used to start your programs, remember their (JACK) interconnections and make your life easier in general. Agordejo does not re-invent the wheel but instead uses the New-Session-Manager daemon and enhances it with some tricks of its own, that always remain 100% compatible with the original sessions. This is a proof of concept version. It aims to show that session management with NSM can be quick and convenient and make the user feel in control. Some functionality has not yet been implemented, most prominently anything related to NSM over network. There is always the possibility to break things when trying out corner cases and hacks. That said, for single-computer sessions with just one daemon and one GUI at the same time Agordejo should provide a good user experience. .SH EXAMPLES Run agordejo. You are now in the Quick View mode. Press New Session and add programs through a single click on available program-icons. You can switch to the Full View mode via the tab to get more options. Try right-clicking on many things to get context menus. .SH "REPORTING BUGS" https://www.laborejo.org/bugs .SH COPYRIGHT Agordejo 0.3.1 - Copyright 2022 Laborejo Software Suite https://www.laborejo.org/ .SH "SEE ALSO" The full documentation for Agordejo is maintained as a multi-lingual html site to your systems doc-dir. For example: xdg-open file:///usr/share/doc/agordejo/index.html The documentation can also be found online https://www.laborejo.org/documentation/agordejo ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/build-documentation.sh0000644000175000017500000000073400000000000021161 0ustar00nilsnils#!/bin/sh #The documentation is built statically and does not belong to the normal build process with configure and make #Its updating is part of the development process, not packaging and running. #The correct out/ dir is already part of git. set -e asciidoctor index.adoc -o out/index.html asciidoctor german.adoc -o out/german.html asciidoctor english.adoc -o out/english.html cp ../desktop/images/favicon.* out/ cp ../desktop/images/1248-transparent.png out/logo.png ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/build.py0000644000175000017500000001022300000000000016322 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ This documentation is licensed under Creative Commons-BY-SA-4.0. Please read the provided documentation/LICENSE file or visit https://creativecommons.org/licenses/by-sa/4.0/legalcode The documentation is built statically and does not belong to the normal build process with configure and make Its updating is part of the development process, not packaging and running. The correct out/ dir is already part of git. .adoc is asciidoctor, not simple asciidoc. """ #Make the readme import sys sys.path.append("../engine") from config import METADATA import subprocess from os import getcwd import os.path assert os.path.exists(os.path.join(getcwd(), __file__)), (getcwd(), __file__) import datetime #Readme with open("readme.template", "r") as r: template_readme = r.read() template_readme = template_readme.replace("", datetime.datetime.now().isoformat()) template_readme = template_readme.replace("", METADATA["name"]) template_readme = template_readme.replace("", METADATA["version"]) template_readme = template_readme.replace("", METADATA["shortName"]) template_readme = template_readme.replace("", METADATA["description"]) template_readme = template_readme.replace("", METADATA["dependencies"]) template_readme = template_readme.replace("", METADATA["author"]) with open ("../README.md", "w") as w: w.write(template_readme) print ("Built /README.md") #Documentation index with open("index.adoc.template", "r") as r: template_index = r.read() template_index = template_index.replace("", METADATA["name"]) template_index = template_index.replace("", METADATA["shortName"]) template_index = template_index.replace("", METADATA["version"]) template_index = template_index.replace("", METADATA["author"]) with open ("index.adoc", "w") as w: w.write(template_index) #Documentation METADATA["supportedLanguages"].update({"English":""}) for language in METADATA["supportedLanguages"].keys(): language = language.lower() try: with open(f"{language}.adoc.template", "r") as r: template = r.read() except: continue #language not yet supported as manual for key, value in METADATA.items(): #all strings if type(value) is str: template = template.replace(f"<{key}>", value) if language == "english": template = template.replace("", "== Introduction\n\n" + METADATA["description"]) with open (f"{language}.part.adoc", "r") as clientPart: template = template.replace("", clientPart.read()) with open (f"{language}.adoc", "w") as w: w.write(template) #Create manpage #Needs help2man manpage_template = f""" [name] {METADATA["name"]} - {METADATA["tagline"]} [usage] {METADATA["description"]} [Reporting bugs] https://www.laborejo.org/bugs [copyright] {METADATA["name"]} {METADATA["version"]} - Copyright {METADATA["year"]} {METADATA["author"]} https://www.laborejo.org/ [examples] Run agordejo. You are now in the Quick View mode. Press New Session and add programs through a single click on available program-icons. You can switch to the Full View mode via the tab to get more options. Try right-clicking on many things to get context menus. [see also] The full documentation for {METADATA["name"]} is maintained as a multi-lingual html site to your systems doc-dir. For example: xdg-open file:///usr/share/doc/{METADATA["shortName"]}/index.html The documentation can also be found online https://www.laborejo.org/documentation/{METADATA["shortName"]} """ with open ("manpageinclude.h2m", "w") as w: w.write(manpage_template) command = f"help2man ../{METADATA['shortName']} --no-info --include manpageinclude.h2m > {METADATA['shortName']}.1" subprocess.run(command, capture_output=True, text=True, shell=True) #help2man for the much simpler nsm-data commandNsmDataMNan = "help2man ../tools/nsm-data --no-info > nsm-data.1" subprocess.run(commandNsmDataMNan, capture_output=True, text=True, shell=True) print ("Built. You still need to run") print ("sh build-documentation.sh") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/css/normalize.css0000644000175000017500000001632500000000000020164 0ustar00nilsnils/*! normalize.css v6.0.0 | MIT License | github.com/necolas/normalize.css */ /* Document ========================================================================== */ /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in * IE on Windows Phone and in iOS. */ html { line-height: 1.15; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Add the correct display in IE 9-. */ article, aside, footer, header, nav, section { display: block; } /** * Correct the font size and margin on `h1` elements within `section` and * `article` contexts in Chrome, Firefox, and Safari. */ h1 { font-size: 2em; margin: 0.67em 0; } /* Grouping content ========================================================================== */ /** * Add the correct display in IE 9-. * 1. Add the correct display in IE. */ figcaption, figure, main { /* 1 */ display: block; } /** * Add the correct margin in IE 8. */ figure { margin: 1em 40px; } /** * 1. Add the correct box sizing in Firefox. * 2. Show the overflow in Edge and IE. */ hr { box-sizing: content-box; /* 1 */ height: 0; /* 1 */ overflow: visible; /* 2 */ } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ pre { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /* Text-level semantics ========================================================================== */ /** * 1. Remove the gray background on active links in IE 10. * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. */ a { background-color: transparent; /* 1 */ -webkit-text-decoration-skip: objects; /* 2 */ } /** * 1. Remove the bottom border in Chrome 57- and Firefox 39-. * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. */ abbr[title] { border-bottom: none; /* 1 */ text-decoration: underline; /* 2 */ text-decoration: underline dotted; /* 2 */ } /** * Prevent the duplicate application of `bolder` by the next rule in Safari 6. */ b, strong { font-weight: inherit; } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font style in Android 4.3-. */ dfn { font-style: italic; } /** * Add the correct background and color in IE 9-. */ mark { background-color: #ff0; color: #000; } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in * all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Embedded content ========================================================================== */ /** * Add the correct display in IE 9-. */ audio, video { display: inline-block; } /** * Add the correct display in iOS 4-7. */ audio:not([controls]) { display: none; height: 0; } /** * Remove the border on images inside links in IE 10-. */ img { border-style: none; } /** * Hide the overflow in IE. */ svg:not(:root) { overflow: hidden; } /* Forms ========================================================================== */ /** * Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { margin: 0; } /** * Show the overflow in IE. * 1. Show the overflow in Edge. */ button, input { /* 1 */ overflow: visible; } /** * Remove the inheritance of text transform in Edge, Firefox, and IE. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` * controls in Android 4. * 2. Correct the inability to style clickable types in iOS and Safari. */ button, html [type="button"], /* 1 */ [type="reset"], [type="submit"] { -webkit-appearance: button; /* 2 */ } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring { outline: 1px dotted ButtonText; } /** * 1. Correct the text wrapping in Edge and IE. * 2. Correct the color inheritance from `fieldset` elements in IE. * 3. Remove the padding so developers are not caught out when they zero out * `fieldset` elements in all browsers. */ legend { box-sizing: border-box; /* 1 */ color: inherit; /* 2 */ display: table; /* 1 */ max-width: 100%; /* 1 */ padding: 0; /* 3 */ white-space: normal; /* 1 */ } /** * 1. Add the correct display in IE 9-. * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. */ progress { display: inline-block; /* 1 */ vertical-align: baseline; /* 2 */ } /** * Remove the default vertical scrollbar in IE. */ textarea { overflow: auto; } /** * 1. Add the correct box sizing in IE 10-. * 2. Remove the padding in IE 10-. */ [type="checkbox"], [type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /** * Correct the cursor style of increment and decrement buttons in Chrome. */ [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type="search"] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. */ [type="search"]::-webkit-search-cancel-button, [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in IE 9-. * 1. Add the correct display in Edge, IE, and Firefox. */ details, /* 1 */ menu { display: block; } /* * Add the correct display in all browsers. */ summary { display: list-item; } /* Scripting ========================================================================== */ /** * Add the correct display in IE 9-. */ canvas { display: inline-block; } /** * Add the correct display in IE. */ template { display: none; } /* Hidden ========================================================================== */ /** * Add the correct display in IE 10-. */ [hidden] { display: none; } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/css/style.css0000644000175000017500000000441600000000000017322 0ustar00nilsnils/* https://github.com/markdowncss/retro */ pre, code { font-family: Menlo, Monaco, "Courier New", monospace; background-color: rgb(20,20,20); } pre { padding: .5rem; line-height: 1.25; overflow-x: scroll; } @media print { *, *:before, *:after { background: transparent !important; color: #000 !important; box-shadow: none !important; text-shadow: none !important; } a, a:visited { text-decoration: underline; } a[href]:after { content: " (" attr(href) ")"; } abbr[title]:after { content: " (" attr(title) ")"; } a[href^="#"]:after, a[href^="javascript:"]:after { content: ""; } pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } thead { display: table-header-group; } tr, img { page-break-inside: avoid; } img { max-width: 100% !important; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } } a, a:visited { color: #01ff70; } a:hover, a:focus, a:active { color: #2ecc40; } .retro-no-decoration { text-decoration: none; } html { font-size: 12px; } @media screen and (min-width: 32rem) and (max-width: 48rem) { html { font-size: 15px; } } @media screen and (min-width: 48rem) { html { font-size: 16px; } } body { line-height: 1.85; } p, .retro-p { font-size: 1rem; margin-bottom: 1.3rem; } h1, .retro-h1, h2, .retro-h2, h3, .retro-h3, h4, .retro-h4 { margin: 1.414rem 0 .5rem; font-weight: inherit; line-height: 1.42; } h1, .retro-h1 { margin-top: 0; font-size: 2.698rem; } h2, .retro-h2 { font-size: 1.827rem; } h3, .retro-h3 { font-size: 1.0rem; } h4, .retro-h4 { font-size: 0.9em; } h5, .retro-h5 { font-size: 0.7rem; } h6, .retro-h6 { font-size: .88rem; } small, .retro-small { font-size: .707em; } /* https://github.com/mrmrs/fluidity */ img, canvas, iframe, video, svg, select, textarea { max-width: 100%; } html, body { background-color: #222; min-height: 100%; } html { font-size: 18px; } body { color: #fafafa; font-family: "Courier New"; line-height: 1.45; margin: 6rem auto 1rem; max-width: 48rem; padding: .25rem; } pre { background-color: #333; } blockquote { border-left: 3px solid #01ff70; padding-left: 1rem; } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/english.adoc0000644000175000017500000006124000000000000017137 0ustar00nilsnils:Author: Laborejo Software Suite :Version: 0.3.1 :iconfont-remote!: :!webfonts: //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/LICENSE. //// //// https://powerman.name/doc/asciidoc https://asciidoctor.org/docs/user-manual/ //// :sectnums: :toc: left :toc-title: Table of Contents :toclevels: 3 = Agordejo // Don't write in the empty line above line. It will be interpreted as author html tag For program version 0.3.1 == Introduction Agordejo (Esperanto: 'place to set things up') is a music production session manager. It is used to start your programs, remember their (JACK) interconnections and make your life easier in general. Agordejo does not re-invent the wheel but instead uses the New-Session-Manager daemon and enhances it with some tricks of its own, that always remain 100% compatible with the original sessions. This is a proof of concept version. It aims to show that session management with NSM can be quick and convenient and make the user feel in control. Some functionality has not yet been implemented, most prominently anything related to NSM over network. There is always the possibility to break things when trying out corner cases and hacks. That said, for single-computer sessions with just one daemon and one GUI at the same time Agordejo should provide a good user experience. == Preamble Session Management leads to simplification of workflows, overview and control over programs and data and a good portion of convenience :) No program exists on its own, because no program can do everything that is necessary for today's music production. This is obvious in a JACK environment, which is fundamentally modular: Different programs fulfill different functions and "talk" to each other by sending data to each other. A sequencer sends MIDI to a sampler or synthesizer, which is connected to a plug-in host for effects etc. Even the most monolithic all-in-one DAWs have to, or want to, eventually connect to the outside world. For example, to connect to a screen recorder or streaming program, include a word processor for recording order or lyrics, or to use a function that is simply not available in the DAW. Much of the work is already done by the JACK subsystem. All programs can share their music in real time, have synchronized timelines and play in the same tempo. What remains is the tedious work of always starting all programs, loading project files, connecting audio channels etc. Session management in general (e.g. specifically written starter script files) and Agordejo in particular do this work for you, or at the very least, greatly simplify it. In contrast to the self-written script mentioned above, you don't have to decide in advance on a setup, but everything is saved automatically as long as you manage everything through Agordejo. === Example * Start Agordejo (Start menu, terminal etc.) * You are now in the "Quick View" to start a session * Press the big button "Start New Session" * Now you get a choice of programs: * A single click with the mouse starts a program * Another click hides (or subsequently shows) its graphical interface * If the program crashes you get a warning and can restart it. * Audio and midi ports can now be connected together in a patchbay. The connections are stored in Agordejo. * To get more programs and advanced features you can switch to the tab "Full View * The name of the session so far is simply a date. By clicking on it you can enter a real name. Like "My song" * If you are finished you can return to the session selection by pressing the button "Save and Close" * Now Agordejo could be closed itself. * All stored data is in a single directory on the hard disk (`~/NSM Sessions/My Song`) * The session can be resumed: After clicking on the name, all programs start automatically and connect their JACK ports to among themselves. "Quick View" is a good start. However, usually one would like to get into the full view, even if it is only to start a program twice or to save manually. == Quick View The quick view is an environment reduced to the minimum. You, as a user, should have to make as few decisions as possible and start an old session or create a new one as quickly as possible (with the at least clicks). For stability reasons, only programs are shown that are known to work correctly with Agordejos session management. The Quick View is only a view. There is no technical difference to the full view and you can switch back and forth at any time. === Selecting a Session There are only two options here: Click on "Start New Session" to do so, or select one of the existing sessions to resume it (if available). === In a Session The name can be changed. We recommend a date in the form YYYY-MM-DD followed by a informative name. Please note that the name change will only take effect (e.g. renaming the session directory on disk) when you close the session. Next, a larger text field is available for notes. Write what you want. TODO lists, lyrics, credits and sources of external samples etc. Programs are symbolized by icons. A mouse click starts the program (which can take a while, we have no influence on that). The status of the program is indicated by a symbol: The "Play" symbol for the running program, the eye means "program window hidden", the "Power On/Off" symbol means that the program has been terminated (or crashed). If the program supports it, a running program window can be hidden. Synthesizers, once set up, do not have to be permanently visible. Just click on the program icon again. Either nothing happens (the program does not support it) or the window will hide. Another click on a hidden window shows it again. Every program itself is responsible for whether it saves its window state. Also, some programs already start hidden. There is *no* way to end or remove a program in this view. To do this, switch to the full view or use the close function of the program itself. There is also *no* possibility to start a program multiple times, e.g. one synthesizer per track. If you want to start multiple program instances through the full view times you can still use the quick view to manage the general session, but it is not defined which program instance a click on the icon (e.g. to hide) affects. Multiple instances are a clear indicator that you are ready for the full view. As you'll eventually discover, not *all* available programs are in the list, and there there is also no possibility to start programs that would work well in a session (e.g. a tuner), but are not explicitly written for it, e.g. do not report their status. It is better to manage these programs in full view. If, unfortunately, a program has crashed, you can only restart it and hope its automatic saving worked. Good luck. == Full View Some concepts have already been explained in the chapter "Quick View". It is expected that you have read this. === Selecting a Session Sessions are displayed as a table, which you sort by clicking on a column header. Here is shown how the session is called and when it was saved the last time, probably the two most important pieces of information. Also shown is how many programs/clients are in the session and whether it contains symbolic links. The latter is probably set to "Yes" if use a sampler, or similar, that contains large audio files. These are initially only linked into the session, and not copied, to save disk space. The displayed disk space usage is not the actual one, but includes the sizes of symlink-targets. Only when you archive the session or replace the links with real files the number becomes correct. Finally, the directory in which the files are actually stored is also given. Sessions represent the directory tree. A session is always a "leaf" and cannot include subsessions. When creating or renaming sessions, you can also arrange them in the tree by using the usual slash notation: `song123` -> `New album/song 123` or `Test/asdf` -> `Romantic pop ballads/My heart will keep beating`. How to organize your sessions and how many subdirectories you create is up to you. It is, however, not allowed to start the name with `/` or use the special characters `..`. The tree view can be deactivated by a checkbox on the left side, e.g. to be able to sort them. This is just a view, your data remains untouched. Each session has a context menu (e.g. right mouse button) with further options: You can choose to rename a session, delete it (including all associated files on the hard disk!) and more. These functions are equivalent to your file manager. If you like, you can also use your file manager to rename, move, or delete the session directories themselves (unless they are is currently open). You don't need to restart Agordejo to do this, it will respond to the changes while running. There is also the possibility to remove a so-called "lock" file. This is a file which is created when the session starts and deleted when it ends. This tells Agordejo which session is currently running. If a lock file exists, the session cannot be opened! This should not happen during normal operation. But if a power failure or similar occurs in the middle of a session, the lock file may remain, although obviously no session is opened. In this case, unlock the session manually by deleting the lock file. Click on "New session" to create one. In contrast to the quick view, you must enter the name directly . As mentioned above, you can use the directory tree. In addition, there are several (almost obligatory) programs suggested to start with. Normally one should accept all suggestions. There is always the option to remove them later. A double click on an existing session (or the "Load Selected" button) does just that. === In a Session The full view is divided into three areas: Program starter, programs in the current session and the session notes. There is also a dynamic menu. On the left side you see the program starter. A double-click starts a program instance in this session. You can also start a program more than once. For available programs please refer to the chapter "Program Database". ==== Running Programs Started programs are located on the right side. A double-click switches the visibility to hide its window if the program supports it. If not, nothing happens. The following information is available per program: * The name (possibly with icon) * A "label" that programs can use freely (e.g. Fluajho shows the loaded .sf2 here) * The program status * Stopped , not running * Ready, running * Launch, If the status halts here but the programs works, it is one that does not specifically support session mode. Agordejo cannot know if it is already running or not. Everything is fine! :) * Other states are only transitions and usually only visible for a very short time, e.g. Open / Loading * Visibility (A cross for visible, blank for invisible) * Changes - Are there currently unsaved changes? * ID - A unique identifier that can be used to distinguish between multiple instances of the same program All other functions are accessible via the menu or context menu. One click on a program selects it, and the client menu in the menu bar will now apply to the client. Alternatively, you can right-click on an entry for a context menu, which is identical to the menu. In addition to self-explanatory functions there are also: * Rename gives the program a self-chosen name, mostly to make its purpose clear and to distinguish it better from others. This feature is only available when 'nsm-data' is running in the session. * Save only tells this program to save * Remove takes the program out of the session. However, no files are deleted in the process. At the moment you have to "clean up" in your file manager by hand. If the client "nsm-data" is in the session (this is the default setting) the lower area provides a large text field for notes. It is the same as in the quick view. Write what you want: TODO lists, lyrics, credits and sources from external Samples etc. ==== The Session Menu In contrast to the quick view, full mode offers menus, which can also be accessed via the usual keyboard shortcuts (Ctrl+S for saving etc.). * Save instructs all programs to save, the session continues to run * Save and Close ends the session, after all programs saved * Abort ends the session without saving the programs * Save As saves the session under a different name and closes the current session without saving. From now on you work under the new name. * Add Client offers the option to add any program, whether it is in the program database or not. * Any installed programs are suggested. Agordejo doesn't check them for usefulness for a music session, or even for runnability. You will find `ls` here as well as `agordejo` itself. == Program-Database Agordejos launcher is based on a program database, which is partly self-generated, partly maintained by hand. As in a start menu Agordejo will offer you only programs that are actually installed on your system. The database is created at the first start. Depending on your system, this can take some moments to a few minutes. If you are reinstalling or uninstalling audio programs, you will need to update the database via the command in the control menu. Program installations and system changes are even possible while Agordejo is running (even in a session). After a DB update you can immediately access all new programs. If you do not see an installed program in our launcher, but you are sure that it supports session management please report it to info@laborejo.org or under https://laborejo.org/bugs . In addition, you can add (in full view) programs that are not in the database. === For advanced users The strict rule is that only programs in the $PATH are included in the database. Absolutel paths are not allowed, even if you enter the program name yourself through the menu. However sometimes you just want to try out software, or you are a developer yourself and want to test without system-wide installation. In the control menu / settings is a tab "Program-PATH", where you can define and add your own search paths. One absolute directory path per line, no wildcards, trailing slash don't matter. For example: `/home/myuser/sources/newsequencer/bin/` These search paths are not stored in the session, but locally in your `~/.config` directory. == Tray Agordejo has a tray icon, if your window manager supports it. A click on the trayicon shows or hides Agordejo. If you close Agordejo using the normal window manager function, such as a click on the [X], the program and the session is not terminated, but minimized to the tray. A right click on the icon gives you access to common functions: You can directly start the most recently used sessions. If a session is already running you can save, cancel etc. Agordejo can also be completely exited here. == Network Sessions The functionality to distribute sessions over the local network is planned for a later program version. == Program parameters As an advanced user, you can start Agordejo in the terminal and add some parameters.. For a complete list please use the --help parameter. For example: * `--session newAlbum/mySong` starts the given session. * `--continue` starts the last active session. * `--hide` starts Agordejo as TrayIcon. * `--url osc.udp://myhost.localdomain:14294/` connects to this server, if available, or starts the internal session server at this address. This is a very technical option and probably not needed. * `--session-root /home/user/production2030` sets the root directory. Only sessions in this directory are displayed, everything is stored here. The combination of `--continue` and `--hide` is essentially what many people expect from Session Management: Resuming at the previous state, without any extra windows in their way. If your system uses a start menu you will find not only the normal Agordejo starter but also "Agordejo Continue" to start this mode directly. == Miscellaneous / Explanations / FAQ *Session Save and Exit responds slowly*: Agordejo is not a standalone program like an word processor. The participating programs in the session are not plugins either. When you end the session a signal is sent to all participating client to save. This may take a few moments where you are able to see "live" how individual programs terminate and disappear from the session. Everything is fine. *I have added a program but it does not save with the session*: Does the program support session management? If not, Agordejo cannot do much. But you can ask the program developers to contact us (info@laborejo.org) and we can work together on support. *The programs hang on exit*: Sorry about that. Actually, the programs themselves are to blame, but we are also interested in improving the situation by offering at least an emergency solution in the future. *Agordejo won't start! I start the program but I can't see anything*: Most likely Agordejo is running, but invisible, because you exited it from the tray last time. Is it in the tray? A message should have popped up, maybe you missed it. If there is no tray in your window manager, the program should always be visible. With all these special window managers in Linux it may be that the tray detection did not work properly. Contingency plan is to delete `~/.config/LaborejoSoftwareSuite/agordejo`. This will NOT remove any sessions, but only local settings such as the visibility of the program window. At next start Agordejo will behave like the very first start. *JACK crashed. A lot of programs hang. What can I do to prevent data loss?* Probably already many programs in the session are not running properly and are not reacting anymore. The best thing to do is to use the 'Abort Session' function and restart everything. If the data has actually been unsaved for a long time, you can also dare to save/exit. It may be necessary to re-draw some jack, or all, jack connections by hand at the next start. If you want to be on the safe side, you can manually make a copy of the session directory in your file manager before ending the session (with inevitable crashes). *A program update broke my session because it can no longer load its files.* Unfortunately, this is a problem that even Agordejo can't solve. It also happens with LV2 plugins and with all other software, such as office programs. If you fear that a program becomes incompatible in the future, write down its version number in the session notes, so that you can at least, in an emergency, reinstall the old program version (even if this is very is cumbersome). *What's better? Monolithic DAW or session management?* Why not both? There is no conflict. Session management is worthwhile with two or more participating programs, which one needs almost always. You should not feel compelled to suddenly make everything modular with individual programs, only because you use a session manager. Agordejo is designed to make your music production easier. If it is faster and more comfortable to manage all plugins and effects e.g. in a single "Carla" instance then you should do exactly that. If you basically want to do everything in Ardour, do that, but start Ardour anyway in session management, because no program can do everything alone and the time will come where you add a second one. Session management is another level of hierarchy. Sequencers or DAWs are not plug-ins themselves. Patroneo does not belong "in" Ardour and Ardour does not belong "in" Laborejo. Already in this example each of the programs fulfils a different role because the others follow a different design philosophy and cannot ever offer the same workflow. And more: Some programs can't host plugins, some can't export audio files. They are not bad programs, but programs that concentrate on one task. Furthermore, there is a lot of software that does not directly do music production, but still is connected in the grander scheme: Open Broadcast Studio (OBS), music player, word processors and graphic programs etc. *Agordejo contains functionality which is not within its scope*: Music production is very complex and complexity is inevitable. It's like a waterbed: if you press down on one side, something bounces up in another place. When you create a "clean and lean" program, which therefore implements only a part of the complete workflow, then the missing part pops up somewhere else. A minimalistic session manager provokes plug-ins (not LV2), helper-scripts, workarounds and hacks. E.g. not to include file management provokes user errors like deleting the wrong files. If the SM knows what to do and it can do it, then let it do it. Or crashes: Technically, crashing programs are not the "problem" of the session managers, but they are part of the software reality. Crashes happen every day and need to be handled. Can Agordejo simplify the work and help to restore good conditions again? Then that should be done. Session management is also an opportunity to simplify even complex technical scenarios, e.g. distributing sessions over the network. == Installation and Start Agordejo is exclusive for Linux. The best way to install is to use your package manager. If it is not there, or only in an outdated version, please ask your Linux distribution to provide a recent version. If not available in the package repository you can build Agordejo yourself. .Build and Install * Please check the supplied README.md for dependencies. * You can download a release or clone the git version ** Download the latest version from https://www.laborejo.org/downloads and extract it. ** git clone https://git.laborejo.org/lss/agordejo.git * Change into the new directory and use these commands: * `./configure --prefix=/usr` ** The default prefix is /usr/local * `make` * `sudo make install` Now the program is available to run via your menu/launcher or `agordejo` in a terminal. Please read README.md for other ways of starting agordejo, which are impractical for actual use but can be helpful for testing and development. == Help and Development You can help Agordejo in several ways: Testing and reporting errors, translating, marketing, support, programming and more. === Testing and Reporting Errors If you find a bug in the program (or it runs too slow) please contact us in a way that suits you best. We are thankful for any help. .How to contact us * Report bugs and issues: https://www.laborejo.org/bugs * Website: https://www.laborejo.org * E-Mail: info@laborejo.org * If you see the opportunity and know that a developer will read it also forums, social media etc.. === Programming If you want to do some programming and don't know where to start please get in contact with us directly. The short version is: clone the git, change the code, create a git patch or point me to your public git. === Translations Agordejo is very easy to translate with the help of the Qt-Toolchain, without any need for programming. The easiest way is to contact the developers and they will setup the new language. However, here are the complete instructions for doing a translation completely on your own and integrating it into the program. You can add a new language like this: * Open a terminal and navigate to qtgui/resources/translations * Edit the file `config.pro` with a text editor ** Append the name of your language in the last line, in the form `XY.ts`, where XY is the language code. ** Make sure to leave a space between the individual languages entries. * Run `sh update.sh` in the same directory ** The program has now generated a new `.ts` file in the same directory. * Start Qt Linguist with `linguist-qt5` (may be named differently) and open your newly generated file * Select your "Target Language" and use the program to create a translation * Send us the `.ts` file, such as by e-mail to info@laborejo.org You can also incorporate the translation into Agordejo for testing purposes. This requires rudimentary Python knowledge. * Run the "Release" option in QtLinguists "File" menu. It creates a `.qm` file in the same directory as your `.ts` file. * Edit `qtgui/resources/resources.qrc` and duplicate the line `translations/de.qm` but change it to your new .qm file. * run `sh buildresources.sh` * Edit `engine/config.py`: add your language to the line that begins with "supportedLanguages" like this: `{"German": "de.qm", "Esperanto: "eo.qm"}` ** To find out your language string (German, Esperanto etc.) open the `python3` interpreter in a terminal and run the following command: ** `from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())` To test the new translation you can either run the program normally, if your system is set to that language. Alternatively start agordejo via the terminal: * `LANGUAGE=de_DE.UTF-8 ./agordejo -V --save /dev/null` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/english.adoc.template0000644000175000017500000001071000000000000020745 0ustar00nilsnils:Author: :Version: :iconfont-remote!: :!webfonts: //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/LICENSE. //// //// https://powerman.name/doc/asciidoc https://asciidoctor.org/docs/user-manual/ //// :sectnums: :toc: left :toc-title: Table of Contents :toclevels: 3 = // Don't write in the empty line above line. It will be interpreted as author html tag For program version == Installation and Start is exclusive for Linux. The best way to install is to use your package manager. If it is not there, or only in an outdated version, please ask your Linux distribution to provide a recent version. If not available in the package repository you can build yourself. .Build and Install * Please check the supplied README.md for dependencies. * You can download a release or clone the git version ** Download the latest version from https://www.laborejo.org/downloads and extract it. ** git clone https://git.laborejo.org/lss/.git * Change into the new directory and use these commands: * `./configure --prefix=/usr` ** The default prefix is /usr/local * `make` * `sudo make install` Now the program is available to run via your menu/launcher or `` in a terminal. Please read README.md for other ways of starting , which are impractical for actual use but can be helpful for testing and development. == Help and Development You can help in several ways: Testing and reporting errors, translating, marketing, support, programming and more. === Testing and Reporting Errors If you find a bug in the program (or it runs too slow) please contact us in a way that suits you best. We are thankful for any help. .How to contact us * Report bugs and issues: https://www.laborejo.org/bugs * Website: https://www.laborejo.org * E-Mail: info@laborejo.org * If you see the opportunity and know that a developer will read it also forums, social media etc.. === Programming If you want to do some programming and don't know where to start please get in contact with us directly. The short version is: clone the git, change the code, create a git patch or point me to your public git. === Translations is very easy to translate with the help of the Qt-Toolchain, without any need for programming. The easiest way is to contact the developers and they will setup the new language. However, here are the complete instructions for doing a translation completely on your own and integrating it into the program. You can add a new language like this: * Open a terminal and navigate to qtgui/resources/translations * Edit the file `config.pro` with a text editor ** Append the name of your language in the last line, in the form `XY.ts`, where XY is the language code. ** Make sure to leave a space between the individual languages entries. * Run `sh update.sh` in the same directory ** The program has now generated a new `.ts` file in the same directory. * Start Qt Linguist with `linguist-qt5` (may be named differently) and open your newly generated file * Select your "Target Language" and use the program to create a translation * Send us the `.ts` file, such as by e-mail to info@laborejo.org You can also incorporate the translation into for testing purposes. This requires rudimentary Python knowledge. * Run the "Release" option in QtLinguists "File" menu. It creates a `.qm` file in the same directory as your `.ts` file. * Edit `qtgui/resources/resources.qrc` and duplicate the line `translations/de.qm` but change it to your new .qm file. * run `sh buildresources.sh` * Edit `engine/config.py`: add your language to the line that begins with "supportedLanguages" like this: `{"German": "de.qm", "Esperanto: "eo.qm"}` ** To find out your language string (German, Esperanto etc.) open the `python3` interpreter in a terminal and run the following command: ** `from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())` To test the new translation you can either run the program normally, if your system is set to that language. Alternatively start via the terminal: * `LANGUAGE=de_DE.UTF-8 ./ -V --save /dev/null` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/english.part.adoc0000644000175000017500000004657000000000000020115 0ustar00nilsnils== Preamble Session Management leads to simplification of workflows, overview and control over programs and data and a good portion of convenience :) No program exists on its own, because no program can do everything that is necessary for today's music production. This is obvious in a JACK environment, which is fundamentally modular: Different programs fulfill different functions and "talk" to each other by sending data to each other. A sequencer sends MIDI to a sampler or synthesizer, which is connected to a plug-in host for effects etc. Even the most monolithic all-in-one DAWs have to, or want to, eventually connect to the outside world. For example, to connect to a screen recorder or streaming program, include a word processor for recording order or lyrics, or to use a function that is simply not available in the DAW. Much of the work is already done by the JACK subsystem. All programs can share their music in real time, have synchronized timelines and play in the same tempo. What remains is the tedious work of always starting all programs, loading project files, connecting audio channels etc. Session management in general (e.g. specifically written starter script files) and Agordejo in particular do this work for you, or at the very least, greatly simplify it. In contrast to the self-written script mentioned above, you don't have to decide in advance on a setup, but everything is saved automatically as long as you manage everything through Agordejo. === Example * Start Agordejo (Start menu, terminal etc.) * You are now in the "Quick View" to start a session * Press the big button "Start New Session" * Now you get a choice of programs: * A single click with the mouse starts a program * Another click hides (or subsequently shows) its graphical interface * If the program crashes you get a warning and can restart it. * Audio and midi ports can now be connected together in a patchbay. The connections are stored in Agordejo. * To get more programs and advanced features you can switch to the tab "Full View * The name of the session so far is simply a date. By clicking on it you can enter a real name. Like "My song" * If you are finished you can return to the session selection by pressing the button "Save and Close" * Now Agordejo could be closed itself. * All stored data is in a single directory on the hard disk (`~/NSM Sessions/My Song`) * The session can be resumed: After clicking on the name, all programs start automatically and connect their JACK ports to among themselves. "Quick View" is a good start. However, usually one would like to get into the full view, even if it is only to start a program twice or to save manually. == Quick View The quick view is an environment reduced to the minimum. You, as a user, should have to make as few decisions as possible and start an old session or create a new one as quickly as possible (with the at least clicks). For stability reasons, only programs are shown that are known to work correctly with Agordejos session management. The Quick View is only a view. There is no technical difference to the full view and you can switch back and forth at any time. === Selecting a Session There are only two options here: Click on "Start New Session" to do so, or select one of the existing sessions to resume it (if available). === In a Session The name can be changed. We recommend a date in the form YYYY-MM-DD followed by a informative name. Please note that the name change will only take effect (e.g. renaming the session directory on disk) when you close the session. Next, a larger text field is available for notes. Write what you want. TODO lists, lyrics, credits and sources of external samples etc. Programs are symbolized by icons. A mouse click starts the program (which can take a while, we have no influence on that). The status of the program is indicated by a symbol: The "Play" symbol for the running program, the eye means "program window hidden", the "Power On/Off" symbol means that the program has been terminated (or crashed). If the program supports it, a running program window can be hidden. Synthesizers, once set up, do not have to be permanently visible. Just click on the program icon again. Either nothing happens (the program does not support it) or the window will hide. Another click on a hidden window shows it again. Every program itself is responsible for whether it saves its window state. Also, some programs already start hidden. There is *no* way to end or remove a program in this view. To do this, switch to the full view or use the close function of the program itself. There is also *no* possibility to start a program multiple times, e.g. one synthesizer per track. If you want to start multiple program instances through the full view times you can still use the quick view to manage the general session, but it is not defined which program instance a click on the icon (e.g. to hide) affects. Multiple instances are a clear indicator that you are ready for the full view. As you'll eventually discover, not *all* available programs are in the list, and there there is also no possibility to start programs that would work well in a session (e.g. a tuner), but are not explicitly written for it, e.g. do not report their status. It is better to manage these programs in full view. If, unfortunately, a program has crashed, you can only restart it and hope its automatic saving worked. Good luck. == Full View Some concepts have already been explained in the chapter "Quick View". It is expected that you have read this. === Selecting a Session Sessions are displayed as a table, which you sort by clicking on a column header. Here is shown how the session is called and when it was saved the last time, probably the two most important pieces of information. Also shown is how many programs/clients are in the session and whether it contains symbolic links. The latter is probably set to "Yes" if use a sampler, or similar, that contains large audio files. These are initially only linked into the session, and not copied, to save disk space. The displayed disk space usage is not the actual one, but includes the sizes of symlink-targets. Only when you archive the session or replace the links with real files the number becomes correct. Finally, the directory in which the files are actually stored is also given. Sessions represent the directory tree. A session is always a "leaf" and cannot include subsessions. When creating or renaming sessions, you can also arrange them in the tree by using the usual slash notation: `song123` -> `New album/song 123` or `Test/asdf` -> `Romantic pop ballads/My heart will keep beating`. How to organize your sessions and how many subdirectories you create is up to you. It is, however, not allowed to start the name with `/` or use the special characters `..`. The tree view can be deactivated by a checkbox on the left side, e.g. to be able to sort them. This is just a view, your data remains untouched. Each session has a context menu (e.g. right mouse button) with further options: You can choose to rename a session, delete it (including all associated files on the hard disk!) and more. These functions are equivalent to your file manager. If you like, you can also use your file manager to rename, move, or delete the session directories themselves (unless they are is currently open). You don't need to restart Agordejo to do this, it will respond to the changes while running. There is also the possibility to remove a so-called "lock" file. This is a file which is created when the session starts and deleted when it ends. This tells Agordejo which session is currently running. If a lock file exists, the session cannot be opened! This should not happen during normal operation. But if a power failure or similar occurs in the middle of a session, the lock file may remain, although obviously no session is opened. In this case, unlock the session manually by deleting the lock file. Click on "New session" to create one. In contrast to the quick view, you must enter the name directly . As mentioned above, you can use the directory tree. In addition, there are several (almost obligatory) programs suggested to start with. Normally one should accept all suggestions. There is always the option to remove them later. A double click on an existing session (or the "Load Selected" button) does just that. === In a Session The full view is divided into three areas: Program starter, programs in the current session and the session notes. There is also a dynamic menu. On the left side you see the program starter. A double-click starts a program instance in this session. You can also start a program more than once. For available programs please refer to the chapter "Program Database". ==== Running Programs Started programs are located on the right side. A double-click switches the visibility to hide its window if the program supports it. If not, nothing happens. The following information is available per program: * The name (possibly with icon) * A "label" that programs can use freely (e.g. Fluajho shows the loaded .sf2 here) * The program status * Stopped , not running * Ready, running * Launch, If the status halts here but the programs works, it is one that does not specifically support session mode. Agordejo cannot know if it is already running or not. Everything is fine! :) * Other states are only transitions and usually only visible for a very short time, e.g. Open / Loading * Visibility (A cross for visible, blank for invisible) * Changes - Are there currently unsaved changes? * ID - A unique identifier that can be used to distinguish between multiple instances of the same program All other functions are accessible via the menu or context menu. One click on a program selects it, and the client menu in the menu bar will now apply to the client. Alternatively, you can right-click on an entry for a context menu, which is identical to the menu. In addition to self-explanatory functions there are also: * Rename gives the program a self-chosen name, mostly to make its purpose clear and to distinguish it better from others. This feature is only available when 'nsm-data' is running in the session. * Save only tells this program to save * Remove takes the program out of the session. However, no files are deleted in the process. At the moment you have to "clean up" in your file manager by hand. If the client "nsm-data" is in the session (this is the default setting) the lower area provides a large text field for notes. It is the same as in the quick view. Write what you want: TODO lists, lyrics, credits and sources from external Samples etc. ==== The Session Menu In contrast to the quick view, full mode offers menus, which can also be accessed via the usual keyboard shortcuts (Ctrl+S for saving etc.). * Save instructs all programs to save, the session continues to run * Save and Close ends the session, after all programs saved * Abort ends the session without saving the programs * Save As saves the session under a different name and closes the current session without saving. From now on you work under the new name. * Add Client offers the option to add any program, whether it is in the program database or not. * Any installed programs are suggested. Agordejo doesn't check them for usefulness for a music session, or even for runnability. You will find `ls` here as well as `agordejo` itself. == Program-Database Agordejos launcher is based on a program database, which is partly self-generated, partly maintained by hand. As in a start menu Agordejo will offer you only programs that are actually installed on your system. The database is created at the first start. Depending on your system, this can take some moments to a few minutes. If you are reinstalling or uninstalling audio programs, you will need to update the database via the command in the control menu. Program installations and system changes are even possible while Agordejo is running (even in a session). After a DB update you can immediately access all new programs. If you do not see an installed program in our launcher, but you are sure that it supports session management please report it to info@laborejo.org or under https://laborejo.org/bugs . In addition, you can add (in full view) programs that are not in the database. === For advanced users The strict rule is that only programs in the $PATH are included in the database. Absolutel paths are not allowed, even if you enter the program name yourself through the menu. However sometimes you just want to try out software, or you are a developer yourself and want to test without system-wide installation. In the control menu / settings is a tab "Program-PATH", where you can define and add your own search paths. One absolute directory path per line, no wildcards, trailing slash don't matter. For example: `/home/myuser/sources/newsequencer/bin/` These search paths are not stored in the session, but locally in your `~/.config` directory. == Tray Agordejo has a tray icon, if your window manager supports it. A click on the trayicon shows or hides Agordejo. If you close Agordejo using the normal window manager function, such as a click on the [X], the program and the session is not terminated, but minimized to the tray. A right click on the icon gives you access to common functions: You can directly start the most recently used sessions. If a session is already running you can save, cancel etc. Agordejo can also be completely exited here. == Network Sessions The functionality to distribute sessions over the local network is planned for a later program version. == Program parameters As an advanced user, you can start Agordejo in the terminal and add some parameters.. For a complete list please use the --help parameter. For example: * `--session newAlbum/mySong` starts the given session. * `--continue` starts the last active session. * `--hide` starts Agordejo as TrayIcon. * `--url osc.udp://myhost.localdomain:14294/` connects to this server, if available, or starts the internal session server at this address. This is a very technical option and probably not needed. * `--session-root /home/user/production2030` sets the root directory. Only sessions in this directory are displayed, everything is stored here. The combination of `--continue` and `--hide` is essentially what many people expect from Session Management: Resuming at the previous state, without any extra windows in their way. If your system uses a start menu you will find not only the normal Agordejo starter but also "Agordejo Continue" to start this mode directly. == Miscellaneous / Explanations / FAQ *Session Save and Exit responds slowly*: Agordejo is not a standalone program like an word processor. The participating programs in the session are not plugins either. When you end the session a signal is sent to all participating client to save. This may take a few moments where you are able to see "live" how individual programs terminate and disappear from the session. Everything is fine. *I have added a program but it does not save with the session*: Does the program support session management? If not, Agordejo cannot do much. But you can ask the program developers to contact us (info@laborejo.org) and we can work together on support. *The programs hang on exit*: Sorry about that. Actually, the programs themselves are to blame, but we are also interested in improving the situation by offering at least an emergency solution in the future. *Agordejo won't start! I start the program but I can't see anything*: Most likely Agordejo is running, but invisible, because you exited it from the tray last time. Is it in the tray? A message should have popped up, maybe you missed it. If there is no tray in your window manager, the program should always be visible. With all these special window managers in Linux it may be that the tray detection did not work properly. Contingency plan is to delete `~/.config/LaborejoSoftwareSuite/agordejo`. This will NOT remove any sessions, but only local settings such as the visibility of the program window. At next start Agordejo will behave like the very first start. *JACK crashed. A lot of programs hang. What can I do to prevent data loss?* Probably already many programs in the session are not running properly and are not reacting anymore. The best thing to do is to use the 'Abort Session' function and restart everything. If the data has actually been unsaved for a long time, you can also dare to save/exit. It may be necessary to re-draw some jack, or all, jack connections by hand at the next start. If you want to be on the safe side, you can manually make a copy of the session directory in your file manager before ending the session (with inevitable crashes). *A program update broke my session because it can no longer load its files.* Unfortunately, this is a problem that even Agordejo can't solve. It also happens with LV2 plugins and with all other software, such as office programs. If you fear that a program becomes incompatible in the future, write down its version number in the session notes, so that you can at least, in an emergency, reinstall the old program version (even if this is very is cumbersome). *What's better? Monolithic DAW or session management?* Why not both? There is no conflict. Session management is worthwhile with two or more participating programs, which one needs almost always. You should not feel compelled to suddenly make everything modular with individual programs, only because you use a session manager. Agordejo is designed to make your music production easier. If it is faster and more comfortable to manage all plugins and effects e.g. in a single "Carla" instance then you should do exactly that. If you basically want to do everything in Ardour, do that, but start Ardour anyway in session management, because no program can do everything alone and the time will come where you add a second one. Session management is another level of hierarchy. Sequencers or DAWs are not plug-ins themselves. Patroneo does not belong "in" Ardour and Ardour does not belong "in" Laborejo. Already in this example each of the programs fulfils a different role because the others follow a different design philosophy and cannot ever offer the same workflow. And more: Some programs can't host plugins, some can't export audio files. They are not bad programs, but programs that concentrate on one task. Furthermore, there is a lot of software that does not directly do music production, but still is connected in the grander scheme: Open Broadcast Studio (OBS), music player, word processors and graphic programs etc. *Agordejo contains functionality which is not within its scope*: Music production is very complex and complexity is inevitable. It's like a waterbed: if you press down on one side, something bounces up in another place. When you create a "clean and lean" program, which therefore implements only a part of the complete workflow, then the missing part pops up somewhere else. A minimalistic session manager provokes plug-ins (not LV2), helper-scripts, workarounds and hacks. E.g. not to include file management provokes user errors like deleting the wrong files. If the SM knows what to do and it can do it, then let it do it. Or crashes: Technically, crashing programs are not the "problem" of the session managers, but they are part of the software reality. Crashes happen every day and need to be handled. Can Agordejo simplify the work and help to restore good conditions again? Then that should be done. Session management is also an opportunity to simplify even complex technical scenarios, e.g. distributing sessions over the network. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/german.adoc0000644000175000017500000006656600000000000016777 0ustar00nilsnils:Author: Laborejo Software Suite :Version: 0.3.1 :iconfont-remote!: :!webfonts: //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/LICENSE. //// //// https://powerman.name/doc/asciidoc https://asciidoctor.org/docs/user-manual/ //// :sectnums: :toc: left :toc-title: Inhaltsverzeichnis :toclevels: 3 = Agordejo // Don't write in the empty line above line. It will be interpreted as author html tag Für Programmversion 0.3.1 == Präambel Session Management führt zur Vereinfachung von Arbeitsabläufen, Übersicht und Kontrolle über Programme und Daten und eine gehörige Portion positive Bequemlichkeit :) Kein Programm existiert für sich alleine, denn kein Programm kann alles leisten, was für heutige Musikproduktion nötig ist. Direkt ersichtlich ist das in einer JACK Umgebung, die prinzipiell modular ist: Verschiedene Programme erfüllen verschiedene Zwecke und "sprechen" miteinander, indem sie sich Daten schicken. Ein Sequencer schickt Midi an einen Sampler oder Synthesizer, der mit einem Plugin-Host für Effekte verbunden ist usw. Selbst die monolithischsten All-In-One DAWs müssen, oder möchten, irgendwann mit der Außenwelt kommunizieren. z.B. um sich mit einem Screenrecorder oder Streamingprogramm zu verbinden, einen Wordprozessor mit Aufnahmereihenfolge oder Liedtexten zu starten, um Musik zu visualisieren oder (um ehrlich zu bleiben) eine Funktion zu nutzen, die es in diesem Programm eben nicht gibt. Einen Großteil der Arbeit erledigt bereits das JACK-Subsystem. Alle Programme können ihre Musik in Echtzeit teilen, verfügen über synchronisierte Timelines und spielen im gleichen Tempo. Was übrig bleibt ist die leidige Arbeit jedesmal alle Programme zu starten, die Projektdateien zu laden, alle Audiokanäle zu verbinden usw. Session Management im allgemeinen (z.B. extra geschriebene Starter-Scriptdateien) und Agordejo im speziellen nehmen Ihnen diese Arbeit ab, oder vereinfachen sie zumindest stark. Im Gegensatz zum erwähnten selbstgeschriebenen Script müssen Sie sich nicht im Vorraus für ein Setup entscheiden, sondern alles wird automatisch verwaltet, solange Sie alles durch den Session Manager Agordejo starten. === Anwendungsbeispiel * Agordejo starten (Startmenü, Terminal etc.) * Man befindet sich nun in der "Schnellen Ansicht" um eine Session zu starten * Der große Knopf: "Starte Neue Session" * Nun bekommt man eine Auswahl an Programmen: * Ein einfacher Klick mit der Maus startet ein Programm * Ein weiterer Klick versteckt (oder zeigt anschließend) dessen grafische Oberfläche * Sollte das Programm abstürzen bekommt man das signalisiert und kann es neustarten. * Audio- und Midiports können nun in einer Patchbay miteinander verbunden werden. Die Verbindungen werden in Agordejo gespeichert. * Um andere Programme und erweiterte Funktionen zu bekommen kann man in den Tab "Volle Ansicht" wechseln * Der Name der Session ist bisher einfach ein Datum. Durch Klick auf dieses kann man einen richtigen Namen eingeben. Etwa "Mein Lied" * Ist man soweit fertig kommt man durch den Knopf "Speichern und Schließen" wieder in die Sessionauswahl zurück * Nun könnte Agordejo geschlossen werden. * Alle gespeicherten Daten liegen in einem einzigen Verzeichnis auf der Festplatte (`~/NSM Sessions/Mein Lied`) * Die Session kann weitergeführt werden: Nach dem Klick auf den Namen starten alle Programme automatisch und verbinden ihre JACK-Ports untereinander. Die "Schnelle Ansicht" ist ein guter Einstieg. Üblicherweise möchte man jedoch bald in die volle Ansicht wechseln, und sei es nur um ein Programm zwei mal zu starten oder um zwischendurch manuell zu speichern. == Die Schnelle Ansicht Die Schnelle Ansicht ist eine aufs Minimum reduzierte Umgebung. Sie, als Benutzer, sollen so wenig Entscheidungen wie möglich zu treffen haben und so schnell wie möglich (mit den wenigstens Klicks) eine alte Session starten oder eine neue erstellen können. Aus Stabilitätsgründen werden nur Programme zum Starten angeboten, von denen bekannt ist, dass Sie korrekt mit dem Session Management Agordejos zusammenarbeiten. Die Schnelle Ansicht ist lediglich eine Ansicht. Es besteht kein technischer Unterschied zur vollen Session und Sie können jederzeit hin- und herschalten. === Session Auswählen Hier gibt es nur zwei Möglichkeiten: Klicken Sie auf "Starte Neue Session" um dies zu tun, oder wählen Sie eine der bestehenden Sessions aus um diese fortzuführen (sofern vorhanden). === In einer Session Der Name kann geändert werden. Wir empfehlen ein Datum in der Form YYYY-MM-DD gefolgt von einem aussagekräftigen Namen. Bitte beachten Sie, dass die Namensänderung erst wirksam wird (z.B. in Form des Sessionvereichnisses auf der Platte) wenn Sie die Session schließen. Als nächstes steht ein größeres Textfeld für Notizen zur Verfügung. Schreiben Sie was Sie möchten. TODO-Listen, Liedtexte, Credits und Quellen von externen Samples etc. Programme werden durch Icons symbolisiert. Ein Mausklick startet das Programm (das kann einen Moment dauern, darauf haben wir keinen Einfluss). Der Status des Programmes wird durch ein Symbol dargestellt: Das "Play"-Symbol für das laufende Programm, ein Auge bedeutet "Programmfenster versteckt", das "Power On/Off"-Symbol bedeutet, dass Programm wurde beendet (oder Absturz). Falls das Programm es unterstützt kann ein laufendes Programmfenster versteckt werden. z.B. ein Synthesizer, einmal eingerichtet, muss nicht permanent zu sehen sein. Klicken Sie einfach erneut auf das Programmicon. Entweder passiert nichts (das Programm unterstützt es nicht) oder es wird versteckt. Ein weiterer Klick auf ein versteckes Fenster zeigt es wieder. Jedes Programm ist selbst dafür verantwortlich ob es seinen Fensterzustand speichert. Manche Programme starten bereits versteckt. Es gibt *keine* Möglichkeit ein Programm in dieser Ansicht zu beenden. Wechseln Sie hierzu in die volle Ansicht oder benutzen Sie die Schließenfunktion des Programmes selbst. Es gibt auch *keine* Möglichkeit ein Programm mehrfach zu starten, z.B. für einen Synthesizer pro Spur. Wechseln sie hierzu in die volle Ansicht. Wenn Sie Programme durch die volle Ansicht mehrfach gestartet haben können Sie weiterhin die schnelle Ansicht benutzen um die Session zu verwalten, allerdings ist nicht definiert auf welche Programminstanz sich ein Klick auf das Programmicon (z.B. zum Verstecken) auswirkt. Mehrere Instanzen sind ein klares Zeichen dafür, dass Sie bereit für die volle Ansicht sind. Wie Sie irgendwann feststellen werden sind nicht *alle* verfügbaren Programme in der Liste, und es gibt auch keine Möglichkeit Programme zu starten die zwar gut in einer Session funktionieren (z.B. ein Stimmgerät), aber nicht explizit dafür geschrieben sind, z.B. ihren Status nicht zurückmelden. Diese Programme sollten Sie besser in der vollen Ansicht verwalten. Ist ein Programm leider abgestürzt können Sie es nur neu starten und auf dessen automatische Speicherung hoffen. Viel Glück. == Die Volle Ansicht Einige Konzepte wurde bereits im Kapitel "Die Schnelle Ansicht" erklärt. Es wird vorrausgesetzt, dass Sie dieses gelesen haben. === Session Auswählen Sessions stellen sich als Tabelle dar, die Sie durch den Klick auf eine Spaltenüberschrift ordnen können. So wird hier gezeigt wie die Session heißt und wann das letzte mal gespeichert wurde, wohl die beiden wichtigsten Informationen. Dann wird gezeigt wie viele Programme/Clients die Session verwaltet und ob sie Symbolische Links enthält. Letzteres steht wahrscheinlich auf "Ja", wenn Sie einen Sampler o.ä. benutzen, der große Audiodateien geladen hat. Diese werden zunächst nur in die Session gelinkt, und nicht kopiert, um Speicherplatz zu sparen. Der angezeigte Speicherverbrauch ist nicht der tatsächliche Verbrauch, sondern beinhaltet die Größen der Symlinks. Erst wenn Sie die Session archivieren oder die Links durch reale Dateien ersetzen stimmt die angezeigte Zahl. Schließlich wird auch das Verzeichnis angegeben, in dem die Dateien tatsächlich gespeichert sind. Sessions stellen den Verzeichnisbaum dar. Eine Session ist immer ein "Blatt" und kann keine Subsessions enthalten. Sie können beim Anlegen oder Umbenennen die Sessions auch im Baum anordnen indem Sie die gewohnte Schrägstrichschreibweise benutzen: `song123` -> `Neues Album/Song 123` oder `Versuche/asdf` -> `Romantische Pop-Balladen/Mein Herz wird weiter schlagen`. Wie Sie ihre Sessions organisieren, und wie viele Unterverzeichnisse Sie anlegen, steht Ihnen frei. Es ist allerdings nicht erlaubt den Namen mit `/` anfangen zu lassen oder die speziellen Zeichen `..` zu benutzen. Die Baumansicht kann durch eine Checkbox an der linken Seite deaktiviert werden, um etwa besser sortieren zu können. Das ist nur eine Ansicht, ihre Daten bleiben unangetastet. Jede Session verfügt über ein Kontextmenü (z.B. rechte Maustaste) mit weiteren Optionen: Sie können eine Session umbennen, löschen (inkl. aller dazugehörigen Dateien auf der Festplatte!) und mehr. Diese Funktionen sind gleichwertig zu ihrem Dateimanager. Falls Sie möchten, können Sie auch einfach die Sessionverzeichnisse selbst umbennen, verschieben oder löschen (sofern diese nicht gerade geöffnet ist). Dazu muss Agordejo nicht neugestartet werden, es reagiert selbst auf die Änderungen. Hier gibt es auch die Möglichkeit eine sogenannte "Lock"-Datei zu entfernen. Das ist eine Datei, die angelegt wird sobald die Session startet und gelöscht wenn beendet. Damit weiß das Programm, welche Session gerade läuft. Ist eine Lockdatei vorhanden, kann die Session nicht geöffnet werden! Das sollte im normalen Betrieb nicht vorkommen. Falls aber ein Stromausfall o.ä. mitten in einer Session passiert ist kann die Lockdatei übrig bleiben, obwohl offensichtlich keine Session geöffnet ist. In diesem Fall entsperren Sie die Session hierdurch manuell. Klicken Sie auf "Neue Session" um diese anzulegen. Im Gegensatz zur schnellen Ansicht müssen Sie hier den Namen direkt eingeben. Wie oben erwäht können Sie dazu den Verzeichnisbaum benutzen. Darüberhinaus wird angeboten verschiedene (fast schon obligatorische) Programme direkt mitzustarten. Im Normalfall sollte man alle Vorschläge akzeptieren. Es gibt jederzeit die Möglichkeit diese wieder zu entfernen. Ein Doppelklick auf eine existierende Session (oder der Knopf "Lade Ausgewählte") machen genau das. === In einer Session Die volle Ansicht ist in drei Bereiche eingeteilt: Programmstarter, Programme in der laufenden Session und die Session-Notizen. Dazu gibt es ein dynamisches Menü. Auf der linken Seite sehen die den Programmstarter. Ein Doppelklick startet eine Programminstanz in der Session. Sie können ein Programm auch mehrmals starten. Welche Programme zur Verfügung stehen entnehmen Sie bitte dem Kapitel "Programm-Datenbank". ==== Laufende Programm Gestartete Programme befinden sich auf der rechten Seite. Ein Doppelklick schaltet die Sichtbarkeit um, sofern das Programm unterstüzt sein Fenster zu verstecken. Falls nicht passiert nichts. Pro Programm gibt es folgende Informationen: * Der Name (evtl. mit Icon) * Ein "Label", das Programme frei benutzen können (z.B. zeigt Fluajho hier das geladene .sf2 an) * Den Programmstatus (auf Englisch, da es sich um definierte Schlüsselworte handelt). * Stopped / Gestoppt, läuft nicht * Ready / Läuft und Bereit * Launch / Startet. Wenn das Programm hier stehen bleibt, aber funktioniert, handelt es sich um eins, dass nicht speziell den Sessionmodus unterstützt. Agordejo kann nicht wissen, ob es bereits läuft oder nicht. Alles ist in Ordnung! :) * Weitere Zustände sind nur Übergänge und meist nur sehr kurz zu sehen, z.B. Open / Läd gerade * Sichtbarkeit (Ein Kreuz für sichtbar, Leer für unsichtbar) * Änderungen - Gibt es momentan ungespeicherte Änderungen? * ID - Ein eindeutiges Kürzel mit dem man auch mehrere Instanzen des gleichen Programmes auseinander halten kann Alle weiteren Funktionen sind durch das Menü oder Kontextmenü zugänglich. Ein Klick auf ein Programm wählt es aus, und das Clientmenü in der Menüleiste gilt nun dafür. Alternativ kann mit Rechtsklick auf den Eintrag das Kontextmenü geöffnet werden, das identisch ist. Neben den selbsterklärenden Funktionen gibt es noch: * Umbenennen gibt dem Programm einen selbstgewählten Namen, besonders um seinen Zweck deutlich zu machen und es besser von anderen zu unterscheiden. Diese Funktion steht nur zur Verfügung, wenn `nsm-data` in der Session läuft. * Speichern weist nur dieses Programm an abzuspeichern * Entfernen nimmt das Programm aus der Session. Dabei werden jedoch keine Dateien gelöscht. Zur Zeit muss leider noch von Hand im Dateimanager "aufgeräumt" werden. Befindet sich der Client "nsm-data" in der Session (das ist Voreinstellung) steht im unteren Bereich ein großes Textfeld für Notizen zur Verfügung. Es ist dasselbe wie in der schnellen Ansicht. Schreiben Sie was Sie möchten: TODO-Listen, Liedtexte, Credits und Quellen von externen Samples etc. ==== Das Session Menü Im Gegensatz zur schnellen Ansicht stehen hier weitere Menüs zur Verfügung, die auch über die üblichen Tastaturkürzel zu erreichen sind (Strg+S für Speichern etc.). * Speichern weist alle Programme an zu speichern, die Session läuft weiter * Speichern und Schließen beendet die Session, vorher speichern alle Programme noch einmal ab * Abbrechen beendet die Session, ohne dass die Programme abspeichern * Speichern Unter speichert die Session unter einem anderen Namen und schließt die laufende ohne abzuspeichern. Ab nun arbeitet man in der neuen Session. * Client Hinzufügen bietet die Option ein beliebiges Programm hinzuzufügen, egal ob es in der Programmdatenbank ist, oder nicht. * Es werden alle installierten Programme vorgeschlagen. Agordejo überprüft diese nicht auf Sinnhaftigkeit für eine Musik-Session, oder auch nur auf Lauffähigkeit. Sie finden hier `ls` wie auch `agordejo` selbst. == Programm-Datenbank Agordejos Programmstarter basiert auf einer Programmdatenbank, die sich teilweise selbst erstellt, teilweise von Hand eingepflegt wurde. Das bedeutet nichts anderes, als das alle installieren Programme in ihrem System überprüft werden (wie ein Startmenü) und Ihnen im Agordejo-Starter nur das angeboten wird, was Sie auch tatsächlich installiert haben. Beim ersten Start wird daher die Programmdatenbank erstellt. Je nach System kann dies einige Augenblicke bis einige Minuten dauern. Wenn Sie Audio-Programme neu installieren, oder deinstallieren müssen Sie die Datenbank selbst aktualisieren. Im Steuerungsmenü gibt es den Befehl. Programminstallationen sind sogar möglich während Agordejo läuft (auch in einer Session). Nach einem DB-Update stehen Ihnen sofort alle neuen Programme zur Verfügung. Wenn Sie ein installiertes Programm nicht in unserer Liste sehen, aber von dem Sie sicher sind, dass es Session Management unterstützt melden Sie es bitte an info@laborejo.org oder unter https://laborejo.org/bugs . Darüberhinaus können Sie (in der vollen Ansicht) Programme hinzufügen, die nicht in der Datenbank sind. Siehe dort. === Für Fortgeschrittene Die eiserne Regel ist, dass nur Programme im $PATH in die Datenbank aufgenommen werden. Absolute Pfade sind unzulässig, selbst wenn man den Programmnamen selbst durch das Menü eingibt. Allerdings möchte man manchmal Software nur ausprobieren, oder ist selbst Entwickler und möchte ohne systemweite Installation testen. In den Einstellungen im Steuerungsmenü gibt es einen Tab "Programm-PATH", wo Sie eigene Suchpfade hinzufügen können. Ein absoluter Pfad pro Zeile, keine Wildcards, Trailing Slash spielt keine Rolle. Zum Beispiel: `/home/myuser/sources/newsequencer/bin/` Diese Suchpfade werden nicht in der Session gespeichert sondern lokal in ihrem `~/.config` Verzeichnis. == Tray Agordejo verfügt über ein Tray-Icon, sofern ihr Windowmanager das unterstüzt. Ein Klick auf das Trayicon zeigt oder versteckt Agordejo. Schließt man Agordejo über die normale Windowmanagerfunktion, etwa ein Klick auf das [X], wird das Programm und die Session nicht beendet, sondern in den Tray minimiert. Ein Rechtsklick auf das Icon bietet Schnellzugriff auf häufige Funktionen: Sie können hier die zuletzt benutzen Sessions direkt starten. Läuft bereits eine Session können Sie speichern, abbrechen etc. Agordejo kann hier auch komplett beendet werden. == Netzwerk-Sessions Die Funktionalität Sessions im lokalen Netzwerk zu verteilen ist für eine spätere Programmversion vorgesehen. == Programmparameter Als fortgeschrittener Benutzer können Sie Agordejo im Terminal starten und dort einige Parameter angeben. Für eine vollständige Liste benutzen Sie bitte den --help Parameter. Eine Auswahl: * `--session neuesAlbum/meinLied` startet direkt die angegebene Session. * `--continue` startet die zuletzt benutzte Session * `--hide` startet Agordejo als Trayicon. * `--url osc.udp://myhost.localdomain:14294/` verbindet sich zu diesem Server, falls vorhanden, oder startet den internen Session-Server unter dieser Adresse. Dies ist eine sehr technische Option und wird wahrscheinlich nicht benötigt. * `--session-root /home/benutzer/produktion2020` setzt das Wurzelverzeichnis. Nur Sessions in diesem Verzeichnis werden angezeigt, alles wird dort gespeichert. Die Kombination von `--continue` und `--hide` ergibt einen Modus, den viele Leute vom Session Management erwarten: dort weiter machen wo man aufgehört hat, ohne dass Extrafenster angezeigt werden. Falls Ihr System über ein Startmenü verfügt finden Sie daher neben dem normalen Agordejo-Starter auch eine "Agordejo Continue"-Verknüpfung für genau diesen Modus. == Verschiedenes / Erklärungen / FAQ *Session Speichern und Beenden reagiert langsam*: Agordejo ist kein Einzelprogramm wie ein Office-Writer. Die teilnehmenden Programme in der Session sind auch keine Plugins. Wenn Sie die Session beenden wird ein Signal an alle teilnehmenden Programme gesendet, dass sie speichern sollen. Das kann ein paar Momente dauern, in denen Sie "live" mitverfolgen, wie die einzelnen Programme sich beenden und aus der Session verschwinden. Es ist alles in Ordnung. *Ich habe ein Programm hinzugefügt aber es speichert nicht mit der Session*: Unterstützt das Programm Session Management? Wenn nicht, kann Agordejo nichts tun. Aber Sie können die Programmentwickler bitten mit uns Kontakt aufzunehmen (info@laborejo.org) und wir können zusammen an der Unterstützung arbeiten. *Die Programme hängen beim Beenden*: Das tut uns leid. Eigentlich sind die Programme selbst schuld, aber auch wir sind daran interessiert die Situation zu verbessen, indem wir in Zukunft zumindest eine Notlösung anbieten. *Agordejo startet nicht mehr! Ich starte das Programm aber ich sehe nichts.*: Wahrscheinlich ist Agordejo unsichtbar, weil Sie es aus dem Tray heraus beendet hatten. Ist es im Tray? Eigentlich hätte ein kleines Nachrichtenfenster aufpoppen sollen. Falls in ihrem Window-Manager kein Tray vorhanden ist sollte das Programm immer sichtbar sein. Bei besonderen Window-Managern (bei der großen Auswahl in Linux) kann es sein, dass die Tray-Erkennung nicht richtig funktioniert hat. Notfallplan ist es `~/.config/LaborejoSoftwareSuite/agordejo` zu löschen. Dabei werden KEINE Sessions gelöscht, sondern nur lokale Einstellungen wie die Sichtbarkeit des Programmfensters. Beim nächsten Start wird Agordejo sich verhalten wie beim allerersten. *JACK ist abgestürzt. Viele Programme hängen. Was tun um Datenverlust zu vermeiden?*: Vermutlich sind bereits viele Programme der Session nicht mehr richtig lauffähig und reagieren nicht mehr. Am besten benutzen Sie die `Session Abbrechen` Funktion und starten alles neu. Wenn die Daten tatsächlich schon lange ungespeichert waren kann man auch ein Speichern/Beenden wagen. Dann kann es aber sein, dass man beim nächsten Start einige Jack connections von Hand neu ziehen muss. Wer extrem sicher gehen möchte kann vor dem Beenden der Session (mit unweigerlichen abstürzen) manuell im Dateimanager eine Kopie des Session-Verzeichnisses machen. *Ein Programmupdate hat meine Session kaputtgemacht, weil es seine Dateien nicht mehr laden kann.*: Das ist leider ein Problem, dass auch Agordejo nicht lösen kann. Es passiert mit LV2-Plugins ebenso wie mit alle anderen Software, etwa Officeprogramme. Falls Sie befürchten, dass ein Programm in Zukunft inkompatibel wird notieren Sie sich dessen Versionsnummer in den Session-Notizen, damit Sie zumindest zur Not die alte Programmversion wieder installieren können (auch wenn das sehr umständlich ist). *Was ist besser? Monolithische DAW oder Session Management?*: Warum nicht beides? Es gibt keinen Konflikt. Session Management lohnt sich ab zwei teilnehmenden Programmen, und auf die kommt man so gut wie immer. Sie sollten sich nicht genötigt fühlen plötzlich alles modular mit Einzelprogrammen zu machen, nur weil Sie einen Session Manager benutzen. Agordejo ist dafür da, ihre Musikproduktion einfacher zu machen. Wenn es schneller und bequemer ist alle Plugins und Effekte z.B. in einer einzelnen Carla-Instanz zu verwalten dann sollten Sie genau das machen. Wenn Sie prinzipiell alles in Ardour machen wollen, machen Sie das, aber starten Sie Ardour trotzdem im Session Management, denn kein Programm kann alles alleine, und der Zeitpunkt wird kommen, an dem Sie ein weiteres hinzufügen. Session Management ist andere Hirarchieebene. So sind Sequencer oder DAWs selbst keine Plugins. Patroneo gehört nicht "in" Ardour und Ardour gehört nicht "in" Laborejo. Und schon in diesem Beispiel erfüllt jedes der Programme eine Rolle, die die anderen beiden nicht leisten können, da sie einer anderen Design-Philosophie folgen. Und mehr: Manche Programme können keine Plugins hosten, manche können keine Audiodateien exportieren. Das sind deswegen keine schlechten Programme, sondern welche, die sich auf eine Aufgabe konzentrieren. Darüberhinaus gibt es eine Menge Software, die nicht direkt Musikproduktion ausübt, aber trotzdem inhaltlich dazu gehört: Open Broadcast Studio (OBS), Musikplayer, Schreib- und Grafikprogramme etc. *Agordejo beinhaltet Funktionalität, die nicht seine Aufgabe ist*: Musikproduktion ist sehr komplex und Komplexität ist unvermeidlich. Sie ist wie ein Wasserbett: Drückt man die auf der einen Seite runter, muss etwas an einen anderen Stelle hochdrücken. Macht man ein programm "clean and lean", und implementiert damit nur einen Teil des kompletten Arbeitsablaufs, dann kommt der fehlende Teil woanders wieder hoch. Ein minimalistischer Session Manager provoziert Plugins (nicht LV2), Helper-Scripts, Workarounds und Hacks. z.B. Dateiverwaltung nicht mit einzuschließen provoziert Anwender-Fehler im Dateimanager (wie das Löschen der falschen Dateien). Wenn der SM weiß was zu tun ist, und er es tun kann, dann soll er es machen. Oder Abstürze: Technisch gesehen sind abstürzende Programme nicht das "Problem" des Session Managers, aber sie sind Teil der Softwarewirklichkeit. Abstürze passieren jeden Tag und nun muss man damit umgehen. Kann Agordejo die Arbeit vereinfachen und helfen den guten Zustand wieder herzustellen? Dann sollte das geschehen. Session Management ist außerdem eine Chance auch komplexe technische Szenarios zu vereinfachen, z.B. Sessions über das Netzwerk zu verteilen. == Installation und Start Agordejo ist exklusiv für Linux. Am besten installieren Sie Agordejo über deinen Paketmanager. Falls es dort nicht vorhanden ist, oder nur in einer veralteten Version, bitten sie ihre Linuxdistribution Agordejo bereitzustellen. Falls nicht in den Paketquellen vorhanden kann man Agordejo auch selbst "bauen". .Abhängigkeiten* * Eine Liste der Abhängigkeit befindet sich in der README.md * Kompilieren und Installieren geht entweder mit einem Releasedownload oder mit der Git-Version: ** Laden Sie von https://www.laborejo.org/downloads die aktuelle Version herunter und entpacken Sie sie. ** `git clone https://git.laborejo.org/lss/agordejo.git` * Wechseln Sie in das neue Verzeichnis und benutzen diese Befehle: *`./configure --prefix=/usr` ** Das Standardprefix is /usr/local * `make` * `sudo make install` Nun ist das Programm durch `agordejo` in ihrem Terminal oder Programmstarter vorhanden. In der Datei README.md befinden sich weitere Möglichkeiten agordejo zu starten. Diese sind zum Musikmachen nicht praktikabel, aber nützlich für Tests und Entwicklung. == Helfen und Entwicklung Sie können Agordejo auf viele Arten und Weisen helfen: Testen und Fehler melden, übersetzen, marketing, anderen Nutzern helfen und schließlich programmieren. === Testen und Programmfehler Falls Sie einen Fehler im Programm entdecken (oder es zu langsam läuft) melden Sie diese bitte auf eine Art und Weise, die ihnen am besten passt. .Kontaktmöglichkeiten * Bugs und Probleme melden: https://www.laborejo.org/bugs * Webseite: https://www.laborejo.org * E-Mail: info@laborejo.org * Wenn Sie die Gelegenheiten sehen und wissen, dass ein Entwickler es lesen wird sind auch Foren, Socialmedia etc. möglich. == Entwicklung Falls Sie an der Entwicklung interessiert sind, melden Sie sich am besten direkt bei uns (s.o.) Kurzversion: clone git, programmieren, einen git-patch erstellen oder uns eine git URL zukommen lassen. === Übersetzungen Agordejo ist mit Hilfe der Qt-Toolchain sehr einfach zu übersetzen, ohne, dass man dafür Programmieren muss. Die einfachste Variante ist es einfach die Entwickler anzusprechen und sie werden die neue Sprache einrichten. Hier ist dennoch die komplette Anleitung, um eine Übersetzung komplett alleine anzufertigen und in das Programm einzubinden. So fügt man eine neue Sprache hinzu: * Öffnen Sie ein Terminal und navigieren zu qtgui/resources/translations * Bearbeiten Sie die Datei `config.pro` in einem Texteditor. ** Hängem Sie in der letzten Zeile den Namen der neuen Sprache an, in der Form `XY.ts`, wobei XY der Sprachcode ist. ** Achten Sie bitte darauf ein Leerzeichen zwischen den einzelnen Sprachen zu lassen * Führen Sie `sh update.sh` im selben Verzeichnis aus. ** Das Programm hat nun eine neue `.ts`-Datei im Verzeichnis erstellt. * Starten Sie Qt Linguist mit `linguist-qt5` (kann evtl. anders heißen) und öffnen von dort die neu generierte Datei. * Wählen Sie die "Target Language", also Zielsprache, aus und benutzen das Programm um eine Übersetzung anzufertigen. * Senden Sie uns bitte die .ts Datei, z.B. per E-Mail an info@laborejo.org (s.u bei Bugs und Programmfehler für mehr Kontaktmöglichkeiten) Die Übersetzung können Sie auch selbst, zum Testen, einbinden. Dafür sind rudimentäre Python Kentnisse nötig. * Im Qt Linguist "Datei" Menü ist eine "Release" Option. Das erstellt eine `.qm` Datei im gleichen Verzeichnis wie die `.ts` Datei.* Bearbeiten Sie `qtgui/resources/resources.qrc` und kopieren die Zeile `translations/de.qm` . Dabei das Länderkürzel zum Neuen ändern. * Führen Sie `sh buildresources.sh` aus * Bearbeiten Sie `engine/config.py`: Die neue Sprache hinzufügen. z.B. `{"German":"de.qm", "Esperanto:"eo.qm"}` ** Um den Sprachstring herauszufinden öffnen Sie den `python3`-Interpreter im Terminal und führen aus: ** `from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())` Um die neue Übersetzung zu testen starten Sie das Programm, falls ihr System bereits auf diese Sprache eingstellt ist. Ansonsten starten Sie agordejo mit diesem Befehl, Sprachcode ändern, vom Terminal aus: * `LANGUAGE=de_DE.UTF-8 ./agordejo -V` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/german.adoc.template0000644000175000017500000001154700000000000020576 0ustar00nilsnils:Author: :Version: :iconfont-remote!: :!webfonts: //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/LICENSE. //// //// https://powerman.name/doc/asciidoc https://asciidoctor.org/docs/user-manual/ //// :sectnums: :toc: left :toc-title: Inhaltsverzeichnis :toclevels: 3 = // Don't write in the empty line above line. It will be interpreted as author html tag Für Programmversion == Installation und Start ist exklusiv für Linux. Am besten installieren Sie über deinen Paketmanager. Falls es dort nicht vorhanden ist, oder nur in einer veralteten Version, bitten sie ihre Linuxdistribution bereitzustellen. Falls nicht in den Paketquellen vorhanden kann man auch selbst "bauen". .Abhängigkeiten* * Eine Liste der Abhängigkeit befindet sich in der README.md * Kompilieren und Installieren geht entweder mit einem Releasedownload oder mit der Git-Version: ** Laden Sie von https://www.laborejo.org/downloads die aktuelle Version herunter und entpacken Sie sie. ** `git clone https://git.laborejo.org/lss/.git` * Wechseln Sie in das neue Verzeichnis und benutzen diese Befehle: *`./configure --prefix=/usr` ** Das Standardprefix is /usr/local * `make` * `sudo make install` Nun ist das Programm durch `` in ihrem Terminal oder Programmstarter vorhanden. In der Datei README.md befinden sich weitere Möglichkeiten zu starten. Diese sind zum Musikmachen nicht praktikabel, aber nützlich für Tests und Entwicklung. == Helfen und Entwicklung Sie können auf viele Arten und Weisen helfen: Testen und Fehler melden, übersetzen, marketing, anderen Nutzern helfen und schließlich programmieren. === Testen und Programmfehler Falls Sie einen Fehler im Programm entdecken (oder es zu langsam läuft) melden Sie diese bitte auf eine Art und Weise, die ihnen am besten passt. .Kontaktmöglichkeiten * Bugs und Probleme melden: https://www.laborejo.org/bugs * Webseite: https://www.laborejo.org * E-Mail: info@laborejo.org * Wenn Sie die Gelegenheiten sehen und wissen, dass ein Entwickler es lesen wird sind auch Foren, Socialmedia etc. möglich. == Entwicklung Falls Sie an der Entwicklung interessiert sind, melden Sie sich am besten direkt bei uns (s.o.) Kurzversion: clone git, programmieren, einen git-patch erstellen oder uns eine git URL zukommen lassen. === Übersetzungen ist mit Hilfe der Qt-Toolchain sehr einfach zu übersetzen, ohne, dass man dafür Programmieren muss. Die einfachste Variante ist es einfach die Entwickler anzusprechen und sie werden die neue Sprache einrichten. Hier ist dennoch die komplette Anleitung, um eine Übersetzung komplett alleine anzufertigen und in das Programm einzubinden. So fügt man eine neue Sprache hinzu: * Öffnen Sie ein Terminal und navigieren zu qtgui/resources/translations * Bearbeiten Sie die Datei `config.pro` in einem Texteditor. ** Hängem Sie in der letzten Zeile den Namen der neuen Sprache an, in der Form `XY.ts`, wobei XY der Sprachcode ist. ** Achten Sie bitte darauf ein Leerzeichen zwischen den einzelnen Sprachen zu lassen * Führen Sie `sh update.sh` im selben Verzeichnis aus. ** Das Programm hat nun eine neue `.ts`-Datei im Verzeichnis erstellt. * Starten Sie Qt Linguist mit `linguist-qt5` (kann evtl. anders heißen) und öffnen von dort die neu generierte Datei. * Wählen Sie die "Target Language", also Zielsprache, aus und benutzen das Programm um eine Übersetzung anzufertigen. * Senden Sie uns bitte die .ts Datei, z.B. per E-Mail an info@laborejo.org (s.u bei Bugs und Programmfehler für mehr Kontaktmöglichkeiten) Die Übersetzung können Sie auch selbst, zum Testen, einbinden. Dafür sind rudimentäre Python Kentnisse nötig. * Im Qt Linguist "Datei" Menü ist eine "Release" Option. Das erstellt eine `.qm` Datei im gleichen Verzeichnis wie die `.ts` Datei.* Bearbeiten Sie `qtgui/resources/resources.qrc` und kopieren die Zeile `translations/de.qm` . Dabei das Länderkürzel zum Neuen ändern. * Führen Sie `sh buildresources.sh` aus * Bearbeiten Sie `engine/config.py`: Die neue Sprache hinzufügen. z.B. `{"German":"de.qm", "Esperanto:"eo.qm"}` ** Um den Sprachstring herauszufinden öffnen Sie den `python3`-Interpreter im Terminal und führen aus: ** `from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())` Um die neue Übersetzung zu testen starten Sie das Programm, falls ihr System bereits auf diese Sprache eingstellt ist. Ansonsten starten Sie mit diesem Befehl, Sprachcode ändern, vom Terminal aus: * `LANGUAGE=de_DE.UTF-8 ./ -V` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/german.part.adoc0000644000175000017500000005502100000000000017724 0ustar00nilsnils== Präambel Session Management führt zur Vereinfachung von Arbeitsabläufen, Übersicht und Kontrolle über Programme und Daten und eine gehörige Portion positive Bequemlichkeit :) Kein Programm existiert für sich alleine, denn kein Programm kann alles leisten, was für heutige Musikproduktion nötig ist. Direkt ersichtlich ist das in einer JACK Umgebung, die prinzipiell modular ist: Verschiedene Programme erfüllen verschiedene Zwecke und "sprechen" miteinander, indem sie sich Daten schicken. Ein Sequencer schickt Midi an einen Sampler oder Synthesizer, der mit einem Plugin-Host für Effekte verbunden ist usw. Selbst die monolithischsten All-In-One DAWs müssen, oder möchten, irgendwann mit der Außenwelt kommunizieren. z.B. um sich mit einem Screenrecorder oder Streamingprogramm zu verbinden, einen Wordprozessor mit Aufnahmereihenfolge oder Liedtexten zu starten, um Musik zu visualisieren oder (um ehrlich zu bleiben) eine Funktion zu nutzen, die es in diesem Programm eben nicht gibt. Einen Großteil der Arbeit erledigt bereits das JACK-Subsystem. Alle Programme können ihre Musik in Echtzeit teilen, verfügen über synchronisierte Timelines und spielen im gleichen Tempo. Was übrig bleibt ist die leidige Arbeit jedesmal alle Programme zu starten, die Projektdateien zu laden, alle Audiokanäle zu verbinden usw. Session Management im allgemeinen (z.B. extra geschriebene Starter-Scriptdateien) und Agordejo im speziellen nehmen Ihnen diese Arbeit ab, oder vereinfachen sie zumindest stark. Im Gegensatz zum erwähnten selbstgeschriebenen Script müssen Sie sich nicht im Vorraus für ein Setup entscheiden, sondern alles wird automatisch verwaltet, solange Sie alles durch den Session Manager Agordejo starten. === Anwendungsbeispiel * Agordejo starten (Startmenü, Terminal etc.) * Man befindet sich nun in der "Schnellen Ansicht" um eine Session zu starten * Der große Knopf: "Starte Neue Session" * Nun bekommt man eine Auswahl an Programmen: * Ein einfacher Klick mit der Maus startet ein Programm * Ein weiterer Klick versteckt (oder zeigt anschließend) dessen grafische Oberfläche * Sollte das Programm abstürzen bekommt man das signalisiert und kann es neustarten. * Audio- und Midiports können nun in einer Patchbay miteinander verbunden werden. Die Verbindungen werden in Agordejo gespeichert. * Um andere Programme und erweiterte Funktionen zu bekommen kann man in den Tab "Volle Ansicht" wechseln * Der Name der Session ist bisher einfach ein Datum. Durch Klick auf dieses kann man einen richtigen Namen eingeben. Etwa "Mein Lied" * Ist man soweit fertig kommt man durch den Knopf "Speichern und Schließen" wieder in die Sessionauswahl zurück * Nun könnte Agordejo geschlossen werden. * Alle gespeicherten Daten liegen in einem einzigen Verzeichnis auf der Festplatte (`~/NSM Sessions/Mein Lied`) * Die Session kann weitergeführt werden: Nach dem Klick auf den Namen starten alle Programme automatisch und verbinden ihre JACK-Ports untereinander. Die "Schnelle Ansicht" ist ein guter Einstieg. Üblicherweise möchte man jedoch bald in die volle Ansicht wechseln, und sei es nur um ein Programm zwei mal zu starten oder um zwischendurch manuell zu speichern. == Die Schnelle Ansicht Die Schnelle Ansicht ist eine aufs Minimum reduzierte Umgebung. Sie, als Benutzer, sollen so wenig Entscheidungen wie möglich zu treffen haben und so schnell wie möglich (mit den wenigstens Klicks) eine alte Session starten oder eine neue erstellen können. Aus Stabilitätsgründen werden nur Programme zum Starten angeboten, von denen bekannt ist, dass Sie korrekt mit dem Session Management Agordejos zusammenarbeiten. Die Schnelle Ansicht ist lediglich eine Ansicht. Es besteht kein technischer Unterschied zur vollen Session und Sie können jederzeit hin- und herschalten. === Session Auswählen Hier gibt es nur zwei Möglichkeiten: Klicken Sie auf "Starte Neue Session" um dies zu tun, oder wählen Sie eine der bestehenden Sessions aus um diese fortzuführen (sofern vorhanden). === In einer Session Der Name kann geändert werden. Wir empfehlen ein Datum in der Form YYYY-MM-DD gefolgt von einem aussagekräftigen Namen. Bitte beachten Sie, dass die Namensänderung erst wirksam wird (z.B. in Form des Sessionvereichnisses auf der Platte) wenn Sie die Session schließen. Als nächstes steht ein größeres Textfeld für Notizen zur Verfügung. Schreiben Sie was Sie möchten. TODO-Listen, Liedtexte, Credits und Quellen von externen Samples etc. Programme werden durch Icons symbolisiert. Ein Mausklick startet das Programm (das kann einen Moment dauern, darauf haben wir keinen Einfluss). Der Status des Programmes wird durch ein Symbol dargestellt: Das "Play"-Symbol für das laufende Programm, ein Auge bedeutet "Programmfenster versteckt", das "Power On/Off"-Symbol bedeutet, dass Programm wurde beendet (oder Absturz). Falls das Programm es unterstützt kann ein laufendes Programmfenster versteckt werden. z.B. ein Synthesizer, einmal eingerichtet, muss nicht permanent zu sehen sein. Klicken Sie einfach erneut auf das Programmicon. Entweder passiert nichts (das Programm unterstützt es nicht) oder es wird versteckt. Ein weiterer Klick auf ein versteckes Fenster zeigt es wieder. Jedes Programm ist selbst dafür verantwortlich ob es seinen Fensterzustand speichert. Manche Programme starten bereits versteckt. Es gibt *keine* Möglichkeit ein Programm in dieser Ansicht zu beenden. Wechseln Sie hierzu in die volle Ansicht oder benutzen Sie die Schließenfunktion des Programmes selbst. Es gibt auch *keine* Möglichkeit ein Programm mehrfach zu starten, z.B. für einen Synthesizer pro Spur. Wechseln sie hierzu in die volle Ansicht. Wenn Sie Programme durch die volle Ansicht mehrfach gestartet haben können Sie weiterhin die schnelle Ansicht benutzen um die Session zu verwalten, allerdings ist nicht definiert auf welche Programminstanz sich ein Klick auf das Programmicon (z.B. zum Verstecken) auswirkt. Mehrere Instanzen sind ein klares Zeichen dafür, dass Sie bereit für die volle Ansicht sind. Wie Sie irgendwann feststellen werden sind nicht *alle* verfügbaren Programme in der Liste, und es gibt auch keine Möglichkeit Programme zu starten die zwar gut in einer Session funktionieren (z.B. ein Stimmgerät), aber nicht explizit dafür geschrieben sind, z.B. ihren Status nicht zurückmelden. Diese Programme sollten Sie besser in der vollen Ansicht verwalten. Ist ein Programm leider abgestürzt können Sie es nur neu starten und auf dessen automatische Speicherung hoffen. Viel Glück. == Die Volle Ansicht Einige Konzepte wurde bereits im Kapitel "Die Schnelle Ansicht" erklärt. Es wird vorrausgesetzt, dass Sie dieses gelesen haben. === Session Auswählen Sessions stellen sich als Tabelle dar, die Sie durch den Klick auf eine Spaltenüberschrift ordnen können. So wird hier gezeigt wie die Session heißt und wann das letzte mal gespeichert wurde, wohl die beiden wichtigsten Informationen. Dann wird gezeigt wie viele Programme/Clients die Session verwaltet und ob sie Symbolische Links enthält. Letzteres steht wahrscheinlich auf "Ja", wenn Sie einen Sampler o.ä. benutzen, der große Audiodateien geladen hat. Diese werden zunächst nur in die Session gelinkt, und nicht kopiert, um Speicherplatz zu sparen. Der angezeigte Speicherverbrauch ist nicht der tatsächliche Verbrauch, sondern beinhaltet die Größen der Symlinks. Erst wenn Sie die Session archivieren oder die Links durch reale Dateien ersetzen stimmt die angezeigte Zahl. Schließlich wird auch das Verzeichnis angegeben, in dem die Dateien tatsächlich gespeichert sind. Sessions stellen den Verzeichnisbaum dar. Eine Session ist immer ein "Blatt" und kann keine Subsessions enthalten. Sie können beim Anlegen oder Umbenennen die Sessions auch im Baum anordnen indem Sie die gewohnte Schrägstrichschreibweise benutzen: `song123` -> `Neues Album/Song 123` oder `Versuche/asdf` -> `Romantische Pop-Balladen/Mein Herz wird weiter schlagen`. Wie Sie ihre Sessions organisieren, und wie viele Unterverzeichnisse Sie anlegen, steht Ihnen frei. Es ist allerdings nicht erlaubt den Namen mit `/` anfangen zu lassen oder die speziellen Zeichen `..` zu benutzen. Die Baumansicht kann durch eine Checkbox an der linken Seite deaktiviert werden, um etwa besser sortieren zu können. Das ist nur eine Ansicht, ihre Daten bleiben unangetastet. Jede Session verfügt über ein Kontextmenü (z.B. rechte Maustaste) mit weiteren Optionen: Sie können eine Session umbennen, löschen (inkl. aller dazugehörigen Dateien auf der Festplatte!) und mehr. Diese Funktionen sind gleichwertig zu ihrem Dateimanager. Falls Sie möchten, können Sie auch einfach die Sessionverzeichnisse selbst umbennen, verschieben oder löschen (sofern diese nicht gerade geöffnet ist). Dazu muss Agordejo nicht neugestartet werden, es reagiert selbst auf die Änderungen. Hier gibt es auch die Möglichkeit eine sogenannte "Lock"-Datei zu entfernen. Das ist eine Datei, die angelegt wird sobald die Session startet und gelöscht wenn beendet. Damit weiß das Programm, welche Session gerade läuft. Ist eine Lockdatei vorhanden, kann die Session nicht geöffnet werden! Das sollte im normalen Betrieb nicht vorkommen. Falls aber ein Stromausfall o.ä. mitten in einer Session passiert ist kann die Lockdatei übrig bleiben, obwohl offensichtlich keine Session geöffnet ist. In diesem Fall entsperren Sie die Session hierdurch manuell. Klicken Sie auf "Neue Session" um diese anzulegen. Im Gegensatz zur schnellen Ansicht müssen Sie hier den Namen direkt eingeben. Wie oben erwäht können Sie dazu den Verzeichnisbaum benutzen. Darüberhinaus wird angeboten verschiedene (fast schon obligatorische) Programme direkt mitzustarten. Im Normalfall sollte man alle Vorschläge akzeptieren. Es gibt jederzeit die Möglichkeit diese wieder zu entfernen. Ein Doppelklick auf eine existierende Session (oder der Knopf "Lade Ausgewählte") machen genau das. === In einer Session Die volle Ansicht ist in drei Bereiche eingeteilt: Programmstarter, Programme in der laufenden Session und die Session-Notizen. Dazu gibt es ein dynamisches Menü. Auf der linken Seite sehen die den Programmstarter. Ein Doppelklick startet eine Programminstanz in der Session. Sie können ein Programm auch mehrmals starten. Welche Programme zur Verfügung stehen entnehmen Sie bitte dem Kapitel "Programm-Datenbank". ==== Laufende Programm Gestartete Programme befinden sich auf der rechten Seite. Ein Doppelklick schaltet die Sichtbarkeit um, sofern das Programm unterstüzt sein Fenster zu verstecken. Falls nicht passiert nichts. Pro Programm gibt es folgende Informationen: * Der Name (evtl. mit Icon) * Ein "Label", das Programme frei benutzen können (z.B. zeigt Fluajho hier das geladene .sf2 an) * Den Programmstatus (auf Englisch, da es sich um definierte Schlüsselworte handelt). * Stopped / Gestoppt, läuft nicht * Ready / Läuft und Bereit * Launch / Startet. Wenn das Programm hier stehen bleibt, aber funktioniert, handelt es sich um eins, dass nicht speziell den Sessionmodus unterstützt. Agordejo kann nicht wissen, ob es bereits läuft oder nicht. Alles ist in Ordnung! :) * Weitere Zustände sind nur Übergänge und meist nur sehr kurz zu sehen, z.B. Open / Läd gerade * Sichtbarkeit (Ein Kreuz für sichtbar, Leer für unsichtbar) * Änderungen - Gibt es momentan ungespeicherte Änderungen? * ID - Ein eindeutiges Kürzel mit dem man auch mehrere Instanzen des gleichen Programmes auseinander halten kann Alle weiteren Funktionen sind durch das Menü oder Kontextmenü zugänglich. Ein Klick auf ein Programm wählt es aus, und das Clientmenü in der Menüleiste gilt nun dafür. Alternativ kann mit Rechtsklick auf den Eintrag das Kontextmenü geöffnet werden, das identisch ist. Neben den selbsterklärenden Funktionen gibt es noch: * Umbenennen gibt dem Programm einen selbstgewählten Namen, besonders um seinen Zweck deutlich zu machen und es besser von anderen zu unterscheiden. Diese Funktion steht nur zur Verfügung, wenn `nsm-data` in der Session läuft. * Speichern weist nur dieses Programm an abzuspeichern * Entfernen nimmt das Programm aus der Session. Dabei werden jedoch keine Dateien gelöscht. Zur Zeit muss leider noch von Hand im Dateimanager "aufgeräumt" werden. Befindet sich der Client "nsm-data" in der Session (das ist Voreinstellung) steht im unteren Bereich ein großes Textfeld für Notizen zur Verfügung. Es ist dasselbe wie in der schnellen Ansicht. Schreiben Sie was Sie möchten: TODO-Listen, Liedtexte, Credits und Quellen von externen Samples etc. ==== Das Session Menü Im Gegensatz zur schnellen Ansicht stehen hier weitere Menüs zur Verfügung, die auch über die üblichen Tastaturkürzel zu erreichen sind (Strg+S für Speichern etc.). * Speichern weist alle Programme an zu speichern, die Session läuft weiter * Speichern und Schließen beendet die Session, vorher speichern alle Programme noch einmal ab * Abbrechen beendet die Session, ohne dass die Programme abspeichern * Speichern Unter speichert die Session unter einem anderen Namen und schließt die laufende ohne abzuspeichern. Ab nun arbeitet man in der neuen Session. * Client Hinzufügen bietet die Option ein beliebiges Programm hinzuzufügen, egal ob es in der Programmdatenbank ist, oder nicht. * Es werden alle installierten Programme vorgeschlagen. Agordejo überprüft diese nicht auf Sinnhaftigkeit für eine Musik-Session, oder auch nur auf Lauffähigkeit. Sie finden hier `ls` wie auch `agordejo` selbst. == Programm-Datenbank Agordejos Programmstarter basiert auf einer Programmdatenbank, die sich teilweise selbst erstellt, teilweise von Hand eingepflegt wurde. Das bedeutet nichts anderes, als das alle installieren Programme in ihrem System überprüft werden (wie ein Startmenü) und Ihnen im Agordejo-Starter nur das angeboten wird, was Sie auch tatsächlich installiert haben. Beim ersten Start wird daher die Programmdatenbank erstellt. Je nach System kann dies einige Augenblicke bis einige Minuten dauern. Wenn Sie Audio-Programme neu installieren, oder deinstallieren müssen Sie die Datenbank selbst aktualisieren. Im Steuerungsmenü gibt es den Befehl. Programminstallationen sind sogar möglich während Agordejo läuft (auch in einer Session). Nach einem DB-Update stehen Ihnen sofort alle neuen Programme zur Verfügung. Wenn Sie ein installiertes Programm nicht in unserer Liste sehen, aber von dem Sie sicher sind, dass es Session Management unterstützt melden Sie es bitte an info@laborejo.org oder unter https://laborejo.org/bugs . Darüberhinaus können Sie (in der vollen Ansicht) Programme hinzufügen, die nicht in der Datenbank sind. Siehe dort. === Für Fortgeschrittene Die eiserne Regel ist, dass nur Programme im $PATH in die Datenbank aufgenommen werden. Absolute Pfade sind unzulässig, selbst wenn man den Programmnamen selbst durch das Menü eingibt. Allerdings möchte man manchmal Software nur ausprobieren, oder ist selbst Entwickler und möchte ohne systemweite Installation testen. In den Einstellungen im Steuerungsmenü gibt es einen Tab "Programm-PATH", wo Sie eigene Suchpfade hinzufügen können. Ein absoluter Pfad pro Zeile, keine Wildcards, Trailing Slash spielt keine Rolle. Zum Beispiel: `/home/myuser/sources/newsequencer/bin/` Diese Suchpfade werden nicht in der Session gespeichert sondern lokal in ihrem `~/.config` Verzeichnis. == Tray Agordejo verfügt über ein Tray-Icon, sofern ihr Windowmanager das unterstüzt. Ein Klick auf das Trayicon zeigt oder versteckt Agordejo. Schließt man Agordejo über die normale Windowmanagerfunktion, etwa ein Klick auf das [X], wird das Programm und die Session nicht beendet, sondern in den Tray minimiert. Ein Rechtsklick auf das Icon bietet Schnellzugriff auf häufige Funktionen: Sie können hier die zuletzt benutzen Sessions direkt starten. Läuft bereits eine Session können Sie speichern, abbrechen etc. Agordejo kann hier auch komplett beendet werden. == Netzwerk-Sessions Die Funktionalität Sessions im lokalen Netzwerk zu verteilen ist für eine spätere Programmversion vorgesehen. == Programmparameter Als fortgeschrittener Benutzer können Sie Agordejo im Terminal starten und dort einige Parameter angeben. Für eine vollständige Liste benutzen Sie bitte den --help Parameter. Eine Auswahl: * `--session neuesAlbum/meinLied` startet direkt die angegebene Session. * `--continue` startet die zuletzt benutzte Session * `--hide` startet Agordejo als Trayicon. * `--url osc.udp://myhost.localdomain:14294/` verbindet sich zu diesem Server, falls vorhanden, oder startet den internen Session-Server unter dieser Adresse. Dies ist eine sehr technische Option und wird wahrscheinlich nicht benötigt. * `--session-root /home/benutzer/produktion2020` setzt das Wurzelverzeichnis. Nur Sessions in diesem Verzeichnis werden angezeigt, alles wird dort gespeichert. Die Kombination von `--continue` und `--hide` ergibt einen Modus, den viele Leute vom Session Management erwarten: dort weiter machen wo man aufgehört hat, ohne dass Extrafenster angezeigt werden. Falls Ihr System über ein Startmenü verfügt finden Sie daher neben dem normalen Agordejo-Starter auch eine "Agordejo Continue"-Verknüpfung für genau diesen Modus. == Verschiedenes / Erklärungen / FAQ *Session Speichern und Beenden reagiert langsam*: Agordejo ist kein Einzelprogramm wie ein Office-Writer. Die teilnehmenden Programme in der Session sind auch keine Plugins. Wenn Sie die Session beenden wird ein Signal an alle teilnehmenden Programme gesendet, dass sie speichern sollen. Das kann ein paar Momente dauern, in denen Sie "live" mitverfolgen, wie die einzelnen Programme sich beenden und aus der Session verschwinden. Es ist alles in Ordnung. *Ich habe ein Programm hinzugefügt aber es speichert nicht mit der Session*: Unterstützt das Programm Session Management? Wenn nicht, kann Agordejo nichts tun. Aber Sie können die Programmentwickler bitten mit uns Kontakt aufzunehmen (info@laborejo.org) und wir können zusammen an der Unterstützung arbeiten. *Die Programme hängen beim Beenden*: Das tut uns leid. Eigentlich sind die Programme selbst schuld, aber auch wir sind daran interessiert die Situation zu verbessen, indem wir in Zukunft zumindest eine Notlösung anbieten. *Agordejo startet nicht mehr! Ich starte das Programm aber ich sehe nichts.*: Wahrscheinlich ist Agordejo unsichtbar, weil Sie es aus dem Tray heraus beendet hatten. Ist es im Tray? Eigentlich hätte ein kleines Nachrichtenfenster aufpoppen sollen. Falls in ihrem Window-Manager kein Tray vorhanden ist sollte das Programm immer sichtbar sein. Bei besonderen Window-Managern (bei der großen Auswahl in Linux) kann es sein, dass die Tray-Erkennung nicht richtig funktioniert hat. Notfallplan ist es `~/.config/LaborejoSoftwareSuite/agordejo` zu löschen. Dabei werden KEINE Sessions gelöscht, sondern nur lokale Einstellungen wie die Sichtbarkeit des Programmfensters. Beim nächsten Start wird Agordejo sich verhalten wie beim allerersten. *JACK ist abgestürzt. Viele Programme hängen. Was tun um Datenverlust zu vermeiden?*: Vermutlich sind bereits viele Programme der Session nicht mehr richtig lauffähig und reagieren nicht mehr. Am besten benutzen Sie die `Session Abbrechen` Funktion und starten alles neu. Wenn die Daten tatsächlich schon lange ungespeichert waren kann man auch ein Speichern/Beenden wagen. Dann kann es aber sein, dass man beim nächsten Start einige Jack connections von Hand neu ziehen muss. Wer extrem sicher gehen möchte kann vor dem Beenden der Session (mit unweigerlichen abstürzen) manuell im Dateimanager eine Kopie des Session-Verzeichnisses machen. *Ein Programmupdate hat meine Session kaputtgemacht, weil es seine Dateien nicht mehr laden kann.*: Das ist leider ein Problem, dass auch Agordejo nicht lösen kann. Es passiert mit LV2-Plugins ebenso wie mit alle anderen Software, etwa Officeprogramme. Falls Sie befürchten, dass ein Programm in Zukunft inkompatibel wird notieren Sie sich dessen Versionsnummer in den Session-Notizen, damit Sie zumindest zur Not die alte Programmversion wieder installieren können (auch wenn das sehr umständlich ist). *Was ist besser? Monolithische DAW oder Session Management?*: Warum nicht beides? Es gibt keinen Konflikt. Session Management lohnt sich ab zwei teilnehmenden Programmen, und auf die kommt man so gut wie immer. Sie sollten sich nicht genötigt fühlen plötzlich alles modular mit Einzelprogrammen zu machen, nur weil Sie einen Session Manager benutzen. Agordejo ist dafür da, ihre Musikproduktion einfacher zu machen. Wenn es schneller und bequemer ist alle Plugins und Effekte z.B. in einer einzelnen Carla-Instanz zu verwalten dann sollten Sie genau das machen. Wenn Sie prinzipiell alles in Ardour machen wollen, machen Sie das, aber starten Sie Ardour trotzdem im Session Management, denn kein Programm kann alles alleine, und der Zeitpunkt wird kommen, an dem Sie ein weiteres hinzufügen. Session Management ist andere Hirarchieebene. So sind Sequencer oder DAWs selbst keine Plugins. Patroneo gehört nicht "in" Ardour und Ardour gehört nicht "in" Laborejo. Und schon in diesem Beispiel erfüllt jedes der Programme eine Rolle, die die anderen beiden nicht leisten können, da sie einer anderen Design-Philosophie folgen. Und mehr: Manche Programme können keine Plugins hosten, manche können keine Audiodateien exportieren. Das sind deswegen keine schlechten Programme, sondern welche, die sich auf eine Aufgabe konzentrieren. Darüberhinaus gibt es eine Menge Software, die nicht direkt Musikproduktion ausübt, aber trotzdem inhaltlich dazu gehört: Open Broadcast Studio (OBS), Musikplayer, Schreib- und Grafikprogramme etc. *Agordejo beinhaltet Funktionalität, die nicht seine Aufgabe ist*: Musikproduktion ist sehr komplex und Komplexität ist unvermeidlich. Sie ist wie ein Wasserbett: Drückt man die auf der einen Seite runter, muss etwas an einen anderen Stelle hochdrücken. Macht man ein programm "clean and lean", und implementiert damit nur einen Teil des kompletten Arbeitsablaufs, dann kommt der fehlende Teil woanders wieder hoch. Ein minimalistischer Session Manager provoziert Plugins (nicht LV2), Helper-Scripts, Workarounds und Hacks. z.B. Dateiverwaltung nicht mit einzuschließen provoziert Anwender-Fehler im Dateimanager (wie das Löschen der falschen Dateien). Wenn der SM weiß was zu tun ist, und er es tun kann, dann soll er es machen. Oder Abstürze: Technisch gesehen sind abstürzende Programme nicht das "Problem" des Session Managers, aber sie sind Teil der Softwarewirklichkeit. Abstürze passieren jeden Tag und nun muss man damit umgehen. Kann Agordejo die Arbeit vereinfachen und helfen den guten Zustand wieder herzustellen? Dann sollte das geschehen. Session Management ist außerdem eine Chance auch komplexe technische Szenarios zu vereinfachen, z.B. Sessions über das Netzwerk zu verteilen. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/index.adoc0000644000175000017500000000221000000000000016605 0ustar00nilsnils:Author: Laborejo Software Suite :Version: 0.3.1 :iconfont-remote!: :!webfonts: //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/LICENSE. //// //// https://powerman.name/doc/asciidoc https://asciidoctor.org/docs/user-manual/ //// :nofooter: == Agordejo Multi-Language Documentation image::logo.png["logo", 320, 180] For program version 0.3.1 This site is part of the https://www.laborejo.org[Laborejo Software Suite] Please choose a language * link:english.html[English] * link:german.html[Deutsch (German)] Further Links * https://laborejo.org/bugs/[Bug and Issues, and other Feedback] * Write to info@laborejo.org for any comment or question. New Session Manager * https://linuxaudio.github.io/new-session-manager/api/index.html[API Document] - How to write your own client * https://github.com/linuxaudio/new-session-manager[Sourcecode] and Readme ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/index.adoc.template0000644000175000017500000000217700000000000020433 0ustar00nilsnils:Author: :Version: :iconfont-remote!: :!webfonts: //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/LICENSE. //// //// https://powerman.name/doc/asciidoc https://asciidoctor.org/docs/user-manual/ //// :nofooter: == Multi-Language Documentation image::logo.png["logo", 320, 180] For program version This site is part of the https://www.laborejo.org[Laborejo Software Suite] Please choose a language * link:english.html[English] * link:german.html[Deutsch (German)] Further Links * https://laborejo.org/bugs/[Bug and Issues, and other Feedback] * Write to info@laborejo.org for any comment or question. New Session Manager * https://linuxaudio.github.io/new-session-manager/api/index.html[API Document] - How to write your own client * https://github.com/linuxaudio/new-session-manager[Sourcecode] and Readme ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/manpageinclude.h2m0000644000175000017500000000317200000000000020242 0ustar00nilsnils [name] Agordejo - Music and audio production session manager based on NSM. [usage] Agordejo (Esperanto: 'place to set things up') is a music production session manager. It is used to start your programs, remember their (JACK) interconnections and make your life easier in general. Agordejo does not re-invent the wheel but instead uses the New-Session-Manager daemon and enhances it with some tricks of its own, that always remain 100% compatible with the original sessions. This is a proof of concept version. It aims to show that session management with NSM can be quick and convenient and make the user feel in control. Some functionality has not yet been implemented, most prominently anything related to NSM over network. There is always the possibility to break things when trying out corner cases and hacks. That said, for single-computer sessions with just one daemon and one GUI at the same time Agordejo should provide a good user experience. [Reporting bugs] https://www.laborejo.org/bugs [copyright] Agordejo 0.3.1 - Copyright 2022 Laborejo Software Suite https://www.laborejo.org/ [examples] Run agordejo. You are now in the Quick View mode. Press New Session and add programs through a single click on available program-icons. You can switch to the Full View mode via the tab to get more options. Try right-clicking on many things to get context menus. [see also] The full documentation for Agordejo is maintained as a multi-lingual html site to your systems doc-dir. For example: xdg-open file:///usr/share/doc/agordejo/index.html The documentation can also be found online https://www.laborejo.org/documentation/agordejo ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/nsm-data.10000644000175000017500000000076200000000000016446 0ustar00nilsnils.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.48.5. .TH NSM-DATA "1" "January 2022" "nsm-data 1.1" "User Commands" .SH NAME nsm-data \- manual page for nsm-data 1.1 .SH DESCRIPTION usage: nsm\-data [\-h] [\-v] .PP nsm\-data is a module for Agordejo. It only communicates over OSC in an NSMSession and has no standalone functionality. .SS "options:" .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-v\fR, \fB\-\-version\fR show program's version number and exit ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639688410.2178724 agordejo-0.3.1/documentation/out/CHANGELOG0000777000175000017500000000000000000000000020531 2../../CHANGELOGustar00nilsnils././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272568.274502 agordejo-0.3.1/documentation/out/english.html0000644000175000017500000017374000000000000020015 0ustar00nilsnils Agordejo

For program version 0.3.1

1. Introduction

Agordejo (Esperanto: 'place to set things up') is a music production session manager. It is used to start your programs, remember their (JACK) interconnections and make your life easier in general.

Agordejo does not re-invent the wheel but instead uses the New-Session-Manager daemon and enhances it with some tricks of its own, that always remain 100% compatible with the original sessions.

This is a proof of concept version. It aims to show that session management with NSM can be quick and convenient and make the user feel in control. Some functionality has not yet been implemented, most prominently anything related to NSM over network. There is always the possibility to break things when trying out corner cases and hacks.

That said, for single-computer sessions with just one daemon and one GUI at the same time Agordejo should provide a good user experience.

2. Preamble

Session Management leads to simplification of workflows, overview and control over programs and data and a good portion of convenience :)

No program exists on its own, because no program can do everything that is necessary for today’s music production.

This is obvious in a JACK environment, which is fundamentally modular: Different programs fulfill different functions and "talk" to each other by sending data to each other. A sequencer sends MIDI to a sampler or synthesizer, which is connected to a plug-in host for effects etc.

Even the most monolithic all-in-one DAWs have to, or want to, eventually connect to the outside world. For example, to connect to a screen recorder or streaming program, include a word processor for recording order or lyrics, or to use a function that is simply not available in the DAW.

Much of the work is already done by the JACK subsystem. All programs can share their music in real time, have synchronized timelines and play in the same tempo.

What remains is the tedious work of always starting all programs, loading project files, connecting audio channels etc. Session management in general (e.g. specifically written starter script files) and Agordejo in particular do this work for you, or at the very least, greatly simplify it.

In contrast to the self-written script mentioned above, you don’t have to decide in advance on a setup, but everything is saved automatically as long as you manage everything through Agordejo.

2.1. Example

  • Start Agordejo (Start menu, terminal etc.)

  • You are now in the "Quick View" to start a session

  • Press the big button "Start New Session"

  • Now you get a choice of programs:

  • A single click with the mouse starts a program

  • Another click hides (or subsequently shows) its graphical interface

  • If the program crashes you get a warning and can restart it.

  • Audio and midi ports can now be connected together in a patchbay. The connections are stored in Agordejo.

  • To get more programs and advanced features you can switch to the tab "Full View

  • The name of the session so far is simply a date. By clicking on it you can enter a real name. Like "My song"

  • If you are finished you can return to the session selection by pressing the button "Save and Close"

  • Now Agordejo could be closed itself.

  • All stored data is in a single directory on the hard disk (~/NSM Sessions/My Song)

  • The session can be resumed: After clicking on the name, all programs start automatically and connect their JACK ports to among themselves.

"Quick View" is a good start. However, usually one would like to get into the full view, even if it is only to start a program twice or to save manually.

3. Quick View

The quick view is an environment reduced to the minimum. You, as a user, should have to make as few decisions as possible and start an old session or create a new one as quickly as possible (with the at least clicks).

For stability reasons, only programs are shown that are known to work correctly with Agordejos session management.

The Quick View is only a view. There is no technical difference to the full view and you can switch back and forth at any time.

3.1. Selecting a Session

There are only two options here: Click on "Start New Session" to do so, or select one of the existing sessions to resume it (if available).

3.2. In a Session

The name can be changed. We recommend a date in the form YYYY-MM-DD followed by a informative name. Please note that the name change will only take effect (e.g. renaming the session directory on disk) when you close the session.

Next, a larger text field is available for notes. Write what you want. TODO lists, lyrics, credits and sources of external samples etc.

Programs are symbolized by icons. A mouse click starts the program (which can take a while, we have no influence on that). The status of the program is indicated by a symbol: The "Play" symbol for the running program, the eye means "program window hidden", the "Power On/Off" symbol means that the program has been terminated (or crashed).

If the program supports it, a running program window can be hidden. Synthesizers, once set up, do not have to be permanently visible. Just click on the program icon again. Either nothing happens (the program does not support it) or the window will hide. Another click on a hidden window shows it again. Every program itself is responsible for whether it saves its window state. Also, some programs already start hidden.

There is no way to end or remove a program in this view. To do this, switch to the full view or use the close function of the program itself.

There is also no possibility to start a program multiple times, e.g. one synthesizer per track. If you want to start multiple program instances through the full view times you can still use the quick view to manage the general session, but it is not defined which program instance a click on the icon (e.g. to hide) affects. Multiple instances are a clear indicator that you are ready for the full view.

As you’ll eventually discover, not all available programs are in the list, and there there is also no possibility to start programs that would work well in a session (e.g. a tuner), but are not explicitly written for it, e.g. do not report their status. It is better to manage these programs in full view.

If, unfortunately, a program has crashed, you can only restart it and hope its automatic saving worked. Good luck.

4. Full View

Some concepts have already been explained in the chapter "Quick View". It is expected that you have read this.

4.1. Selecting a Session

Sessions are displayed as a table, which you sort by clicking on a column header. Here is shown how the session is called and when it was saved the last time, probably the two most important pieces of information. Also shown is how many programs/clients are in the session and whether it contains symbolic links. The latter is probably set to "Yes" if use a sampler, or similar, that contains large audio files. These are initially only linked into the session, and not copied, to save disk space. The displayed disk space usage is not the actual one, but includes the sizes of symlink-targets. Only when you archive the session or replace the links with real files the number becomes correct.

Finally, the directory in which the files are actually stored is also given.

Sessions represent the directory tree. A session is always a "leaf" and cannot include subsessions. When creating or renaming sessions, you can also arrange them in the tree by using the usual slash notation: song123New album/song 123 or Test/asdfRomantic pop ballads/My heart will keep beating. How to organize your sessions and how many subdirectories you create is up to you. It is, however, not allowed to start the name with / or use the special characters ... The tree view can be deactivated by a checkbox on the left side, e.g. to be able to sort them. This is just a view, your data remains untouched.

Each session has a context menu (e.g. right mouse button) with further options: You can choose to rename a session, delete it (including all associated files on the hard disk!) and more. These functions are equivalent to your file manager. If you like, you can also use your file manager to rename, move, or delete the session directories themselves (unless they are is currently open). You don’t need to restart Agordejo to do this, it will respond to the changes while running.

There is also the possibility to remove a so-called "lock" file. This is a file which is created when the session starts and deleted when it ends. This tells Agordejo which session is currently running. If a lock file exists, the session cannot be opened! This should not happen during normal operation. But if a power failure or similar occurs in the middle of a session, the lock file may remain, although obviously no session is opened. In this case, unlock the session manually by deleting the lock file.

Click on "New session" to create one. In contrast to the quick view, you must enter the name directly . As mentioned above, you can use the directory tree. In addition, there are several (almost obligatory) programs suggested to start with. Normally one should accept all suggestions. There is always the option to remove them later.

A double click on an existing session (or the "Load Selected" button) does just that.

4.2. In a Session

The full view is divided into three areas: Program starter, programs in the current session and the session notes. There is also a dynamic menu.

On the left side you see the program starter. A double-click starts a program instance in this session. You can also start a program more than once. For available programs please refer to the chapter "Program Database".

4.2.1. Running Programs

Started programs are located on the right side. A double-click switches the visibility to hide its window if the program supports it. If not, nothing happens.

The following information is available per program:

  • The name (possibly with icon)

  • A "label" that programs can use freely (e.g. Fluajho shows the loaded .sf2 here)

  • The program status

  • Stopped , not running

  • Ready, running

  • Launch, If the status halts here but the programs works, it is one that does not specifically support session mode. Agordejo cannot know if it is already running or not. Everything is fine! :)

  • Other states are only transitions and usually only visible for a very short time, e.g. Open / Loading

  • Visibility (A cross for visible, blank for invisible)

  • Changes - Are there currently unsaved changes?

  • ID - A unique identifier that can be used to distinguish between multiple instances of the same program

All other functions are accessible via the menu or context menu. One click on a program selects it, and the client menu in the menu bar will now apply to the client. Alternatively, you can right-click on an entry for a context menu, which is identical to the menu.

In addition to self-explanatory functions there are also:

  • Rename gives the program a self-chosen name, mostly to make its purpose clear and to distinguish it better from others. This feature is only available when 'nsm-data' is running in the session.

  • Save only tells this program to save

  • Remove takes the program out of the session. However, no files are deleted in the process. At the moment you have to "clean up" in your file manager by hand.

If the client "nsm-data" is in the session (this is the default setting) the lower area provides a large text field for notes. It is the same as in the quick view. Write what you want: TODO lists, lyrics, credits and sources from external Samples etc.

4.2.2. The Session Menu

In contrast to the quick view, full mode offers menus, which can also be accessed via the usual keyboard shortcuts (Ctrl+S for saving etc.).

  • Save instructs all programs to save, the session continues to run

  • Save and Close ends the session, after all programs saved

  • Abort ends the session without saving the programs

  • Save As saves the session under a different name and closes the current session without saving. From now on you work under the new name.

  • Add Client offers the option to add any program, whether it is in the program database or not.

  • Any installed programs are suggested. Agordejo doesn’t check them for usefulness for a music session, or even for runnability. You will find ls here as well as agordejo itself.

5. Program-Database

Agordejos launcher is based on a program database, which is partly self-generated, partly maintained by hand. As in a start menu Agordejo will offer you only programs that are actually installed on your system.

The database is created at the first start. Depending on your system, this can take some moments to a few minutes.

If you are reinstalling or uninstalling audio programs, you will need to update the database via the command in the control menu. Program installations and system changes are even possible while Agordejo is running (even in a session). After a DB update you can immediately access all new programs.

If you do not see an installed program in our launcher, but you are sure that it supports session management please report it to info@laborejo.org or under https://laborejo.org/bugs .

In addition, you can add (in full view) programs that are not in the database.

5.1. For advanced users

The strict rule is that only programs in the $PATH are included in the database. Absolutel paths are not allowed, even if you enter the program name yourself through the menu. However sometimes you just want to try out software, or you are a developer yourself and want to test without system-wide installation.

In the control menu / settings is a tab "Program-PATH", where you can define and add your own search paths. One absolute directory path per line, no wildcards, trailing slash don’t matter.

For example: /home/myuser/sources/newsequencer/bin/

These search paths are not stored in the session, but locally in your ~/.config directory.

6. Tray

Agordejo has a tray icon, if your window manager supports it. A click on the trayicon shows or hides Agordejo.

If you close Agordejo using the normal window manager function, such as a click on the [X], the program and the session is not terminated, but minimized to the tray.

A right click on the icon gives you access to common functions:

You can directly start the most recently used sessions.

If a session is already running you can save, cancel etc.

Agordejo can also be completely exited here.

7. Network Sessions

The functionality to distribute sessions over the local network is planned for a later program version.

8. Program parameters

As an advanced user, you can start Agordejo in the terminal and add some parameters.. For a complete list please use the --help parameter.

For example:

  • --session newAlbum/mySong starts the given session.

  • --continue starts the last active session.

  • --hide starts Agordejo as TrayIcon.

  • --url osc.udp://myhost.localdomain:14294/ connects to this server, if available, or starts the internal session server at this address. This is a very technical option and probably not needed.

  • --session-root /home/user/production2030 sets the root directory. Only sessions in this directory are displayed, everything is stored here.

The combination of --continue and --hide is essentially what many people expect from Session Management: Resuming at the previous state, without any extra windows in their way. If your system uses a start menu you will find not only the normal Agordejo starter but also "Agordejo Continue" to start this mode directly.

9. Miscellaneous / Explanations / FAQ

Session Save and Exit responds slowly: Agordejo is not a standalone program like an word processor. The participating programs in the session are not plugins either. When you end the session a signal is sent to all participating client to save. This may take a few moments where you are able to see "live" how individual programs terminate and disappear from the session. Everything is fine.

I have added a program but it does not save with the session: Does the program support session management? If not, Agordejo cannot do much. But you can ask the program developers to contact us (info@laborejo.org) and we can work together on support.

The programs hang on exit: Sorry about that. Actually, the programs themselves are to blame, but we are also interested in improving the situation by offering at least an emergency solution in the future.

Agordejo won’t start! I start the program but I can’t see anything: Most likely Agordejo is running, but invisible, because you exited it from the tray last time. Is it in the tray? A message should have popped up, maybe you missed it. If there is no tray in your window manager, the program should always be visible. With all these special window managers in Linux it may be that the tray detection did not work properly. Contingency plan is to delete ~/.config/LaborejoSoftwareSuite/agordejo. This will NOT remove any sessions, but only local settings such as the visibility of the program window. At next start Agordejo will behave like the very first start.

JACK crashed. A lot of programs hang. What can I do to prevent data loss? Probably already many programs in the session are not running properly and are not reacting anymore. The best thing to do is to use the 'Abort Session' function and restart everything. If the data has actually been unsaved for a long time, you can also dare to save/exit. It may be necessary to re-draw some jack, or all, jack connections by hand at the next start. If you want to be on the safe side, you can manually make a copy of the session directory in your file manager before ending the session (with inevitable crashes).

A program update broke my session because it can no longer load its files. Unfortunately, this is a problem that even Agordejo can’t solve. It also happens with LV2 plugins and with all other software, such as office programs. If you fear that a program becomes incompatible in the future, write down its version number in the session notes, so that you can at least, in an emergency, reinstall the old program version (even if this is very is cumbersome).

What’s better? Monolithic DAW or session management? Why not both? There is no conflict. Session management is worthwhile with two or more participating programs, which one needs almost always.

You should not feel compelled to suddenly make everything modular with individual programs, only because you use a session manager. Agordejo is designed to make your music production easier. If it is faster and more comfortable to manage all plugins and effects e.g. in a single "Carla" instance then you should do exactly that. If you basically want to do everything in Ardour, do that, but start Ardour anyway in session management, because no program can do everything alone and the time will come where you add a second one.

Session management is another level of hierarchy. Sequencers or DAWs are not plug-ins themselves. Patroneo does not belong "in" Ardour and Ardour does not belong "in" Laborejo. Already in this example each of the programs fulfils a different role because the others follow a different design philosophy and cannot ever offer the same workflow. And more:

Some programs can’t host plugins, some can’t export audio files. They are not bad programs, but programs that concentrate on one task. Furthermore, there is a lot of software that does not directly do music production, but still is connected in the grander scheme: Open Broadcast Studio (OBS), music player, word processors and graphic programs etc.

Agordejo contains functionality which is not within its scope: Music production is very complex and complexity is inevitable. It’s like a waterbed: if you press down on one side, something bounces up in another place. When you create a "clean and lean" program, which therefore implements only a part of the complete workflow, then the missing part pops up somewhere else. A minimalistic session manager provokes plug-ins (not LV2), helper-scripts, workarounds and hacks.

E.g. not to include file management provokes user errors like deleting the wrong files. If the SM knows what to do and it can do it, then let it do it. Or crashes: Technically, crashing programs are not the "problem" of the session managers, but they are part of the software reality. Crashes happen every day and need to be handled. Can Agordejo simplify the work and help to restore good conditions again? Then that should be done. Session management is also an opportunity to simplify even complex technical scenarios, e.g. distributing sessions over the network.

10. Installation and Start

Agordejo is exclusive for Linux. The best way to install is to use your package manager. If it is not there, or only in an outdated version, please ask your Linux distribution to provide a recent version.

If not available in the package repository you can build Agordejo yourself.

Build and Install
  • Please check the supplied README.md for dependencies.

  • You can download a release or clone the git version

  • Change into the new directory and use these commands:

  • ./configure --prefix=/usr

    • The default prefix is /usr/local

  • make

  • sudo make install

Now the program is available to run via your menu/launcher or agordejo in a terminal.

Please read README.md for other ways of starting agordejo, which are impractical for actual use but can be helpful for testing and development.

11. Help and Development

You can help Agordejo in several ways: Testing and reporting errors, translating, marketing, support, programming and more.

11.1. Testing and Reporting Errors

If you find a bug in the program (or it runs too slow) please contact us in a way that suits you best. We are thankful for any help.

How to contact us

11.2. Programming

If you want to do some programming and don’t know where to start please get in contact with us directly. The short version is: clone the git, change the code, create a git patch or point me to your public git.

11.3. Translations

Agordejo is very easy to translate with the help of the Qt-Toolchain, without any need for programming. The easiest way is to contact the developers and they will setup the new language.

However, here are the complete instructions for doing a translation completely on your own and integrating it into the program.

You can add a new language like this:

  • Open a terminal and navigate to qtgui/resources/translations

  • Edit the file config.pro with a text editor

    • Append the name of your language in the last line, in the form XY.ts, where XY is the language code.

    • Make sure to leave a space between the individual languages entries.

  • Run sh update.sh in the same directory

    • The program has now generated a new .ts file in the same directory.

  • Start Qt Linguist with linguist-qt5 (may be named differently) and open your newly generated file

  • Select your "Target Language" and use the program to create a translation

  • Send us the .ts file, such as by e-mail to info@laborejo.org

You can also incorporate the translation into Agordejo for testing purposes. This requires rudimentary Python knowledge.

  • Run the "Release" option in QtLinguists "File" menu. It creates a .qm file in the same directory as your .ts file.

  • Edit qtgui/resources/resources.qrc and duplicate the line <file>translations/de.qm</file> but change it to your new .qm file.

  • run sh buildresources.sh

  • Edit engine/config.py: add your language to the line that begins with "supportedLanguages" like this: {"German": "de.qm", "Esperanto: "eo.qm"}

    • To find out your language string (German, Esperanto etc.) open the python3 interpreter in a terminal and run the following command:

    • from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())

To test the new translation you can either run the program normally, if your system is set to that language. Alternatively start agordejo via the terminal:

  • LANGUAGE=de_DE.UTF-8 ./agordejo -V --save /dev/null

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272568.2778354 agordejo-0.3.1/documentation/out/favicon.ico0000644000175000017500000001027600000000000017611 0ustar00nilsnils  ( @ BBBCCCKKKBBBnnnnnnnnn444;;;]]]nnnlllJJJJJJvvvvvv  &&&&&&RRR%%%(((RRR~~~~~~SSSRRR ......ZZZ ZZZRRRQQQ%%%((( 666 666bbb bbb>>>>>>././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272568.2778354 agordejo-0.3.1/documentation/out/favicon.png0000644000175000017500000000122300000000000017613 0ustar00nilsnilsPNG  IHDR V%(gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs``wtIME waIDAT8ˍ?KQƟij-irj %r5eDP[HK6B" BiQ4sGryqL4*+yrI2Jhk}g\g$QFK):.}sY 4D<'y)~S V6oM$j.I) %CYh~T_,- V WgIj/ 3E!cT"f?5@:Yo)kx7:Y[@-3A^?@̗()xU@S +7$ɻU-4PWB}$s"4? b LL_h%tEXtdate:create2020-07-24T11:10:17+00:00ε%tEXtdate:modify2020-07-24T11:10:17+00:00PftEXtSoftwarewww.inkscape.org<IENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272568.1878345 agordejo-0.3.1/documentation/out/german.html0000644000175000017500000020103500000000000017622 0ustar00nilsnils Agordejo

Für Programmversion 0.3.1

1. Präambel

Session Management führt zur Vereinfachung von Arbeitsabläufen, Übersicht und Kontrolle über Programme und Daten und eine gehörige Portion positive Bequemlichkeit :)

Kein Programm existiert für sich alleine, denn kein Programm kann alles leisten, was für heutige Musikproduktion nötig ist.

Direkt ersichtlich ist das in einer JACK Umgebung, die prinzipiell modular ist: Verschiedene Programme erfüllen verschiedene Zwecke und "sprechen" miteinander, indem sie sich Daten schicken. Ein Sequencer schickt Midi an einen Sampler oder Synthesizer, der mit einem Plugin-Host für Effekte verbunden ist usw.

Selbst die monolithischsten All-In-One DAWs müssen, oder möchten, irgendwann mit der Außenwelt kommunizieren. z.B. um sich mit einem Screenrecorder oder Streamingprogramm zu verbinden, einen Wordprozessor mit Aufnahmereihenfolge oder Liedtexten zu starten, um Musik zu visualisieren oder (um ehrlich zu bleiben) eine Funktion zu nutzen, die es in diesem Programm eben nicht gibt.

Einen Großteil der Arbeit erledigt bereits das JACK-Subsystem. Alle Programme können ihre Musik in Echtzeit teilen, verfügen über synchronisierte Timelines und spielen im gleichen Tempo.

Was übrig bleibt ist die leidige Arbeit jedesmal alle Programme zu starten, die Projektdateien zu laden, alle Audiokanäle zu verbinden usw. Session Management im allgemeinen (z.B. extra geschriebene Starter-Scriptdateien) und Agordejo im speziellen nehmen Ihnen diese Arbeit ab, oder vereinfachen sie zumindest stark.

Im Gegensatz zum erwähnten selbstgeschriebenen Script müssen Sie sich nicht im Vorraus für ein Setup entscheiden, sondern alles wird automatisch verwaltet, solange Sie alles durch den Session Manager Agordejo starten.

1.1. Anwendungsbeispiel

  • Agordejo starten (Startmenü, Terminal etc.)

  • Man befindet sich nun in der "Schnellen Ansicht" um eine Session zu starten

  • Der große Knopf: "Starte Neue Session"

  • Nun bekommt man eine Auswahl an Programmen:

  • Ein einfacher Klick mit der Maus startet ein Programm

  • Ein weiterer Klick versteckt (oder zeigt anschließend) dessen grafische Oberfläche

  • Sollte das Programm abstürzen bekommt man das signalisiert und kann es neustarten.

  • Audio- und Midiports können nun in einer Patchbay miteinander verbunden werden. Die Verbindungen werden in Agordejo gespeichert.

  • Um andere Programme und erweiterte Funktionen zu bekommen kann man in den Tab "Volle Ansicht" wechseln

  • Der Name der Session ist bisher einfach ein Datum. Durch Klick auf dieses kann man einen richtigen Namen eingeben. Etwa "Mein Lied"

  • Ist man soweit fertig kommt man durch den Knopf "Speichern und Schließen" wieder in die Sessionauswahl zurück

  • Nun könnte Agordejo geschlossen werden.

  • Alle gespeicherten Daten liegen in einem einzigen Verzeichnis auf der Festplatte (~/NSM Sessions/Mein Lied)

  • Die Session kann weitergeführt werden: Nach dem Klick auf den Namen starten alle Programme automatisch und verbinden ihre JACK-Ports untereinander.

Die "Schnelle Ansicht" ist ein guter Einstieg. Üblicherweise möchte man jedoch bald in die volle Ansicht wechseln, und sei es nur um ein Programm zwei mal zu starten oder um zwischendurch manuell zu speichern.

2. Die Schnelle Ansicht

Die Schnelle Ansicht ist eine aufs Minimum reduzierte Umgebung. Sie, als Benutzer, sollen so wenig Entscheidungen wie möglich zu treffen haben und so schnell wie möglich (mit den wenigstens Klicks) eine alte Session starten oder eine neue erstellen können.

Aus Stabilitätsgründen werden nur Programme zum Starten angeboten, von denen bekannt ist, dass Sie korrekt mit dem Session Management Agordejos zusammenarbeiten.

Die Schnelle Ansicht ist lediglich eine Ansicht. Es besteht kein technischer Unterschied zur vollen Session und Sie können jederzeit hin- und herschalten.

2.1. Session Auswählen

Hier gibt es nur zwei Möglichkeiten: Klicken Sie auf "Starte Neue Session" um dies zu tun, oder wählen Sie eine der bestehenden Sessions aus um diese fortzuführen (sofern vorhanden).

2.2. In einer Session

Der Name kann geändert werden. Wir empfehlen ein Datum in der Form YYYY-MM-DD gefolgt von einem aussagekräftigen Namen. Bitte beachten Sie, dass die Namensänderung erst wirksam wird (z.B. in Form des Sessionvereichnisses auf der Platte) wenn Sie die Session schließen.

Als nächstes steht ein größeres Textfeld für Notizen zur Verfügung. Schreiben Sie was Sie möchten. TODO-Listen, Liedtexte, Credits und Quellen von externen Samples etc.

Programme werden durch Icons symbolisiert. Ein Mausklick startet das Programm (das kann einen Moment dauern, darauf haben wir keinen Einfluss). Der Status des Programmes wird durch ein Symbol dargestellt: Das "Play"-Symbol für das laufende Programm, ein Auge bedeutet "Programmfenster versteckt", das "Power On/Off"-Symbol bedeutet, dass Programm wurde beendet (oder Absturz).

Falls das Programm es unterstützt kann ein laufendes Programmfenster versteckt werden. z.B. ein Synthesizer, einmal eingerichtet, muss nicht permanent zu sehen sein. Klicken Sie einfach erneut auf das Programmicon. Entweder passiert nichts (das Programm unterstützt es nicht) oder es wird versteckt. Ein weiterer Klick auf ein versteckes Fenster zeigt es wieder. Jedes Programm ist selbst dafür verantwortlich ob es seinen Fensterzustand speichert. Manche Programme starten bereits versteckt.

Es gibt keine Möglichkeit ein Programm in dieser Ansicht zu beenden. Wechseln Sie hierzu in die volle Ansicht oder benutzen Sie die Schließenfunktion des Programmes selbst.

Es gibt auch keine Möglichkeit ein Programm mehrfach zu starten, z.B. für einen Synthesizer pro Spur. Wechseln sie hierzu in die volle Ansicht. Wenn Sie Programme durch die volle Ansicht mehrfach gestartet haben können Sie weiterhin die schnelle Ansicht benutzen um die Session zu verwalten, allerdings ist nicht definiert auf welche Programminstanz sich ein Klick auf das Programmicon (z.B. zum Verstecken) auswirkt. Mehrere Instanzen sind ein klares Zeichen dafür, dass Sie bereit für die volle Ansicht sind.

Wie Sie irgendwann feststellen werden sind nicht alle verfügbaren Programme in der Liste, und es gibt auch keine Möglichkeit Programme zu starten die zwar gut in einer Session funktionieren (z.B. ein Stimmgerät), aber nicht explizit dafür geschrieben sind, z.B. ihren Status nicht zurückmelden. Diese Programme sollten Sie besser in der vollen Ansicht verwalten.

Ist ein Programm leider abgestürzt können Sie es nur neu starten und auf dessen automatische Speicherung hoffen. Viel Glück.

3. Die Volle Ansicht

Einige Konzepte wurde bereits im Kapitel "Die Schnelle Ansicht" erklärt. Es wird vorrausgesetzt, dass Sie dieses gelesen haben.

3.1. Session Auswählen

Sessions stellen sich als Tabelle dar, die Sie durch den Klick auf eine Spaltenüberschrift ordnen können. So wird hier gezeigt wie die Session heißt und wann das letzte mal gespeichert wurde, wohl die beiden wichtigsten Informationen. Dann wird gezeigt wie viele Programme/Clients die Session verwaltet und ob sie Symbolische Links enthält. Letzteres steht wahrscheinlich auf "Ja", wenn Sie einen Sampler o.ä. benutzen, der große Audiodateien geladen hat. Diese werden zunächst nur in die Session gelinkt, und nicht kopiert, um Speicherplatz zu sparen. Der angezeigte Speicherverbrauch ist nicht der tatsächliche Verbrauch, sondern beinhaltet die Größen der Symlinks. Erst wenn Sie die Session archivieren oder die Links durch reale Dateien ersetzen stimmt die angezeigte Zahl.

Schließlich wird auch das Verzeichnis angegeben, in dem die Dateien tatsächlich gespeichert sind.

Sessions stellen den Verzeichnisbaum dar. Eine Session ist immer ein "Blatt" und kann keine Subsessions enthalten. Sie können beim Anlegen oder Umbenennen die Sessions auch im Baum anordnen indem Sie die gewohnte Schrägstrichschreibweise benutzen: song123Neues Album/Song 123 oder Versuche/asdfRomantische Pop-Balladen/Mein Herz wird weiter schlagen. Wie Sie ihre Sessions organisieren, und wie viele Unterverzeichnisse Sie anlegen, steht Ihnen frei. Es ist allerdings nicht erlaubt den Namen mit / anfangen zu lassen oder die speziellen Zeichen .. zu benutzen. Die Baumansicht kann durch eine Checkbox an der linken Seite deaktiviert werden, um etwa besser sortieren zu können. Das ist nur eine Ansicht, ihre Daten bleiben unangetastet.

Jede Session verfügt über ein Kontextmenü (z.B. rechte Maustaste) mit weiteren Optionen: Sie können eine Session umbennen, löschen (inkl. aller dazugehörigen Dateien auf der Festplatte!) und mehr. Diese Funktionen sind gleichwertig zu ihrem Dateimanager. Falls Sie möchten, können Sie auch einfach die Sessionverzeichnisse selbst umbennen, verschieben oder löschen (sofern diese nicht gerade geöffnet ist). Dazu muss Agordejo nicht neugestartet werden, es reagiert selbst auf die Änderungen.

Hier gibt es auch die Möglichkeit eine sogenannte "Lock"-Datei zu entfernen. Das ist eine Datei, die angelegt wird sobald die Session startet und gelöscht wenn beendet. Damit weiß das Programm, welche Session gerade läuft. Ist eine Lockdatei vorhanden, kann die Session nicht geöffnet werden! Das sollte im normalen Betrieb nicht vorkommen. Falls aber ein Stromausfall o.ä. mitten in einer Session passiert ist kann die Lockdatei übrig bleiben, obwohl offensichtlich keine Session geöffnet ist. In diesem Fall entsperren Sie die Session hierdurch manuell.

Klicken Sie auf "Neue Session" um diese anzulegen. Im Gegensatz zur schnellen Ansicht müssen Sie hier den Namen direkt eingeben. Wie oben erwäht können Sie dazu den Verzeichnisbaum benutzen. Darüberhinaus wird angeboten verschiedene (fast schon obligatorische) Programme direkt mitzustarten. Im Normalfall sollte man alle Vorschläge akzeptieren. Es gibt jederzeit die Möglichkeit diese wieder zu entfernen.

Ein Doppelklick auf eine existierende Session (oder der Knopf "Lade Ausgewählte") machen genau das.

3.2. In einer Session

Die volle Ansicht ist in drei Bereiche eingeteilt: Programmstarter, Programme in der laufenden Session und die Session-Notizen. Dazu gibt es ein dynamisches Menü.

Auf der linken Seite sehen die den Programmstarter. Ein Doppelklick startet eine Programminstanz in der Session. Sie können ein Programm auch mehrmals starten. Welche Programme zur Verfügung stehen entnehmen Sie bitte dem Kapitel "Programm-Datenbank".

3.2.1. Laufende Programm

Gestartete Programme befinden sich auf der rechten Seite. Ein Doppelklick schaltet die Sichtbarkeit um, sofern das Programm unterstüzt sein Fenster zu verstecken. Falls nicht passiert nichts.

Pro Programm gibt es folgende Informationen:

  • Der Name (evtl. mit Icon)

  • Ein "Label", das Programme frei benutzen können (z.B. zeigt Fluajho hier das geladene .sf2 an)

  • Den Programmstatus (auf Englisch, da es sich um definierte Schlüsselworte handelt).

  • Stopped / Gestoppt, läuft nicht

  • Ready / Läuft und Bereit

  • Launch / Startet. Wenn das Programm hier stehen bleibt, aber funktioniert, handelt es sich um eins, dass nicht speziell den Sessionmodus unterstützt. Agordejo kann nicht wissen, ob es bereits läuft oder nicht. Alles ist in Ordnung! :)

  • Weitere Zustände sind nur Übergänge und meist nur sehr kurz zu sehen, z.B. Open / Läd gerade

  • Sichtbarkeit (Ein Kreuz für sichtbar, Leer für unsichtbar)

  • Änderungen - Gibt es momentan ungespeicherte Änderungen?

  • ID - Ein eindeutiges Kürzel mit dem man auch mehrere Instanzen des gleichen Programmes auseinander halten kann

Alle weiteren Funktionen sind durch das Menü oder Kontextmenü zugänglich. Ein Klick auf ein Programm wählt es aus, und das Clientmenü in der Menüleiste gilt nun dafür. Alternativ kann mit Rechtsklick auf den Eintrag das Kontextmenü geöffnet werden, das identisch ist.

Neben den selbsterklärenden Funktionen gibt es noch:

  • Umbenennen gibt dem Programm einen selbstgewählten Namen, besonders um seinen Zweck deutlich zu machen und es besser von anderen zu unterscheiden. Diese Funktion steht nur zur Verfügung, wenn nsm-data in der Session läuft.

  • Speichern weist nur dieses Programm an abzuspeichern

  • Entfernen nimmt das Programm aus der Session. Dabei werden jedoch keine Dateien gelöscht. Zur Zeit muss leider noch von Hand im Dateimanager "aufgeräumt" werden.

Befindet sich der Client "nsm-data" in der Session (das ist Voreinstellung) steht im unteren Bereich ein großes Textfeld für Notizen zur Verfügung. Es ist dasselbe wie in der schnellen Ansicht. Schreiben Sie was Sie möchten: TODO-Listen, Liedtexte, Credits und Quellen von externen Samples etc.

3.2.2. Das Session Menü

Im Gegensatz zur schnellen Ansicht stehen hier weitere Menüs zur Verfügung, die auch über die üblichen Tastaturkürzel zu erreichen sind (Strg+S für Speichern etc.).

  • Speichern weist alle Programme an zu speichern, die Session läuft weiter

  • Speichern und Schließen beendet die Session, vorher speichern alle Programme noch einmal ab

  • Abbrechen beendet die Session, ohne dass die Programme abspeichern

  • Speichern Unter speichert die Session unter einem anderen Namen und schließt die laufende ohne abzuspeichern. Ab nun arbeitet man in der neuen Session.

  • Client Hinzufügen bietet die Option ein beliebiges Programm hinzuzufügen, egal ob es in der Programmdatenbank ist, oder nicht.

  • Es werden alle installierten Programme vorgeschlagen. Agordejo überprüft diese nicht auf Sinnhaftigkeit für eine Musik-Session, oder auch nur auf Lauffähigkeit. Sie finden hier ls wie auch agordejo selbst.

4. Programm-Datenbank

Agordejos Programmstarter basiert auf einer Programmdatenbank, die sich teilweise selbst erstellt, teilweise von Hand eingepflegt wurde. Das bedeutet nichts anderes, als das alle installieren Programme in ihrem System überprüft werden (wie ein Startmenü) und Ihnen im Agordejo-Starter nur das angeboten wird, was Sie auch tatsächlich installiert haben.

Beim ersten Start wird daher die Programmdatenbank erstellt. Je nach System kann dies einige Augenblicke bis einige Minuten dauern.

Wenn Sie Audio-Programme neu installieren, oder deinstallieren müssen Sie die Datenbank selbst aktualisieren. Im Steuerungsmenü gibt es den Befehl. Programminstallationen sind sogar möglich während Agordejo läuft (auch in einer Session). Nach einem DB-Update stehen Ihnen sofort alle neuen Programme zur Verfügung.

Wenn Sie ein installiertes Programm nicht in unserer Liste sehen, aber von dem Sie sicher sind, dass es Session Management unterstützt melden Sie es bitte an info@laborejo.org oder unter https://laborejo.org/bugs .

Darüberhinaus können Sie (in der vollen Ansicht) Programme hinzufügen, die nicht in der Datenbank sind. Siehe dort.

4.1. Für Fortgeschrittene

Die eiserne Regel ist, dass nur Programme im $PATH in die Datenbank aufgenommen werden. Absolute Pfade sind unzulässig, selbst wenn man den Programmnamen selbst durch das Menü eingibt. Allerdings möchte man manchmal Software nur ausprobieren, oder ist selbst Entwickler und möchte ohne systemweite Installation testen.

In den Einstellungen im Steuerungsmenü gibt es einen Tab "Programm-PATH", wo Sie eigene Suchpfade hinzufügen können. Ein absoluter Pfad pro Zeile, keine Wildcards, Trailing Slash spielt keine Rolle.

Zum Beispiel: /home/myuser/sources/newsequencer/bin/

Diese Suchpfade werden nicht in der Session gespeichert sondern lokal in ihrem ~/.config Verzeichnis.

5. Tray

Agordejo verfügt über ein Tray-Icon, sofern ihr Windowmanager das unterstüzt. Ein Klick auf das Trayicon zeigt oder versteckt Agordejo.

Schließt man Agordejo über die normale Windowmanagerfunktion, etwa ein Klick auf das [X], wird das Programm und die Session nicht beendet, sondern in den Tray minimiert.

Ein Rechtsklick auf das Icon bietet Schnellzugriff auf häufige Funktionen:

Sie können hier die zuletzt benutzen Sessions direkt starten.

Läuft bereits eine Session können Sie speichern, abbrechen etc.

Agordejo kann hier auch komplett beendet werden.

6. Netzwerk-Sessions

Die Funktionalität Sessions im lokalen Netzwerk zu verteilen ist für eine spätere Programmversion vorgesehen.

7. Programmparameter

Als fortgeschrittener Benutzer können Sie Agordejo im Terminal starten und dort einige Parameter angeben. Für eine vollständige Liste benutzen Sie bitte den --help Parameter.

Eine Auswahl:

  • --session neuesAlbum/meinLied startet direkt die angegebene Session.

  • --continue startet die zuletzt benutzte Session

  • --hide startet Agordejo als Trayicon.

  • --url osc.udp://myhost.localdomain:14294/ verbindet sich zu diesem Server, falls vorhanden, oder startet den internen Session-Server unter dieser Adresse. Dies ist eine sehr technische Option und wird wahrscheinlich nicht benötigt.

  • --session-root /home/benutzer/produktion2020 setzt das Wurzelverzeichnis. Nur Sessions in diesem Verzeichnis werden angezeigt, alles wird dort gespeichert.

Die Kombination von --continue und --hide ergibt einen Modus, den viele Leute vom Session Management erwarten: dort weiter machen wo man aufgehört hat, ohne dass Extrafenster angezeigt werden. Falls Ihr System über ein Startmenü verfügt finden Sie daher neben dem normalen Agordejo-Starter auch eine "Agordejo Continue"-Verknüpfung für genau diesen Modus.

8. Verschiedenes / Erklärungen / FAQ

Session Speichern und Beenden reagiert langsam: Agordejo ist kein Einzelprogramm wie ein Office-Writer. Die teilnehmenden Programme in der Session sind auch keine Plugins. Wenn Sie die Session beenden wird ein Signal an alle teilnehmenden Programme gesendet, dass sie speichern sollen. Das kann ein paar Momente dauern, in denen Sie "live" mitverfolgen, wie die einzelnen Programme sich beenden und aus der Session verschwinden. Es ist alles in Ordnung.

Ich habe ein Programm hinzugefügt aber es speichert nicht mit der Session: Unterstützt das Programm Session Management? Wenn nicht, kann Agordejo nichts tun. Aber Sie können die Programmentwickler bitten mit uns Kontakt aufzunehmen (info@laborejo.org) und wir können zusammen an der Unterstützung arbeiten.

Die Programme hängen beim Beenden: Das tut uns leid. Eigentlich sind die Programme selbst schuld, aber auch wir sind daran interessiert die Situation zu verbessen, indem wir in Zukunft zumindest eine Notlösung anbieten.

Agordejo startet nicht mehr! Ich starte das Programm aber ich sehe nichts.: Wahrscheinlich ist Agordejo unsichtbar, weil Sie es aus dem Tray heraus beendet hatten. Ist es im Tray? Eigentlich hätte ein kleines Nachrichtenfenster aufpoppen sollen. Falls in ihrem Window-Manager kein Tray vorhanden ist sollte das Programm immer sichtbar sein. Bei besonderen Window-Managern (bei der großen Auswahl in Linux) kann es sein, dass die Tray-Erkennung nicht richtig funktioniert hat. Notfallplan ist es ~/.config/LaborejoSoftwareSuite/agordejo zu löschen. Dabei werden KEINE Sessions gelöscht, sondern nur lokale Einstellungen wie die Sichtbarkeit des Programmfensters. Beim nächsten Start wird Agordejo sich verhalten wie beim allerersten.

JACK ist abgestürzt. Viele Programme hängen. Was tun um Datenverlust zu vermeiden?: Vermutlich sind bereits viele Programme der Session nicht mehr richtig lauffähig und reagieren nicht mehr. Am besten benutzen Sie die Session Abbrechen Funktion und starten alles neu. Wenn die Daten tatsächlich schon lange ungespeichert waren kann man auch ein Speichern/Beenden wagen. Dann kann es aber sein, dass man beim nächsten Start einige Jack connections von Hand neu ziehen muss. Wer extrem sicher gehen möchte kann vor dem Beenden der Session (mit unweigerlichen abstürzen) manuell im Dateimanager eine Kopie des Session-Verzeichnisses machen.

Ein Programmupdate hat meine Session kaputtgemacht, weil es seine Dateien nicht mehr laden kann.: Das ist leider ein Problem, dass auch Agordejo nicht lösen kann. Es passiert mit LV2-Plugins ebenso wie mit alle anderen Software, etwa Officeprogramme. Falls Sie befürchten, dass ein Programm in Zukunft inkompatibel wird notieren Sie sich dessen Versionsnummer in den Session-Notizen, damit Sie zumindest zur Not die alte Programmversion wieder installieren können (auch wenn das sehr umständlich ist).

Was ist besser? Monolithische DAW oder Session Management?: Warum nicht beides? Es gibt keinen Konflikt. Session Management lohnt sich ab zwei teilnehmenden Programmen, und auf die kommt man so gut wie immer.

Sie sollten sich nicht genötigt fühlen plötzlich alles modular mit Einzelprogrammen zu machen, nur weil Sie einen Session Manager benutzen. Agordejo ist dafür da, ihre Musikproduktion einfacher zu machen. Wenn es schneller und bequemer ist alle Plugins und Effekte z.B. in einer einzelnen Carla-Instanz zu verwalten dann sollten Sie genau das machen. Wenn Sie prinzipiell alles in Ardour machen wollen, machen Sie das, aber starten Sie Ardour trotzdem im Session Management, denn kein Programm kann alles alleine, und der Zeitpunkt wird kommen, an dem Sie ein weiteres hinzufügen.

Session Management ist andere Hirarchieebene. So sind Sequencer oder DAWs selbst keine Plugins. Patroneo gehört nicht "in" Ardour und Ardour gehört nicht "in" Laborejo. Und schon in diesem Beispiel erfüllt jedes der Programme eine Rolle, die die anderen beiden nicht leisten können, da sie einer anderen Design-Philosophie folgen. Und mehr:

Manche Programme können keine Plugins hosten, manche können keine Audiodateien exportieren. Das sind deswegen keine schlechten Programme, sondern welche, die sich auf eine Aufgabe konzentrieren. Darüberhinaus gibt es eine Menge Software, die nicht direkt Musikproduktion ausübt, aber trotzdem inhaltlich dazu gehört: Open Broadcast Studio (OBS), Musikplayer, Schreib- und Grafikprogramme etc.

Agordejo beinhaltet Funktionalität, die nicht seine Aufgabe ist: Musikproduktion ist sehr komplex und Komplexität ist unvermeidlich. Sie ist wie ein Wasserbett: Drückt man die auf der einen Seite runter, muss etwas an einen anderen Stelle hochdrücken. Macht man ein programm "clean and lean", und implementiert damit nur einen Teil des kompletten Arbeitsablaufs, dann kommt der fehlende Teil woanders wieder hoch. Ein minimalistischer Session Manager provoziert Plugins (nicht LV2), Helper-Scripts, Workarounds und Hacks. z.B. Dateiverwaltung nicht mit einzuschließen provoziert Anwender-Fehler im Dateimanager (wie das Löschen der falschen Dateien). Wenn der SM weiß was zu tun ist, und er es tun kann, dann soll er es machen.

Oder Abstürze: Technisch gesehen sind abstürzende Programme nicht das "Problem" des Session Managers, aber sie sind Teil der Softwarewirklichkeit. Abstürze passieren jeden Tag und nun muss man damit umgehen. Kann Agordejo die Arbeit vereinfachen und helfen den guten Zustand wieder herzustellen? Dann sollte das geschehen.

Session Management ist außerdem eine Chance auch komplexe technische Szenarios zu vereinfachen, z.B. Sessions über das Netzwerk zu verteilen.

9. Installation und Start

Agordejo ist exklusiv für Linux. Am besten installieren Sie Agordejo über deinen Paketmanager. Falls es dort nicht vorhanden ist, oder nur in einer veralteten Version, bitten sie ihre Linuxdistribution Agordejo bereitzustellen.

Falls nicht in den Paketquellen vorhanden kann man Agordejo auch selbst "bauen".

Abhängigkeiten*
  • Eine Liste der Abhängigkeit befindet sich in der README.md

  • Kompilieren und Installieren geht entweder mit einem Releasedownload oder mit der Git-Version:

  • Wechseln Sie in das neue Verzeichnis und benutzen diese Befehle: *./configure --prefix=/usr

    • Das Standardprefix is /usr/local

  • make

  • sudo make install

Nun ist das Programm durch agordejo in ihrem Terminal oder Programmstarter vorhanden.

In der Datei README.md befinden sich weitere Möglichkeiten agordejo zu starten. Diese sind zum Musikmachen nicht praktikabel, aber nützlich für Tests und Entwicklung.

10. Helfen und Entwicklung

Sie können Agordejo auf viele Arten und Weisen helfen: Testen und Fehler melden, übersetzen, marketing, anderen Nutzern helfen und schließlich programmieren.

10.1. Testen und Programmfehler

Falls Sie einen Fehler im Programm entdecken (oder es zu langsam läuft) melden Sie diese bitte auf eine Art und Weise, die ihnen am besten passt.

Kontaktmöglichkeiten

11. Entwicklung

Falls Sie an der Entwicklung interessiert sind, melden Sie sich am besten direkt bei uns (s.o.) Kurzversion: clone git, programmieren, einen git-patch erstellen oder uns eine git URL zukommen lassen.

11.1. Übersetzungen

Agordejo ist mit Hilfe der Qt-Toolchain sehr einfach zu übersetzen, ohne, dass man dafür Programmieren muss. Die einfachste Variante ist es einfach die Entwickler anzusprechen und sie werden die neue Sprache einrichten.

Hier ist dennoch die komplette Anleitung, um eine Übersetzung komplett alleine anzufertigen und in das Programm einzubinden.

So fügt man eine neue Sprache hinzu:

  • Öffnen Sie ein Terminal und navigieren zu qtgui/resources/translations

  • Bearbeiten Sie die Datei config.pro in einem Texteditor.

    • Hängem Sie in der letzten Zeile den Namen der neuen Sprache an, in der Form XY.ts, wobei XY der Sprachcode ist.

    • Achten Sie bitte darauf ein Leerzeichen zwischen den einzelnen Sprachen zu lassen

  • Führen Sie sh update.sh im selben Verzeichnis aus.

    • Das Programm hat nun eine neue .ts-Datei im Verzeichnis erstellt.

  • Starten Sie Qt Linguist mit linguist-qt5 (kann evtl. anders heißen) und öffnen von dort die neu generierte Datei.

  • Wählen Sie die "Target Language", also Zielsprache, aus und benutzen das Programm um eine Übersetzung anzufertigen.

  • Senden Sie uns bitte die .ts Datei, z.B. per E-Mail an info@laborejo.org (s.u bei Bugs und Programmfehler für mehr Kontaktmöglichkeiten)

Die Übersetzung können Sie auch selbst, zum Testen, einbinden. Dafür sind rudimentäre Python Kentnisse nötig.

  • Im Qt Linguist "Datei" Menü ist eine "Release" Option. Das erstellt eine .qm Datei im gleichen Verzeichnis wie die .ts Datei.* Bearbeiten Sie qtgui/resources/resources.qrc und kopieren die Zeile <file>translations/de.qm</file> . Dabei das Länderkürzel zum Neuen ändern.

  • Führen Sie sh buildresources.sh aus

  • Bearbeiten Sie engine/config.py: Die neue Sprache hinzufügen. z.B. {"German":"de.qm", "Esperanto:"eo.qm"}

    • Um den Sprachstring herauszufinden öffnen Sie den python3-Interpreter im Terminal und führen aus:

    • from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())

Um die neue Übersetzung zu testen starten Sie das Programm, falls ihr System bereits auf diese Sprache eingstellt ist. Ansonsten starten Sie agordejo mit diesem Befehl, Sprachcode ändern, vom Terminal aus:

  • LANGUAGE=de_DE.UTF-8 ./agordejo -V

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272568.0978336 agordejo-0.3.1/documentation/out/index.html0000644000175000017500000007366400000000000017477 0ustar00nilsnils Agordejo Multi-Language Documentation

Agordejo Multi-Language Documentation

logo

For program version 0.3.1

This site is part of the Laborejo Software Suite

Please choose a language

Further Links * Bug and Issues, and other Feedback * Write to info@laborejo.org for any comment or question.

New Session Manager * API Document - How to write your own client * Sourcecode and Readme

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272568.2778354 agordejo-0.3.1/documentation/out/logo.png0000644000175000017500000010615400000000000017137 0ustar00nilsnilsPNG  IHDR a pHYsodtEXtSoftwarewww.inkscape.org< IDATxw%E`$$e iUyU\30u]UUYu͂ " (d 90 0s=?/9]]o9 stUo$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I4D IRm8BY Xc , E$XwڟL"$pw B`u`ɿw)ܖ{sdt|Z88:NSmɶ? fJtΝlyYK~/oV~W5Hw5ۿ,'.;fي@BfXFK?et_,$|9: vvVkfy6HIKl< pTKI؎Q;آ~/%% PxdX>Nl~N[&HwfJ-#5Iq9fKp;p!Lt)ԗ:Dz~z#oW2捤Ls޶Fk^AzFKH: =4_FJw_s0t.z~x_>>KJ@>D:2vd/SL蝇 َar.mO?׸gwzǩ{3G6 4v&n3noK_pĮ t_cII_\ҵ{^H9|Fyvw_lvsP"=txCOkgj5S5yb 8dZmDoR7 }Tۡ}(BJGuGFJ8NZEim:)4ӋE3>ަx&v\VAmyJXH;JK2'ImPMCG=k|GKwwڤ' j5L1i? :*D Rә,!.[of?mǀ:`4}\'Ma$=tx)9jwV<ۻ,Ṥ}YviMS?܎l qipK>7+>C[vP:^I1? KOs4y6cHџoHokieՎ$nʏ6|\nG*ߨH'KNix5y>YAmI G(Jfj n&9`.}gmv HS\۵њ}ixm| 5g[G49}Wkm>CNdtV3VGg}F{Š.[!:7غ5 8 x&gs^t6' iщ#\^p_um9/ɸ-l?uq}M{i5X({T= V H *dNXJWeK]y{ϼͮy){t ! H|/Is$篌%m&=ڸϿwvQĺ4꼄 #߮7LQ]LiÙ?O=D;I0IE]Gy)]QJZyDR"Xf3) }Q`GGt҈,W7㴖p״HώztZR2Dw3ݚIj Vu-m>HEŚ׀ư 844:Reؖx/FHIwO҉H~Lz X6Y x,NPԢ=>~NoqۥIw]L*FYFLB]I#W'] %Zk\+_Tw&4Ţ{=VǢ`8B;xXtX{FߴU[vm6H L؏ I xS}^ :iյ~-&%uC&ix3_-c]RYlATmJ"` (½UlG-%݇~mkqHnKH~' }/9w=LmWnƵHߣa>Sxd(ĺ1*Ҕ>_[EXztc)<>7ݯu2G҈ۊ/,큳@g?χM y4:ۑ|݇Z}> aAp1u~g ) i\t#i.[߭S/iR=ŴWn/47FC\}29}(|yM*k}D=Y=/+@e=}HJv"Mh"46qb}**mؘfԶxnK˧$iz*ІԬZ\"_HSˣ4An͵\֧Q9p=GblVޤJ[Bt)}.lFě>6 T_ܹ= I7\?7Pv6o_O#%Jə p{&i~N/sR]~Mel'$zkHz k$L}H@KOx\O3h:KIo?uQ/tX\JOen"Y熌wBL:v%QEzFPUO L7mQ$ QOOAJH0E|TϬ)اZ 8H37r{N V(i1)VWn\Gz/%uR-oVms6W=^O=JF>DfiJSxzWRvk_F6SK $(?ElF( Yܣ>NMom'>9WfmmҪaTﱴfԞTgb-߶%Ufug믕zkYIs+^[yvha7 q%stho{yWd}ؖ{*SIm% (=vv@_H* BÙYؚ7d|L((ҳڜq=e4E-omG|բ}9׫H7v+yW!׫Zph1c[ -wn ޒSL-ʬ=_+XJJPۙcLq5mm/5&OJҙgmKUaK@<_i[a{҂g| 8>{kęS?J<_emBZ 2 hw=rRfߐT -Ք:ΤK~J ib D{[nmH*i3ڟIۯͿF6u.KP>:<7^DYDZ|eQ` *ሪn|BRA(kQnU%)ʀ֨}H eр}NIGغ~./ˀoF1i!GK7u"^G치<5&KtR6_,!-P7OtG=}=`N0-v) DVߋ;j Bu.)ϙAdZ2"۶-zzTmMTmÚ[wsWB*E !=|^OLC9'>>;E}!]YN7#!wB5Y(ӶH1uF=#3Io9mǓgz[kiB`%A5I 6,oi5S9XSN q:Sxr?<ۂ43‘A͕Ŀ\xCp j 8ǑTd}/VbHӤ#|5h&vWYVpFA:xM8q 2l#[Hu#H5ܔH;pSvUtԲkI'-i73+qdCI\B< ~ =K}P^Sj4YJHQ p7қD2} ȕL2~ '86Ok`]s)p*{` GkQs"%ז7>^VNEhk;\Dڷm9T.z@sHGi.6_;^L*N_F0:9]9+À[2ıT-ݏ3]63(zDͨ%#!7^t|H ?;<'pߵ_롎QpO `Ns[yu$8?h-( %=t4"Ei 75,Mpjyvy5"&%k}q nX` t &=8p]I]X6nl7F@wi*N"_'a3Ի`EjHZ<~K 0mIW &?=p:4`848ebNY;:55( с_i߁^>6x'_ aG|~gLGcn %A` א;DZ1HxJk(0;H/qǣoUmHu>Am+>-98{8#p]idU.ܐ\Go):q{et#wD!j(ҋHߥב'P#FHbBy8N~hXJuQFK IDAT;]lj}{^*bQv uJyCpNqʽIS5G0+]SCR|*T)ͭE!6y1YL|):5W[@a"R_+l'm CtȺ$:WÔ~V|1p :d&:+:/p*n҈A7M}8x8:@yl+c'QW?n'&xW4`^A=ZF'Ր(E5'HվK ,&l&4py w&YCn)G'. ܓڟ]JOZ57+:R9TK,:1D6w+Xg/x]C)vVB͙L;E,6ĢMm`x_n(5H7 41Z 9tJx+~:b1/#z!~Quu|o@m9Eںa1%q&4H fl楤p [nu{itSi]RXW0TMV%F_ }&ٰ#MkT zp`N3i_H]_>:S,M,^{s%t"EY]IpSGAr\t#X 2GjCjf2#NOɑp'[3ߝ'& u ﳆ萈QS-#3Tgf[ψt-86:WCn=zLq^\긗Z?Li&>nѿlӠ/t:Yif}}0CW, ~ԲE>]&~:< A8p^gG~&w'uԨZ?Li&4p_?0`5̔lnʣ?kY |N Ęah `9ˁCkv֥z]p_L3FuJR$6̠/@4cJ1 pSftY3DW}(%^AN)׀fWW]Rõyvet!pnT58߹Djjx!ᆮ$5E8[[tԱ2縨adjtT2]S.^kփ/ڳ9HQW N>OK~[^MokA!XհR0j)dۅ6ITN ~E܍Z]LRpzΡBS_T ◤'R ^H8>* G%-6ִnj+j8V]'Y5u^gCJ؁T~_`qiЫ] Ib"Gk=tz_õt^՞Qv'/07O6QQL }=p5ݬaTWWpS^0jx+^˃A[Ϝl]u\XcԐ;L,k=tz_ԌSP5ը%h>ܮQJ9Z5H#* .~0ϴa$mF\jSCRz5ܔ{C^ ?dLpxI5\F]-ǸZ>׵ġ!u_MM? Z5a_8V\4ig "VCBR#non$>,2n ^Peu"F.$^^Oz0}dN 竖Ѓ_Srtc:~sijC75zΡz"nc%iA_2xbF[jl3Y`tiqC|NHz`췎]jAX&Sùu$֋`LzqNAU:AdrWt?!pT}+QT^satjvþWټzT5 sf%7^Ӂ2n@7yރl^\4uMQ(*éZ t.s :Ֆ\v<~s;i*G V^q{fְ[(A`xuLK|p]p_R{{TQ*_c]Aj5L 'Zi$:#."%J"̩;Ib}4K2m7u;t-UqS~]LڑY{i8Jv Khqik Ds0:lդ^$j838e`NQn-ނ ]Cܑ3J2;MfV 66hq綸6C.ݕvOjvѣT5鸨Z-yRB7Ǡ L ;"A>4jva?~:UM16vL(9 k[gN+캖ی4j "-|'ҔI`_Ojfeqp2pep]͎ڵ~lA a9iH E/aCSIJ`w` ۹:Uⶏ| 7H#,{6n6&~ oȡ瀇{{l[N a V{ѣT=8uF:S,2jeNҽ9NHvQWG=p_C\zjwP1vh3wNRV:n^FtKj֝Z">t<:wpFp @ZCсLݼeџ_s&ʸtK6xX\AL2 ˇUׁk OϰgFS/I+=Y<<*o.}?4K[t[iVg*er R ;^vi0݀5'4F 8u˯ &Me~u7un(Br ӏEF:!6zq׻SmYI{7:+x5|"+pXtO- `. % :(.`SH#5\J]]K| 86:iN>I|MAt1s 22s /YHА6v!}V#H~(Y97+Iײ=X oq(h{kHj'׺3`":RyF҇{ȹ4 R} co[x=Ǿk퟇:wu\xY~44Tgkҝgw" [<@VTnӉfT{\lܟ ] x>ppc2VBf t0u<g zyt4g4IKGڐk6F>4|1p5htx.rm53~Hq6b'KG' ?x+޵fHf%L#ZD7LZ]Kn 2lBZj=J6F ~twyB{6BZ翔%fj^ms⮡? w sgGrڌ=ڝT?§;]mx)iNBS5_!$ơVKL{wүӁG1HoDkׅQM9-i>Q;P2Jk׳|jqCiapPk$ [v?m؍ %'G[.!MݍtobPZ}/ՂolԿpĨ'6 -o<^MBGЬ6Y+u&ѮE52s_Ë& F sо'*a$1~GY}32Jz_=T.!AUL/?sb'bz!%[m7Eڍz#+ ?bq& 'p]̣&;gHCo }#pKM;]?\B]#rf %k={%zdn|՜xT8u :9Ԟmb!iVVJ*?]L@w%~d_BQ ǓF Dz!qS=g)]1 _EsS!FĬt!-+}. M1֔ZA =d!q#>ITӰbUۗT."处ŷ}Hmxpۼ(<6o'ݘ埃XJJԐx C7ehp;ж\S&>B ;^ a7ly5ϿʻbL^i;w0ڋ Sۉ5unob^H|BTRfkwަNe4#r'j;' yJ^H*%Po+(م4W^m1p6!g~:fBG-e&uvt{]={vD{]qORQ<Qgj+i }Uc=)s܏*l`}}H2KK臯 y{}鵋(3jiP'Io/ҳ-߶lnfθ$֢܋ڋZQ'Sfd?"z_֮ *No1>ǢQ6y/S[ӄ"vdJם,5->\c-iGQgRn"gZ L}ن ?VbU7(1Gk'gpS~A, >ԇ~y&}#F Usb<%7ω?;iېVX|YH]PW>4;g9[_S.!(M}T4 q7m7gO+O]F+~Nm;p|c3ĝ OxXpi01agc-*_a E~-ҾDCX-7佖A+z ` 6і )"_UI+=op]؛\EkaqM/(ܟہ}[L"V\Odjcttf bl!m…/Aڭ=9 co{^;i>Sţ2IBƟOզϑ-k1S;žř#w{zۀ2m&gU^ۥ>F%Y/h/sٶAE]j!p\ hЉNEH^ W]ܯD$y5_sZW[dvC1\8a7/ۉ&6#~z/hf؏H+EU[Tw([T 0~N˙ܣMmǭ(_ яUgk{ݻ_N 7O cI.п7蛤`M Gܫ~QAmVD9 qXE.ߗ+3!׃!%S/۴Hs gj T_gܯ, MmGe*FVЯ*ּS>|v^hzn^F;6OlWt HȾX ~}#K%_ôo?fڕ+K~lْ=\ IDAT\:hCbOHsڄ4ՠd?܇I*ԷEi3cX?SWЧ ikzUnic?&{jYvj[oXA?zfėngJAmut_07VKntS%'0(c)3äF-ܿ\m%a|~ sois+H3'I{UΥ;t84 p)Rb$l"eMN0jHMIz<1G1}4m<1JAeٴ]BUrXcӴd/zJ_tſP«H4*Iv]YЦSŕtjpLHTήQϥSA: tlhGSva7ie3Ttߒ)v[W}E'6Їv!~>5_4Spm,m3zԴպ*ďฃ2Uvnci KtjoV,p[$& 0ޔ)CU{mɘw Qk+Ia#[]j_->hImSͩ!ItQj0+/I[KˇmjO {1iŹmNZllq*'VmU{F) 8jr:Q;_.vȟf(ڳ4FE:TO-y>;/6"VP+IK1v p)ʙMaU"Sp ՚9:IQj֙u37J"%IJ eoBW(~ }kZUQ/0/ukI/~糼3 fs%JݤED5p?|Q9&Vy$Uy0 * ( ."DfqQ-jbQ}5*.15 HMdUff?ۿ[oթSUwy3wSu`H-n;+k5P{Ƕ0kkfǸՀ!bPIh!GL*0n"~嶴"ĬmE3&uĬw\`-wenDg76~r[4 -X-iఖ=Le U7 @0Wi=8wgg`  36n L<|Z1<~o.#&x)v~^9mߠ^L:嶌 bxT -X_3@-SFQ9vfMs0k_60:oq>O|;w'\nsb.bWO6MvG0]3a6NdL8̀y y-og}@}"X3Π pjFu ˁ2ڮö3pQb\p֛atlK>b ہÁcoښD&D KpG/oP,A !CrNg6is=R? 0n_X7.28%>aO܏I]4W+H"fӁZs>.3'{Zܿa䋔a >FA z؇qqy81ԗm7@#1u{- mx!91v=Ȱzf/͖}O$LW[_&;A']UF"D682ד~~Ey.i}s DdPe--sĵOF >[F|/;앷LmaZ+x1&ܰ!%Dۉ'ۈHgl]εuҬ:sWݕ.s owϖeD݉1 a;rڝt̖݈ٟprWFIt;3 ۉ:\S6הuFfʯtݤ$=vۮ];5DX>I~n;m ^ת ^kf)E_29y;: EeUN D`N8y: ``8a(|uzuumGi79-| }Dlqۣr5e =^g=Ntq}=g8)2]d|a]>Arl'niDv-'{3?08_: D0Z"@p1Py;,'&XOW{7K{q:_;Uu;5j"v5pQZ*q^Jj!F"X硴x}`ۋ?|%s;wV2Yb_A|_A|79$iQ 䕛1اQq'K$I}GFh<`vZ/ .]|%k8p$I(oi3ƈ7y ۞D {`YǍ$ITt@KR07k&ӉA?e$Ib+1[v% X)KQ>R7m-$Ip8LZ8|J2pm pѢ{O1 -f?+$IݳYҔ17n m-; 7-,8I$-Vϵ]ICfn-EA&^qm4?$IZ6gO]I#`n@ O- 8I$-FwַݯjaF`y P@ Kww,F`硷h2yxv#Ih\ǺdCxog/oo~v#4 M7(s %Ms&!DrL݉ ]tAI$ Ƨ{Zb3!ڡ|3?Mpk%p`ۍ3">//*_Gۜ pI 3{bu=["I$uRdC2Zڠf>\v#49 Mm7bJ<8o/#f|pXl[f$IzxFu󻟂qm7Bi'UҚwŶ1BG":Y$Id^/9N`;r}Zh|H긱h^- \q*>ǭ1t;H$iޱ}(K$-8x)ۡ enzM{wCXduRLc .d܏J$izcfnۺ~'4N6h^8SӀ׶܎qw1m7;>Ijv#I!;:37%?`Pwj)n앪8أ톌lݐ 8 8ؽH$itk ! /ڳ2wMpح.4S $IX}rV-~<}m6YĸkEক8 m6ݐo$In |юk2Lmxg?6%17}14XKݐ7݀Ed;kpݐ=lҺSPM72uc<^z>K?Pez $Id1N|&pߑؙA!I$5"x^NIq0Fh U$I>_nQ" N37= h<$I4I~zW2FIZ| M'`(xI$iҼ4s_'M _?He9H$IAV)#Iqg,_;!I$M}xpD$M=h?0cY<0$Idu-E4ae$I$Iٜa:80FəP%I$Ijt0% sn2'I$InR6 }OMJI$IEd p-e&xDzjrK8`$I$IZD̸Gf_6{)I$IҢpx.p/໙u zH;(I$IR&p@B[7'naw&xiS$I$I@|_#f `]7O<w-nG+1uL΃Ð݀fbn=ˈ@mS=6Оa5ql#r5[ng8&.vi`obvpqiI\."l5p !>̶bˈkb XI|6IBKnv6pXۍqO" w!ncR\KJ>:]F TxA^Cd ;_!nv"oMm!G=/^D-Vٵ)O&^׻6\ 7qݻ"s7w0r5[״}?܅(s3;>d <xpfv11fW/œN|J,*/mC*x>gc)P/qUXnSĘ?LHKE;Vn ޱS:׻uk%?kSs^,s:ߵmc_Wmt[F{w=_㣩=t!>y?*`gb_C^ot[ϫ]gYE_!ozXCד/y aU۸8V-[) ~u[9NP~O<|‡fYhl s?$`9q!n{@6˵%čCܧI)U7_Z\!jͲLW6_,/h_e?*OK=k ^F{&7KY=nk{"jO·Ny "93(M";1݈d/9|9u6'nnk;9b<\'6XC3yH6W<6.<@Ա 9̬uXߏܠ+&cZ e?a_{c5ծc9*GC~Ň\Le;!"] S _J_4]|+˾9a;+ DEa#BWev}lݙj=AӈvJX; sLT[X;j[&cNdptN3N}MfeKd9~5W2 @}ETt:ɒv";<+w}jO[5h ┏7Yzڽ\ o%r4s7o{\WʲWWϔ+vJ2S\ݏ4\5uI3Ð}-I?g,Zt l5i`îD&N΍B;Z=ˇPYR3s v6-%IXǿ]ªD8䞳jkUŠ} c7}Un:c+j{p'Ʃ9ASJyl{]22ĖI9О]uT._],i8WGO=YTFd*,/#^+6F^FGlLȼսm*SΏlݳ31t.DyFw7 ;y2?B7Ƞm|x^<*XOPf5s9{$ Y'4z->LcYԻvw&%wދna N&իz(]oV3k޺Kk7I-O[qrZiܑcqBͺAۖ?QS_OSmy"?u;YsWon)7V4hC,B3DN]M J?Jʻ>.Qh߫uP L=|9%tPRX&ޏXl=_>H`w\J-ZGyUwi-qv{e9n_ƲU&Y {_UD ,>Sb9}[ϗΧ&>ݱ'ߏ7xv:^Qw1Z|NYI ] IDATzb3Tw]Iǘ֌K}9r#P68YJle]A4}꺼PJc W{`Uj/ˠ:;Q5'' zŒ REu&ȿܝ]ά[U%m`1({wC3WDr龹Qb?,5gP=-%o~|ז{EMru|]: ~I~ᤳ~J~&)[\oDrOtaK?p[G'S,9;d ,nϪT+̺h y/zu1v~*iDr5iC6Ct0s㭍D;YT+g?zlC.!u ~Y_%+^/{kq+p9e e 6սہ>5(c:/c̩k]I/]jʎ)ؤt\Is>gZ1͔PJ}W&AҜ-,#zz.MltH`tԩO7^TO~P.\~ _=[5Vm,)>,'We1ye)IZMT'Q8n%vJ}$ۙxTYf];N\ au R)WQ~? c;-]Τ4u}5%M|6Ta kBlnC}ȢmgWi`a&w¤k>3+3Izglv:&w -^kˑs=_Ftl52Ucm/J+~Nff˕}l/3i_W*z69 ZN.K}_҈\9It 1XE|b7$hڳ0\N čPEϠA Mޭ;H%^8㹝,ȻiU}gF~pk(Hi@t*f.o> _\}ԩʾܻwkτl;P? q|n(uOwf[j5ţS~Ur',sl9e\S܃oGz2M]}_ܘKퟨLz{vSk0tgwYn]c IRN"olI)73֍-3V3)]G )|41 *Sc 6Q= 3<9쪟6/Q?eK݃,lC`^hw<xc= @RҳB+O$͜Պ\K}W{3T:QSDM;vPS8w1̺eXզy,G1mbWYS?n ]X߯mPxqW:NfLC#=K tJnu(DnM$7PA>)47 %pR˧h݉G݃_?X}˷\*X4C̞6 -!dǢDuyKYF3 nVY9uruʥ56O.5yjRu/qSW.,{E]u]'gƧ Le]ÛZ,&λ_ͣ 9p gg.8q]Vo%o"Npl:[?'W7.j= [CqpLz&"=9B]fϸ? g a:纙 vjj;ϓ{|%q#ז fMD?Fg܃3D`a<2nMƁ1R>0[RnGRL7<#wtۓQ\ϟ=Lj@?*SL Q=29zRO1&ыCc~#F@qxHZ!i` MI|,I Rbr'(=ghz.'!osrzgn %2R3MJꯁ{"fpk;wxI+}kq.@ܝ:;cM5nV_ n;KOh?O!q _vټϦ>0xR_O`{/LYIdAm:.׍>[]D0 mCvG1i'H,`|~tOwmr1W!ƨ"I^w]Dz:怡Bߣ^Jj{-.nZAWժ?U>sf:g.S]5 j˵}=n: ]JwYY;aIs+T^Bx&[º٤kj~ܵq#=׬.Zn} q5u_C{$ T7>8&=d3X$D|P$wm`U'F҃8/:ÙD#s ߓȈ*H/>Pu^gh6A̮eĽ6HsDKiYbɝ]Ng7-Ww ^ hnAq.U [bf:KH;enCY, Vi9Ϧ^m$+u}*'1}y$yeyM:ʴkRb&ι#ʀ[HbMs$Iًƽ\B4ݚs3~ƽn8/&P 1qǾ3n:MW}q6e5*R?[bQ?pe%D^^_+2rʑ=Kzֵ5u{bDd~5k#krRulWu2:W7 !=k&0Y?X}q_z܇̸ױtz=zS|zbr6 xЩ[ #}uР̓ʀ- up&'&;!n5[RK̀Lq G"C5joOO`2'xh3.95j+:71`r2 %z4ԝTzި xbŦnG(mkuN^LXGS, z6v62s3<(hV]ϯ$}^"KVyv4lK%26 ޖ6F-XPRq-s!ND\oG l٫[Ц1׈]_Fu qޛ$iΠ8*;\Gd#so2ѨK]-oe.\LdIgwme^qȀXJd5XӦkc3O'j t8o2QwqÛj}#ҤvKd/17DJpl#5])uos; >'k}x}>iM&8uAbx BL'Դ3Nqf,\ Y}U빸aSӮ^ʧBݜr{>CjKt? EK.K~1a/א&8ک?C$I}Aڤ͝F,R:~%ۉMČW1Uz6S}Xt;uK%qcAbǯS[ASmNMO;fRjw ɯQ.^CzRz3w%fk}\FǓ'js2ҁ'3dZ\ʲR4ˠpU .%u39ݓ]b=[dYRх98~75hSN{`}MJsI5u2I$峙{ BTcKH%y%RpV~ ߬Yv/t'1xtENy@FwYK2QBYlL2sDSQfLz}iK̺:K,5{]^ ڿsV[F% u,~Y,U2*BudjI/"_UwCMu˙!Yh,MDW7*p[?~T5rQ⦭J]σ2q=/d,[63KwKe}&e@ ^Ar5.5VRzXN̎WTw-ۿmJrT $S[NѩT_3XGUu+m̌uAt !&XfU\W1deϤZ[v֟anTB=ʚl*(un: 􉅟WUڷSO%U^EvK:'b ǎh4\Gg0oGOO0w,.pךlGFtḵuX|٘U榻ŤXRXr+[JXROE Qmp_雨?.S,wN?fX89¾oۨ:ˆ(<:s;RKHg~~Q73QEePf`_\fl 1XVf(vpeͺ΢ٸkrgS\,e=kk꽰^7xX6^ՔgeQOdlg]~I:z@6;ۆ}:ϊ|\b=O\*bNh1yCU[g׺$oB$ڗL㾉3w<~_.RIR.Ci!q@Ơ-v@k-TJn:` q6&↩n}Jta"p a3'=r}0w*7#*l˞w;2pw0,| 3@b7Yfyw&S(Yu_.WVrXҋ,UԹ7Llc;1qD V_USHdx}b<8b;;?pL"0gf]@u\ϫ{tTzd: J;kg1hPzR{q©r>16`1hs$qW=l[d ⳰QwNT>qvqZܹL+@;{v$]'^Ͽ'ơw cߙ֖=\)+#O$Oxȸ)QD2Kﰒ@: Sn&>h /ˈ/9 4BMY?%w<M1)7Bx|I%DvKEBl~r%pmAta+v굜GzwR7؏M:zqkZ.$=Y5# KYV݀Iw噭Mҳ>qEެu,3M IDAT`uݞP~p973D,Ս:L,LvKg|eKS3KʝgJtp91/ܗbuH}tnx .>k@J 햕+atAIR1-D&piz 8^`[krBq޾Y\* ~/ftvKwӮ;{ا ˩ kVnDv[/K#ED6V/67uu9 O'(nzn%2Q_ǑS,;fpryf;%b-?'T9@v̟Hf91)ɧilt5m?ۈls?Ew|l[uەch5sך\]N{8qlωgmҫk9sA;L6\bÈ"w*Yg#l#eo ΋?I,ޮk }ԹN&PnF,<0uf8v1/MdM^!ί΅ofpm&3qM,gE FL^Q"hJ"Q̝řZήj3t܌mt\fY.~_ne_4ιsxYMtݙ8xVʵ{Q. Changes belong into template/documentation/readme.template) # Program version ![Screenshot](https://git.laborejo.org/lss//raw/branch/master/documentation/screenshot.png "Screenshot") This README is just a short introduction. Consult the manual (see below) for more information. # Contact and Information * Website https://www.laborejo.org * Bugs and Issues: https://www.laborejo.org/bugs * Git Repositories for all programs: https://git.laborejo.org * Documentation and Manual https://www.laborejo.org/documentation/ # Installation and Starting ## Download ### Release Version If the latest release is not available through your package manger you can build it yourself: Download the latest code release on https://www.laborejo.org/downloads and extract it. ### Git Version It is possible to clone a git repository. `git clone https://git.laborejo.org/lss/.git` ## Dependencies * Glibc * Python 3.6 (maybe earlier) * PyQt5 for Python 3 * DejaVu Sans Sarif TTF (Font) (recommended, but not technically necessary) #### Build Dependencies * Bash * GCC (development is done on 8.2, but most likely you can use a much earlier version) ### Environment: * Jack Audio Connection Kit must be running ## Build and Install ./configure --prefix=/usr/local make sudo make install ## Starting If you installed through a package manager or yourself simply use your application launcher or terminal to start the executable `` You can also run after extracting the release archive or cloning from git, without make or installation. If you did so, for additional features please link tools/nsm-data to your executable PATH. Use the manpage `man ` or run ` --help` (or local variant `./ --help` ) to see available command line parameters. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.984499 agordejo-0.3.1/documentation/screenshot.png0000644000175000017500000047623600000000000017560 0ustar00nilsnilsPNG  IHDR5؂ZsBITO IDATx^xI@.5.돻.BwOo3aoY-!|{p=-UoOzg4 HHHHH kjRK     :HHHHH : @uƥkuN8êomc?"ކwj:G;#Q   \t#n|+RgO']2cvjԛ5etrKKū8/O;:S@"=_`ll,޾G)(WΪ*B]8s w{o^=SUzM?qqq~W/ٵmH!lWٱe]SL!   P {sxk|}~ҥgJEj⣥(C*,wK+u=hq'ѝ}S4cO+RCE!   %е׀Gl^\(S[whҼыgY(8(8}`*5MM߾yf|[l= 70XuDX=>] 7)<,0 rYAX_7|iъͅNǮiGFDmجBj~~j^Fqy_}j߹W&- W.d U-0`3'#FšId skie K|Jz]{ܱiٓGRwn]{QaDG߻}}?+"c`CvԨ׊Хw̿e >bbcb`z$o~\2~ϙ:phݾϚ_-P,Rl)6v\n5mծK#v=$   K =/QͫD{ Ukԁh5t1EO3xnժۗmVztPOXBkX<{POw8lSaUOED*TĄlgh`Թ{e+>{G܄$yP/u+ :Sӽ`Xw{Tr5k"%!#'.WL|KN8سcV-9~5"6j6zhn.`z!#'\2))WF Z>G'w㚜EwS!$&- 5}Ym}qȁ 3Hbg3̟5|a._gV,xӄլtXRp,Ph`h'OA=Ϛ2ʥc%K#yӻkS:tT(WïJUQ'tO $|zz~~>jWԱwܾq Qz `ގyaG~4c~wϿ׬h6c-4is?0 ][T-测h峘߹Ȩwoihj'TU!!mbQ, N5(0[v]8 -'8ynFr1  .L1Ϗ$ E|^: ?߱` ->tɣCȁ]8 ߇ /?sq) oܲbAٓJZװh5pAwo^_`MP /;'=yG߽} "Zͺȏ 6F3HHHԒ@!@QU ?(}ٲHq)[Y"C;/S!ERȃiiLx5fR|~L?&y娈HL#.Hh?1%|x%j d@Btppp]7߸v~ZZ >& )gj*70cU˙[Y #~ .zbmWZ+Op( 4U\$9(`WVFܔBaab8MCӖ8|Mq`V6fđ: GV$k4_磰 r30=ΞUMl\;.# #T ㈡Ox݂h| 7vToXfO-vkDDM| @Bv=Zа>Q9@pT! Vm܍JpЯ{k 6<`^(_@Q~'W&gv()"|Vrb j*gIHHH@$Uwn]_Bebșᅋ3Oů]:]ݵ6|Q|R~HHHH cK))a-QElً EOZtjϟ^=RJ?V3;.a@I~)8w^7`goc ]z<6&$/_>J_xة(5&.wo^ɛ V   PcNKzT@DPppп[)t   PKIT*@$X/99P}   ,H ,*7eNGVP @V&ԝHHHH `Pr*L$@$@$@$ DGGge; d)f), de ʽOIHHH:Y˩0 @V&@ +>u'   rd.$@$@$@$@YԝHHHH 府 detrSw    ,G@r*L$@$@$@$ ʽOIHHH:Y˩0 @V&@ +>u'   rR0c>e-Egm첝 ]G.8:Vt6FL_ r $Uk513{9ӲehCτvԹ!AsBsaaL.XxkI ݾqeH1 +p*ljf~YjmضCctm **jYB~C#6BLKJ.tLfo߸ƥí=\ iYyY}(YnUOӨ!cܼ*0b`sv/ȭ. Ak:vPܨ =LG\8#)^||<~*SE1{._vpr֯ZC/TUF'uwi߾~پYs7թp1 ~ֵuhQὒxL$@$@$@$@b3 KL g/Ze|+V)INGB{DdĆUq6_<޻=yv==yt/6&%JQGGO_ٸfdfUݹu5[n/BںN}Q|0/,M`GBqW̙6KJoCVEpȾGo;**[)ϝᅃ|Nvٲoߴ"|ҹS&9B蘜eG^ȭ$@$@$@$@$,(80Q@f0D]%IMHmckGc-_*XZZ#ߺzz߾~V6rLpHZrB+mlŸ;oq)o_ml cgl`R"zAP$omc'\8!!(䏊Sg& yPO)a݊ [y.9!+ SHHHHH@ ;0i `9{KQF> h7cl*ؽm `"xFy WWR9DoRLy<mem w80BJo\de:h"G2]PMZ˺B2cyN $e)@׃ gOviVsc&̴IHHH8e7]Ȩf542.ZFBtsm;w07(D 30g{y7l (blbG !fqH\_/;!6DN=--ѹ{?%||۠a/c=; Wm %VTR}_crǎb%,Y{~ڴO x 8\?}'vmzBll܌;yxTICFM3sf&՝$@$@$@$@C@өpi)=[A+.uIF d|B2r%DpQ6v!,IHHHH +P7tيO_ :xVWN$@$@$@$@r g\UH$@$@$@$@$n+Q     %(S$@$@$@$@$nx (566ƃ-zc>$@$@$@$@$]AJ޷`fjA u?%   r LLCC鞄oTT$@$@$@$@$@@nn`D$$DWHHHHH C2>  CBaHHHHHH@8 d&t2SoQV    H#:i$@$@$@$@$LEYIHHHH  @f"@ 3e%    4HE`ɨ>.>N3^3,<4ye124iE/KVBcQ&(ϑ }CЃJ @#;6˱KP8`Yr.k`h}IэЈxAppDhѢ %[iT-FFRe|}|(s׮^ҔU*]ZbE?FEXH (^m]݌/-%$ "`-/e705=S}hE?q:@xDqy*00 HIVϹdv@&'?LރH ӓaim'I~{'5r̼_~E3-7ixݜ"OKI0IY,~P} u%#7c<,,Dv|*T1d ZXX'Ϗs@.Kٮc7 KO>fׯ޾~imcsV;c^ׯ+ IH #2|s򂄑CwM#STDs%EO ?U[Uܹyc{^tqRK.exa,Sa?DhURs9=}DX1UrIlw$u%<]]]CC#=`/̼`=]t^&eÂ?|pPCPP;eܽ}TeC܂RZm8~…Z(W& @'sۆׯ@NeFR5ܺN&ثKzE?$c@ҨLȻm_EʵeV.]E sr.V{s0 KFqUn$&LH@ -2k"^i.0__rܢux W.ھi8.,VT5ߕYPn ,Oe{:v7ɅQr}ϒ4mF 6/\n㺕&rwsmѦlTH٫rʟwOa*pdf"HκɯHTMfEez;ԎH 9cq$#242H!vFWb9d04J)2`BE0ݵj߾fd) ("pyD9.?ųGFO >=Pwo 1}>Tt<>o~ Nk&~qAaF) -YVq5"oGn+~W/߸%!@$D/C},eFV66ņK-MMْE~%    ?N I@"a#tXG IV~HHHH2` e     M&HHHH2":W( &t~XVK$@$@$@$@+HHHH~: ,%    H@FD$@$@$@$@Ւ @F$7i @ `+$&X} MMu @F o`` $$T@_@FQ@$@$@$@$@ `3A!h$yHHHHH Hb@F cY    dd$@$@$@$@$tBeIHHHH : @Z$ظXk:n\yyΝM\ | 8[J.ltqEkϝ͈0liIuE%    M  ٱBؘd΍ffKϏEHHHHHH*HT(0(7hr r5qZxj߾z^Juqs+Uw~vѯ^BzيU&\0ņ'"|jNy'M\&L$    % w:yMMm;xdۅ oߍk&# iik98:!'t͐ @&!]sw䄙VF cA ((@^x燸+N[XXYwp,Xs.P7ȖjaXHHHHԒj @jsusReN    " JzOP9xz*ZtԨCPNnAztNttTW.k>a2q<\ݾ&u?߼z~h߿{B#^zҽǑ^ol2 \S%    LM@өp E 7jGQ̞nkcs c)ev)? @'TPCIHHHH@P6$@$@$@$@$xxHHHHԋOjC$@$@$@$@J P'IHHHH@$P|Xzu9!   9zzzqZ[[f+ %OWDtL.@Retuԯ˩ @%pI%+s8NPBHHHHH sQ.07+ó$@$@$@$@$VUwR    PNr>urN((.ҢmV% PB@}AIA" N@' =}~qqqKK[;znPEH~7utW.kl\Zg}w_ H 3P1ɃOOϱwW[lwxworl :j@ht*TF&۷kKL(Q115sܫ`bQQW.t,n-g**2_="&>_=rʃ1*}5o[v}=== ;6- j<**C//*ZBvVaaKNS$@$@O?;,]c=yvGe*TY2g*5hԱCq2jϟ<̀a}Iw纞>ltma0vgL퐿8fz $0&$NFb%!@!._vE^֭XWCY=;tvbރWXPԱ F+8`i飘 ֛dFO¬?\,pa?K-œhsaaDzN;1S__8} ?C`B΋wR52>{z7mb3H@ þƐ K+`0L)@\ 8[eϝ C]^[+kQ*n %KcUZWW0Q r")qdw8 wDwQ|X{ d5*^>,ˆ0Ăؒϛ_9\aF?_o K̘֞4s+.R/M,#If7wX+Iju dai&ڗ*SX5D؏x[.[QbbP-$Z[[D&70DB~)/c@V0!+(\ࠀ( dY°/w7hDF!Q<`<H0jf9=y[~qL?~bs]zZp&s%BC>y~}6o![\/=B\+5jѶu.YI"æPahHRlRq #b gn{cZ=&6q|Ħ槣"#ut511rnHP@@9+dgO-eէ)\6B6,c=yقbWD\[|^A@\61SRB*R30k55A\ 6UKANYډX=yt/O M QVu F:$>+,HH@x؇5^v-"3*vkѨ%_l߰ _⁁]Խ?NaHw}JXeĴKn(]:pͺ)=Y).SHH@m H ڰT;d'P؆X FH@ ,תDc݆idlܰYk,K|w46Bճ%/|t}>NJUj"T˧*Ȇ pcH"CpP!"$#2Fvs";^<{guGu A̰g?ml93rژRlj{ )^4:zz%у;[pi WZD L@Z4 M[ BTN7m.F| 0ȏ*`<C:B-Bᐈ=b191"hT$-IH P @^o/uV-Эe1ٗ 50i=~@?ćq|ę=aS߰j13uRDvԑRʚ%sZwޮsȨsaӰlkx x2g5vy3#.~euRo_zTNa O503 }};#s0=ԾKoDT~~<56[w^ٳ]SS>CW1+6It 4 PChhpFM}e`z:J*H4[n#̙a|iIHHH2[[gNSA`t< ADz~ l% alj"IiIHHH2#e^$@$@$@$@$ Csa* dM5Z dQi}ښ%5DIԒ@<ހ^RAjTH P48H:zxl3fe~HHԀ@{uuutub%^8AR J@"hiiFD)jIH28ec cb$E尟; 7EsNaڿ^F)jI@.k+ߛ[X=D uz5Es H$щ#ûlx'tAͼi\nϪ:jJ _8wF-y*J S& IDAT󏊎UHBG~D@p~ mlmWa?-.e3@y%۱jZ*%3䨑G@BE4ReFOfy9ɧTHwzY2@y%{1:jJT3U$,#Ci& \H o'ߎ8 dށ4J.LAGTJnI%|vgryԜ@km|%LCgNȰJ)o3 ̕'r$L&(-g-\(O鲕z6v.͖l )C>UkMڴP x-xiV6 fN |%JӠ3.+k D_AY\N9rFrddϏoݻ}r\wcll*kڲSGtJ sj}Ut6#XYعu UpUdM߅'XZTV{IZC T$؊ܽW.Ԭ3^kvK%?}ph?_Hܡ[/a ^FC^Lwp.͖mٳDϟ߽Zcis䊊r̭kqՅe˞fǖs 򕪗*S>4$6{޻bxqq1|աNwFOzM[w(ZT\lΟ: TR¹lHPfؙR##ePo%K'L>L:?tb I*/ P١FŘؘWϞ?WA| u1GG쎉;XA<n~>މ # 9H,-Mn^>-}Z[6m᢫?~ʼȈe+Y~0BBn\9E<}x"Jidҹzzm;t*XO^r*T*$-K߽bŝ׬gnf9v&Ģ%hgbjKNݹqNBr6[&t_ut_|ՋRMӢu9r`7,#T'nmO;x䋧^P f 鐯/{Wb l-0!3j(^L ޿!?~d:H=0T"ccrx߿n4n?*sR8z~Wb${תmVl^kˆ({!#'9q nԼ +AK_F,G8F>G8p/Qfc 71ѩ aY=]Poor[^4w>n0a!.C:(kW,VhI`[#t' 1?MXOGSUa?T5-iNpglbڣ5\(z8>Υ˯^>/>.ga˻pXEj/֨`?#!}ը$%q ضswo|!,,Y||<B~ZW.DPy OK\jzsəjuQP':&{eWv{0OP/>bϜ.zX`૯o`-缙[ :Eno/ ]`ZaUT*GP8у_?{FEDx 4${1яʕ+/d˖'C?%KYVo&#pdNt:űL#`hh誈N)ss \B`)I:R"BEKr xp\B hC…''|;xġPTu܉* |x;yCeŕҟ@⥬!టiH|@W.(KgC0q cZ HT歫xzF~ys k+a*c^tZ4e>FXZZ%}ۗ^߾`$| 摺y\L]6#&:귪Tܹ.'D?/XCƶ72p7~<=܃K,+cB1vװypϟ<$3 LK|sAy%b>o1'$7*PuhXl9#cPTBJTT~EVxj q ŊqM± 2{SFf0bWnɃ17-_٩pV. BN8o1 X"xwE2*AX6Qa}̝"yK|,H͎ (;TÚ9Lܼ,_X+ZI). N,= NJ.M9&$awWEg3H CzkwZ EIG_W5V'$  L^b^߾zִU̷py_ 44(Y{ߡ/?`EWERLͰA WnXDs~5M[+[$jC@|a _~ڥ.2(*01N0+֐P k4oݡE.X75hUTOD;8l֦SP߶D;k+_z{qGh pSݿ}TTɤ""oݼ!)|Xt]2Xlk~6F{+Ȇ)\&"|I}(62<{X?}(jE%댈0ܼ0aaxɣqs~S NuEPPW/Y*ύ;7rL lxN} &xPV.!D)D~xDn|ϟ?3dWUz-ş#4صE\V8x#̈́ 0Ʌ?4a3PMa nv[mx;F޹Znm:!۵/]6?c 3ݹ]^FƈtlYM_/B.[ V- [82Gөp EM^ 5Qzaϟ>{d)H&;[>̯l*W/Ry[³!#gVZM5 ?D/_<_ҹ, ""$QpƟH'J.Wk5SG(tؿ.u:$ehp̌ݳgNb5O!Ee+ 3@ =WRWH MHX"q@j3rUV3uOBEK{5var vOqjUV0F$@$@$@$u =â$cE{.4h2- @"8}0 n&8gfO˖йߣKJK׹lssϜ=uLzU^BŪO%޻u-,CB%ňٓS*=j2]ALjiWstlR;silli-8Wab:c&;DFEx<~9Ӓ(yvء{wnȭP;r`b&%`iiAVV֒J&Ub HR ^xaU|iC{ܻf, >.^SSCW+@=Dck$ ԉtp|U5 .ZLyۦ5n\@v9,v5-ܹyM-&&PtIP7p\|? KkݺaƜ%D6]]}߳##XW7_VKϾmmWnZkddԶCCFO;T.o_8w2,,4[C͙߼~j1`jhj PVwK%ZVhHHj5NS{F8n:ڥKWĒwo`kبV{=c:ehU^?4tclJ8_TZWH:qb޿ԙ  rw{+.B;_Xx)Cnown$!!M&ӥS؃wLi٦CԊG=phnǕI 66&::q%G\έ땫X]n^kL:$0= >yt.~>noM+wؘٹE}:\z}s_bCF,TXK[[׋l;3`͓GmZiս@Vu_RF}\߬[x򌅰~|j5jeuto/p530g<>~e#ƿ}ٓB)3:~G3@U p [.ôxMDDIQ|) |a2??_$;ԥwyoW/EB4oRv} ܼ~5铇[tR&tM;u]hȨO=-Zml֬\T>K9+ZdV,йG2[=b!(ϟ<6]!FWOlpqƌf_gQ622n]obbڪme gK<^\%Β\r!0qWojÍݨQJP:-:`m  C~c耡n(PRR%,I[*r'OrGsw>=w5wwK+I/e2ٳy+m0RC ]н/oFl|&g}ܟ"6n,K+n咧dq 3CG33iX-k3`fb|&-)f0H"q̶Ι8J\Ca0LIN>x؛,{۞C;#suˮߖ,'/3HLEUe٢.=.RS;Ip :Wɂ.=" P@uш1ᾈ?CꄪNVcf5H 29Agգ" Q%KʀyB ޾ ??p!?}ohjxXHA~nNE/ll ߂ &27?߆VHL,xGA&CG!@ C(z9]XY,@p} ٚٷڹeͱ#01@Ƀ28¿d-bkԶiWff1D a٢Y`â#Ƅ-?vG'W,% fʗq2NN{Sget̢gO;ɥ}ěz6f-wF̮~uf45%8URV^4w¤$[K zG shZ.ݴnjY~88Q~ŊPUlbau&EF ,׈imX[Vbu"%kVSshndd N"11#q pm”w[NU|9Ӧ#by!n\=1xn8`X1PZ 8?Wrs8ȃb߄^ ~F>W>oc',o &,R&GBKE28m{dZk}+k]t YO4{?6ijn=!`) _?_/(c*[0(\P V V9 '@",,x¯ƧFX:V9y^p4yEـ~K> gP>{-ep-).y$KS5ytx]FYhbvEMS/aEEQ\nF:í򟎉p-I e$ oB^%x|1~66v$OLL44-:1dqђcm>p|ʴ9NOJLVSbzc-i/?A>XDE |*s?_-ZEf,10Hzgb"X JNg VZo~߻/ձKw^ݺkhh$  syqM4XgXaعyPY#x:k]Q"FOW+_!\ 摤cc:rߎ:cgP-M\%+{E7Ra5/{ia656mQ| & [ `' x{t M'!,M~I%&$|N+ϒP.W#+>[_78mK+LaAA~|ΐcflb>&|KJ.lZk.apC"B:ggiʮѲuFRsㅅij\,_p2|CNDjd姾! @WyYgwϣwpe!Ai[R^RVE**e뵛v**11Wπ#L̂_;u,Ia`YL:8Q `dؐ!X#e{ MFZ^WۂJN>tLD4vuvnX0JJJ `ԞoÆ\MɧY+le\-,z{zn9sؘhp8%P,K$=Z ] $uA /G/).Qңp20H2q,?WҀF4{t84H;% ; 8AU!)abFo~Bc+X0%0Xal%I>m ,6_"Ba y(L+ 0аQ40"P,'=b9O'[@{_CF#/Jyr陙pA@dɔAFcDž :H2e k%ݺq|Z>nurg+MG@ /ؤaXH0)|bG0v-,XYDT$u )GRq. ݰxz2|>ÕYY%!Č40C+΅~T -g@d $Bh5 ;cϤ0+w܏$Oxh-PIg-B SwWֺ JJ`:=b){˂X@;:Ryi3__/ic rD,D0 `m8fTU.hڬvm28 %WCZ z7uDhP{ܹ1JL&*~LKQeR~}yS_b0+[|>.lS<=EGOϡ |^QN!}M>`FqqlDP6N.*prH6T> 3~7F[9݄`٩k?;2 kC‹g>Z`mc>b~_.ibdIpB͛rAx;'Wa"h9n{U\\4|0PrB9i訨SήȆ6z|$Z{\w8w;inO#)1~q#GJn x [\HڢuEEaU REC3m}@pZ(u03ϝ> pf<ʇ[ڳ3=ޏritpYd> s?+q#L@",TǏ:U}Б`ecт‚Sf}uCG5vύy}@3Ȥ8"͹SE'^nqmCYe 21}(&^8ǰQm>[v؋ "OXBWYBOzZ CŲaRN(ı'FHAiiۉNُ|#wq,p,Ff[.Vx/]R |}S=GXT\>:&O6T/eY5.Ze-bkGz#xuS>Y~ e^I8.&+3pѯkb"_wl6mGQVq'LGa~~Xk=%8#u*[DU6o&0!2MSrAmTG l4PncH Ȅg~>ljwacRZpl}TY)*J8´ RTBq*RfwF(hӤ.~F O,b4)eTQDIiզױf>Zҝ]`&*|O-[)vx|\ǭ{GC]~;o9yt k og nݼ)eʯ(&~(_ N0|E z<~x/M9G)8/DPQUOGI }MIf*h>yky6-\ԧiXFS8ľHi0-X1o{8'1c%Z%SU=kⰰzb`us]MYlLc((hEDJp 'X)Lu^9 v"XY%M W4]][7|˗ W?thhhtcr!Ln 7@|Ԛ>MkE * Pʗ P{=Yg xP&˞JK |2euG#@#@#@# (ƛbptI-leE>WZ\e:d f| ȗԴ?(?~IM3|qihhe)Pk@n#GT8Ԣ|.[S?>1qy] @;T;@ |Tt7̷lp\x`dddKQK'XFS KyxABbT,Y|ev]o/X~ܽ N.+kPTQ*~I0CGMڽ&im}Lv;g*meԻdv Kk֯5*3@]xoX#+֮Ų;m;do,mo+J>ecԊѪkjjn=mm}ꢵ!mG3y\5Uv\XB*{ D_u}yet]4rYx5i Ix- ͫ^lnd%%xGP_wQ  ;ͳ[+|}bY}\Why3*RtU43P{XY|ꔩ:TUiccQ,{7S_-ZҭiC¸o޼mujExWtZjxRGET` &SGW/3]WE> Dx+lȁma!/sr`S qKUUC^d=^q/?7,*+إEaIð!P@Х3ֶy-Z;n"6[}).*WeUiZMu0*gR@ gy eONr>/hG <]ڵ{UP`PTuV}TN{Ta"ɒw7xτ[\ThQ.L6>##mʟYlV6#M}.ުgJU@/wnV#LkeR@ffՋHKn묬qBGы}cihrʉAC荟2?yt_L@fTt5Ntt<|pqY4zsߚY16mADLXF~Y{9d CVf?ji>YҤ]~MYE5:2™yy9H^(jϟz [&}C"bÎN,)T~506:b9^N .iƽW{?jIC!yxQ$y/Yj‡t%i뢢4tE/#Xt#O?NHDIr  k= uv@e.0d M #KFkl4d^{;`j}2 ׽P#t>^#VKG% |l_M}8v-bHE}m.@0׊iiuq™ɤr޿ས_'^b znfYhjt($KS`tưOINtpK$/wJKLgcc<K 瘴!YwCI& buvܑ!˩h^hP'^ vlpzYBݲJ JTr堝,h@EwYAN}`ĕqK!A:zFƂ2P P2X>Ak!A J*UO,5D3> CU IDAT(I운9 o hCMk@G 운&)Z pV(`qabB\ _ A-8 >E8\tFv_.) aEEQ\nF:í򟎉pJ1_,& &$ l1a^>obeK/1N~ wQQfԭ[ZSmCھ=ĀL`265ͭ $Ri OGs&y位뗰ceCF$+PC{6c1Z.zx ~F6然m;ʴj) 5;vww@ed'M'DZhkpy<|kS69pIaʂ$'_tCc7Srҹj G2}]<).% jcK['WOaM-mU&M.~ڻ Lx"s>N"I,P7jl- %eb7$btX923H`nttIOK%O+ E68D0%f= dms5H]svK<~F𷤸,%/'|hYMmȔ(UOЗϟ<?E7!'%~ 96"ܒ6>+TIYE[KBwSh4Id~P&ҥNHX!I]lFzKj. (]-,z{zn9sؘhp8=( `J捭~=r Ǘdᓃpχ b*,i<WFQ2]NV'lԸJOS#3;#*Aj[530{ohiiFҒ%aŮ{jR~~/oݸԳbBl-/drW "%,I,2-bi ;kp{4|HU[ @LCGM޳s'm۹[i 0|o |[s_+SĀ?>'^M2C[A>/G ) ;QM ml= ɂ{F'gqy)]qإm2i0h`4NI`rGˊW:i(^=k.<)+3p_(U8Ddj| ]SK+;[\iiE!FOIW D|I˻iʟ\zzR&^'.. uu~0IK9݋\ U?;8Ήccb֍|! 9: 9m A_~/ `1 Az4dmE0Z:zXb&,]td`ߵ6KbKRugZWQIvB 4nlwVlyA hblLp)(S@=o&6vQDsTн^U+w7Th7(2>?-R 1&qRhu@Vm &wlXϖ͂ãw=U[gJNb;Q[A~"# :q <e۾c7-=px 8zA!Ī)L {2!-9,KNRU< (Xk1oK & A/"JdAq\/RR M"*xg[RrIk<ԽqtS:#s aj|ʦ܋A="4-, 撴}t|۷233]wh-Mq,WJ06HDCavGN&&|<dw3$'; <;wARs~S2 zRB\0EA@ e>9Kº E'miY!Ox1ZB +Ojv0sPgص[ermt.`o޸^D8r q^lZZ:ɉ@7=RcEE,F=PC0|#Ǧ)Q_ @;s⟒)uE]|23%Jנ[GUyV|2nZj+Zp%Hbh{0SSΟ k/ JĹi=ɝT2R~Aa;{{ɻUMi7ϱ(dloq7q^#'üUih>>u>q#aOc>=Rʨ:~ ܻӪM;USU),t,Y yY 6 ߈1'?ǯE ]&:V֨~ [ǰPiYˆnޠt &{({aΎyOK 88#&4:hn_܇wwy5nW@R{b]CEU=%=IEMLaJID^H;L7"@S/ ?] w''GEx0>#H^A# 'NZH_tKk"FvfQ,dy-#M~B"6An2ep.!@Sz7t>hF1D 2ࢤ3ph-Z1l`4Inq,"}}^QuT /lտJKI^|-5vDz! zhؔSx ;=c hgS<44_)-@s ?oъǨFa_`+(++;|r?E{ګ dnDiE2,ˮ C~8W ?4=pzYG$5ǝ1i)sӎ'u0r/p Z׵] ʯNEuA@ %9_k?gWZ_ڐxcckp֚jFƖcO=|`Ghhz9|#cQY8FٿE?si7F:۵Y%6MEⷃkVfh+=:¥US3WR@?eGDK\} v)O{Y aĺMTT >-- qӧA&d;x?Ϟ-c788 rKcqy/~?OYZZ9άyqq/߿\_R7%} 30ls{A6*Y;Cb=%fԟ.QgC^⯮/֚7y0-G?cϥ%ׯIʲypV R 4S:2`Xޅ- 8ܾym#;-{K]<+++ճgh$"MѬyK&mBɁ]A#P/+cn~ (v ێqȶ^).:À9(48w&648xh+Fw% jvVEcKu,v1#ޠ@7pX߹)hXwPVR |qcEEE衽I k6$39kʘBJmo\(gH;=ɔ]rQ*H}֘|H-[7ks"6we#;txf1c;O4?gIbK(ɩ쪿]29"N7SW/-:*l)).&S_/Dpkqbrd?rpIdG,,_sN, PGfn@$&o\޾Y~?HDyݵÜF|S8 cQvn2odfBppN zV1;<}DkqN:};~x݋ hSܩ(ΒgJ6N*² ;[|sbm6{x&ɘ9wˀ{wllli=e)ɉAEk2|4x*+)Yo N޽C *rppٰf MM5v>ϞXY"bgٙFV ۷ܼWZ#] yxʪ(s٢Y}uRSlmcq҂¼'9:w$>VA3ɳwp2o~eD!,[!.&99ѦErGҔW(Qc' 4ؘwDT, 6 SfaDޣ7Tv-X3Yle཰F͝5_ Q,7xYԬ˲->?!.v٢JJJi-JCCkVڢ$ݰyvӺkUnx?Ι7%q-] {Έ _>;|m_wBEܬ<#{ݽ_dCf?ꓝ IC 6tp*P?.-EE#KdrJJl6B0HpsZr))pH2e3bc?y=wQfDv)|5>歙yc==xKcd"xAl+҅3yycsm0L P|6 ).R`"(&LI<_Ifde)&Y0幟Ow hm3d$3D*JL]5k :f4"kϞtltp_k:z!:215Ų2'ڗh vpҦ mG(ޏCr6v~ Ƹ 솪A+?*",,4 \*(~h@=}åT a;tvӠWHǼDI|DF~w#S M*e f5tr蚴A.^hm\dƜ>jdئ$q+\w.hCL;L !L6)GCCDPz_TwkXgh cB+Z;%*CNk;U-'6|B_BcLN4<,M[G7PDIqtq{ wOsKZQUmd~+i< _a _/J|nf1,N%.n]{#/$=er]wU$‹3 <,?4`x8fvnoFssˎ"UP(.)QQCD23`I@xx{JRRdxXě8秫I驩օQ,V!-UWO%;s|Á} AHJmnĺƦ/0|>(vdĴBay)Dj_x`?a CFuE68T,VKK+XP86Wkn>U$>i@9 g+8^/h(8)",ܿycc%ֶǃWAL5܇_eDkq_GOvlY f2-,g^yBd, ם^FF9]!#C0Qdc';L6%G {wZZy|jjpЕ{^x])UBz7X>.R.:<(d*533%%Jz~\2-m|I:u鮯o1882WAH/`&fiEUk&0LUng?TV>`5e6K,A fnX@~˞/(h [ !h"C W-/xI]TXFIjg@22uHd}CCD؈ 6@23RVFe^1v-,X&$ti3__/?Ne&ĐV{Y-;dÊ "GLWa#4E6M p Sv\ 0zB7 d -| P?bv%K6,55IEEOmF8,,0dAUI)@T &<p8%K?+0p-~tvI [`=srC# 4`qn< \Nv`m {ں1ޢ" Wtdn"J3¾xQ$%@^ؤGn5HnU@pKa~؈gYW-*\~`B{da];s9ga` ΥbSr…AH6f b: *ʩ\w~[7m}֣FR͐={G9ۥ3߮ݾGt)bݻWvCv3転5>. {)ȩ]iĶEW_AƖh#ˏs ռeY|z! Ŏ^.PTʙop j,S(/>,J]̒A5[ᓀb&1 LJKqq'(N | ɧ)~u[Ol. CߦZ@@7&>`ނ@:Q/ڒ g6j"wݲzJc[싧OL;É=GO&韫(G8x/Jh2]}V8#';3 ܱucscF:?l$(J7_J)w¥k4`C@I|Ϛ[4,۩swlONYuKD5> I޼wG'mGҔW"PΝ{'23z?}KpX9YM6CUG](?+Vo4V#WM甆ۆ+0pzh4b[ᢓV]=HdM'޳z|acRZpl}rDl*JMU",A45vk^3͂}~{[ Xan/<͙D}'+,,9uXhhjj#ݧ0;ڴ]5U²ԄW͒4l7PKSU*D3c-W"L_U8=nXoط҈( nP/ wawD]fhQ/7Ҳb<}AZa}j~t 44rMpPGeSpH>pqrR|Nxn@! _޷ױ)D!;Hĩ,~dH2*R89!bT#],|" _8M-UrqؼF.A7FF@ Bm Bw$hx4>LglބϜ27Ԙz@N&in}*L5Z,_ǀV#4444_+9!n? vSC#2щc_vn7!PcWsM;FFFF!lJ=Cgc.X޴a#ЇΦ&%?~x:ޏ+9a'7϶oY-EE՛ļ:wl2nUT,CnoGm}Np Wvc&ܻm}l\@FEU5bԔ{xP,2Reo}-xi$YF(6P NynְK c2Y:jM3 Oku^nVGW/3]Wmt]GEv?N+,'<M̿=-Ƌ>|ysӯ)x*ظ<=-8tش +rBR\ C:ڹE<>_(T$}A~^֎/dVS 2x5Tt9bÀ ?M-m(hgT2 3xVV6RͱydgeT)ll fV6v㷲n mTX*f*%i+];&06ۣcWw&;X+@BF~! ѵڞ&} x;pظh$lle7wu f[wn"#LkY2 y"p6`3G.njڨ{06z ѓop.u7!A:}&t5MLE,^IW (yj$VFvbFG9xx#<Ӻ3K7Z fbu5O6]~MYE5:2™yy9`F.TyŒ aFolbgx1a@<.gi)k2jS}n]JJJo7KO***"^?RIYiڝE/iƱGAzcX^nw'W?GV w۰g"`>WΡ%~߾w6)\aMlH-v^o*?lkעπzI .?A*ir9{n]ssqXOAN\ճKěNk'^``\<^,\N?{ռ{测&Ns5f1#{軵~jk^-ME6l01-uNW[QM[R221{QXT˧'ۢ㻘rFO-L ;.;ޭk?y7h'kl]9oK!c"_?_lA~"?"joL1::zqoA.$I4Q6k ̗7aP\wuwo^U3l)ss*{:kCWo((^Y,VHp?V:<`cӆ+NE]#drlNU]rk- 7 'I"kG>J6Nߩ񸥏&'ثpH /S đKWm+W/kdWW-A ˾agm*}EGu'h1l_3֥{op'LX[:v-CGN>vQ]֎œȞdie @i'w,arr"FZZʵ˧E]`ؾ䤄捧^ ^6}4utvtvvg2pqvkʼܜQgHY<#lAԃ^@zNB} &Γqӵ}I3~޸"%\ezf~ O*M64k!w\I. %<{Coy/`Z޿k۴@d[6j%qSLŌtԢT#{?7gǜ];Kw/ݾn´> S۳ t[Z!^ݱ9x K g~OMM2cGg2ȃ!ߵ{J':w'qHpj" * }8Mi_#Hbh(@,9Ņ~я fٓGg]bl<#{T\e.[s`.FV̓*g_.ueƖ?Nm i;WG-,mX.Ϙ ?oDyK)bbE &,%I73Px'|W/lZJy)rOيU-)ᕔPpKJr[WjCaˠOA R;BTJVdDHDXo_hE v,"UUe>ܗ(_y@?%P'KUM]}݆?eKJ̉}*y+[8:{x L-IOG~K9+1oY,x!wo#@wRR_N6 Ni @8#cQA8-RpZ5~vG +*)5B(Za^Ϟ>.((022#L:k++0[P7 i^2ʧo6RWR (<#؄d㓝ǦWVG(gN7rp}i-15ֽ9UěaN,.XW6~pz^.vc?` z=XX98߹h^440h Xq/_fB(JKJ,L@!A:zTtYpoaKL;g3DqTy NPH}{^ H:, ˵ɘk6$:ŗ-,x)AЃ)NIb6'[SC&TKK'??O#a-_y#[R%e1J* =: ةk76q*[1u[*jRZt!IG[4^t 2g+)))`UZ=;H B+NnΗ[+S xƖN,I4&$gFYH%.P'vf?j>#?plx"n@b?/'+CE*==Fhǽoi;WGQ%%J:pAS~~^q3!,b?z.:p #n 5o siki䖓 iEU>]ʌtq|3E)_T `\9}WT8ն+6 ljZAa c'+SJM jGW\ϤҐPdJ]M8 AD@>< ,r&XGB[g8֍|9') Wu˳K(+5l|ilBS]M0ӘSZѓ(X.xYY3]t8\wZyuO]=&8u`pržqs{xOS=  p(XPU[ .y9CY.KtNkҹY m1-|Xhmj1e먔0.[\zɕٜL )QSKKiCΧR\Hɡ],JZbIqɡ}'N͞x0#g' C0QZ tz8c-1EI4bq>N3 rf}QUaP{EQq!7i6|^\<͚ҖJmnw@dq>ں뫠2im۬U[0#,anyzh刐*EjvsR3dy,~?_dxe@Ҿ\jλy;6,gۓX W/}!Cxn@!J2 JYkz:UJ`2Iߨig@ j8~ׯ#Kg;t$x@Cn&&|GB$_iLf57Ͳo)&ڷl-Z;G\w; b<6g'ntki̼1u uV`i"`ww'vw݅k J4#G?.uv7Nwt<99N]ˑےACMf!KQeMN0t .f y%O w@K>ڨqCFcS*djf9Hw !,cQm=~GuptC.?e.ƭ'yd)ȔWfp",SFSƖɓa8%lF{/#l|߹' ,{hn_zԤ !@!o Y9 '?-tIXqi$p@z:s Kpg|c/102!Q8œ{|D / )y͂Wώ0f| @zH]!9#NC<;3*hrAC> b|`1_?cBlЄ Cb w̤Y9yx*Xֵ^ <2 g`-g7/ ֠ͻᰳKg>VIaZ0X *;LIw94g9bT|qDhpVڊҽװM #(墁hc ;/?7cIdQ{s" 5Frcf-`Q}aVoڇP%q?x8eCc_QC{67lM&b't^ħawo]Zv;_82pzӡ>bגOѳOj) >[Y[8M&|c+F*$0V&W&)*蕔Xq o,u ̏3pW{Lb@[&IHwhRdޫϚh*ʃX{ IDATJ;G~nnN|lu|Vf}S Zͳ?Mr庶Vbrj=3QROmH4‰+?9IqvれPa3ouzO?(]th%F,oJeR WA~]vZ0be4,RϠ<(i4|3z4jժEfuY9ox9vS &Fں>SL ]Ǯ^/f%4n@;CP$Ww n2>\}@N6_Eu܂ ).xh`2d;pjmDxh9P)UEXf̗HSs__y+ T4t (\Jh8v$s,56qz0^ t ,] F9P@@jR:pq2O-IM_~9)d?@swVݗ󅦟4Ds^E~JsɶPAGqK!e劋JpȚ|Y)XVJc9@s4h9s@*|nBT"V50NO`*-AI7́8H)+[kJ9@s@9 u @VpW52v K&X&7X+SdD~}XKSHsr@UESV\Rd8"-#';w4hY "6'JYunm7-,9j^QzBE奱q>SQS7aO{&AEmwY_N=Z6h$+'cO7#FO¤܌9UTRX(\%W![N߽Er19ȟBvt i|49|ĈKb``AB[$O4X~~޵KgEOirJcN>)9Ќ8oּeh85%ݛ> /ϝD{3]i È?)eEsF, Vέ[,(0adR;L470ߩ}c']z=l6۪Q(ȩJm9hΤ4kg߽i4$ @mmv9.x*%g)S0nwu5 gԘLnO~snɤ{7/<V6vOбC,3sW/oa>{QVLYٯQ/=.y͚ 6:FG} D("#0/Y͋{).ڵ;w륣GwLgZ"S`REdΔs1lu޾y5cIQа}ػkϾɤU>zUƸsC~ ?cDFiA+*(v4vAYYL͠]:wr}[ֶmJKK:tcѤi7"[Fƪ%s8"%=p%Cu6r<~ĉzܵ򧰐gOƺ zcħuɼ w"XķnZ&L2иVTDxFFz!Hi#њM;Љ**^<~`JFм N|s5Phd\kS:s0_/'&Q"$Ae utΞ8:f┫sm? t,\ûNGFfFY}斑?dh6R1 u4u k?6jI qþSutF]8s,5eauSV/EXufCGNJ{(D:Om\N0bhkoLGAAv}]-. rF_W[YI: \(yث ެ̌~m՘A5J] k݇\EWٰHMM [vc<!qguX^:1r1y Mo݉?x̦cVRVZl\?h8j)߾^> z;ϰnt'(`gO~kVk3jd]]=|g[c F&?y +k rXvTTU!؜;7$?"v)c&LU IIF"[+|} ނL0HJb&$M`S>SXj}y5h X=X, 6nz?"UCϊų̟Sۅ 5Ӷ:rr"SCå6|R'mcu`5g땳NVRRi߱ m%GD@-1&3.)_l.i!Jjaimg*+) 2(nW.L̍r] Mj?*+S/p9sssL mo`ĩ\"}ͬ8&|&:??&B"t>s\/۹em .W|ȱ_bM+ڽ7`1IKO}U|Lv[,8>P@1f5jb᩼%k )(h۴u`^8 E#M0xU]ϟ9SzJ䍬5gM7cb+Iׯ ̢',YݹuelgoOWB(5oɇ@3'>}r#W2GUM ّjjyq !,7"8Le!@jڡU ),#_X`{miiEDޭ:bBDC(j$ e43FPA9)\qC\m+aՑ=4|[216(U 6& صuu !ܻ aBuD|:+K`┈e@H>[U'.  YٙuAnoaJ _ 9sXپޞFaw > @9sA^_={YOhh\k BddݤFwa`XU  z73c*ki+'/-9[2Jd%CΣ=}zc %%o`lFJL[tWUB|tuo^#[{/А1"#C6kEhOYVCIG(a3~& `2.:{\QӑDp?==c.@[~>^?ptA Tgs$D|bc #ĹGO]As'Y]i-+ H`r8m###" 8F-Պ2`_A@y0BXn^숪jj:fhdd%d99Glm8T(p@˷L@CCCX,.Zؿ;A.A{#a +c ],mMKF޻9,$NW`r>Sʍ[-'/%~Na!pJ` C 1JJh`9e %?)!Q" a@jik y`ԴH -%d oFz[BR..B) (G-\I:#b0ڕFd+;Rc 2Tע m|& l -M.=xk' ;C[6&86}v`MMvH :v$5Dw|DOXMmbzZ.&*`JH)FI9|l͸F)G]3HO)gxd2)ytQWl233yqD|Dw%ߋn/Z-XH˽.+yE%X/|MAib6u[KKSZE^M];#BRV"Xܼjt!ʡ#ǒ7iG|VJ˫{`PЩضM9^1dP:O$>+hYJU ,e @Ww{QPn*ode˓x`oc׎pr0c{yyc'?i:BymmI Eʎ _  MkFz8awuъ;ΤNvafzj v֝7m2ghH&+%H1d`Z3Y33K5<iI0޼va舱;ui&N;>>oaD`]r?AXYsLU 7Yﺕ !@rA㑟—,_. W w# qI\)|5J1f"PAhlP%%ʛWO<b+l}/ B'7PE-Z/— D?nK۽cF&c w_D[D[ ;U$`Fb1ָ(OpIs}!d`ȮPe8jD^)%*">1/\FH ׯ4mj HdD\W/`URI< ly܇i}J޼xڣ33 00ÄklLTt 6~7:t3AhpbWĸŻDEU]>PSVQm\K^^@v[ܡyfŃw.`u.]7U΢Ryl:En0Э2R_ԱCP!~U(>j""O 7i:avm,Џ8@9C1UONN06#,D[~>]oZ͓,,"ȱ4_ 󅶉H4$޶ppdrȦߪ |A^~ۢ18}mHGV%oUg"oLK*1i%FR? PK^~.9zVHrSġ6+ E%Brq>Bl>^3cA&(GH8i?F.Vs8Ox[7PN#lX/= ;+.bc ZyRG9y۞#8Ɉߧ`e%`jE&%Nz‘J'*ɣp.geQ.4e6bLπO s 8 ;1=c?ނT!} >{)|hy&m$ %\"oHTD%i8+-q1 YyV 65YAAd"qECY<بЃ$؋gӋ1p-!8g܌dޏ+Y9KV!Qv4 %[אlf(QQ_C8%9π] ROAh:pF-zuQ2 nG1gѴ G_B#s[ްj \_!lG3~ 4Kԥ a+\ͺMJKp3n܉ Djx0*QحێDU }%^PJ8YCp$ @3' :j"D"=OŰ/6'5UTn 1AMV>P;ӷmvCq>2LI'gaa㐸 &%$e¨LGf8?.wBbܟ@kܥبjɸJ R})hߎ9iSWu/Yųf-Z㩊R e_C],SUUNMKCF,d|S2nzRʂh&7_d݇q۟.6p6K~/@ba3/=zO?ˆ:mP^J^Rtra>[ "c42FھB$?aoa;CqL`p.jʷd7n2&! "I:&"mG+{h[v6T6/%|p#FF!# pх4ppYn=`YAsSeTMt IDATd"Yf\FK_NآJ@kGl܉$\QI g]Ln$">~GФi˫ XXMgs,aVْ_/1V҈h9 Jb @5U @QS$'`Jpʊ|Y)A+Ub' Ls)Q,-!ʈ C?9@śJ9 u 5Th+h˩&$%R$fˆh0+`44hH' mۆ700cǎΝCaFƌQK'hh97uof9q jiaԷٱr"Kff&**fF9@ś߂~~~<˗c{oA?M$4~ H [VaF)ntQW߭}1G+JO46gsKU(nAk7mRtѿkKr)]*BM-[+7gfWuw#~w M8dE#Jȫ ܨvՕXmUHzx s^׿|lzzoG} ;Wp,0.S,3Ijٙ_"?|v[*1]z; =fG*e @ s ut a-S~TLsc*1a : :+!' -Z۴qG`gU>-m~^X@!Ch`]פ mTJyuȈJa~*DeE7kiV/n[833nSf-IIJL)NIjJ@$LUK}\6N`lR{>osI}A $>vx'qӓJDFӏ$\ݻh(H۶~\+[DGVC- ChTأ9R4YV)(*ww$%ZuMr>Oʪ 5epϸ^);{쬬3'&'*)+[7mʦͣ7tԸi[޽~1k/e gr-?]]55Y,355&˚mA} L@Ii3`;W=_<>~7maӣ]_=ACrXLߺ=Fدׯ!MmԤŧЏ䟼?Z9|nkH("#^7`' }ֱ"Ҡ?38SXфl*Y&浳x3bK&}<ݷ}/Zd5[ٵSpb0\mUԾ%%<1OIPơKZZ:e_>u[}i_#]/7pj}P|woY P-D\ċeں~ a7x4{pʡ|OUUwy᥶C^p(*)G}q,/[ߥeWKTq2>A$Ink9vItT+ds2ֶx `rOhi7/ +KC!c7k)@yMlF&L623B>\xV <Ԕ+:uOV-._dO`Z++4]>{,%*ʮleۮYKRNiaA$=<'ɸ{RVfБ44yUrXRfS8?zׯ׮]AUjG)fYǫՆͦ\sX{޼g .X{"+ĝeUUj Ll%[)*+m۰rcru X}m<&uLvmoB8+;8uWPRXX N]V6mwm]IL.2=d**[$UXO[&$ȼ DUQ .E:C {[No .>-9vV`wX& [,=ċBVA1 |uqU=EE]\4A50p۫ǯ=C?Mjgj\uH{|0 6>w3/&-Fe"`ǂq\UUis ^P*L78sCAaqzwE#dfi%ܻ(> <{c:v0y3<ZܹGߒ:P)0xg>72"s6|-ZwG;?lむ( ,8u;W{շ6{[PPwZH ^|bAtt vlZ%pڜ9_<xXPὛ&uDDTJ3@9x X@4n?p gBFLa+rs`B%=Phy(|q8Iln^^Y&:+)!nۆ%p053DBC%ؕjhDX X2vҬϟB"h xrX "f妦^^^V%%>J>xpƜ\)s\,HKK[(Me ,[7,>wѺ**O6:ga۷DߠQX07>cBn4{o= q-CXTq9N?ځдm0Dylt䐑z!!.0C>?US$+ JA EElvdiiZ*dn\B Q&ˤ  !DAS#C`hco} !q"'nYX "aflroI0 FaxjR.DB F^A0#eJa_'1B?BC,a"xO7~a>beE?N0MX趃SgOb|;ODxp:8S.PVV嫚OJ~ osDO ߣ;_?EPZϭ0CpM;/[SMXud9oUW !ĄXt L M;w0z=:`ʂ.<cu xWVC9x( yk{^D R,nv67~Q \=3s_owJ@> SI|(9Y훧PJуҁ _Ix0"s<%PQ& Q ,5ZresNB*rX "fh~*Kpb%۰]Cg?[Vu,}Y*lل #d|Wʹ/.!9vvD?e״i-x1'jq7E3怘&| 0 ƾؤ#!2eQӱj.UuuD"g0DEM='G$R0)`~30W+WI1j1V,˘eC"rʓrU5A9 aA.9e- ;$NY0o(˃]G6Brrh+bg,(}AArD Ɇ'Ծ{.7%a! `@NFAQ #G9+BpBn#{,8v6" )) ^CK;M0A^>+zûrxBa\SC;'+0YJʡOrpq$<5r.Y Esֿ4QM~ta^>Gt`]D$@9guu\ZMM-J990 (0k 1mcA'6M' ( lYK_WTTDlE%'FZ(w/.)M!ԩ;F{)a1Cl`R?U!]]7<2S"?7N?fJ:vmlR7<4ȄSS42i=kNר{iܼS(_]#A}UtXXh)GAi*ode˓?!\'nBpA WyeYA0%E VւOQҶUCb/(~#嶠-Sy_lH ;8g/G(Z~S-/F%Iէwa[Cp"6*8_vܲk6z8열BDNb"`GHwhRO"=}W/5kOU X}$o\m6>}XXXܾ}<=6l&}yJ- H NprhhTᡡg^,zO?Uj+O)֗WRKN.g7$u#n/ ,StI" IDp jqFR4RU=F!L P*GoPd 5!>GH]YY7LHXOd+<d5jĈW-T|iȚ.kױ+jjFSزjeٱr 005 e9ru-)H #y4p̿b (BIq:i|o$UQAʣu;/E5Vi!h{',-)zpk+IBD|  ;v-_JW.xP8M\M9"zǦIChhHR$VHh9@s@R$H$]Bs8 u!@\4hp^+=Ḿ?RDpXAS‘C,SV)ʗ"N};th9sڢ#ViObb"41?#42<4h|HV3Z%4ԗWіMJđ&8L_e*??t 4h99H%%qۯ]6o66634OFMśN >eX22ٸ g'G\"#<4~kx{{ݻ&<|R4t,(RasU7ij־lajn~☣'TT^395ơ%ada }Nc./,Ku[<~^}%][4|$W($UkhZ ܵXz|$IX/o IDATS']v!,!i9[lljr ,ݿiL)SRVhnqǥKUUUō9-rr sjq5,nݥ }Ӳm5eFGE=Vpj5+|Kzp;Z:ٷۿ{ SP,Zb)Ew'8W;ѳpʲ3޾s 1 !a~/c&)9Qq_pwԲ333Ġy_3' />7i)g2⩘5O_B^UCÓ%rh.s]n=B/?ls_" i hk 99MOK8 :tѵ{k@\ܵWN]]-/--Aww~~,ZeԔo MJoޞDaCZJR."=s utŲ)f*10eN;;Ջ\V88vGCUe+&8]i9_"RJUENÓЪIA~^Vv´,   ݚcɉ P: -9ު7yI2T"%G!F"t! 88֤IVܩ#nP^8dXY%$82T/ ,>>]jSB|N:I{o44sS;{~޾q=p};\ f穳-\t6*3bX5j׾#^|L6 3k=`*&%v)S`Mg2ٳ\S32SSc2>%tϙ *UI99#޸vͿwyC+F<{rSn8)6vmYYYf斯^<%8fE Z2Q/=.LAfK^=>jBZ0BՂSaaՋAdA8'%[q=0x=OP"+';i6vΟEyAáAgÂqjj;rذ%5V~!6:FG} .G*GO]^da6jO[G/ܩr )[eeeڵu8~de>ót>^;`h.=KJlu QcY7az#Ç޽#RZZڲeKq)Yq)>^3fx5քYܿ{WKY&lcwhM,/4!9z^6r񺲲2ƀu&|< s, +Msٜ5n3-ŊOuʟ]ï=|DF1/׏MXlguΆ|DZBaQFEFs^iא^?5C۰fqbBQf}ţ{s G#9!A?7Ա|.M.] dz4UT,Ϸa-d*|.!CaAנ-qG''׭W/Q!٣u};7aboDT|N?)ų!{%%%l۴Ю9 /Y.¶֡=ڷ 3m_>svvP }Ͼ1zΡ[~v4cYmֳfVcOB d2f]Փ,-_'|s\/Zi~)9 /]?s:Zx88 k;Ԭy g\2[Ļ[lݸ:r\#Ξ[兒~!l!H)(*"RM]A(POװ Iz(+IX6s!\ XI󓪫kXbW/ "uҵX`b $`lk)^n5ЊjWan`x}abXl6 Xm(xȁgOhoXW\/۴u9ݿSm")_|-e5r28sQQ8YZ?b!B(eRЪk ('0L"`bb`Ao8o)oB(anZZ5D{i3cONJV N\z.Bj -KcD!YQ -āaoҌk }= ߺ¢fl=,۶iI۶kAIoT[CC#w)6*iMhec^-;+>"~L(Df}NlǛW|%ᐁN |;v2y!8;|B'ٵuuJ~^,(݄K0J aфk`F Hx1((@GW$Z= ]й; >yoHxse3wxk#) E9C8v00SR *ؐ dC+o(%L=S=pe-0`] sg@*@j[ k0 ׵"K W0Aj`a#8c5 l7]g<O;wu2a$ȓvˋ&8@V=?&ৣgl4:t"lpªx9@ 0cڿkszz+1B#Nׂ,q[" :BdDxxhPfr-E9tT"l7D|C?GzjYK^_U55 #5n,pVᯖT,\<01Nz:9rȈHQab X,sR!< [MA!#L kCyS Gˑ1p픔U8@ IkˮI#+K j!PPTydY ƒ, p2ӿexrx*譥)-E<(VB1U' D[?44;;A"-mAG[WQ3aꮭkaEƣ-;p+b*%7 SrrP9YyUUz!+U.`!%XM MCzDBD,"WNC4v%PܹfejjjAo$t]ݴjPTTDZ²Ђ@f׊#02_2,$n,Jb׈LoOw"Rώr F(aq 99"p>zx*zº~ uM-*ddAXjک0+++^IIXAA>1Ax^@.X!%nP Ep\=(W=RM,5PC^fEX0G~?k3nt"XXs"5IDŠ͔җ/exhуJ_O@F!7wF,50e ?PL!EB28"dkӧiiY23K5<iI7]:b,r) n ɸA|1}<0,Cֵk7Ԑ(kaXڠ`o^2` gZ_IBe$&p@ohlFq1;9hW3^E~O+%ߥE+~B[a:MVdX-cݸ9+X:zإ \1j].wFѻ?tVD3\R\i;Ȼdֶm eb2pQ89Y%?Xj|)N*03Z 4GsG oHf N|乽~ѵ[/s S" N= #ǧexu-BzT,D0%'% 9K؋ghoi&M")@Xo^=1f":?~|yV]Npr'xv/rk5DFfQ{IRZ*qDT\x{C,d+RB\,V$8$Vصq%O T{hU)3mغ>#) X8a>DE,),1Y9t{^~Rq AG mޅv8 C{Fq3燇C%Rʹ#C9H%8Sh-PG^ǀ.$DgLHpF?>("I5vdge < ʑyIwn ;/Hr8 lɣOz:3o]] ȇ540<޼'?.'8@`]0yڍ;i҆dᣏ[V8y:< ²bE:zpȱa0>/wƏ?SV%҅SxKZu'Nm$-Jc`EPSuѪ"qvȨn;Ҋ?(E`S}RSE()*}7KHL߲ 9į`mƿ2LI'gap'# O@D3.FKڣLU:ogT[L4v):a,{YTEY %O89{Wh}!rg D q/$@58E̋M__8]XR4 K{gUqbD nn]{Wյu;\; kfcgGfoY7[{xνޛkV3կa6>2NGAND'LjUgvcfX3TV=1Q^C&9q^B/ORz.]$`-GGn! bW\(.ԵmA~f2gdɩ{\Y y ɬ|%v:|CKxD,[N8 Sמn\.ëA<TcɿQRRB{o@TUQ:F^/igϞRRRBn|mI^is!!?NԦDrd`/,\l&97XZ,Q94+fI z_诂񑒂D6@ 6qX&HI@ښemeҡb"@~$%Hu"@@ TPˌ$RTPd`R //OF! D"@B@ \^~RnzV.KK?&."apF7cBbsd D"@(?3'Y儫kj$qr\'.,J Hk1'D$I D"@~ZubQz.rZZuuO؜Y35RP*⫫J8 s~Dݥy1f_$+J`K N8l|N=*]T~ V6Mri3#FU[ͫYU(J%m]"@OE@f rEE-kֲ444pQFTCzq9OMpª|5lܮcwjVyy9x\Z\LdqdrR_<\(A ,j,]0-R'<1偉W I㦽xÒLTL[Έ@' rDž;۹8),sc˖/? 3w*- mSqSv"@e2f Nm׸zzJJPq(${**@PAQҼu#^8o[^nNNM[P@RS,Rl;+^CYYafܴTKK}Q \ܯ4Nc]ctW6J"@d ̙::!_YYe_2ܽs`pW^58ܳ.@W`am7}=A&Z2GZtغm3xM[?}ŢYyy]aS{Uii)x \5k =ޖBEUdٵbF;6RRV3KgQ/;uSuKsصKI"@ Ȗ(T(pbPߡ]%'5Oڡe&/_ HCC ?+AGa2'^Ԩi ^y#OK>n^0u;T ;Hϝ: &@y.00e*h&;=266ok#,,*ܵݛB޸zFP?f^xHA s>XɃ(/Cx9ZaQF,<.-шExJ0~GZZ:=QR<-5qR_4;LY(W/$_'o+|o]\0ٺ) VRJ% Ԓ@0Eطc}JJrVfz9EG[7=Ɏ*annn.܊\ 4?^jj^ۭ Wxn]aJ_02/~lqp.=\3S[K)ڶ_XZZǫtR4?7'G襓J**Ҫ/Qx% 9-ᕤWY\$@ D [b:T'996r @DJJ"`Hr7>f0`X80'q7!'uܼ#PچDi#[s'`Զo6uރͬ_(dmh)68`/ wl^ qkԸ)(`Ska=&*$V|ik2&^S W]=}&Gql B_yypuM =ZZl؍Krp?wd :bn\N BJS}9BKc+Q(Q0wȭV,*U8hlh%½~SۮtPD(E׻E뎘Pq޼.rHA Zccb"vkWGP*}tu닙+tzbO$ޠ6Tst"Z.,LGa1Yc,.jX+\U%y ] I %:vu0Y<~x`TrdfѩfT0/<׫p!#t9R"@ ?0 @aUQQ~ZZ-~%x޿{Mm9DH`k "` Tls:#ȝ=uh)'8𵄘"Ap?gJx#g (>ytCXRq7rbWBK?}c&v.S{7Д wnZ->zZvQN.>O=r?t '*&M]i㙧p9s|?WƉ%>'|u'NMUE5]06(Gn 3Ek7އת]Wоl4bB.3S=ͥ<{Kܡ<, m G14 +(*bjBV(֯?oX <z뀧SM]=>]݀6~B; !Y՗7G+LHCK~wϼn:TZS"@ ?ZvLG-)᠛^b" l'[?K/HqMkpV/SvwJ5TRc+Ed0g|d hƔ89HoL-V`v\:{@<;,R D"9 WQ_VnK |mc9|BoY kwT$ے%GNEIIN7'-x; D"@2g0+>p2_I('H-GdG6F"D"@@ E D"@Lm@A+;3}lc36ԭaj^Yʌn* D"@ dQnnPc[]+EAyZlFV!сr_%I D"@~|8`*lm5^@%T[M[GE7 Ր"@ D|he??#ޓ{enjRDf̭n?.z\T 0dd322*LoБtu-/]]]O%Oѡxiڼլy!GxTp{$UƪrYT:gT:eH D<*L'/QUHWK~M6fdbdjv'U5s}cwr u pf;ٶN]("wo{M|m_=6( *Kg>~׆{vmZ( Dl9Qq9\~z{kvjjJ*;vݲmL599֖SpVF(((GϞx 9t$ XB ,'yyy9yBlUn}N_)5J N@ KyGj֨Z\ڶ6;bAA٥Y|9zICkbj>v4Ss)9ِlٺs3c]7UU ML߅GpBؾ7~B] qik؄ ܳcSN'[jץG+p5Hq a-(Ɔ4?@wԠpJqݹ6?xQlL##R u{mw/(ra8<233h }L5thֳc&mΜgSSG飢,u/|U W>e!#O1 yFo;Xͱm ߿;zhOJrQT65j {gfIIfʌ_ >-[˲acͪY9~((5(s"@@ ȖG95:%%>%/loob)**\yuo3'k٥N;iVP?^Μ2ͫZJֱo3߹!-:4]jςΜ<4qtձK]z[gRbs.M[2OML͌LL_|QF;`kYS̛9=(zA|Ԝ/_N]Okֲk?126qn֢TC^` 6rЩ@bfϞ_/Y8C[FY691sh(=Bԙ]<rϼ͜PEEeJM\Zpmj?̄f/m M!.zflb:ԃv5cV/Yh^V[הKUʜl= "0E8ϖJ45̘i鰙or:{0_UqDu4}RÆYy`mn.!&342f Q?$(p6wʚ"@ ! [.@W5(Z"$ja1B[M^~aNɈBpprns ?iёja<;t~X Sfπ!>Ͻ{0NJy 7pіX ,wڠ4ց C"ZGX ]۴/=CֽϳO-+xe[Ŏ;Xۺ]gTzeJ ϟ=vmF`Ο,`/4U[dMY1⯪04 0yw I!O%0fټegEk7ekJb:hiaںUu)P0* DTlD&WJVg}ERZ#-6;tXa]mFԡs7ȿxD"w!~/Dޅھa;#5hx>O[paD,ﳵ}_X\vAmk>3*<.]%=L.?5vtggg Wߡ~ԑAƮ\_Χ[ʔĂaF >n/Wh ?ef\L k ɲK%LS3EK祟ԅX'0~ʌ7Ա/< }UH͚P#ag" =e_aL|v"~ѵgeH’cf`ʁCF9Yo/fm-8 DKײ+ܑ;wqKJ*9rVvN+?3*!;+#!cv ,kQ#Ns8wI'OauãcMͭ$ @tQP i[L,UgD ߽1~A*  D@ F},ZxCC44z˖ [䘛{= bjV}m "ox L 11^COV0aj"@dc(k"@H 'I2$@"@E *G}Jr=z~999|)"@(͇"@#e@7&x͛7"@k4]7" J@v6;v  D]dʂ"mȺk.&[@%/\ܪ]J@Q~68yÎ! #bݮQf|/*+)]?m\~u9|oskPCMu 7N"@r*UO7jԨ#GԨQ˗5, WUm]Xn];$~3V5[T/,'ܸOXhpL%,۠{vת]C,o^]<{,;+S<#: ,?-#dȈI wo])Cu9/(\"Jx>ytڻk~/'efnѥ6 Ǟ_<}c埳RgiԣeNo edc<"D]ƍ .qҚ|XWPSHˈ¿6EEi<yyL LR.g4l]=}tQRQ4l|OKXF(OrbBE2)((" a"ѐ/Gi"+3ISW`jVm%Ͻ^xI53hN NJB IDAT4oc4kS ?-5%%qչ9#gC Yq߂Yݟ>~x~jag/;vݢU[/06mѶkcŦ/lumٺwAgOL704=[?~xMZ4hԄ_o.48֬٭SSWPJIJtxXh䡘kr95qR^f]{{x`+-#cS5u )5WgZ?uRGUyА7`7l䬢f/MM),XjFD񲲲LL X Zݺ[W}ƬөvQFq} 淢T:lsY|?yd7VbKkؽ`--v^^mKޞwS]zzQRhdb6dDcSH0'7T= _#+`2c'Pr*'(#D>5"##ׯ_?Zݻwo,n:wljjZd1>&䑿ai8{w/Eo8h>e3yvk};7Me_㣣«l?yji)u èS ʺVlR.[+)X4Bժ3{E)o۸)OG#f/=rhԹiK23҇d}e]غ}֥& :v wmDn\X[=/|׶'H}Yzsˑ㧯X4+?O`֮ϟ>ҭG1f[xv-zP"߼ S~MkCnm);7b =~FtTԚ87D>BnN1N_͢ `KGEyع;86{.3iqRf,qp\Zv[7,g ,}kwڶ&1>7~C-KaQ1s8{{p';qhhea; ;S7dJ1W\ޱ6>& B[jvmEX>ȺG!1ʵMQ~ٴO0mVڍFA9gƼ%ALc9?? 6v?rjժC+ 0pxy&B{7vn)Bn۰,=gem8W==&A]]}#͢ԮSɥgJejjiaEkVD!j7@RN=[K/܂8+OkKyx723ox:O@#O=͙1/}c~ wP?^"̫02~O߲Y F0S4cŕ ' 6أ:: k5mwQfFf}&փA]PB;0y=տKFftti1EӰߋ',T r 7p2/ti?{Nr?5lj3`%E3dgg L=@y6 n^< ׯu/(&?G}L.o%܀3ӷ^s똔ZF7e\pAق߼ssiidd"ٿ)YYBRBg*A `aIl7 ¯?KgP4u4px71s&9[IIQyuWGN@,Fٷh~qҧ vڒN :N(';:Aiۊ@vy\+gp` c ,tA\B D% _ˮc]ܒJJ%(ge򳳷(PϨ'$$4k+L;v޽ :L;BWT޷ .HD@ `<̴EMx+D\|3Gޔd*nL-Z&3MAnOTsr?UYofYCjG!Db>HqMke0 1f֥KMMMOOOl =@njmZ;ct6m\C. ]6n/xd,񍳦 D2 ^v3ԆalJ 2UNj={dfE@UyXmw{p]ĵoи*^#[U|("@ _l}RḒOu'D s.@ D"@ D# [.@Xeg̖ۙ<I"/>KN0E&ڃC D"@*2IdҊ {cU% X p<Ş:zF_%NR% Jc?ǫ6Ո"@% [s}`h_KKKOOI†m/|Z*5. DH$ྂħL`QI@ DT*碠~h;ߪ_m Ge t=\9s%u\ 2LEG^ VoHfrq)__Xo]Xّtu}657*)Xh9?[*NԼeu|R D@ٚW3ы JRҐcUM,lh/(^M˖ ,sKZK_Orb"qviyrFX˓dX[2Ҫ5b?^<{2$8W&b,&zCϜ<9^p:%Sʟ7h}ÕkL7)7ȱ'I"@ 2N@ B75ղxyyyYYrEE oFfgg$8YrrYNRbCOŧr)V9 X޹y}0K P\ 0P 6&yO\Xb:?'u?Hk 'D" N@ m^$j*c/ScOA^:3s1 IMM<ٿ **:~X~~"ޖ FF&=wD>`!>~855y:l]wΞ>4Kt96:}`hHPb4uֳ~Dw'%& 'ACG5tt*,(~xihLW/4i\M]]WDaMLGl^͂_PsAtµ&)>Ƶ Voԙ7Iڒ_wTg3y+ee-ݾ.,ck!vwo [2,Bu슦zxƥgTa6pώM8Oڻ)L_"3~cBl5VLWC#'^8~ SSst̽;7A8"@yȖpSW1?~UUU N0 MyԳwZvk9f>qD;0@*r&M׮Z -Rfc$͜U "%عu\̝`n 0mo|m\Sf?>in+lu\IYٵM ~6o 6k!W(1.-(ȇ/7wS" K|s{[YӦ6  DdnAdžAAAǿ `XXX`6S8RNN6 Sz{5hw!s;'-`R۠xgd/3S?FG1#|߹d[~["0X< 9w6=u<Kײ8t '-ewx >sacde)5EPϞ:2hXLo;e'|VSc'LwtrRow(O+t@ֳﯿ/NKM}p%rmwƆE8=6.^YwS" F7s;+ _[ 5~:qd_ć0bS D|-ꟙ޹[RRYg(ge3EuiqTXz`>,Ex?EY*l}ig§+t>Plzw]rl9'9ԬE0AUWS)MxU> D@li?΢!='$$`kEp`O!,V_~)j؍'1!ؒngN,?V$D"@ ?624trs ҳ䣢/K/X[$,ٙT aKɿ́8س5%uvD"@ mJ" IDATD2ULkk2;S=?Ue6j"@ZUfhkkK-=F ' "I"@K *ATϡK rr~WpX:}*ɗ!D"@ ? 2ڶrjռ^fvn!Hgo)`C?̃"<?I7j"@ DT20*X鈁=xSTT_D@q#PP:D"@ D' [Fܬ^G[*g딟gZCYve/K#PU_n)(*^9~]3>7dgbT "@ D! k6ww"8 q茁v~PSUael9Ls|ċyڠ[Ak̕(Lz ݬU^^Ntć;?V \7J@ D"1@24x'/<.fL`3'[&$baad \ 'mͩUީiKQc,EYi PV D"@>% [@?% ~4wݺjtI(T=9({r|~`ZbSL^+Ο>bdb6dDcS8;,'7.4ttNq|y= ^I/?r&Ծ.n}/uaR٭s e%7/=[ knY]QACa)O*b)|)o_|8ML 0A^~߹Nff{c&B؄;'%Ƌ(n4tǮ[PP hhR1o%T;Ͽv鴁+p,l㜴h>}tSڵ^}kiro{\Gd>s@Jr"1+\45|>asƥ7;74>s|TTTVoܷ`ָZSVV<'ûRաD"@ -z1P |<}!P/E nlWTǻ[ N.won7z OmXack7vǞ%]2}7Z|>tHpcMoό#& z ۭfd!kSuo\ξàaV-SVhOEm /΍66計fihjM7pcYT4cAdxXT{6hL]{^7unǰaDҚHEUdٵ3۠W))+ϙ߳pш0vm&ãZ;db?F8j/?lԔo|Ү[57z q8HnFgHN[F̌^t^~~uMie05YaNkGnDE@wSQ}&o?xεLz C0%|?ժU fTL,.=`ZLZ|JBuك2A D! [3KJ4U[IO  }db5%K]]I<}yX^ö3phݾ I;71uu] F8+Pp)qRJЄpm6*p072rRɀ4eT+eU(yL,+]G]CS qRd)h *)=[Tٵ{?CcS4jxsHZ{*hԔ$&hAʏܜX@^^ʘ\UR w# nT[zխm4uJjihoKSʷ48H03IJ᤬*8̛k\-2|D M̂߼wcK}%D"@dlкTՔXZm@5432Wm://Q23sTUY<^ig!!\843opC15VӍ KFrKtJ l}F,7- Qx\[Oँgn0U-1db#-5H OpҒss֮_j$~7} -yбX,,(Xyڋhj0Uwu E?uO-vmߩ`:#F|^ٺvuJLCMX۴e[TLCƔ ?؞}b Z Tb?BG3oؘ5"`<}Ң[,ر1Q T^CIZ\& /y"AҪꥪܼ~%%6umG7uuM&#kԬ9+|umۅ Gba ݫ0%k5lGL/3@"@ DE@fp0/3 ~֨fxMmĴ&jFt{dmY2^Qp/=j[01`|xCǹr_? {`;&%|}{o&# c7/chAa7Х״Y4MAH+R`y<~szQ8TD=[XvRwe%Xkg 51 |¨]Ks_ھqg 6PkDK>:ܴԠ7/%ʔ;EN Np1f>!o.;.oh >Cƫ!&e6p%yoQX-2ๆ̐2(++8woot"@}K+%vN-)]N xgg_ZWX9UvDݧ~av^>!5-MҳS96ocֱIH31y=C ȿ/ݰl/U%~Yd$@UۀVUΏJ *" ;eTY`Ccj9 Y(#zD UN 6ꃴgѼ nz\êTi |*+lj4/,A5(wuѩ,  XZm1ڦ}WϫD}Wk/_H˗‰ D"@-?N8 } _@` w*WۅSȲB A[ߡ BC]>#M‰ D"@- 86>_ <_ck))"P98̫r) D"2 (OLzJ UK@ںQjD"@ D"Pe3UHKuYJs%_yX "@ D" ,lc#}}ѭ"aKJH^)zB D"@d2$ Οr8Q7}jGS]JPB\i𱕊*ԩ{U::E?&@Zdl-BN_}PAJߦ d6*)=}y پ=cnau15X%}!D,Rf,EE G1!++>H\QQ8TQI(\ֳF|ꃹ0dm/8F-8չG6GJ{~]F_ }FԊ}풏/I𻋛mq_QUŮԪTL:+^Br)E#߼̱ h wkՂԈ DG% UUXYEIIITR(RWTWWgXҭ{z)Xʪ*߸ё0[d3_񃻕."KqF&M⼅V:~*ߖSӒoPb,uAͮ5['ka;m:X16mdk7w:}%xvj{£79M,pGn_yA0n޺vYJJu ;,:ǽD^ ңX%@>|x殮uku ->nȨݻa[As&FGF۶){p϶$/hXn*gN^͈j4^L4kpԶ ֦]-b,[ V'ϝreNmc=Ю_WtwV-r~h6(Ip+o]ѯ˵ ؅>{٣g@kY7dd匌 `IGbg"@xyUTX\\X$'+7$ijR񖑖>`=?"GrRų͛6|6l'0+3ʌtqacq9YPηt fM2m;xm@/E:w#Md'8l||V ?ʟ=~(-%%6&*; #4\72iW; \Hu"&)ɉ7deeԩǔU U[&#WΞ}܍,dV^PqFƦg;yt14n{i'#ݻߵ/NɊǗӮ۝F~ҭ73 hѶkk0q3/;Ѩ&S,ŗg*^H3p(<蒒dg#lܜ8=ܢ9ߌn㘙j<'sKPGj<Od+9; (T1Y*5~xkE)D"^ǀʗğ:u J_R*#@qӁDKJT"RCk b꽡^1Q։›S7xL3brV3:4s4xa4#&%!v`UTTQedS˄qFJzzSk|G aA1:9W6p&1&|.nLbllIfwWA6wcpУ[Ӵ9"[22C2p׹KYPS‚ MFCJRwMЪe"!_ MNV^:PηV9c?т:WIhp)$/7G;~Rx+@)D"^湒S KYIIhbqB%/At.I`-1{ D,툱ضv6FҹS` !@{@] q19wySGyAܸrgLicNOJgչGoq X#QB\Uܜ\)@pyxMDʀ<`ٺbw}J I;r *ozT4u`ĤuM-3>^bkJG^x?CT 6Z M<>9|*11`mRRJL,ᘺn~2jb;3U>Vmg cOi$|G~aßaTT;X .h"vaE/"@ E GJJR]E JJ ㆛;kܜr'۷phגaE4%%%WONAo<Ύ;h#|A#3"þAIFL:܊߶W?,=lKvꪍ;۱a"eUf1NV;Gacby5S _m:tQl7mp7/g-0_DLҩ)E&+/ϸբMZ2T|*oީ *h4sShM ]z}xV"ХGFf/ڶiuYf^ -aZA D4bAc&aX@ 3n$%$`< \Vm^=ݚ4c~L0woOiI5 "@Gz _NVVA5uuEK3ujթQ_5%/"*e "!.:@OO|QBH?o[i׹GvVfw6푈ySZ3X+?uh_ȿ="(b9pnٳrLvg1x-Sf-݀z[0%_O7tٶG z EgAF6;SWlܳm#;F"^>|={ěQF1Qw\|1;G١4\bB$&߼b>0 OTVcmKVo13OBN:w$'m۸J^[Awa0Sn],]q̘0a- pi@X^G!s?=S R~dv:qOfN{N IDAT%±B=s~4د9k;Gf/^yDtᬰKg *KPw.u["Ogh_?|S" DDLD2 ڽMbbY(-_ {2~cr\VN&;'刉`oYV}XTxVaR̫В<2qߎV{<{RА׼΍ų&r#'5ۂ m]cԖ̈YKeH[wׄ2"@w̩MCC],j *zdeeץJgMc*x } 2B}s []"@ D@rx;Fnq9s-tXkݦkcoєB D"@<WJ zMuD"@~*sL3jI: D Dy ʏ% D"{2J=rQU~={JI"@ D^-[;7((C'ODb w=""@ DAaiϞ=aaa#GJ'D"@ D\`^CCܹsbbbɬsVnvWZN֭:2Aʉ D"@@~@Dǀ2]`Y˶嶼jҹ[Iڈ D"@@ $66Au)$D"@ 5 /J?ӫ_>_ںC׮_\T+g YOUn߁#^XPmgɳ}uijdg]8}(-5mZh W~ hڬim;{}1E%~ "l"fff\>{c("D"@ o'v8"^)TN^am:tuytWAAiʬ7񐔔Tb+TRV2vҜM֪Sfҙ#7~v6_$ D"@G~mmJYY47纆Hlbi%ӵ(77'23QM]kOﳭxDž:0 ?/$xji mںXpE)?g>1Tedd2G"@ D" +>׮k$.&WrRh*+"tk _O[ǢBx *%Ӷc7%%/MYE=#- 7⼅Gp $&B D"@~? ?q=b-Zj:]ѫUgui0ҘHUU4l̾c"p~V戢%liIʪpx "@ D"=BĄ8X֭;0J تEӯä;y X#, @m )|iiL'"ICsu m&{VFڿ bb" 5^D"@ D: V:w\)X80\Z} 115CbffanEEm())>s|艳Gy^xSS?*" '`HIN6_f%:z߽ :{]lG-[0~PV0 D"@rطo(x1 ؘW3:߽\G㸖S?~pVzZ縊se^$&n۴-Gpf_)KRK D"@/ P}Ç:th) TQY%:*H"@ D=!a:C xsc{˧RfD"@ D"^ W~?.=#ٹオP'"D"@ -*s2ۖPD"@ DKrK[ْqqq54(ٯAAzHf7nܤC"އ_ܱE͛ZWWp}6WX~Rٶa}(. D"Pf@^^vƍ RTTo{HTTo=}% 1J544 tX4k.~rҴ+[' u!FUUԊ~OI Dw'PNɬϙ3gׯ_o mO_\T{2I)3){=uzݩKOiޞ9q8++Qnw:"Ï8nѶW*j?>~01!+N0,1ts 7^,))RC%[6o]1t[.-{pF'&>|ppO1[DfTTRmtgO!۳H?i^؇ܼO!dEC[Q':=}HwuvR~6;[5oV۠ٓa"Dh#'{ZuۍOMMtDWDDZFz\́ӳwo>yx3ﰗ3azEED7E#70m_P[w;w/,*}2WqGӆfVЌ' 8uvvvNM%e:޿ĸ{rӇo+joۮcWH?vhߧlwuhSv| ߹= } Em239a(m}z\""ϞLII5v2Z%JHábSVNГG!ijg/,*,s󲇻+~}CI\2o_~6 D"8rrrXȇv}d=>@c9~IoaF8jS" Dm~ 8666x,@VVVe;)6& 0R1gX[&;adgeݽyͺehȂ O7u;c ;uD) `v';3ZnѺ}eL\<^kn|}qq.tdѭ PXP'X ȎEbo[*,'rsrM1p|\ykh>~A?!!SFC$%%kC "u+x}.EEE\^ULt$Ȅbݼz9h`Iq3G4P`AJxO߼_$v5 T^/|·ri^+ gTeg ‚BIUֱK|p-"*Цͥed12AoBZM D/!PtCff_O>HHHTo  qE7B`(0aGFzzZ*,0F+`꟝+5%Í+Ž-d,hKMoU!{ҥkO@54[jhN%`Vmڃ!ژ,HU}pAe EFhiiH.9et"@ T7oޜ8q] #G>}n]`bA=ޞnOܜ$ٯX:(m!0`]\ZZ}țGXgpp*˥s'Gۼ7~ޜ2W.j7~]p}Tz $+ٸu᜜/܍ͬEjt`V=66QFU=iT=MngЧwpgtu[F.q6}.rXɛqE^|=Ԙ;6ahC=Kا Τc FN8kبt^M]MVPT*A~hݎF߲eccn|=m Nn7@5ڿvȉ/ڶ 1b)o_ 1b|NsPjæ1jNQQAw\[cP{aÙ&M@Xoy-Gx+7oR+ [ "#qZ ק؀bV*Uqʼ ]T4"@J@Ȥ!{Da&dNn*j *bkan~0G̲!y]CB[V\iɣ~qT/&0NT\ 迸\*8x,o s)|p#͚XXVF:nT74"@1a~9!hh;? nAdoPvvv.] `;;N:qY֬T4 D"@BzaR]׫WN0~2DMM#Û"?&<5gD D"@^@CmTWA?Ҽ_' y/ \|BE"@ D'2@Qud5 DH֚*F UL*W rzr $˼RT}#ɫtRH D"@H116ԯ %9*xdjD7ՈG){痈A"D"@ ?@s`[65SUaM|_C8tfhmnjR[䗒S)'D"@ Tc@1O)5uO}BQiSuD/"_vrPbM}@,{wF斝ի]Pa@5+,35HƇwYϋ3}+Onn+t& 2|ǘPFƍDEEؘs'|\AXZFZsG?|υmn)-)uy|塘تM{Wߵq傌4vF rlXX>䘝9h8u2ҝ]N.mGMҩb峞܇VX6kuhV\;2rr?^:sC-v*,,zx ' ]>[AS3IiiOh8'QQf5/*& SuahdzvFߠbFi-g׬e &*z ƶ6rnPE,p*v󒞖" !@ɉO6kҬ`; y9bT bbk,Q(2 a IDAT&'+fPhq5"@T/'~Y/&;o,)@@[Jx{v/)-?ynna /H]ai 6ZD84jlc [ڌ3m /$@M0s8V``Ĝ1wYdDs&\ ͫ3LfwDv"hÊyΞ}ԭ{ܼa&m{70;w ^}2d72ikˀ"څԺsX3))fмߠ @L{SކaO 574hQi3ң STX{*mjם4sQRB\dg(dk]lcמm 뛌:+ul_.kjݢY&ZgeVv&eř(-#.޹uܥkSJJJ =x|=zP'#"*\7nwz6mdݤ c'ruvN]3:w8^H3`e1 Z'JUoC3AC\tzIL6GSzתS.| D@R(^H;yyl˯#equqh֒Q[.3RT'TU?#''K.|%}=`㖦nM}ca!ob1l`˾ [>L߻u =[XPu&#Xtd8/TTݾ_‚TQUɤ+(`"?!vuyX&TsI\zޭԒ5>q ȩQʺ7r3}|JJCzUԜ^CQ%X[6oԇMGS%%-߆=sw А7z,l{7Jr Rnj/]h, ^chYBߚ7ƵI#҉ XH| ~mY9, w/Mp =MZ8߹$t |5Wj\]tʠ߿{"׋yo7Ϟ=VD DO'PV d 3=3X6%}b%%nxdff0J`)*Maj93#M_C?3,6Uu Bdgg!^EVNo /Ĩьk}z={ҁ 43fI˶00ocᣔg`x@b)b!7l/ ϖm:qj߹"&D_6ݼe;`V ~d(;[i_*L  kYù72"'!! VzLlL;`g*S1Dc?m|<vWZwo,.*dϯ3_ 2!@a3ULmͬzs֭ ,{&(CtšJM޷иl| PO-*西Ov]Q؂5pQg*A{y}h&$f3`c1T͈ A_5—I*x?v[&ew]qg _F|StV^!  й'-*d D'PVp$^4YN:^ Ek enK!LaoicݸioB ϧw`kǬ+0ϊe8f"\ Bėc(".t/#ujGE=j@X99x@{ xW0 Vb%+.&Ӷcw6vv$2Yah;yd m;uHOę'4u$,N**b)+#p&|4у3>MϷ_ׁٱVs}6<v5 Р@oiPHAQ9S?[&ֵs8ju9Yo_64k*H"=%~|f.X%ulHT99wo^r?[!6kOt01˾Y(s 貅[U,8޶~= 'a2jՆt@١)XXޱClutl\AJ yƬ% Yo"?(Dߗ@LDgFih;? nA-QZNY,ŝL,w0FԮkHKJXoQqr?Nބŏ.d%lpY+ :zS)Nԇ67AƤc# +.2qS("@6+d`|"s?_ :3`>{rjKgv8b⢢Аwo]b-N8U ?'~!5?@UU @FV9"@ D"2J@xxT)=֭Yы"@ D ^SiK Kq q<);'I+).8͉l-"@ D@;qPSS}Ϯ= rrM]]O;t/6j> D"@>q~%__(.}I6nSװORNjx9|򲐃UTTRxhne DϨʍMXOGTM*XabFv5r"@<ۂa\iyEFIJY /NSf O5n)/eRn]P".>q܋ ŠNWٍCMv1V^l}!ቶo߲NWSDBRrR[ {Ȉ)**\ᨔ&,Gy_F۴Ȯ нg_o5I"@Q1Q/\0eB}V 싊  STXҹ*ҨqFGޖŰxyVV![ۈ|!#hlu044 }4/*sQ'Nѭo0Uׯ^Μ!&~G57iwjjwhxMB߿3ik(I-_Ĕ1)1 0;*Y,?e,thV]]IɄXw\ D"@~@r߲vݫxl ZmnXrޢi9ٻ驩L 6Z]C3&:쩣_>.ttj*)d޿3%a@؂%U 1jfn~mF9sb*^nb g1V%E%o^Vf~;v^XTte6C@c#"Ŧk8 0U p C޽6;[5oV秏qfHEYYBCvظvӮݻslѪN <O~а[W_#&1.g/sٶo;`(FiVf&FY4sݪmOM66&K  g2%EW/-,gԄ]tѻSxdwoϜ8Y⇐wjTa{aՌ; 3D[lX0h@?HbB]Ǯp33ҏX+..jߩk𛗧md/cPm^v3zYV2.Oz;ePgx]=}tʥ'߾Bƭ;>}KZFzKD"@I*RzTL6[yX[2 aA2 qޞ}s߸v~ތs`̛X9gOCW ᖅ99\bTݻm͚z>7@fڬxӶ_޺]&͘s3"Hlٙ iWO/ kҹڛ7ҍ4nݺm kW.ÄWfllfM)3IJI2y?[tBTa] 8ed̝9>=57ri-EEKL;&:277&쩣^UU1XS 58e͜&V[uNȒSQsi IAH72n:&&p}g]q':7] G9p_6+͞:fu)Ih׃;7?J^vEFF2V-޲a\ !ĸnz[bqnպ >KX|Ԅ2fToOwXs-Z3Ɵ9qhJ**r-[dNūAD"@~@rjf=nvn<| H i[n-cae!$U?,u62cbJ8## o^j0?08 PNؘ(ROMwcbX^u._8]XP'X}Lo/wfY-R8 xx sss}y˶׮G `bf[-<}t?9)![8VE"==/\1Y|ĬYpd8 {&ԡm,7 622"߲e{DE~֭SP0&^C?F;7rxX,Y`}x\>7mwӧF `F+(*"`#Fw5 TUB`cj׀Zl;X1@qq~mxW #w6YP[ (YR##]P+\P,Ǣ b|r߂("@^!@5аV^rƛ!Wbfۨ=fȫjRr";KrREAatٲrޣ^7ͷt!!.I@MM2Xq;# &&*NOpm ~}v$+,є$:]E5 ? LbRURҹ{/eeՒb#ܼre|c+f}JJ9nPװa9La;z}ݻ7-۶G䉠&ɉe4 KC& SeV\؇=C?ة>f%&ę[b`O`$'&֮SQs f[zy|Ԁõtj~I',ZPpʰ+,̎ [0)S 񵞑 ,9Μs^!'1wSRU@MC2p wo7>k>V"p˜ =u gsʹу x>d _m?nO}X ~봇O;҉ DT9P|wu=gwλ~ᡑ^bdp0\=֣#Sߤ!f(qFNp \w#oۼ;j֢5bTN{f`sMxH ͔ۺi׶T1?p(fPޫCE`WyE~ 7mr܏tUX79F =K]qyXlڽ㑙!**>@kQQkes2t>+]U`y5aŖDF7Rm'~ g@N=utgX=$0L`jC(k/0i;ܻ ,vaFm<9/JJK l6F& oDA83=]CWA)é"m[rݟ?ٱu뗁$  :`İ~U4RU?(H"@I @(#r22t_%"RTΞv0۰Б͉ܽmS~~^e"[P#FMpڽ&GKavٷQ얔,Kz}F\pjwYg?zfh0ƀf޸39+nExXTdPn=q<ވHNJz*\F3;f48j 9"k+ouy3'M:be^Sǝ Ҳu?a4n ŽW{gY`bW`]\ZZ}țGxb^-6`oٷk1= N0&+ivM9;bx --\<{2n.Sft#N EPńa T>@ IDATL1[Xv`U|©UaecC=Kا @D"@~6#ol 3IL攑'T?NDBDZKQiEE5j ӈW]FmTr+U: M޲a>]('liIX`lH2ZĮw&~UOC '&ǙS8[P#ѫ]ZX**-RURC"WF >` `J'P\Tܫ@%"@ D!PY18}F“x˖ pK CT?xĤ)s0ywW*S D?@r:cISbgO/@M'D"@*s奫Q*D"C Mؖ.iJ D?@9UIX)y&D"@ 90aia).!.RC$;'SupwGYYYPS"@ D P4 55ڣ '/mqATB D"@@utDDDJpQv]q5zTe֓g,+I)UEcW5[ުRHz D!@chX}!bn n jZVʀY]{۶iÛzl٬Bfzz)'ݮ:P]ZЇ'}x|I\|z5+,3}xs527ye+TL&6ȣ!z1H)..c>pc0):ª#V-ɉ?s3@5 ,:o+C͋."*HKMyyyE}~Wn#ͥd7 2cvA޸re74K|8me#c6a£Ny-ӇcS3:ggfdggq0@K5BD"@ Ս@hh3/Z%nA`݉3$y]{u\YxZ a0 5=wiaĥk4-, ߽e󤤥{E&-ڲj&fjȤN2c]9|ʂ4eR/UKflY6ed0ٹu7ҨUbR<,]||̞k-wﶵZkdf~#?AL?:n|XBLvo[ϚBjwo^x6l'ߓ33cg,=vp'_2%pqoS qp-"@ Պ@rbkdjfǮf:qG$I!(ݻ||e7m1$u`qq1"ss1qLu ==.Oo^=Y WڴM@˶ݺw[4GUKՀ !n[ϟB?m`DPXZG| C||hpy|lHxMa#'owsØytOxJZZ5+480 1B*Xs^m(iնPY&!ok‘n_1 JSau +!)[Q%fo7n {TJ>}|7'7B&QK&@n_?su#f .7¤'4!Cέu'VQ^X`=An>ԮS _!Lp歡 !0dd*(LbD"@s+(}U6};wW ڴEk]/n{ ~H#)ɉJJ윲~XB܏4!3WGOR"RCDD`EqǷƪj a[*s+ёႪp÷3η.™CL<T%Eʩ\\R*<613`9xKIVU 3[!Bx йn-Zں HX0ayco];׫` mwo˷dϬ05 fҬ=6ob]F1׼IL"O1##g s|׀>3/࿡̸{G ú:ˀX299|-,B D?$PX9z+#KW:&/#=)dvobk]vb`ۅ}|]v^ئʪe~|/,LKK>!lڸ,BKoXwˎP*M0,<;pݎkA۱>&* _ؒc:E"Cм\V|Y{gqPTP1PI[,.[<LD@[.oߺ%͂gys̜z̙,:jJ^_9÷4* %ƔHضqvJne5Ey=m0Hr=+<"@tsݳ{voV9}{*xxt_Žtܓ7kEuSPx_ԴE+EejcYȅ -21o~>^X-zd5° k c5' L,DAcp Xhcn:*x|!c?Y;4jq59%5UT =bAPP.)Qe~qdV,^>Cҩ ƸyKS33ssYN|>a~61vܗWdta;**eddԂ(A:̄=bq槏߲>rߨj-D׃- Oݺvvū؁mo [;(:Mo0!t"@H,X-R IMRQ05;uXy+mQ_эzn3]K,;2X{#FMwas,)ص8B>.;lx.=͜JAI9)!)v {Qf6dgv.S GMxVH| jEca 6./?KW*o7xԀ!c jc6?ɱM;⻾ps&o/\IB\bA܈CFNZR~)!Lxz;; Ku1o:$Β ]1B޿{X ocG,_ ׂ"}04pl/V6/[CZJ&g`'W,&\J(_ô߂ +hCx;uփu`}_:p{U/VنcptߘS⺋?4-o:gʰSdس D"@DX##㸥=zGG ;HVF:-=#ظ K=wK(IQhj"!+UJC+WIUu Y蟍O*L\J˯D)$ hB%A.^`PAU(.RvOtjcGH9Y!oZ4(/'˙ ~mBʡ[D"P"? z9s֥y. R@fգp\5+FN{o f7GہX`R: Uykԏ"U&D"@*h9󿏄L |C@=)T< n˻zd@5"@>D}W& gM 4*F7;4@ D" QzS]D"@ D@9%7man]x(/oQB D"@/rL[JJIbԴTy/##k'//*/oc("@ DD 0F#HIIihh߿w.vܥt>*Ut"@ DD\^r\mLE&ZX[5nٱ'=Qvp3ׄl|~EyK'X^JXŤi<~ҽwi $PDUs>'<_}("@Co?Yqv͚LINEoV/C؝ޛ(6&zMir瞷dzdѡzV,i 7[1|ԄzA1㍔փ1VSK; gO~FHMMѩzОXv.|/-L[X蚶vU]z߀!bbW.3/c-AsFGT ?~`Ӧ}6m- g3ƙ݀!V2~~?GҦ= N]X gq&yx2COopݳsruQ[(1nЖ +8;nP+JJQ? ~L$%$7l8IE))CC޲aݻs@uFDO :^sHJJ$oc7`Ҕd<ɏRg̵Un7j,GPӊEs|Ȉy9ׯ]|+S!2bk=tSVN]Ǚ >v=zPOҮ5z ;ut55VQQy~ ٷێ]?9)Ա ;X;u zw-q a|w̬];?tn >0wc8r[6KJokiw}#CBӯ=l5v]] Aoxu1z~7'oJ!D?AJѤҕk|R};[Bb$㥗g_Cw444g]rƥ3'zy]SI6'_bP+(]FFf- f>qhԸ)]DUll; B&s3&6se ͟<\rKMTj0"_tt9YFRZ=59AcS>elXnҪM3skv7_f\&̽Xu=~pw2L&-<ܟ^6呓0/Ȱ ,0l|ԑy&&1}1l |#}IO8vhϜic^U1lH4s Sٵnm~f~)/^DyڰzioA|^psX`# oʰU99yl\dp9𰳔Ysy3&9wvZDL^bޗW?yxgS  DD+F1t Y}q]{ӂEx2B: 6 aTcbYbcTU5 e.ee u+##Xw)++ auMMƖ6UU1M 2g>//98u‘IP+lD%TW^}7Xq11Lj1Q"I5J8tKUUyȾ\ij ceqqY#[AAaͣa?MvR5F IDAToxo,۵1L<4.[`#IU5V\g#75grbVi01MLcGFG/YY[!|"}v#j A(sf;2kto[:'y2qfG3|MLL,|Hz'b6_1o9L K  6(bܾ~8BzFv~疦D"G-\`XuX&.!zCqSvk_Jy7>. T cSSRۉ^A2>Px{K+W c}CIES?32 1QPTd\ \;0R6o^ہlܤ%C3)pչŢrΔi38޼p섩b7Sn^ഠزz^<73#SR0~IQA)6MkP¥ /ؓf+~]EwAD׬A`wBL-\ު f^tŊǦ:***4ޅ'[*w8e8÷-(\?O7DL"!Ao!\ȵ)_?hhA8MKDkk(h*f䥦gǧfV1Ss!..$=.Imw gOwї '?h$F(nס 4 /"?efO>`NW]] 5h u++J9ϟ4ײT1?peez {{=bVw`t-0ϗOMܞ=p,֛r5\NN6?~x`xؔsպm;y &On=Fٜ75v <ؔis @nbcm-;F&Aq(8 `-̹K,uS`bk[JVV]yb/u +! M?p/=aB?wڬ͜ž#LԪOȸL`a.v]Ei )Ř߱i舱NύpI^U1k/+sEq+ j/f'ij-Ї"@+FF@LMMޣwt4w0tZzv>W쵳L%G'L;AB\ nF%eHEe l)ZJRo64+gs7UP D1~rcD}?@Bv hi y9ٌ_(^"@@iD~r,VKK]-.Q,,-!.%+?DQVZ<'A15R0-aLD 9HNJ[@5"@ D! j&\KW*wV #S`o_~HêHE6[>~>/bsHg"@(rlK{&UNtƷJJJ!^oҌ^d!O$D'Pf"k{"@HXP>`Y4/F"@ʘ@9eJ*gsŒ'a"@`l J?=D?h9zj, D _]@ "@*xWI5"@ D"PY(m7ktAY("@ D+j,\y Ee\VmRJ(asrf[]=}|쑐* /VHt"@ D@\ݥ{/ \QMm߽ZYY$n7o+%&&VCL,?/ r}ZP$#_fD"PRb ʛܽGC}g+LHWPP,$*M %9YFƭ}X5OOO+iE \Ak I"EmPxZZ3g]ZZ*(( 2hg^Pݭ鴯"M31&KD߫;U(@Y*jF@mP}{e¤l&#l;{2iB*WVTHo2SJ"YQQD$ 6({Rx jr.nѪM!a"@(%}VnDGى @^e8Qb2E68dޟx5g,_$E5@ʐ=e* j{/8+c?2uk(E2D+5^^3gEK?D%/^+8UG@5&Pޯ`mQ8Ԣ3tw0( i?Դ0 #^e?K8FlY?/szWXwSEDTQBl @R"@ DToe7PƵI+V/3(L|u&z~VsW ~/9e*˜^e !DfTkfƽHM#D,^T~@*CkfXFx3u##%5"@\rW|c2׾h<  "I@Kb_e,_#W;'&UBUn ^pW^l2~x\HE"@*pÖf@HVǶQD`<->𻤢WZF Up۠(ɲ{KGSKSx0MQ$Cw(4~.1֢˶EyklSiDT]BlP<~X.)d $1" B߈bU#PjFzZ1/h^ŀHD g%\dҥu`hiZI"@֠T~گGZ"P} }9urrr$sP?!D*O`j)I< /zWΥ"Pu ~9Musd$eHb DL`?7773+[\;/B(A/bVZ@BB"+;XU0 DT\?Ԧ~U;ғjL\Ұ}p0 DJiok ՋuQ%JJ"@;Bt"P 7`Qnj D! Ȩ9䂤=zGGt"@ D"@D} JAd("@ DTCTN&"@ DAD҉ D"@@5$ rSW IS D"@Ь)H,ô4AH׫p_sХGFGD ̌߾&%&VR@Vf6KKK ddVlؙG$,Z[޿s]J$D"@ U2t.A`3-sytl1q PАt-zzzzZ]]KJ*#QH"@ DTr5\@_º{*j?_>s,&:JFFfiܷ}cfFzl:vh}Jrҹ>~EPtԏn = F|BƝ=oL^AӇNa\>unu۶SwY9!AϟHMN2s yx?"Qy' D"@}rbbGO%4?t=۾}h߹wlXaٮ3rrstsssֳ%KMCsʬE<}޲ fffBFkq1wZSskog|G@CVf>ks\lqSL~dwdvɬb)[#"@ D'PE99INJiO)槒 Bآ+_,eH"@ DR!LwtHIJiji0av!TLL u604j  c\;x`1AV> !nYtpra 3Y|v]6E7~iX$U™"*_tH"@ DR`5u ,EOZz껷^@ʹW>-y rFF_Bjy>YN.X{}!5Glc*;<ܣ/'#\BFVZ]8uXekKJ{]/$L D"@DX#Txc P: D"@ "E@KK]%AZ*HPN D"@h @4"D"@ BrJ"@ D$@h iE D"@ʅ9傕 %D"@ ITۀI8WZJ11%DI+"@ DjB ??/???=# wTZ@FZ GzefeA1C D"@@_dn.K) ..#uKX?e#D"@ DhZ4Α)Z>Ru kxJ"D"@ D,D~G1K{_S=Je"@ D(]$ "@ D"@D9"  D"@T g"@ DrDH "@ D"P10vT 5Z,l-ZY)UV]kߑ3,iVofRSS?|b6jˎ ˶L*"@J Ʒ25~9νǎ?;}15kܻ]Hi%:|&͚KKD<|^Ɋ*\uu4a IȯO9{|EE~je{333op=ҹS%&t֋rE7˰U(tm{Ϝ{z׶ mZ2mUF+(wb#>_K75c>4[߾~.q\튍.qQ"@*0l#^CS^\"99ٰk-mڳ%falĩyV-NMKS[OfXe֩l&w'r.>.n]r}BB۾G~SSWع{ BsofnձKwؠ)sw?K!/<=u8 16%&'$o5%..+Q^ndKTFg*bl)))M-]z\uJD"@"P.FMZ .))ye_/Z?]l= 71n!!%WKS7LJLQw1cFAQ/췦$ jeAQI)ϫBz?s,>>?H-a9qq 4lء=4>vpw/ȻcQg63{wI4+)=q̬,Nt8td a܌_?G^:ey/HύACFu0߳O|gۏӭ`ɪEs Z60x gjg$55v]MMCwB&֮ W/8 :VFZՕ0eQKθIӐ1?/!iܴcaF~ n_">"S/{$$ 9N]ݬ,;0 Dh Rrsr|^y$,|Q.x&?pLQQ I{vl2uF??pk8uvJrwTj"@ ՛@PTR6nObbq**j1ZmւI943-'9yx̄ o0#;;}s߶nX9{ڸN5_ZFZP'khjq*²KgMrVظfLx7p(vvXs]j!`dsU=}Ą% oߴڶ#LONM4 χL_xqY-t~7h\).ddj]`枝ԸISXۉ M[0X :ukSG/_43!.nO;ϛ9vTXjA#W,]8]NVH8dT/-; М1wɭE>wr!ƵKo^ݷ9xXi%jݾ\&Ly9ƭ]>}H61n>xĘϝ1!ϴ پV&[7\t.g!W.8 Ƶ0qu{&|Wޞ۴ڣ/ԥ 3?e$_ m(xY7^W_sZ#%а S3 3KNLOπO;g#voE? DG\=NspsDL=\WWty aC0#wv218ڌ ֤ hHAdQzZN]AW/_toӶ})H *Cш81ė^Uʊ}5) ---%-ϫ08xӹRded3ҙČ4\q .gOۚp{jYuݹ>m-v<|1^/ܙ4$S8a!!A(&Fwdefژ >5kkҺ9,?ˆ~zZ:\D9~Gn^ IDATc]zl㍫<X8!$;-(A^^!--Lp}n#wUմ ?p:. )4+oOf+N424Biֶi v4]:9 T ܟc, ~ h  xΏGK,!>OU@2gFF ,wʫQ#*~"@ Տ@1!G`4\<=]Qa&Mb41!56:~ +(*2%`ATUrsUT0 c`og̹K0~ /BP I&8LF1TOUMj m [DD@E~e"@0Ưs1j̵M&N5mVgdfk01˕ U0'HܴEK\xy (ӶᡌYϕF-BԂt S ^槌lDGtVN:+/<1BRp?\r0h$x /0Ucb[`U;O429 P+ !oسoAXk1055 [t`(2|3ZM,Ҁ /2xف7x<`^bgu"PBRJJR9@hZb|q$HHEfV&_&#=Z1hfzz:fԈ2Ht3XhgzM2sB^qb5ou:l c9 :@_`34.e+S샆3@2}!FS!<|j:2֜27]jnzqPF$>y1kRDcN'0&=<\m;vEXYѳwJ2p?(H"$ƶ#X0w=]<0|.)<' v-c,]WF 7Dxa )֪]^A!M $91d%\o]{ w#х vd4DgB \XɬY3 y¯HNJ DLu} 0ne0( X?DGO !4sI&Lo,DFI .]Ε]2~ M(m"@ rN !A̐30zd'.=c]j,ȯފ,!XaH~ ;c&NҬ /""|-0ԊUoޞVm;{{h!lھOZZ[7rW4bYlJ<|劊D|bmx38+p)Hffٶ0vB{$Dk?of)W&%%4tQc'Fp$ +6()+cX3mظɈ1A]p0.|9dvsڵxi9C`m5^:;n뷧&cݤU.aN̘G#™M9)\Ml߿9yv1a~)#--$+/!U {x>/\Q [мEਾ yp027|$ })4"== 33͚´!(/çJJJz.hgObɉ_*L% >TB D! 4RS#tZzFom=ָf`{VvDrӶ.8`gr*%D"@('[4ňlF&kx--w cye*-WJ)oؓo^wET> D"@ %R][t  :zPpj D"@'r<,?xDF~х!D"@4Tib"@ D"PeK[־#gJ_26?~6mQc'M{oB֫W$F D"@c6bǩLضJl ]rb v*Y*=Udp!_$]>w?;vhrJ6xR D"@J@\sv856&Fxp+yU+ώխcY/| /0g-A^\\«]d6SHNnszylB_KGgE \U "@ D8rqt$vѧS8GVw8xUAO'opݳsru]KCp-NoA Bp{ikPą38Rwٓ8㬖NtU.D{qn'zk9o^U>/=44$ $D"@@U'P.2%},&&fܬP[!s.)N~I~}^F:t5E9w0a񫪫ii|SGWKZ`;;cl;uq",, hcf АM w,,ZMΥI]> v``17^@P&S8,x䲰j_~9Lq`1S{,kIx a\ii)]zX^8K@` ,ׇr sK܂V+IHc`؄-֍Ącvٹ7݀5ܞ>*"D"@e mXK _^4Hoki9Ꚛ-m:UU~4xs8VTT8uV^.+$&`g$׫{tjilӇ\1E^FF:,x&133VQUCL" d礧'F&b#}v#j r ljdLIIf j(--KB`.BUMw4d̥HHJIIJ[ĩg!BH飫mE(3n=[ز7FHڔN D"PU4)`ݵek}ܸz1>. \^2:86k`<KW. npj}T.Y-c8]K&%$ FQQ Clx%B/_4ow*X?5>~x,Z q4l4z௢:`Ȉ.ofҊ:v]0~ fDaCxwA544Qf den?CXid zޮ}')iiTڵ{/FuJ`b_D0FB^Aw߁/< HX]μYRB>jש;r7-)))8 [ "@ DT-o!Sls^<3f4 g}>>/_vrU^m7p JqqXNbBaͿ  AQh3} /Y pv-W+/{a^5Zn[bBk9fVM,pob`7v}y{Sfncf[ /PCSsLߩm GA׮c쫪=O89Y C D"Pm52j.1{􎎎$tY@z -"@ D"@JC sxxF^N6#}q8Ҽ|ł AQ: D"@ eN2GJ"@ D%@ iF D"@ʜ9e $D"@ K:;k8 VV>ٱ^%(jY( Zikw(hB:"@ D!Pۀ25~9&)ă{eM[ҕn+#[ʶgG?}pUs |gws8ۘn\[%srrp"]EXUVx”Y?"9em E܍tw',4) )=w:vxoωm 0`ȸX'1}(23s<:b\:F۴LJHo}' 1{u)Sgܤi:} i>buu 6X5G"h'DJCl;vieIL"_b,YhT_Z60x'OSGEU s0bGfYsl6;86I {wn>t2xheUժ޷/E|/-,8L N֣Oz K DM\E%ef-RR`7k=1O\M-ڈQ9qt`@{A#p<; i5X׭Zy٦-[[y G!!%)1|}дI˕B1ifII 8ġKN!522rPcYpl܄}L pW~ѩF M-5w/ڵ8L`-8皸r*)  ԴW t"@ DT'XOç\?z[>/2O{ؠmp 53#&b]7M@zG#祷Fн_HJJAC'`ؑcU$GL߾~޹uv-s9xzԸ)X7̫*g}'G6{ sKM]sS3l!g|_zaou!ͅe HGD| ~yL#vzEOYY71D:8h׬qt,P7R̭">c,А@+177WZZJWlh=fo`LID 'V+''?cy ?bJbb5n\~31t+.`} WFxs{+Nw`!(?Z:enOV^|գD"@ DT32`ϭ<ݟvk:-ykDB 55ʖVg꣢fܴ?k?MZ|tv}LO& Gsc zA9f)6ccTur&KJ*k*>c'L}ȩCnOa)oȸy,컈ae1?q,L!ؕK\aF܂{d/$^vJ䷯.2:u}\8B%n7p( #(+<<aHL a.eeenj \سoA7~Ys:z<ꕖ^y׻XJη v"U ?=-e%. D"@+rqXByѻon\$//,LyʭSW!V.=#al+phDD30ӕUuX[2X Q]]sfVTAT >XK)qwf^B]S+_37/7m{ DAX~kzsՋQl1 KCv~ VaNa܅˃- z{xa[J\5pw3&MǪev<$W '1"]P6ퟻ=<܌=`V[/;kW,ٶy5\k7c u=)ׯ4& %QNUMD"@$yFM7G q\XSXi#>q[ {7Z|̤%bk 냑k6ܵ1nۡ+S`3V؝SCC&ݶjc_"6Xu֛n=b#;PHv̩0v1MUmoS\] 5.$1qh)JFW^Z`mX @291ԢS.`\""H0n "[Xѵg_u/_/j#_ m<-oz`Vλ$C5Bg >x#].&D"@@u'P3acPWosGsqnY[N}5u?tfǫ#AX-SΪu^bޣ1sj IDAT^~{Jj2vF};vgk-h Lڗ[9F?fؠk6nth;?̄4tXiύp+{= ×"t1jd d=~q\';Ջoyj!n &5k`3')z:ըe,N\Mk[C"h0؏oB DjL@QsAC}c ]VF:-=#߷%Bq;,VD%iK D"@.-Z"D^N6#3h}J ѽ$_%Ѽe> ~Z4# D"@$==NKt yJ8Zv:zkUhH"@ D0D@FSTE6TEIg"@ DD+j2 D"@HT$m"@ DT2r*z"@ D"PHT D"@dTrPD"@ D" P."@ D"P D"@@E "iS]D"@ D P@"@ D$@@EҦ D"@@% ;'D"@ IMu"@ DJ&@@%wUO D"@*9I"D"@ LJ"@ DT$r*6E D"@*9VdT= D"@XqUB1~-x5kk w+?}.I D"@ D| OKM5j\[<~~^B V~TO+%$Wmض~= E)D"@ D@E1Jxp}啕$rumʔKBF.\6 "m I"@ D6r_))ik/7ml:x?<70i\ 7 DAH"KD"@ D Sejt;qdk }Vx$%%ޱiqGO̸xD3kNIIF!X8IK D"@52j.Jjjrc ]VF:-=#߷k^@jJk]"@ D"@D~o0rFRҼ|WAAR!@\Ubii[K7ǂTt"@ D"@ʛ@9qn7`hv~w|"@ D"@ U|)AD"@ DT P-D"@ D! I{ D\n].Y e d*ݲc?isJM3.)T@9J\݊5[LZr/QҪ_?kqlZl=|5Jlԙ <=}JTU7muA9sj}E+YoFO)W4Džq8Ohj[ 5dҔPyD=BBlܩWo/^xe>~,: eR]dzbxZLTTp б{BK\BfڴmI#˶J/ͪ]{vnIQdfۖLhekW.1y|勞(''g҅3pFsU 5kRKٓrK_ZȈkF];v-e%xFqq/rAEL)j58Q# Tנ($@G-kOCK}peW )l/J\T5xVYz˵K[_z= 8Iӯ]>W2럫*ij*^&]Ԍ/P*M1;[iJ[[)G1m̩#o3bbyyFqg W}}Vi<\"E*~CofѼeL<}ey<P֮=mڰG(huU/Y2srKdog}|/ BBBi>y2g֤y |YѮe^xN>[ێ<  cG8u18q%xJuܹ}SM$5uKڑ7o\ov)R|p]8*MѼV,juCԮGI7j:mwn-[2krL1|Nñ5soM"5>w/a'oqyEs,ԤE| V` 5CͰuTUsV..[q V-[ɣVm*ݵfRbRʔȺaɒ&Kw6᫺VFS3P^JUkP#m̙yE_h64(g+W.Ι5a%˔᜙6i\@"n|Zasfh:WM4idL,L?=yx2c)J=18$uҿa)>J%Sd)eܙt.?S0Gyl}cbVI0@^D:ҴܾixhK6MQeϑ(Q"Ffc6܆RGʭ OS۠i5^TڴV8i=,Jdѝ?U+W>??_u|(c{ڴgMtL5JٶT[l!ї cm37?Dgͮj4yԊ3wXOcCzm e*$O \٩ט_ZfyV%qUiix5˕!A`KN3qϓ&Qj6E4OwFik>NJ<+ HX'G 0ȀFaI Ç'-J*?9Xp9s%=E)-tтE*M{y׫6ȀޝBvϣb gt9}8?. -_* hϟuokW ӕW{fn؆ t;jPb%>49ᓿ@C wV5^5gh4ny{v}{uܻqXGg~G<;ٽͽ[_=yK0͚6+HE]B۷nb(Q<ש(W|FЧKR<9"v5 }t[g6$N8a_ҧGgjI1&.iPTyB:t[8wVmr~ JnOӇZAS}颹vj֬WL$v#t+Pxޝ_dn%KYR V\B)ݻǍңs;6wh CݻMo+Um^[nxg̀fo1MU"!>J'Zw$,/ѧgwhPjS_pϙޫk3NtǤf2wׅflӱFͯ&>rh Y!A Cd+V~Rwz`u԰~IF?}qa]] ̙}w&&urlBv7Fm3meWR7q qS<+VsN=qLLӦKwy/O?\CPP0[4G$"J([_@{wKաF^c6,XYV,UW;)Qa2O }[l9;&uA*[qݚ/_@͆LWQyp5G|)^>ci&VҥˀOf֮@eKA"݉ž]p.s1 ٹ mk <<.I,٘HYfw:N"Qmg˚Ö DK*iï/2oޱɺ1}ϣGLMD+O^mΜ>6K&S+[^ƒ"}]IDkkN?;w>-P#aB,x;j495fw͞E,<~Z5`σ{qsC2^2̓?-%:8j&斑-eT.2!PFF/B@,S%J"Cu4a!w^RHͺ*׬ZZK,<4E OKPۑ3NΏsGݠj(đz CdQg^ C!3*w=|8>zU W,ߨyW'0.Rj| 4۶}RSI'~$(,̽UjͰqSedd Mcľ֮,US 0\6˘1}lφ1ED_495f z=bVQua0,X$>4z" r#^=/fQoX|ELy?o)vͮԤD/ذU fnH}A!y,Ehn5g57Z76z(˘nE۳4SDF/aGȤT@OVyJ)B_9GlyƳfr*Cܽ= ׬4h6Vx -eVu2(%ʔ-_=1aR<`% @[аy;oSĈ-Vy# (/Kx$JShY3{N &NbifGgΝ#7۠h h ]{i2 Cxܟ{+}؋gOx x=vR#9@t#혭]/*S~K]VNݰOP}RR3IcۭL*5 Mv&GJj Z<CQM'L3ͫ` <}R`5h&$I[,2~Pe(Tc4HK̡}ҿsmwQ*jrGLhSvT<{D ? (#-gch 4S1MӤQ^̩̽%ߠBC1$EO͘Ӥjɒdfxa"z?6BDع[WyyDvoݮSצ=z`@0Ȍ^b:D(YzU 7xD?؅39d2kɏ>"cf`JԤD!czKV\+L1[5gUW5 BMo [8h>  RT%̫Rfч޽{bmIUh&aŠ/sQ>5Ճ7 h9eFࠁx(,snShG):Iڿf͘ظ[.Ln8*1Х爱b^ a#Q:[5{fn8:bSި:H=jߙ7q˗.ef a;1tx J pH .aa.]aOQfxMZ̞^@k_عֱhrlT`cYGQp6l;{e<,fl|"rI#(_h:\Z* E4E&DP#3~ҽϬĩoXmAvߴxlUb W5 EmuN֬rTOGL2eDNAS*0ArgVxEL%{`Ѷ0T3NK\Χ+`V/>8:CE`o2(LYvN{ŲEc_!_v#ere\+i s0Edl3V N}ɽb]Y?IA#MWcaM>-q,VsPԝw|| dI# 4?ҥݾm3Ǡk>1Mf]cgOyx?<%K@P:3`k%nqT5qq*omz&a&s%רyUo*̟~(o[v8'ۋ&>{B M4{w޷癶lՌ!?<=%IQ>Gq('zx~W0>wpP2U}ݹGxX6iDq6;a?-c|E[7>xp/vHHB  &@X,Z! B@/ P)B@! 0 {ئ. n,[0mω}(zLbƆ=օ2cF7o"v9sVu<&5[vk8vWQ`qgN({q\8Hq4p@IJ=w~&WWL>n/qN ̙eD]ٱ]O8ѝ;ƽB1M~7z"%/~Fr#ߏ~9TS&~o[[W9'/XXاck|'&~5g+=AmOAlcyW]/B@!6bkPR-pϙ\٤E>9&a9Wk5rU,=fwGp; a*MuơޓCl(V9@b^tВ2 3g6\pO¾[w6'6Z;O~b/'lmHR{%f! >,2|Ma{gsAnj۽\Ν5-qnf9_o՘sݑWwqQlZϢϞ\Qa<mFG/^oQ#fOSSԤTr2ft孼jKL;4mц#"<=oX ;Pr!}KuY`6WwYcTSJbl<^x54u#Jz?kvfpqz=:8$h%Ġ3|@v/?\YQ<_wɣygŸ" )fNeu@ʒ%{E m_PȫW{wnݾu#0ʡ(]B)MWMOڳoXhئ k뺠툉7j2Fs7k,n}PwnzB̔)WQY^ˆ昩(]pihsfMf/6(땿,z&[4GG&-r *>` <MM~=#;;Nҝ&Hȥ~ JMJ2n(&T=fw! J V&SHy "G ,ȠeW9eOGfp``ͅk|ѹ[1#yX'_|ɛ緃Δq37u7KM4Zьy,v!s&Eze!Gz9)S)߻۫!q.!"x4i9cJ(pM;ֽg/yp29mt#[)T*ՋΉ"41^NjD24A}Q/SF}:Ƿt5?wї;ho٘;w^}k43Fd _8C SmVZ5Rg(D݆HP::;KADFn޸hY,׭Y ֬T)_q4[ac۵}??RM jVr^NiZ#XeDBIlP p͒9-kAJ 1k viYȕ|Pڂ;ΞQY5ΆŘiP@[ڶYZf9H_ ȍ4Y04 ã#b<2#s'<<\2eB I2dy*xo @@S-9OӸ'UG'+?Rg(q3f̄nSy 1'RRĬ=Jظy+V%N$qbx5 sQ=lXz뙷mA$hL}x3v6};wcgoyh5+ (1w! {N V&hzP(]]J$cK}[p`=3uzo\ rFv6-F~Lf9oA;o^gXȘ"}9eʝ:.8gmު =:^`/bjZĜڠFҦOrGҐu#"'UUM~.K89ME`Rt%wW$vjeC=]}(m TN{'"< -51)M:'_'F튡stJsԎ 94@ o,jSYaA,VU 7̀ז3ę "u]<[ve&]^ROCænt Xb&@Dlۮdx@"UUĠ[YGf㎃*I52fgiV ,ZJN\! bnͲFM[*SnFL{~m)6x♳DPDu w[2XO*yDjQv+ -ޤy\ra.qTH|4rKG3%]{a+Y1 1El?:ݟxCII2:V#âf3;5w|Hz9ѫwԬU7G\L %0ҋDqϐB0't2Ɲ포\G"ozY"{J@M~4mdEWuqJ4 <|xW,B@@ eњaq7{A>OfjWrn6u(vn3mR|re0& ǫ4nSRc?+wޟgN~e$G Ҝ3(]zC6i29vۨIdOY9A{xmcf@{?;ěew|| dICtҥݾm3;=V{|m8VϞݿplhm,Ο35/zbkZvE0أj팛Sɕ;߃Ps_AE]P?9Tf@{:B@ة(iƏZ1?rncor{st Gz{h~柃p؎^>e94-b/u92oJ"B@)1zSb_! B@ ޕ P/$B@! Ll^ )6t&˹SSf.ңOc9$6~Zl흻N#Q}''+q_;9H$~S$ɇKMYe$n{.^:䀁wX@J!bkǟ5j:[\:Y5Tv߻sb)8KSJŕ,?[ǜٻGnV~}TߴE}%}ˠzOc~ qy'=+D|<Ջ 8-<F KxpҩSl勔X\T.ϽscE*53fI69?7rLBEMCyFsNK9׆s$I\$!pQrrYS6nQLGΙ91\ucpJ14Zv%wZE9gmڰK\E_G>}|Ἑfb9KxXc+-: ߓSkթ䜆kږ.֝w0mۺaw^G/[~*y{_˂ \Xxr+uvL8kD5eN\%t;oRLr͟95v^H1{{wZIWܸ0QkW//]̍rIDAT4GQj6x5{-gʜ6Eo7`{n[1U?yUoR;:qw~YqbnR;sƵ}v֕W/ѶC6wUYs˗/[ZM&ƅyeoеg8m8ڼ ]\\!@TsgMy1O ,ҨYk*meK߼qzP#ѫ#P[6V ƺw;|3G.]HGtiN_|U7,mV8~3םz:9=u2앚#yh+Ir/rx飞;l䄭Z7"ѫDGG&-r)/cԂ3H%keW |dOhvyecD-[ A 1X 0ζ}/x<̝U~G*[ w͒57K7]߻K) ٪ 5?tkݻǍ,0\ʝ;w9"'5,o(TN !wf[&3a,C2Jhfhʔ{٢{5m&@zF_;v`.hԬUM͛&Mڮϙ=3+UWAC%׬#[SdC">sXV:i*5vl]8rHmfNzq?QDϒe*L͝$E #^%JyF?~rێ]Ozf-e%S.O'1$[W"Bn ߃ р(/T(#Р,U@G;/RC{o٘;w^} JpFݼ ͏.#Ot& kҝ>>Ta]"xS 8,,l![4} dq㜡:E>3ٲ.j]2eBq7,,14kW.Q&wnC!m-dɒ m4sOzD(wqɬpS砖S X9q̋Yͱ#1[q%>Cg4sE$l@mTJxpOmoLQe4DWZZ?bQk-L_[b)K.4SG]\KT>qkK̀кٻf#aJY$uCwN!bYYvNc@`nu Ik.7Os-N~neA F*'ӦudzŔ-WQ/|i q6B be| yX/i߅Հi/B~hv1оkVIȱwnm8޹}s"E>PzZOXWa|%|>QmbÇQ_y wb%~u^W"> OjdQKdgՊEvY@MT7o:Q$I'6FV˕+o UԀUQFqDHE&V#,AVGvvhwnS8Ib&1挓U$1fMY +vcg̘jT(J$XHᦽ Cd Yohta6E7n\6W`Oo)jm5_H-H&"1n;h1)ssrxܟ{y]Xjiױ5+56mN/,}}&[['O"*N3֋3OVd>QBWG1(y4[8+GHͩf8?Ufiô H{366v;s6e-G/]f@RtrvVg˚~lt:#*W-5ٺy7whegќ/DQ6ۍAىbٳKT-V#FlC!JݻgqgpOEk|ۘMW3OըUX #;{]4ÇꚅXEkj7vR)k3o}v!#'XfwY.'R; %B'4xa 3{w4m#%ȱTL33ƎvݽsKA#jʢn!+ІGSYQ3$Dg6[~Z8G<.LsgKmZĩhTbG.hV^眧D>W3ld?Cݼq@an2ULg &V-hFexQl1'~Fdɓ;QU^>)i| ˢ]*Lo6}\Ξۼo{OyS6֐=ɭd@K~L}<(ʮRrm%!1 !"pϷǟ(I!8KOtioۜ<}ifE!Ȱ"Qc`Y=ߨMϜ)߫Rc&TPт=Z;m2!aB@! 7-> ,,. """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library import datetime #Third Party #Our Modules from engine.start import PATHS from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. from engine.jackclient import AgordejoJackClient from .nsmservercontrol import NsmServerControl from .watcher import Watcher from .findprograms import programDatabase class Callbacks(object): """GUI methods register themselves here. These methods get called by us, the engine. None of these methods produce any return value. The lists may be unordered. We need the lists for audio feedbacks in parallel to GUI updates. Or whatever parallel representations we run.""" def __init__(self): global jackClient self.message = [] #Session Management self.sessionOpenReady = [] self.sessionOpenLoading = [] self.sessionClosed = [] self.sessionsChanged = [] #update in the file structure. redraw list of sessions. self.sessionLocked = [] # incremental update. Sends the name of the session project and a bool if locked self.sessionFileChanged = [] #incremental update. Reports the session name, not the session file self.clientStatusChanged = [] #every status including GUI and dirty self.singleInstanceActivateWindow = [] #this is for the single-instance feature. Show the GUI window and activate it when this signal comes. self.dataClientNamesChanged = [] self.dataClientDescriptionChanged = [] self.dataClientTimelineMaximumDurationChanged = [] #in minutes. this is purely a GUI construct. the jackClient knows no limit!. #JackClient Callbacks. For the GUI they are mirrored here. These are mutable, shared lists. #The callback functions are in jackClient directly and api functions can call them. self.setPlaybackSeconds = jackClient.callback_setPlaybackSeconds def _dataClientNamesChanged(self, data): """If there is a dataclient in the session it will allow us to read and write metadata. The GUI instructs us to send a write instruction over OSC, we wait for the OSC answer and forward that to the GUI. If the dataClient joins the session it will trigger an unrequested callback (from the GUIs perspectvive). If the client leaves the callback will send a single None. This is a sign for the GUI to reset all data to the nsmd state. We do not mix nsmd data and dataClient. A dataclient can join and leave at every time, we keep the GUI informed. """ for func in self.dataClientNamesChanged: func(data) def _dataClientDescriptionChanged(self, data): """see _dataClientNamesChanged. In short: str for data, None if nsm-data leaves session""" for func in self.dataClientDescriptionChanged: func(data) def _dataClientTimelineMaximumDurationChanged(self, minutes:int): """ This callback is still used, even if nsm-data is not in the session. It will then purely be a roundtrip from gui widget -> api -> gui-callback without saving anything. For compatibility reasons it will still send a "None" when nsm-data leaves the session. The GUI can then just continue with the current value. It just means that the values will not be saved in the session. """ for func in self.dataClientTimelineMaximumDurationChanged: func(minutes) def _singleInstanceActivateWindow(self): for func in self.singleInstanceActivateWindow: func() def _sessionOpenReady(self, nsmSessionExportDict): """A project got opened, most likely by ourselves, but also by another party. This will also fire if we start and detect that a session is already open and running. """ for func in self.sessionOpenReady: func(nsmSessionExportDict) def _sessionOpenLoading(self, nsmSessionExportDict): """ A session begins loading. Show a spinning clock or so... """ for func in self.sessionOpenLoading: func(nsmSessionExportDict) def _sessionClosed(self): """The current session got closed. Present a list of sessions to choose from again. This is also send at GUI start if there is no session open presently.""" for func in self.sessionClosed: func() def _sessionsChanged(self): """The project list changed. This can happen through internal changes like deletion or duplication but also through external changes through a filemanager. Always sends a full update of everything, with no indication of what changed.""" listOfProjectDicts = nsmServerControl.exportSessionsAsDicts() for func in self.sessionsChanged: func(listOfProjectDicts) return listOfProjectDicts def _sessionLocked(self, name:str, status:bool): """Called by the Watcher through the event loop Name is "nsmSessionName" from _nsmServerControl.exportSessionsAsDicts Sends True if project is locked""" for func in self.sessionLocked: func(name, status) def _sessionFileChanged(self, name:str, timestamp:str): """ This happens everytime the session gets saved. Called by the Watcher through the event loop. Name is "nsmSessionName" from _nsmServerControl.exportSessionsAsDicts. timestamp has same format as nsmServerControl.exportSessionsAsDicts""" for func in self.sessionFileChanged: func(name, timestamp) def _clientStatusChanged(self, clientInfoDict:dict): """A single function for all client changes. Adding and deletion is also included. GUI hide/show and dirty is also here. A GUI needs to check if it already knows the clientId or not.""" for func in self.clientStatusChanged: func(clientInfoDict) def startEngine(): logger.info("Start Engine") global eventLoop assert eventLoop global nsmServerControl nsmServerControl = NsmServerControl( sessionOpenReadyHook=callbacks._sessionOpenReady, sessionOpenLoadingHook=callbacks._sessionOpenLoading, sessionClosedHook=callbacks._sessionClosed, clientStatusHook=callbacks._clientStatusChanged, singleInstanceActivateWindowHook=callbacks._singleInstanceActivateWindow, dataClientNamesHook=callbacks._dataClientNamesChanged, dataClientDescriptionHook=callbacks._dataClientDescriptionChanged, dataClientTimelineMaximumDurationChangedHook=callbacks._dataClientTimelineMaximumDurationChanged, parameterNsmOSCUrl=PATHS["url"], sessionRoot=PATHS["sessionRoot"], startupSession=PATHS["startupSession"], ) #Watch session tree for changes. global sessionWatcher sessionWatcher = Watcher(nsmServerControl) sessionWatcher.timeStampHook = callbacks._sessionFileChanged sessionWatcher.lockFileHook = callbacks._sessionLocked sessionWatcher.sessionsChangedHook = callbacks._sessionsChanged #This is the main callback that informs of new or updated sessions callbacks.sessionClosed.append(sessionWatcher.resume) #Watcher only active in "Choose a session mode" callbacks.sessionOpenReady.append(sessionWatcher.suspend) eventLoop.slowConnect(sessionWatcher.process) #Start Event Loop Processing eventLoop.fastConnect(nsmServerControl.process) eventLoop.fastConnect(jackClient._setPlaybackSeconds) eventLoop.slowConnect(nsmServerControl.processSingleInstance) #Send initial data #The decision if we are already in a session on startup or in "choose a session mode" is handled by callbacks #This is not to actually gather the data, but only to inform the GUI. logger.info("Send initial cached data to GUI.") callbacks._sessionsChanged() #send session list c = currentSession() #sessionName if c: callbacks._sessionOpenReady(nsmServerControl.sessionAsDict(c)) #Send client list. This is only necessary when attaching to an URL or using NSM-URL Env var #When we do --load-session we receive client updates live. #But this little redundancy doesn't hurt, we just sent them. Better safe than sorry. for clientId, clientDict in nsmServerControl.internalState["clients"].items(): callbacks._clientStatusChanged(clientDict) else: callbacks._sessionClosed() #explicit is better than implicit. Otherwise a GUI might start in the wrong state #nsmServerControl blocks until it has a connection to nsmd. That means at this point we are ready to send commands. #Until we return from startEngine a GUI will also not create its mainwindow. logger.info("Engine start complete") #Info def ourOwnServer(): """Report if we started nsmd on our own. If not we will not kill it when we quit""" return nsmServerControl.ourOwnServer def sessionRoot(): return nsmServerControl.sessionRoot def currentSession(): return nsmServerControl.internalState["currentSession"] def sessionList()->list: """Updates the list each call. Use only this from a GUI for active query. Otherwise sessionRemove and sessionCopy will not have updated the list""" r = nsmServerControl.exportSessionsAsDicts() return [s["nsmSessionName"] for s in r] def requestSessionList(): """For the rare occasions where that is needed""" callbacks._sessionsChanged() #send session list def buildSystemPrograms(progressHook=None): """Build a list of dicts with the .desktop files (or similar) of all NSM compatible programs present on the system""" programDatabase.build(progressHook) def systemProgramsSetWhitelist(executableNames:tuple): """will replace the current list""" programDatabase.userWhitelist = tuple(executableNames) #Needs rebuild through the GUI. We have no callback for this. def systemProgramsSetBlacklist(executableNames:tuple): """will replace the current list""" programDatabase.userBlacklist = tuple(executableNames) #Needs rebuild through the GUI. We have no callback for this. def getCache()->dict: """Returns the cached database from buildProgramDatabase. No automatic update. Empty on program start db = { "programs" : list of dicts iconPaths : list of strings } """ return programDatabase.getCache() def setCache(cache:dict): programDatabase.setCache(cache) def getNsmExecutables()->set: """Cached access fort fast membership tests. Is this program in the PATH?""" return programDatabase.nsmExecutables def getUnfilteredExecutables()->list: """Return a list of unique names without paths or directories of all exectuables in users $PATH. This is intended for a program starter prompt. GUI needs to supply tab completition or search itself""" programDatabase.unfilteredExecutables = programDatabase.buildCache_unfilteredExecutables() #quick call return programDatabase.unfilteredExecutables #Session Control #No project running #There is no callback for _sessionsChanged because we poll that in the event loop. def sessionNewTimestamped(): """convenience function. Create a new session without requiring a name and add suggested infrastructure clients""" nsmExecutables = getNsmExecutables() #type set, cached, very fast. connectionSaver = METADATA["preferredClients"]["data"] dataMeta = METADATA["preferredClients"]["connections"] startclients = [] if connectionSaver in nsmExecutables: startclients.append(connectionSaver) if dataMeta in nsmExecutables: startclients.append(dataMeta) #now = datetime.datetime.now().replace(second=0, microsecond=0).isoformat()[:-3] now = datetime.datetime.now().replace(microsecond=0).isoformat() name = now sessionNew(name, startclients) def sessionNew(newName:str, startClients:list=[]): nsmServerControl.new(newName, startClients) def sessionRename(nsmSessionName:str, newName:str): """only for non-open sessions""" nsmServerControl.renameSession(nsmSessionName, newName) def sessionCopy(nsmSessionName:str, newName:str, progressHook=None): """Create a copy of the session. Removes the lockfile, if any. Has some safeguards inside so it will not crash. If progressHook is provided (e.g. by a GUI) it will be called at regular intervals to inform of the copy process, or at least that it is still running. """ nsmServerControl.copySession(nsmSessionName, newName, progressHook) def sessionOpen(nsmSessionName:str): """Saves the current session and loads a different existing session.""" nsmServerControl.open(nsmSessionName) def sessionQuery(nsmSessionName:str): """For the occasional out-of-order information query. Exports a single session project in the format of nsmServerControl.exportSessionsAsDicts""" return nsmServerControl.sessionAsDict(nsmSessionName) def sessionForceLiftLock(nsmSessionName:str): nsmServerControl.forceLiftLock(nsmSessionName) callbacks._sessionLocked(nsmSessionName, False) def sessionDelete(nsmSessionName:str): nsmServerControl.deleteSession(nsmSessionName) #While Project is open def sessionSave(): """Saves the current session.""" nsmServerControl.save() def sessionClose(blocking=False): """Saves and closes the current session.""" nsmServerControl.close(blocking) def sessionAbort(blocking=False): """Close without saving the current session.""" nsmServerControl.abort(blocking) def sessionSaveAs(nsmSessionName:str): """Duplicate in NSM terms. Make a copy, close the current one, open the new one. However, it will NOT send the session clossed signal, just a session changed one.""" nsmServerControl.duplicate(nsmSessionName) def setDescription(text:str): nsmServerControl.setDescription(text) def setTimelineMaximumDuration(minutes:int): nsmServerControl.setTimelineMaximumDuration(int(minutes)) #Client Handling def clientAdd(executableName): nsmServerControl.clientAdd(executableName) #status hook triggers clientStatusChanged callback def clientStop(clientId:str): nsmServerControl.clientStop(clientId) def clientResume(clientId:str): """Opposite of clientStop""" nsmServerControl.clientResume(clientId) def clientRemove(clientId:str): """Client must be already stopped! We will do that without further question. Remove from the session. Will not delete the save-files, but make them inaccesible""" nsmServerControl.clientRemove(clientId) def clientSave(clientId:str): """Saves only the given client""" nsmServerControl.clientSave(clientId) def clientToggleVisible(clientId:str): """Works only if client announced itself with this feature""" nsmServerControl.clientToggleVisible(clientId) def clientHideAll(): nsmServerControl.allClientsHide() def clientShowAll(): nsmServerControl.allClientsShow() def clientNameOverride(clientId:str, name:str): """An agordejo-specific function that requires the client nsm-data in the session. If nsm-data is not present this function will write nothing, not touch any data. It will still send a callback to revert any GUI changes back to the original name. We accept empty string as a name to remove the name override """ nsmServerControl.clientNameOverride(clientId, name) def executableInSession(executable:str)->dict: """Returns None if no client with this executable is in the session, else returns a dict with its export-data. If multiple clients with this exe are in the session only one is returned, whatever Python thinks is good""" for clientId, dic in nsmServerControl.internalState["clients"].items(): if executable == dic["executable"]: return dic else: return None #Global Datastructures, set in startEngine nsmServerControl = None eventLoop = None sessionWatcher = None jackClient = AgordejoJackClient() #Create before callbacks callbacks = Callbacks() #This needs to be defined before startEngine() so a GUI can register its callbacks. The UI will then startEngine and wait to reveice the initial round of callbacks ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/comparedirectories.py0000644000175000017500000000356500000000000017515 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) The Non-Session-Manager by Jonathan Moore Liles : http://non.tuxfamily.org/nsm/ New Session Manager, by LinuxAudio.org: https://github.com/linuxaudio/new-session-manager With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application 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://stackoverflow.com/questions/24937495/how-can-i-calculate-a-hash-for-a-filesystem-directory-using-python import hashlib from _hashlib import HASH as Hash from pathlib import Path from typing import Union def md5_update_from_dir(directory, hash): assert Path(directory).is_dir() for path in sorted(Path(directory).iterdir(), key=lambda p: str(p).lower()): hash.update(path.name.encode()) if path.is_file(): with open(path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash.update(chunk) elif path.is_dir(): hash = md5_update_from_dir(path, hash) return hash def md5_dir(directory): return md5_update_from_dir(directory, hashlib.md5()).hexdigest() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9878325 agordejo-0.3.1/engine/config.py0000644000175000017500000000576400000000000015102 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- #Do not change these during runtime! METADATA={ #The pretty name of this program. Used for NSM display and Jack client name #Can contain everything a linux file/path supports. Never change this or it will break the #session, making your file unable to load and destroying saved Jack connections. "name" : "Agordejo", #Set this to the name the user types into a terminal. #MUST be the same as the binary name as well as the name in configure. #Program reports that as proc title so you can killall it by name. #Should not contain spaces or special characters. We use this as save file extension as well #to distinguish between compatible program versions. In basic programs this will just be e.g. #patroneo. But in complex programs with a bright future it will be "laborejo1" "laborejo2" etc. "shortName" : "agordejo", #A very short description used in various places: Desktop file, overview on the website, #release announcements, entries in software directories etc. "tagline" : 'Music and audio production session manager based on NSM.', "version" : "0.3.1", "year" : "2022", "author" : "Laborejo Software Suite", "url" : "https://www.laborejo.org/agordejo", #English is automatic. "supportedLanguages" : {"German":"de.qm", "Italian":"it.qm", }, #Show the About Dialog the first time the program starts up. This is the initial state for a #new instance in NSM, not the saved state! Decide on how annoying it would be for every new #instance to show about. Fluajho does not show it because you add it many times into a session. #Patroneo does because its only added once. "showAboutDialogFirstStart" : False, "preferredClients" : {"data":"nsm-data", "connections":"jackpatch", "proxy":"nsm-proxy"}, #Various strings for the README #Extra whitespace will be stripped so we don't need to worry about docstring indentation "description" : """ Agordejo (Esperanto: 'place to set things up') is a music production session manager. It is used to start your programs, remember their (JACK) interconnections and make your life easier in general. """ + "\n" + """ Agordejo does not re-invent the wheel but instead uses the New-Session-Manager daemon and enhances it with some tricks of its own, that always remain 100% compatible with the original sessions. """ + "\n" + """ This is a proof of concept version. It aims to show that session management with NSM can be quick and convenient and make the user feel in control. Some functionality has not yet been implemented, most prominently anything related to NSM over network. There is always the possibility to break things when trying out corner cases and hacks. """ + "\n" + """ That said, for single-computer sessions with just one daemon and one GUI at the same time Agordejo should provide a good user experience. """, #this is the dict-comma. "dependencies" : "\n".join("* "+dep for dep in ("nsmd: New Session Manager", "grep")), } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/findicons.py0000644000175000017500000001445200000000000015603 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") from os import getenv import os import pathlib import re """ https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup $HOME/.icons (for backwards compatibility) $XDG_DATA_DIRS/icons /usr/share/pixmaps """ EXTENSIONS = [".png", ".xpm", ".svg"] SEARCH_DIRECTORIES = [pathlib.Path(pathlib.Path.home(), ".icons")] XDG_DATA_DIRS = getenv("XDG_DATA_DIRS") #colon : separated, most likely empty if XDG_DATA_DIRS: SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/hicolor") for p in XDG_DATA_DIRS.split(":")] SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/scalable") for p in XDG_DATA_DIRS.split(":")] else: #If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used. SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/hicolor") for p in "/usr/local/share/:/usr/share/".split(":")] SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/scalable") for p in "/usr/local/share/:/usr/share/".split(":")] SEARCH_DIRECTORIES.append(pathlib.Path("/usr/share/pixmaps")) SEARCH_DIRECTORIES.append(pathlib.Path("/usr/share/icons")) #for icons wrongly put directly there. But many do. SEARCH_DIRECTORIES.append(pathlib.Path("/usr/local/share/icons")) #for icons wrongly put directly there. #TODO: this may become a problem in the future. If a user has *MANY* icon themes installed this might take too long. And all because we wanted the Shuriken icon in Archlinux... SEARCH_DIRECTORIES = set(p.resolve() for p in SEARCH_DIRECTORIES) #resolve follows symlinks, set() makes it unique def run_fast_scandir(dir, ext): """ https://stackoverflow.com/questions/18394147/recursive-sub-folder-search-and-return-files-in-a-list-python """ subfolders, files = [], [] try: for f in os.scandir(dir): if f.is_dir(): subfolders.append(f.path) if f.is_file(): if os.path.splitext(f.name)[1].lower() in ext: files.append(f.path) for dir in list(subfolders): sf, f = run_fast_scandir(dir, ext) subfolders.extend(sf) files.extend(f) except PermissionError: pass except FileNotFoundError: pass return subfolders, files def _buildCache()->set: result = [] for basePath in SEARCH_DIRECTORIES: forget, files = run_fast_scandir(basePath, EXTENSIONS) result += files #Convert str to real paths result = [pathlib.Path(r) for r in result if pathlib.Path(r).is_file()] return set(result) global _cache _cache = None def updateCache(serializedCache:list=None): global _cache if serializedCache: #Convert str to real paths logger.info("Filling icon cache with previously serialized data") _cache = set([pathlib.Path(r) for r in serializedCache if pathlib.Path(r).is_file()]) else: #Already real paths as a set logger.info("Building icon cache from scratch") _cache = _buildCache() logger.info("Icon cache complete") def getSerializedCache()->list: #list of strings, not paths. This is for saving in global system config for faster startup global _cache return [str(p) for p in _cache] rePattern = re.compile("\d+x\d+") #we don't put .* around this because we are searching for the subpattern def findIconPath(executableName:str)->list: """ Parameter executableName can be a direct icon name as well, from the .desktop icon path. Return order is: svg first, then highest resolution first, then the rest unordered. so you can use result[0] for the best variant. It is not guaranteed that [1], or even [0] exists. This is not a sorted list, these extra variants are just added to the front of the list again""" global _cache if not _cache: raise ValueError("You need to call updateCache() first") svg = None bestr = 0 #resolution best = None #Did we get an icon name directly? Remove the extension #For example "ams_32.xpm" becomes "ams_32" exeAsPath = pathlib.Path(executableName) if exeAsPath.suffix in EXTENSIONS: executableName = exeAsPath.stem #for ext in EXTENSIONS: #all extensions # if executableName.endswith(ext): # executableName = executableName[:-4] result = [] for f in _cache: if f.stem == executableName: if f.suffix == ".svg": svg = f else: match = re.search(rePattern, str(f)) #find resolution dir like /48x48/ if match: resolutionAsNumber = int(match.group().split("x")[0]) if resolutionAsNumber > bestr: #new best one bestr = resolutionAsNumber best = f result.append(f) if best: result.insert(0, best) if svg: result.insert(0, svg) if not result: logger.warning(f"Did not find an icon for {executableName}") return result if __name__ == "__main__": """Example that tries to find a few icons""" updateCache() print("Search paths:") print(SEARCH_DIRECTORIES) print() for exe in ("zynaddsubfx", "patroneo", "jack_mixer", "carla", "ardour6", "synthv1", "ams_32.xpm", "shuriken.png"): r = findIconPath(exe) if r: print (f"{exe} Best resolution: {r[0]}") else: print (f"{exe}: No icon found ") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/findprograms.py0000644000175000017500000003510600000000000016321 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") import pathlib import configparser import subprocess import os import stat from engine.start import PATHS import engine.findicons as findicons def nothing(*args): pass class SupportedProgramsDatabase(object): """Find all binaries with NSM support. Resources are: * Agordejo internal program list of known working programs. * Internal blacklist of known redundant programs (such as non-daw) or nonsense entries, like Agordejo itself * A search through the users path to find stray programs that contain NSM announce messages * Finally, local to a users system: User whitelist for any program, user blacklist. Those two have the highest priority. We generate the same format as configParser does with .desktop files as _sections dict. Example: { 'categories': 'AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack;', 'comment': 'Easy to use pattern sequencer for JACK and NSM', 'comment[de]': 'Einfach zu bedienender Pattern-Sequencer', 'exec': 'patroneo', 'genericname': 'Sequencer', 'icon': 'patroneo', 'name': 'Patroneo', 'startupnotify': 'false', 'terminal': 'false', 'type': 'Application', 'x-nsm-capable': 'true'} In case there is a file in PATH or database but has no .desktop we create our own entry with missing data. We add two keys ourselves: "agordejoExec" : this is the one we will send to nsmd. "agordejoIconPath" : absolute path as str if we found an icon, so that a GUI does need to search on its own """ def __init__(self): self.progressHook = nothing #prevents the initial programstart from sending meaningless messages for the cached data. Set and reverted in self.build self.grepexcluded = (pathlib.Path(PATHS["share"], "grepexcluded.txt")) #created by hand. see docstring #assert self.grepexcluded.exists() self.blacklist = ("nsmd", "non-daw", "carla", "agordejo", "adljack", "agordejo.bin") #only programs that have to do with audio and music. There is another general blacklist that speeds up discovery self.whiteList = ("thisdoesnotexisttest", "patroneo", "vico", "fluajho", "carla-rack", "carla-patchbay", "carla-jack-multi", "carla-jack-single", "ardour5", "ardour6", "nsm-data", "jackpatch", "nsm-proxy", "ADLplug", "ams", "drumkv1_jack", "synthv1_jack", "samplv1_jack", "padthv1_jack", "luppp", "non-mixer", "non-timeline", "non-sequencer", "non-midi-mapper", "non-mixer-noui", "OPNplug", "qmidiarp", "qtractor", "zynaddsubfx", "jack_mixer", "hydrogen", "mfp", "shuriken", "laborejo", "guitarix", "radium", "ray-proxy", "ray-jackpatch", "amsynth", "mamba", "qseq66" ) #shortcut list and programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce. self.userWhitelist = () #added dynamically to morePrograms. highest priority self.userBlacklist = () #added dynamically to blacklist. highest priority self.knownDesktopFiles = { #shortcuts to the correct desktop files. Reverse lookup binary->desktop creates false entries, for example ZynAddSubFx and Carla. "zynaddsubfx": "zynaddsubfx-jack.desktop", #value will later get replaced with the .desktop entry #"carla-jack-single" : "carla.desktop", #We CANNOT add them here because both key and value must be unique and hashable. We create a reverse dict from this. #"carla-jack-patchbay" : "carla.desktop", #"carla-jack-rack" : "carla.desktop", "ams" : "ams.desktop", "amsynth" : "amsynth.desktop", } self._reverseKnownDesktopFiles = dict(zip(self.knownDesktopFiles.values(),self.knownDesktopFiles.keys())) #to lookup the exe by desktoip name self.programs = [] #list of dicts. guaranteed keys: agordejoExec, name, agordejoFullPath. And probably others, like description and version. self.nsmExecutables = set() #set of executables for fast membership, if a GUI wants to know if they are available. Needs to be build "manually" with self.programs. no auto-property for a list. at least we don't want to do the work. #.build needs to be called from the api/GUI. #self.unfilteredExecutables = self.buildCache_unfilteredExecutables() #This doesn't take too long. we can start that every time. It will get updated in build as well. self.unfilteredExecutables = None #in build() #self.build() #fills self.programs and def buildCache_grepExecutablePaths(self)->list: """return a list of executable names in the path (not the path itself) Grep explained: -s silent. No errors, eventhough subprocess uses stdout only -R recursive with symlinks. We don't want to look in subdirs because that is not allowed by PATH and nsm, but we want to follow symlinks If you have a custom user path that does not mean that all its executables will automatically show up here. They still need to contain /nsm/server/announce Your binaries will be in unfilteredExecutables though """ result = [] testpaths = os.environ["PATH"].split(os.pathsep) + ["/bin", "/sbin"] executablePaths = set([pathlib.Path(p).resolve() for p in os.environ["PATH"].split(os.pathsep)]) #resolve filters out symlinks, like arches /sbin and /bin. set() makes it unique excludeFromProcessingSet = set(self.blacklist + self.userBlacklist) whiteSet = set(self.whiteList + self.userWhitelist) excludeFromProcessingSet.update(whiteSet) for path in executablePaths: self.progressHook(f"{path}") command = f"grep --exclude-from {self.grepexcluded} -iRsnl {path} -e /nsm/server/announce" #command = f"grep -iRsnl {path} -e /nsm/server/announce" #Py>=3.7 completedProcess = subprocess.run(command, capture_output=True, text=True, shell=True) completedProcess = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) #universal_newlines is an alias for text, which was deprecated in 3.7 because text is more understandable. capture_output replaces the two PIPEs in 3.7 for fullPath in completedProcess.stdout.split(): self.progressHook(f"{fullPath}") exe = pathlib.Path(fullPath).relative_to(path) if not str(exe) in excludeFromProcessingSet: #skip over any known file, good or bad result.append((str(exe), str(fullPath))) for prg in whiteSet: self.progressHook(f"{prg}") for path in executablePaths: if pathlib.Path(path, prg).is_file(): #check if this actually exists result.append((str(prg), str(pathlib.Path(path, prg)))) break #inner loop return list(set(result)) #make unique def buildCache_DesktopEntries(self): """Go through all dirs including subdirs""" xdgPaths = ( pathlib.Path("/usr/share/applications"), pathlib.Path("/usr/local/share/applications"), pathlib.Path(pathlib.Path.home(), ".local/share/applications"), ) config = configparser.ConfigParser() allDesktopEntries = [] for basePath in xdgPaths: for f in basePath.glob('**/*'): try: #TODO: this whole part of the program is a mess. Confiparser and Qt in a bundle segfault too often. self.progressHook(f"{f}") except Exception as e: logger.error(e) pass if f.is_file() and f.suffix == ".desktop": config.clear() try: config.read(f) entryDict = dict(config._sections["Desktop Entry"]) #Replace simple names in our shortcut list with full data if f.name in self.knownDesktopFiles.values(): key = self._reverseKnownDesktopFiles[f.name] self.knownDesktopFiles[key] = entryDict #in any case: allDesktopEntries.append(entryDict) #_sections 'DesktopEntry':{dictOfActualData) except: #any bad config means skip logger.warning(f"Bad desktop file. Skipping: {f}") return allDesktopEntries def setCache(self, cache:dict): """Qt Settings will send us this""" self.programs = cache["programs"] #list of dicts findicons.updateCache(cache["iconPaths"]) logger.info("Restoring program list from serialized cache") self.nsmExecutables = set(d["agordejoExec"] for d in self.programs) def getCache(self)->dict: """To carry the DB over restarts. Saved by Qt Settings at the moment""" cache = { "programs" : self.programs, #list of dicts "iconPaths" : findicons.getSerializedCache(), #list } return cache def build(self, progressHook=None): """Can be called at any time by the user to update after installing new programs""" if progressHook: #receives one string which indicates what files is currently parsed. #Just the pure path, this will not get translated! #The purpose is to show a "we are not frozen!" feedback to the user. #It doesn't really matter what is reported back as long as it changes often self.progressHook = progressHook logger.info("Building launcher database. This might take a minute") self.progressHook("") self.programs = self._build() #builds iconPaths as side-effect self.unfilteredExecutables = self.buildCache_unfilteredExecutables() self.nsmExecutables = set(d["agordejoExec"] for d in self.programs) self._buildWhitelist() self.progressHook("") self.progressHook = nothing logger.info("Building launcher database done.") def _exeToDesktopEntry(self, exe:str)->dict: """Assumes self.desktopEntries is up to date Convert one exe (not full path!) to one dict entry. """ if exe in self.knownDesktopFiles and type(self.knownDesktopFiles[exe]) is dict : #Shortcut through internal database entry = self.knownDesktopFiles[exe] return entry else: #Reverse Search desktop files. for entry in self.desktopEntries: if "exec" in entry and exe.lower() in entry["exec"].lower(): return entry #else: #Foor loop ended. Did not find any matching desktop file return None def _build(self): self.executables = self.buildCache_grepExecutablePaths() self.desktopEntries = self.buildCache_DesktopEntries() findicons.updateCache() leftovers = set(self.executables) matches = [] #list of dicts for exe, fullPath in self.executables: self.progressHook(f"{fullPath}") entry = self._exeToDesktopEntry(exe) if entry and type(entry) is dict: #Found match! entry["agordejoFullPath"] = fullPath #We don't want .desktop syntax like "qmidiarp %F" entry["agordejoExec"] = exe if entry["icon"]: foundIcon = findicons.findIconPath(entry["icon"]) else: foundIcon = findicons.findIconPath(entry["agordejoExec"]) if foundIcon: entry["agordejoIconPath"] = str(foundIcon[0]) #pick best resolution else: entry["agordejoIconPath"] = None matches.append(entry) try: leftovers.remove((exe,fullPath)) except KeyError: pass #Double entries like zyn-jack zyn-alsa etc. elif entry and not type(entry) is dict: #There is a strange bug I can't reproduce. At least catch it. logger.error(f"Wrong entry type: {type(entry)} for {entry}") for exe,fullPath in leftovers: pseudoEntry = {"name": exe.title(), "agordejoExec":exe, "agordejoFullPath":fullPath} matches.append(pseudoEntry) return matches def buildCache_unfilteredExecutables(self): def isexe(path): """executable by owner""" return path.is_file() and stat.S_IXUSR & os.stat(path)[stat.ST_MODE] == 64 result = [] executablePaths = [pathlib.Path(p) for p in os.environ["PATH"].split(os.pathsep)] for path in executablePaths: self.progressHook(f"{path}") result += [str(pathlib.Path(f).relative_to(path)) for f in path.glob("*") if isexe(f)] return sorted(list(set(result))) def _buildWhitelist(self): """For reliable, fast and easy selection this is the whitelist. It will be populated from a template-list of well-working clients and then all binaries not in the path are filtered out. This can be presented to the user without worries.""" #Assumes to be called only from self.build startexecutables = set(self.whiteList + self.userWhitelist) for prog in self.programs: prog["whitelist"] = prog["agordejoExec"] in startexecutables """ matches = [] for exe in startexecutables: entry = self._exeToDesktopEntry(exe) if entry: #Found match! #We don't want .desktop syntax like "qmidiarp %F" entry["agordejoExec"] = exe matches.append(entry) return matches """ programDatabase = SupportedProgramsDatabase() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/jackclient.py0000644000175000017500000001015600000000000015733 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library import atexit import sys #Third Party import engine.jacklib as jacklib from ctypes import pointer #Our Modules from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. from engine.jacklib.helpers import get_jack_status_error_string class AgordejoJackClient(object): """Singleton. Created in api.startEngine. If client cannot be started the program will exit from here. Most error sources are already ruled out in start.py.""" def __init__(self): status = jacklib.jack_status_t() self._jacklibClient = jacklib.client_open(METADATA["name"], jacklib.JackNoStartServer, status) err = get_jack_status_error_string(status) if not status.value == 0: #Decide if a name collision is important or not. Agordejo cannot be started two times anyway. So this would be another client called Agordejo? if status.value & jacklib.JackNameNotUnique: logger.warning(f"Another JACK client called {METADATA['name']} exist. We will rename ourselve, but this is most likely a real problem. Agordejo is not supposed to be started twice. Please investigate!") else: logger.error("JackClient error: " + status.value) sys.exit(0) #atexit will trigger atexit.register(lambda c=self._jacklibClient: jacklib.client_close(c)) #Callbacks. They are mirrored by the api Callbacks without the callback_ prefix so a GUI can directly access them. #However, they are mutable lists. And we define all actual callback-sender here. the api calls them via our Object/Instance self.callback_setPlaybackSeconds = [] def _setPlaybackSeconds(self): """Added to the fast event loop. Therefore called VERY often. Yes, this is not a super accurate function because while we iterate transport already progresses""" pos = jacklib.jack_position_t() #pos._fields_ state = jacklib.transport_query(self._jacklibClient, pointer(pos)) #this actually sets the pos and info. We need this, even if we don't use "state" here. #if not pos.frame_rate: # return currenTimeInSeconds = round(pos.frame / pos.frame_rate, 8) for func in self.callback_setPlaybackSeconds: func(currenTimeInSeconds, not state == jacklib.JackTransportStopped) #current time in seconds and if playback is running #Public API. Called via api.jackClient.rewind() def _seek(self, frame:int): jacklib.transport_locate(self._jacklibClient, frame) def seek(self, seconds): pos = jacklib.jack_position_t() state = jacklib.transport_query(self._jacklibClient, pointer(pos)) #this actually sets the pos and info. We need this, even if we don't use "state" here. self._seek(int(seconds * pos.frame_rate)) def rewind(self): self._seek(0) def playPause(self): pos = jacklib.jack_position_t() #pos._fields_ state = jacklib.transport_query(self._jacklibClient, pointer(pos)) if state == jacklib.JackTransportStopped: jacklib.transport_start(self._jacklibClient) elif state == jacklib.JackTransportStarting: pass else: jacklib.transport_stop(self._jacklibClient) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/jacklib/COPYING0000644000175000017500000003556400000000000015716 0ustar00nilsnils GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/jacklib/README.md0000644000175000017500000000356000000000000016131 0ustar00nilsnils# pyjacklib Python bindings for `libjack` using [ctypes], which allow you to write JACK client applications in Python. The library provides a low-level, almost unaltered mapping of the `libjack` [C API], plus a few additional convenience functions. The source code repository contains a few [example scripts] to show its usage. **Note:** **pyjacklib** *as a stand-alone project is in an early beta-stage and the API may still change slightly before a 1.0 release. You have been warned!* ## Dependencies To use the library, your system needs to have the following installed at run-time: * The [JACK] library * A Python 3 implementation, which supports `ctypes` To build and install the library you need: * [setuptools] * (optional) [pip] ## Building and Installing You can download and install **pyjacklib** directly from PyPI using `pip`: ```con pip install pyjacklib ``` Or you can download the latest source archive or clone the git repository and run the following from inside the unpacked source directory resp. the root of your checkout: ```con python setup.py install ``` You can also build a wheel with: ```con pip wheel . ``` ... and install it using `pip install`. ## License **pyjacklib** is licensed under the GNU Public License Version v2, or any later version. Please see the file [COPYING] for more information. ## Authors Created by *Filipe Coelho (falkTX)* as part of [Cadence]. Turned into a stand-alone project and enhanced by *Christopher Arndt*. [C API]: https://jackaudio.org/api/ [Cadence]: https://github.com/falkTX/Cadence.git [COPYING]: https://github.com/jackaudio/pyjacklib/blob/master/COPYING [ctypes]: https://docs.python.org/3/library/ctypes.html [example scripts]: https://github.com/jackaudio/pyjacklib/tree/master/examples [JACK]: https://jackaudio.org/ [pip]: https://pypi.org/project/pip/ [setuptools]: https://pypi.org/project/setuptools/ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/jacklib/__init__.py0000644000175000017500000000012200000000000016752 0ustar00nilsnilsfrom .version import __version__ # noqa from .api import * # noqa ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/jacklib/api.py0000644000175000017500000016220300000000000015775 0ustar00nilsnils#!/usr/bin/env python3 # -*- coding: utf-8 -*- # JACK ctypes definitions for usage in Python applications # Copyright (C) 2010-2020 Filipe Coelho # 2016-2021 Christopher Arndt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # 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. # # For a full copy of the GNU General Public License see the COPYING file # ------------------------------------------------------------------------------------------------- # Imports (Global) from __future__ import absolute_import, print_function, unicode_literals from ctypes import (ARRAY, CFUNCTYPE, POINTER, Structure, byref, c_char_p, c_double, c_float, c_int, c_int32, c_size_t, c_uint8, c_uint32, c_uint64, c_ulong, c_void_p, cdll, pointer) from collections import namedtuple from sys import platform # ------------------------------------------------------------------------------------------------- # Load JACK shared library try: if platform == "darwin": jlib = cdll.LoadLibrary("libjack.dylib") elif platform in ("win32", "win64", "cygwin"): jlib = cdll.LoadLibrary("libjack.dll") else: jlib = cdll.LoadLibrary("libjack.so.0") except OSError: jlib = None raise ImportError("JACK is not available in this system") # ------------------------------------------------------------------------------------------------- # JACK2 test try: if jlib.jack_get_version_string: JACK2 = True else: JACK2 = False except AttributeError: JACK2 = False # ------------------------------------------------------------------------------------------------- # Pre-Types c_enum = c_int c_uchar = c_uint8 class _jack_port(Structure): _fields_ = [] class _jack_client(Structure): _fields_ = [] # ------------------------------------------------------------------------------------------------- # Defines ENCODING = "utf-8" JACK_MAX_FRAMES = 4294967295 JACK_LOAD_INIT_LIMIT = 1024 JACK_DEFAULT_AUDIO_TYPE = "32 bit float mono audio" JACK_DEFAULT_MIDI_TYPE = "8 bit raw midi" JACK_UUID_SIZE = 36 JACK_UUID_STRING_SIZE = JACK_UUID_SIZE + 1 JACK_UUID_EMPTY_INITIALIZER = 0 # Meta data _JACK_METADATA_PREFIX = "http://jackaudio.org/metadata/" JACK_METADATA_CONNECTED = _JACK_METADATA_PREFIX + "connected" JACK_METADATA_EVENT_TYPES = _JACK_METADATA_PREFIX + "event-types" JACK_METADATA_HARDWARE = _JACK_METADATA_PREFIX + "hardware" JACK_METADATA_ICON_LARGE = _JACK_METADATA_PREFIX + "icon-large" JACK_METADATA_ICON_NAME = _JACK_METADATA_PREFIX + "icon-name" JACK_METADATA_ICON_SMALL = _JACK_METADATA_PREFIX + "icon-small" JACK_METADATA_ORDER = _JACK_METADATA_PREFIX + "order" JACK_METADATA_PORT_GROUP = _JACK_METADATA_PREFIX + "port-group" JACK_METADATA_PRETTY_NAME = _JACK_METADATA_PREFIX + "pretty-name" JACK_METADATA_SIGNAL_TYPE = _JACK_METADATA_PREFIX + "signal-type" # ------------------------------------------------------------------------------------------------- # Helper functions def _e(s, encoding=ENCODING): if encoding: return s.encode(encoding) return s def _d(s, encoding=ENCODING): if encoding: return s.decode(encoding) return s # ------------------------------------------------------------------------------------------------- # Types jack_client_t = _jack_client jack_default_audio_sample_t = c_float jack_midi_data_t = c_uchar jack_nframes_t = c_uint32 jack_port_id_t = c_uint32 jack_port_t = _jack_port jack_time_t = c_uint64 jack_unique_t = c_uint64 jack_uuid_t = c_uint64 jack_options_t = c_enum # JackOptions jack_status_t = c_enum # JackStatus jack_transport_state_t = c_enum # JackTransportState jack_position_bits_t = c_enum # JackPositionBits jack_session_event_type_t = c_enum # JackSessionEventType jack_session_flags_t = c_enum # JackSessionFlags jack_custom_change_t = c_enum # JackCustomChange jack_latency_callback_mode_t = c_enum # JackLatencyCallbackMode jack_property_change_t = c_enum # JacKPropertyChange # JACK2 only: jack_port_type_id_t = c_uint32 # enum JackOptions JackNullOption = 0x00 JackNoStartServer = 0x01 JackUseExactName = 0x02 JackServerName = 0x04 JackLoadName = 0x08 JackLoadInit = 0x10 JackSessionID = 0x20 JackOpenOptions = JackSessionID | JackServerName | JackNoStartServer | JackUseExactName JackLoadOptions = JackLoadInit | JackLoadName | JackUseExactName # enum JackStatus JackFailure = 0x01 JackInvalidOption = 0x02 JackNameNotUnique = 0x04 JackServerStarted = 0x08 JackServerFailed = 0x10 JackServerError = 0x20 JackNoSuchClient = 0x40 JackLoadFailure = 0x80 JackInitFailure = 0x100 JackShmFailure = 0x200 JackVersionError = 0x400 JackBackendError = 0x800 JackClientZombie = 0x1000 # enum JackLatencyCallbackMode JackCaptureLatency = 0 JackPlaybackLatency = 1 # enum JackPortFlags JackPortIsInput = 0x1 JackPortIsOutput = 0x2 JackPortIsPhysical = 0x4 JackPortCanMonitor = 0x8 JackPortIsTerminal = 0x10 JackPortIsControlVoltage = 0x100 # enum JackTransportState JackTransportStopped = 0 JackTransportRolling = 1 JackTransportLooping = 2 JackTransportStarting = 3 # JACK2 only: JackTransportNetStarting = 4 # enum JackPositionBits JackPositionBBT = 0x10 JackPositionTimecode = 0x20 JackBBTFrameOffset = 0x40 JackAudioVideoRatio = 0x80 JackVideoFrameOffset = 0x100 JACK_POSITION_MASK = (JackPositionBBT | JackPositionTimecode | JackBBTFrameOffset | JackAudioVideoRatio | JackVideoFrameOffset) # enum JackSessionEventType JackSessionSave = 1 JackSessionSaveAndQuit = 2 JackSessionSaveTemplate = 3 # enum JackSessionFlags JackSessionSaveError = 0x01 JackSessionNeedTerminal = 0x02 # enum JackCustomChange JackCustomRemoved = 0 JackCustomAdded = 1 JackCustomReplaced = 2 # enum JackPropertyChange PropertyCreated = 0 PropertyChanged = 1 PropertyDeleted = 2 # ------------------------------------------------------------------------------------------------- # Structs class jack_midi_event_t(Structure): _fields_ = [ ("time", jack_nframes_t), ("size", c_size_t), ("buffer", POINTER(jack_midi_data_t)) ] class jack_latency_range_t(Structure): _fields_ = [ ("min", jack_nframes_t), ("max", jack_nframes_t) ] class jack_position_t(Structure): _fields_ = [ ("unique_1", jack_unique_t), ("usecs", jack_time_t), ("frame_rate", jack_nframes_t), ("frame", jack_nframes_t), ("valid", jack_position_bits_t), ("bar", c_int32), ("beat", c_int32), ("tick", c_int32), ("bar_start_tick", c_double), ("beats_per_bar", c_float), ("beat_type", c_float), ("ticks_per_beat", c_double), ("beats_per_minute", c_double), ("frame_time", c_double), ("next_time", c_double), ("bbt_offset", jack_nframes_t), ("audio_frames_per_video_frame", c_float), ("video_offset", jack_nframes_t), ("padding", ARRAY(c_int32, 7)), ("unique_2", jack_unique_t) ] class jack_session_event_t(Structure): _fields_ = [ ("type", jack_session_event_type_t), ("session_dir", c_char_p), ("client_uuid", c_char_p), ("command_line", c_char_p), ("flags", jack_session_flags_t), ("future", c_uint32) ] class jack_session_command_t(Structure): _fields_ = [ ("uuid", c_char_p), ("client_name", c_char_p), ("command", c_char_p), ("flags", jack_session_flags_t) ] class jack_property_t(Structure): _fields_ = [ ('key', c_char_p), ('data', c_char_p), ('type', c_char_p) ] class jack_description_t(Structure): _fields_ = [ ('subject', jack_uuid_t), ('property_cnt', c_uint32), ('properties', POINTER(jack_property_t)), ('property_size', c_uint32) ] # ------------------------------------------------------------------------------------------------- # Callbacks JackLatencyCallback = CFUNCTYPE(None, jack_latency_callback_mode_t, c_void_p) JackProcessCallback = CFUNCTYPE(c_int, jack_nframes_t, c_void_p) JackThreadCallback = CFUNCTYPE(c_void_p, c_void_p) JackThreadInitCallback = CFUNCTYPE(None, c_void_p) JackGraphOrderCallback = CFUNCTYPE(c_int, c_void_p) JackXRunCallback = CFUNCTYPE(c_int, c_void_p) JackBufferSizeCallback = CFUNCTYPE(c_int, jack_nframes_t, c_void_p) JackSampleRateCallback = CFUNCTYPE(c_int, jack_nframes_t, c_void_p) JackPortRegistrationCallback = CFUNCTYPE(None, jack_port_id_t, c_int, c_void_p) JackClientRegistrationCallback = CFUNCTYPE(None, c_char_p, c_int, c_void_p) # JACK2 only: JackClientRenameCallback = CFUNCTYPE(c_int, c_char_p, c_char_p, c_void_p) JackPortConnectCallback = CFUNCTYPE(None, jack_port_id_t, jack_port_id_t, c_int, c_void_p) # JACK2 only: JackPortRenameCallback = CFUNCTYPE(None, jack_port_id_t, c_char_p, c_char_p, c_void_p) JackFreewheelCallback = CFUNCTYPE(None, c_int, c_void_p) JackShutdownCallback = CFUNCTYPE(None, c_void_p) JackInfoShutdownCallback = CFUNCTYPE(None, jack_status_t, c_char_p, c_void_p) JackSyncCallback = CFUNCTYPE(c_int, jack_transport_state_t, POINTER(jack_position_t), c_void_p) JackTimebaseCallback = CFUNCTYPE(None, jack_transport_state_t, jack_nframes_t, POINTER(jack_position_t), c_int, c_void_p) JackSessionCallback = CFUNCTYPE(None, POINTER(jack_session_event_t), c_void_p) JackCustomDataAppearanceCallback = CFUNCTYPE(None, c_char_p, c_char_p, jack_custom_change_t, c_void_p) JackPropertyChangeCallback = CFUNCTYPE(None, jack_uuid_t, c_char_p, jack_property_change_t, c_void_p) JackErrorCallback = CFUNCTYPE(None, c_char_p) # ------------------------------------------------------------------------------------------------- # Functions try: jlib.jack_get_version_string.argtypes = None jlib.jack_get_version_string.restype = c_char_p except AttributeError: jlib.jack_get_version_string = None try: jlib.jack_client_open.argtypes = [c_char_p, jack_options_t, POINTER(jack_status_t), c_char_p] jlib.jack_client_open.restype = POINTER(jack_client_t) except AttributeError: jlib.jack_client_open = None try: jlib.jack_client_rename.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_client_rename.restype = c_char_p except AttributeError: jlib.jack_client_rename = None try: jlib.jack_client_close.argtypes = [POINTER(jack_client_t)] jlib.jack_client_close.restype = c_int except AttributeError: jlib.jack_client_close = None try: jlib.jack_client_name_size.argtypes = None jlib.jack_client_name_size.restype = c_int except AttributeError: jlib.jack_client_name_size = None try: jlib.jack_get_client_name.argtypes = [POINTER(jack_client_t)] jlib.jack_get_client_name.restype = c_char_p except AttributeError: jlib.jack_get_client_name = None try: jlib.jack_activate.argtypes = [POINTER(jack_client_t)] jlib.jack_activate.restype = c_int except AttributeError: jlib.jack_activate = None try: jlib.jack_deactivate.argtypes = [POINTER(jack_client_t)] jlib.jack_deactivate.restype = c_int except AttributeError: jlib.jack_deactivate = None try: jlib.jack_get_client_pid.argtypes = [c_char_p] jlib.jack_get_client_pid.restype = c_int except AttributeError: jlib.jack_get_client_pid = None try: jlib.jack_is_realtime.argtypes = [POINTER(jack_client_t)] jlib.jack_is_realtime.restype = c_int except AttributeError: jlib.jack_is_realtime = None # JACK2 only: def get_version_string(): if jlib.jack_get_version_string: return jlib.jack_get_version_string() return None def client_open(client_name, options, status, uuid=""): if jlib.jack_client_open: return jlib.jack_client_open(_e(client_name), options, status, _e(uuid) if uuid else None) return None def client_rename(client, new_name): if jlib.jack_client_rename: return jlib.jack_client_rename(client, _e(new_name)) return None def client_close(client): if jlib.jack_client_close: return jlib.jack_client_close(client) return -1 def client_name_size(): if jlib.jack_client_name_size: return jlib.jack_client_name_size() return 0 def get_client_name(client): if jlib.jack_get_client_name: return jlib.jack_get_client_name(client) return None def activate(client): if jlib.jack_activate: return jlib.jack_activate(client) return -1 def deactivate(client): if jlib.jack_deactivate: return jlib.jack_deactivate(client) return -1 # JACK2 only: def get_client_pid(name): if jlib.jack_get_client_pid: return jlib.jack_get_client_pid(_e(name)) return 0 def is_realtime(client): if jlib.jack_is_realtime: return jlib.jack_is_realtime(client) return 0 # ------------------------------------------------------------------------------------------------- # Non-Callback API _thread_callback = None try: jlib.jack_cycle_wait.argtypes = [POINTER(jack_client_t)] jlib.jack_cycle_wait.restype = jack_nframes_t except AttributeError: jlib.jack_cycle_wait = None try: jlib.jack_cycle_signal.argtypes = [POINTER(jack_client_t), c_int] jlib.jack_cycle_signal.restype = None except AttributeError: jlib.jack_cycle_signal = None try: jlib.jack_set_process_thread.argtypes = [POINTER(jack_client_t), JackThreadCallback, c_void_p] jlib.jack_set_process_thread.restype = c_int except AttributeError: jlib.jack_set_process_thread = None def cycle_wait(client): if jlib.jack_cycle_wait: return jlib.jack_cycle_wait(client) return 0 def cycle_signal(client, status): if jlib.jack_cycle_signal: jlib.jack_cycle_signal(client, status) def set_process_thread(client, thread_callback, arg): if jlib.jack_set_process_thread: global _thread_callback _thread_callback = JackThreadCallback(thread_callback) return jlib.jack_set_process_thread(client, _thread_callback, arg) return -1 # ------------------------------------------------------------------------------------------------- # Client Callbacks _thread_init_callback = _shutdown_callback = _info_shutdown_callback = None _process_callback = _freewheel_callback = _bufsize_callback = _srate_callback = None _client_registration_callback = _client_rename_callback = None _port_registration_callback = _port_connect_callback = _port_rename_callback = None _graph_callback = _xrun_callback = _latency_callback = None _property_change_callback = None try: jlib.jack_set_thread_init_callback.argtypes = [ POINTER(jack_client_t), JackThreadInitCallback, c_void_p ] jlib.jack_set_thread_init_callback.restype = c_int except AttributeError: jlib.jack_set_thread_init_callback = None try: jlib.jack_on_shutdown.argtypes = [POINTER(jack_client_t), JackShutdownCallback, c_void_p] jlib.jack_on_shutdown.restype = None except AttributeError: jlib.jack_on_shutdown = None try: jlib.jack_on_info_shutdown.argtypes = [ POINTER(jack_client_t), JackInfoShutdownCallback, c_void_p ] jlib.jack_on_info_shutdown.restype = None except AttributeError: jlib.jack_on_info_shutdown = None try: jlib.jack_set_process_callback.argtypes = [ POINTER(jack_client_t), JackProcessCallback, c_void_p ] jlib.jack_set_process_callback.restype = c_int except AttributeError: jlib.jack_set_process_callback = None try: jlib.jack_set_freewheel_callback.argtypes = [ POINTER(jack_client_t), JackFreewheelCallback, c_void_p ] jlib.jack_set_freewheel_callback.restype = c_int except AttributeError: jlib.jack_set_freewheel_callback = None try: jlib.jack_set_buffer_size_callback.argtypes = [ POINTER(jack_client_t), JackBufferSizeCallback, c_void_p ] jlib.jack_set_buffer_size_callback.restype = c_int except AttributeError: jlib.jack_set_buffer_size_callback = None try: jlib.jack_set_sample_rate_callback.argtypes = [ POINTER(jack_client_t), JackSampleRateCallback, c_void_p ] jlib.jack_set_sample_rate_callback.restype = c_int except AttributeError: jlib.jack_set_sample_rate_callback = None try: jlib.jack_set_client_registration_callback.argtypes = [ POINTER(jack_client_t), JackClientRegistrationCallback, c_void_p ] jlib.jack_set_client_registration_callback.restype = c_int except AttributeError: jlib.jack_set_client_registration_callback = None try: jlib.jack_set_client_rename_callback.argtypes = [ POINTER(jack_client_t), JackClientRenameCallback, c_void_p ] jlib.jack_set_client_rename_callback.restype = c_int except AttributeError: jlib.jack_set_client_rename_callback = None try: jlib.jack_set_port_registration_callback.argtypes = [ POINTER(jack_client_t), JackPortRegistrationCallback, c_void_p ] jlib.jack_set_port_registration_callback.restype = c_int except AttributeError: jlib.jack_set_port_registration_callback = None try: jlib.jack_set_port_connect_callback.argtypes = [ POINTER(jack_client_t), JackPortConnectCallback, c_void_p ] jlib.jack_set_port_connect_callback.restype = c_int except AttributeError: jlib.jack_set_port_connect_callback = None try: jlib.jack_set_port_rename_callback.argtypes = [ POINTER(jack_client_t), JackPortRenameCallback, c_void_p ] jlib.jack_set_port_rename_callback.restype = c_int except AttributeError: jlib.jack_set_port_rename_callback = None try: jlib.jack_set_graph_order_callback.argtypes = [ POINTER(jack_client_t), JackGraphOrderCallback, c_void_p ] jlib.jack_set_graph_order_callback.restype = c_int except AttributeError: jlib.jack_set_graph_order_callback = None try: jlib.jack_set_xrun_callback.argtypes = [POINTER(jack_client_t), JackXRunCallback, c_void_p] jlib.jack_set_xrun_callback.restype = c_int except AttributeError: jlib.jack_set_xrun_callback = None try: jlib.jack_set_latency_callback.argtypes = [ POINTER(jack_client_t), JackLatencyCallback, c_void_p ] jlib.jack_set_latency_callback.restype = c_int except AttributeError: jlib.jack_set_latency_callback = None def set_thread_init_callback(client, thread_init_callback, arg): if jlib.jack_set_thread_init_callback: global _thread_init_callback _thread_init_callback = JackThreadInitCallback(thread_init_callback) return jlib.jack_set_thread_init_callback(client, _thread_init_callback, arg) return -1 def on_shutdown(client, shutdown_callback, arg): if jlib.jack_on_shutdown: global _shutdown_callback _shutdown_callback = JackShutdownCallback(shutdown_callback) jlib.jack_on_shutdown(client, _shutdown_callback, arg) def on_info_shutdown(client, info_shutdown_callback, arg): if jlib.jack_on_info_shutdown: global _info_shutdown_callback _info_shutdown_callback = JackInfoShutdownCallback(info_shutdown_callback) jlib.jack_on_info_shutdown(client, _info_shutdown_callback, arg) def set_process_callback(client, process_callback, arg): if jlib.jack_set_process_callback: global _process_callback _process_callback = JackProcessCallback(process_callback) return jlib.jack_set_process_callback(client, _process_callback, arg) return -1 def set_freewheel_callback(client, freewheel_callback, arg): if jlib.jack_set_freewheel_callback: global _freewheel_callback _freewheel_callback = JackFreewheelCallback(freewheel_callback) return jlib.jack_set_freewheel_callback(client, _freewheel_callback, arg) return -1 def set_buffer_size_callback(client, bufsize_callback, arg): if jlib.jack_set_buffer_size_callback: global _bufsize_callback _bufsize_callback = JackBufferSizeCallback(bufsize_callback) return jlib.jack_set_buffer_size_callback(client, _bufsize_callback, arg) return -1 def set_sample_rate_callback(client, srate_callback, arg): if jlib.jack_set_sample_rate_callback: global _srate_callback _srate_callback = JackSampleRateCallback(srate_callback) return jlib.jack_set_sample_rate_callback(client, _srate_callback, arg) return -1 def set_client_registration_callback(client, client_registration_callback, arg): if jlib.jack_set_client_registration_callback: global _client_registration_callback _client_registration_callback = JackClientRegistrationCallback(client_registration_callback) return jlib.jack_set_client_registration_callback(client, _client_registration_callback, arg) return -1 # JACK2 only: def set_client_rename_callback(client, client_rename_callback, arg): if jlib.jack_set_client_rename_callback: global _client_rename_callback _client_rename_callback = JackClientRenameCallback(client_rename_callback) return jlib.jack_set_client_rename_callback(client, _client_rename_callback, arg) return -1 def set_port_registration_callback(client, port_registration_callback, arg): if jlib.jack_set_port_registration_callback: global _port_registration_callback _port_registration_callback = JackPortRegistrationCallback(port_registration_callback) return jlib.jack_set_port_registration_callback(client, _port_registration_callback, arg) return -1 def set_port_connect_callback(client, connect_callback, arg): if jlib.jack_set_port_connect_callback: global _port_connect_callback _port_connect_callback = JackPortConnectCallback(connect_callback) return jlib.jack_set_port_connect_callback(client, _port_connect_callback, arg) return -1 # JACK2 only: def set_port_rename_callback(client, rename_callback, arg): if jlib.jack_set_port_rename_callback: global _port_rename_callback _port_rename_callback = JackPortRenameCallback(rename_callback) return jlib.jack_set_port_rename_callback(client, _port_rename_callback, arg) return -1 def set_graph_order_callback(client, graph_callback, arg): if jlib.jack_set_graph_order_callback: global _graph_callback _graph_callback = JackGraphOrderCallback(graph_callback) return jlib.jack_set_graph_order_callback(client, _graph_callback, arg) return -1 def set_xrun_callback(client, xrun_callback, arg): if jlib.jack_set_xrun_callback: global _xrun_callback _xrun_callback = JackXRunCallback(xrun_callback) return jlib.jack_set_xrun_callback(client, _xrun_callback, arg) return -1 def set_latency_callback(client, latency_callback, arg): if jlib.jack_set_latency_callback: global _latency_callback _latency_callback = JackLatencyCallback(latency_callback) return jlib.jack_set_latency_callback(client, _latency_callback, arg) return -1 # ------------------------------------------------------------------------------------------------- # Server Control jlib.jack_set_freewheel.argtypes = [POINTER(jack_client_t), c_int] jlib.jack_set_freewheel.restype = c_int jlib.jack_set_buffer_size.argtypes = [POINTER(jack_client_t), jack_nframes_t] jlib.jack_set_buffer_size.restype = c_int jlib.jack_get_sample_rate.argtypes = [POINTER(jack_client_t)] jlib.jack_get_sample_rate.restype = jack_nframes_t jlib.jack_get_buffer_size.argtypes = [POINTER(jack_client_t)] jlib.jack_get_buffer_size.restype = jack_nframes_t jlib.jack_engine_takeover_timebase.argtypes = [POINTER(jack_client_t)] jlib.jack_engine_takeover_timebase.restype = c_int jlib.jack_cpu_load.argtypes = [POINTER(jack_client_t)] jlib.jack_cpu_load.restype = c_float def set_freewheel(client, onoff): return jlib.jack_set_freewheel(client, onoff) def set_buffer_size(client, nframes): return jlib.jack_set_buffer_size(client, nframes) def get_sample_rate(client): return jlib.jack_get_sample_rate(client) def get_buffer_size(client): return jlib.jack_get_buffer_size(client) def engine_takeover_timebase(client): return jlib.jack_engine_takeover_timebase(client) def cpu_load(client): return jlib.jack_cpu_load(client) # ------------------------------------------------------------------------------------------------- # Port Functions jlib.jack_port_register.argtypes = [POINTER(jack_client_t), c_char_p, c_char_p, c_ulong, c_ulong] jlib.jack_port_register.restype = POINTER(jack_port_t) jlib.jack_port_unregister.argtypes = [POINTER(jack_client_t), POINTER(jack_port_t)] jlib.jack_port_unregister.restype = c_int jlib.jack_port_get_buffer.argtypes = [POINTER(jack_port_t), jack_nframes_t] jlib.jack_port_get_buffer.restype = c_void_p jlib.jack_port_name.argtypes = [POINTER(jack_port_t)] jlib.jack_port_name.restype = c_char_p jlib.jack_port_short_name.argtypes = [POINTER(jack_port_t)] jlib.jack_port_short_name.restype = c_char_p jlib.jack_port_flags.argtypes = [POINTER(jack_port_t)] jlib.jack_port_flags.restype = c_int jlib.jack_port_type.argtypes = [POINTER(jack_port_t)] jlib.jack_port_type.restype = c_char_p if JACK2: jlib.jack_port_type_id.argtypes = [POINTER(jack_port_t)] jlib.jack_port_type_id.restype = jack_port_type_id_t jlib.jack_port_is_mine.argtypes = [POINTER(jack_client_t), POINTER(jack_port_t)] jlib.jack_port_is_mine.restype = c_int jlib.jack_port_connected.argtypes = [POINTER(jack_port_t)] jlib.jack_port_connected.restype = c_int jlib.jack_port_connected_to.argtypes = [POINTER(jack_port_t), c_char_p] jlib.jack_port_connected_to.restype = c_int jlib.jack_port_get_connections.argtypes = [POINTER(jack_port_t)] jlib.jack_port_get_connections.restype = POINTER(c_char_p) jlib.jack_port_get_all_connections.argtypes = [POINTER(jack_client_t), POINTER(jack_port_t)] jlib.jack_port_get_all_connections.restype = POINTER(c_char_p) jlib.jack_port_tie.argtypes = [POINTER(jack_port_t), POINTER(jack_port_t)] jlib.jack_port_tie.restype = c_int jlib.jack_port_untie.argtypes = [POINTER(jack_port_t)] jlib.jack_port_untie.restype = c_int jlib.jack_port_set_name.argtypes = [POINTER(jack_port_t), c_char_p] jlib.jack_port_set_name.restype = c_int jlib.jack_port_set_alias.argtypes = [POINTER(jack_port_t), c_char_p] jlib.jack_port_set_alias.restype = c_int jlib.jack_port_unset_alias.argtypes = [POINTER(jack_port_t), c_char_p] jlib.jack_port_unset_alias.restype = c_int jlib.jack_port_get_aliases.argtypes = [POINTER(jack_port_t), POINTER(ARRAY(c_char_p, 2))] jlib.jack_port_get_aliases.restype = c_int jlib.jack_port_request_monitor.argtypes = [POINTER(jack_port_t), c_int] jlib.jack_port_request_monitor.restype = c_int jlib.jack_port_request_monitor_by_name.argtypes = [POINTER(jack_client_t), c_char_p, c_int] jlib.jack_port_request_monitor_by_name.restype = c_int jlib.jack_port_ensure_monitor.argtypes = [POINTER(jack_port_t), c_int] jlib.jack_port_ensure_monitor.restype = c_int jlib.jack_port_monitoring_input.argtypes = [POINTER(jack_port_t)] jlib.jack_port_monitoring_input.restype = c_int jlib.jack_connect.argtypes = [POINTER(jack_client_t), c_char_p, c_char_p] jlib.jack_connect.restype = c_int jlib.jack_disconnect.argtypes = [POINTER(jack_client_t), c_char_p, c_char_p] jlib.jack_disconnect.restype = c_int jlib.jack_port_disconnect.argtypes = [POINTER(jack_client_t), POINTER(jack_port_t)] jlib.jack_port_disconnect.restype = c_int jlib.jack_port_name_size.argtypes = None jlib.jack_port_name_size.restype = c_int jlib.jack_port_type_size.argtypes = None jlib.jack_port_type_size.restype = c_int try: jlib.jack_port_uuid.argtypes = [POINTER(jack_port_t)] jlib.jack_port_uuid.restype = jack_uuid_t except AttributeError: jlib.jack_port_uuid = None try: jlib.jack_port_type_get_buffer_size.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_port_type_get_buffer_size.restype = c_size_t except AttributeError: jlib.jack_port_type_get_buffer_size = None def port_register(client, port_name, port_type, flags, buffer_size): return jlib.jack_port_register(client, _e(port_name), _e(port_type), flags, buffer_size) def port_unregister(client, port): return jlib.jack_port_unregister(client, port) def port_get_buffer(port, nframes): return jlib.jack_port_get_buffer(port, nframes) def port_name(port): return _d(jlib.jack_port_name(port)) def port_short_name(port): return _d(jlib.jack_port_short_name(port)) def port_flags(port): return jlib.jack_port_flags(port) def port_type(port): return _d(jlib.jack_port_type(port)) # JACK2 only: def port_type_id(port): return jlib.jack_port_type_id(port) def port_is_mine(client, port): return jlib.jack_port_is_mine(client, port) def port_connected(port): return jlib.jack_port_connected(port) def port_connected_to(port, port_name): return jlib.jack_port_connected_to(port, _e(port_name)) def port_get_connections(port): ports = jlib.jack_port_get_connections(port) if not ports: return for port_name in ports: if port_name is None: break yield _d(port_name) def port_get_all_connections(client, port): ports = jlib.jack_port_get_all_connections(client, port) if not ports: return for port_name in ports: if port_name is None: break yield _d(port_name) def port_tie(src, dst): return jlib.jack_port_tie(src, dst) def port_untie(port): return jlib.jack_port_untie(port) def port_set_name(port, port_name): return jlib.jack_port_set_name(port, _e(port_name)) def port_set_alias(port, alias): return jlib.jack_port_set_alias(port, _e(alias)) def port_unset_alias(port, alias): return jlib.jack_port_unset_alias(port, _e(alias)) def port_get_aliases(port): # NOTE - this function has no 2nd argument in jacklib # Instead, aliases will be passed in return value, in form of (int ret, str alias1, str alias2) name_size = port_name_size() alias_type = c_char_p * 2 aliases = alias_type(b" " * name_size, b" " * name_size) ret = jlib.jack_port_get_aliases(port, pointer(aliases)) return ret, _d(aliases[0]), _d(aliases[1]) def port_request_monitor(port, onoff): return jlib.jack_port_request_monitor(port, onoff) def port_request_monitor_by_name(client, port_name, onoff): return jlib.jack_port_request_monitor_by_name(client, _e(port_name), onoff) def port_ensure_monitor(port, onoff): return jlib.jack_port_ensure_monitor(port, onoff) def port_monitoring_input(port): return jlib.jack_port_monitoring_input(port) def connect(client, source_port, destination_port): return jlib.jack_connect(client, _e(source_port), _e(destination_port)) def disconnect(client, source_port, destination_port): return jlib.jack_disconnect(client, _e(source_port), _e(destination_port)) def port_disconnect(client, port): return jlib.jack_port_disconnect(client, port) def port_name_size(): return jlib.jack_port_name_size() def port_type_size(): return jlib.jack_port_type_size() def port_type_get_buffer_size(client, port_type): if jlib.jack_port_type_get_buffer_size: return jlib.jack_port_type_get_buffer_size(client, _e(port_type)) return 0 def port_uuid(port): if jlib.jack_port_uuid: return jlib.jack_port_uuid(port) return -1 # ------------------------------------------------------------------------------------------------- # Latency Functions jlib.jack_port_set_latency.argtypes = [POINTER(jack_port_t), jack_nframes_t] jlib.jack_port_set_latency.restype = None try: jlib.jack_port_get_latency_range.argtypes = [ POINTER(jack_port_t), jack_latency_callback_mode_t, POINTER(jack_latency_range_t) ] jlib.jack_port_get_latency_range.restype = None except AttributeError: jlib.jack_port_get_latency_range = None try: jlib.jack_port_set_latency_range.argtypes = [ POINTER(jack_port_t), jack_latency_callback_mode_t, POINTER(jack_latency_range_t) ] jlib.jack_port_set_latency_range.restype = None except AttributeError: jlib.jack_port_set_latency_range = None jlib.jack_recompute_total_latencies.argtypes = [POINTER(jack_client_t)] jlib.jack_recompute_total_latencies.restype = c_int jlib.jack_port_get_latency.argtypes = [POINTER(jack_port_t)] jlib.jack_port_get_latency.restype = jack_nframes_t jlib.jack_port_get_total_latency.argtypes = [POINTER(jack_client_t), POINTER(jack_port_t)] jlib.jack_port_get_total_latency.restype = jack_nframes_t jlib.jack_recompute_total_latency.argtypes = [POINTER(jack_client_t), POINTER(jack_port_t)] jlib.jack_recompute_total_latency.restype = c_int def port_set_latency(port, nframes): jlib.jack_port_set_latency(port, nframes) def port_get_latency_range(port, mode, range_): if jlib.jack_port_get_latency_range: jlib.jack_port_get_latency_range(port, mode, range_) def port_set_latency_range(port, mode, range_): if jlib.jack_port_set_latency_range: jlib.jack_port_set_latency_range(port, mode, range_) def recompute_total_latencies(): return jlib.jack_recompute_total_latencies() def port_get_latency(port): return jlib.jack_port_get_latency(port) def port_get_total_latency(client, port): return jlib.jack_port_get_total_latency(client, port) def recompute_total_latency(client, port): return jlib.jack_recompute_total_latency(client, port) # ------------------------------------------------------------------------------------------------- # Port Searching jlib.jack_get_ports.argtypes = [POINTER(jack_client_t), c_char_p, c_char_p, c_ulong] jlib.jack_get_ports.restype = POINTER(c_char_p) jlib.jack_port_by_name.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_port_by_name.restype = POINTER(jack_port_t) jlib.jack_port_by_id.argtypes = [POINTER(jack_client_t), jack_port_id_t] jlib.jack_port_by_id.restype = POINTER(jack_port_t) def get_ports(client, port_name_pattern=None, type_name_pattern=None, flags=0): return jlib.jack_get_ports(client, _e(port_name_pattern or ''), _e(type_name_pattern or ''), flags) def port_by_name(client, port_name): return jlib.jack_port_by_name(client, _e(port_name)) def port_by_id(client, port_id): return jlib.jack_port_by_id(client, port_id) # ------------------------------------------------------------------------------------------------- # Time Functions jlib.jack_frames_since_cycle_start.argtypes = [POINTER(jack_client_t)] jlib.jack_frames_since_cycle_start.restype = jack_nframes_t jlib.jack_frame_time.argtypes = [POINTER(jack_client_t)] jlib.jack_frame_time.restype = jack_nframes_t jlib.jack_last_frame_time.argtypes = [POINTER(jack_client_t)] jlib.jack_last_frame_time.restype = jack_nframes_t try: # JACK_OPTIONAL_WEAK_EXPORT jlib.jack_get_cycle_times.argtypes = [ POINTER(jack_client_t), POINTER(jack_nframes_t), POINTER(jack_time_t), POINTER(jack_time_t), POINTER(c_float) ] jlib.jack_get_cycle_times.restype = c_int except AttributeError: jlib.jack_get_cycle_times = None jlib.jack_frames_to_time.argtypes = [POINTER(jack_client_t), jack_nframes_t] jlib.jack_frames_to_time.restype = jack_time_t jlib.jack_time_to_frames.argtypes = [POINTER(jack_client_t), jack_time_t] jlib.jack_time_to_frames.restype = jack_nframes_t jlib.jack_get_time.argtypes = None jlib.jack_get_time.restype = jack_time_t def frames_since_cycle_start(client): return jlib.jack_frames_since_cycle_start(client) def frame_time(client): return jlib.jack_frame_time(client) def last_frame_time(client): return jlib.jack_last_frame_time(client) def get_cycle_times(client, current_frames, current_usecs, next_usecs, period_usecs): # JACK_OPTIONAL_WEAK_EXPORT if jlib.jack_frames_to_time: return jlib.jack_get_cycle_times(client, current_frames, current_usecs, next_usecs, period_usecs) return -1 def frames_to_time(client, nframes): return jlib.jack_frames_to_time(client, nframes) def time_to_frames(client, time): return jlib.jack_time_to_frames(client, time) def get_time(): return jlib.jack_get_time() # ------------------------------------------------------------------------------------------------- # Error Output # TODO # ------------------------------------------------------------------------------------------------- # Misc _error_callback = None jlib.jack_free.argtypes = [c_void_p] jlib.jack_free.restype = None try: jlib.jack_set_error_function.argtypes = [JackErrorCallback] jlib.jack_set_error_function.restype = None except AttributeError: jlib.jack_set_error_function = None def set_error_function(error_callback): global _error_callback if jlib.jack_set_error_function: _error_callback = JackErrorCallback(error_callback) jlib.jack_set_error_function(_error_callback) def free(ptr): return jlib.jack_free(ptr) # ------------------------------------------------------------------------------------------------- # Transport _sync_callback = _timebase_callback = None jlib.jack_release_timebase.argtypes = [POINTER(jack_client_t)] jlib.jack_release_timebase.restype = c_int jlib.jack_set_sync_callback.argtypes = [POINTER(jack_client_t), JackSyncCallback, c_void_p] jlib.jack_set_sync_callback.restype = c_int jlib.jack_set_sync_timeout.argtypes = [POINTER(jack_client_t), jack_time_t] jlib.jack_set_sync_timeout.restype = c_int jlib.jack_set_timebase_callback.argtypes = [ POINTER(jack_client_t), c_int, JackTimebaseCallback, c_void_p ] jlib.jack_set_timebase_callback.restype = c_int jlib.jack_transport_locate.argtypes = [POINTER(jack_client_t), jack_nframes_t] jlib.jack_transport_locate.restype = c_int jlib.jack_transport_query.argtypes = [POINTER(jack_client_t), POINTER(jack_position_t)] jlib.jack_transport_query.restype = jack_transport_state_t jlib.jack_get_current_transport_frame.argtypes = [POINTER(jack_client_t)] jlib.jack_get_current_transport_frame.restype = jack_nframes_t jlib.jack_transport_reposition.argtypes = [POINTER(jack_client_t), POINTER(jack_position_t)] jlib.jack_transport_reposition.restype = c_int jlib.jack_transport_start.argtypes = [POINTER(jack_client_t)] jlib.jack_transport_start.restype = None jlib.jack_transport_stop.argtypes = [POINTER(jack_client_t)] jlib.jack_transport_stop.restype = None def release_timebase(client): return jlib.jack_release_timebase(client) def set_sync_callback(client, sync_callback, arg): global _sync_callback _sync_callback = JackSyncCallback(sync_callback) return jlib.jack_set_sync_callback(client, _sync_callback, arg) def set_sync_timeout(client, timeout): return jlib.jack_set_sync_timeout(client, timeout) def set_timebase_callback(client, conditional, timebase_callback, arg): global _timebase_callback _timebase_callback = JackTimebaseCallback(timebase_callback) return jlib.jack_set_timebase_callback(client, conditional, _timebase_callback, arg) def transport_locate(client, frame): return jlib.jack_transport_locate(client, frame) def transport_query(client, pos): return jlib.jack_transport_query(client, pos) def get_current_transport_frame(client): return jlib.jack_get_current_transport_frame(client) def transport_reposition(client, pos): return jlib.jack_transport_reposition(client, pos) def transport_start(client): return jlib.jack_transport_start(client) def transport_stop(client): return jlib.jack_transport_stop(client) # ------------------------------------------------------------------------------------------------- # MIDI jlib.jack_midi_get_event_count.argtypes = [c_void_p] jlib.jack_midi_get_event_count.restype = jack_nframes_t jlib.jack_midi_event_get.argtypes = [POINTER(jack_midi_event_t), c_void_p, c_uint32] jlib.jack_midi_event_get.restype = c_int jlib.jack_midi_clear_buffer.argtypes = [c_void_p] jlib.jack_midi_clear_buffer.restype = None jlib.jack_midi_max_event_size.argtypes = [c_void_p] jlib.jack_midi_max_event_size.restype = c_size_t jlib.jack_midi_event_reserve.argtypes = [c_void_p, jack_nframes_t, c_size_t] jlib.jack_midi_event_reserve.restype = POINTER(jack_midi_data_t) jlib.jack_midi_event_write.argtypes = [ c_void_p, jack_nframes_t, POINTER(jack_midi_data_t), c_size_t ] jlib.jack_midi_event_write.restype = c_int jlib.jack_midi_get_lost_event_count.argtypes = [c_void_p] jlib.jack_midi_get_lost_event_count.restype = c_uint32 def midi_get_event_count(port_buffer): return jlib.jack_midi_get_event_count(port_buffer) def midi_event_get(event, port_buffer, event_index): return jlib.jack_midi_event_get(event, port_buffer, event_index) def midi_clear_buffer(port_buffer): return jlib.jack_midi_clear_buffer(port_buffer) def midi_max_event_size(port_buffer): return jlib.jack_midi_max_event_size(port_buffer) def midi_event_reserve(port_buffer, time, data_size): return jlib.jack_midi_event_reserve(port_buffer, time, data_size) def midi_event_write(port_buffer, time, data, data_size): return jlib.jack_midi_event_write(port_buffer, time, data, data_size) def midi_get_lost_event_count(port_buffer): return jlib.jack_midi_get_lost_event_count(port_buffer) # ------------------------------------------------------------------------------------------------- # Session _session_callback = None try: jlib.jack_set_session_callback.argtypes = [ POINTER(jack_client_t), JackSessionCallback, c_void_p ] jlib.jack_set_session_callback.restype = c_int except AttributeError: jlib.jack_set_session_callback = None try: jlib.jack_session_reply.argtypes = [POINTER(jack_client_t), POINTER(jack_session_event_t)] jlib.jack_session_reply.restype = c_int except AttributeError: jlib.jack_session_reply = None try: jlib.jack_session_event_free.argtypes = [POINTER(jack_session_event_t)] jlib.jack_session_event_free.restype = None except AttributeError: jlib.jack_session_event_free = None try: jlib.jack_client_get_uuid.argtypes = [POINTER(jack_client_t)] jlib.jack_client_get_uuid.restype = c_char_p except AttributeError: jlib.jack_client_get_uuid = None try: jlib.jack_session_notify.argtypes = [ POINTER(jack_client_t), c_char_p, jack_session_event_type_t, c_char_p ] jlib.jack_session_notify.restype = POINTER(jack_session_command_t) except AttributeError: jlib.jack_session_notify = None try: jlib.jack_session_commands_free.argtypes = [POINTER(jack_session_command_t)] jlib.jack_session_commands_free.restype = None except AttributeError: jlib.jack_session_commands_free = None try: jlib.jack_get_uuid_for_client_name.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_get_uuid_for_client_name.restype = c_char_p except AttributeError: jlib.jack_get_uuid_for_client_name = None try: jlib.jack_get_client_name_by_uuid.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_get_client_name_by_uuid.restype = c_char_p except AttributeError: jlib.jack_get_client_name_by_uuid = None try: jlib.jack_reserve_client_name.argtypes = [POINTER(jack_client_t), c_char_p, c_char_p] jlib.jack_reserve_client_name.restype = c_int except AttributeError: jlib.jack_reserve_client_name = None try: jlib.jack_client_has_session_callback.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_client_has_session_callback.restype = c_int except AttributeError: jlib.jack_client_has_session_callback = None try: jlib.jack_uuid_parse.argtypes = [c_char_p, POINTER(jack_uuid_t)] jlib.jack_uuid_parse.restype = c_int except AttributeError: jlib.jack_uuid_parse = None try: jlib.jack_uuid_unparse.argtypes = [jack_uuid_t, c_char_p] jlib.jack_uuid_unparse.restype = None except AttributeError: jlib.jack_uuid_unparse = None def set_session_callback(client, session_callback, arg): if jlib.jack_set_session_callback: global _session_callback _session_callback = JackSessionCallback(session_callback) return jlib.jack_set_session_callback(client, _session_callback, arg) return -1 def session_reply(client, event): if jlib.jack_session_reply: return jlib.jack_session_reply(client, event) return -1 def session_event_free(event): if jlib.jack_session_event_free: jlib.jack_session_event_free(event) def client_get_uuid(client): if jlib.jack_client_get_uuid: return _d(jlib.jack_client_get_uuid(client)) return None def session_notify(client, target, type_, path): if jlib.jack_session_notify: return jlib.jack_session_notify(client, _e(target), type_, _e(path)) return jack_session_command_t() def session_commands_free(cmds): if jlib.jack_session_commands_free: jlib.jack_session_commands_free(cmds) def get_uuid_for_client_name(client, client_name): if jlib.jack_get_uuid_for_client_name: return jlib.jack_get_uuid_for_client_name(client, _e(client_name)) return None def get_client_name_by_uuid(client, client_uuid): if jlib.jack_get_client_name_by_uuid: return jlib.jack_get_client_name_by_uuid(client, _e(client_uuid)) return None def reserve_client_name(client, name, uuid): if jlib.jack_reserve_client_name: return jlib.jack_reserve_client_name(client, _e(name), _e(uuid)) return -1 def client_has_session_callback(client, client_name): if jlib.jack_client_has_session_callback: return jlib.jack_client_has_session_callback(client, _e(client_name)) return -1 def uuid_parse(uuid_cstr): if jlib.jack_uuid_parse and uuid_cstr is not None: uuid = jack_uuid_t() res = jlib.jack_uuid_parse(uuid_cstr, byref(uuid)) return uuid if res != -1 else None return -1 def uuid_unparse(uuid, encoding=ENCODING): if jlib.jack_uuid_unparse: uuid_str = c_char_p(b" " * JACK_UUID_STRING_SIZE) jlib.jack_uuid_unparse(uuid, uuid_str) return _d(uuid_str.value, encoding) return "" # ------------------------------------------------------------------------------------------------- # Custom _custom_appearance_callback = None try: jlib.jack_custom_publish_data.argtypes = [POINTER(jack_client_t), c_char_p, c_void_p, c_size_t] jlib.jack_custom_publish_data.restype = c_int except AttributeError: jlib.jack_custom_publish_data = None try: jlib.jack_custom_get_data.argtypes = [ POINTER(jack_client_t), c_char_p, c_char_p, POINTER(c_void_p), POINTER(c_size_t) ] jlib.jack_custom_get_data.restype = c_int except AttributeError: jlib.jack_custom_get_data = None try: jlib.jack_custom_unpublish_data.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_custom_unpublish_data.restype = c_int except AttributeError: jlib.jack_custom_unpublish_data = None try: jlib.jack_custom_get_keys.argtypes = [POINTER(jack_client_t), c_char_p] jlib.jack_custom_get_keys.restype = POINTER(c_char_p) except AttributeError: jlib.jack_custom_get_keys = None try: jlib.jack_custom_set_data_appearance_callback.argtypes = [ POINTER(jack_client_t), JackCustomDataAppearanceCallback, c_void_p ] jlib.jack_custom_set_data_appearance_callback.restype = c_int except AttributeError: jlib.jack_custom_set_data_appearance_callback = None def custom_publish_data(client, key, data, size): if jlib.jack_custom_publish_data: return jlib.jack_custom_publish_data(client, _e(key), data, size) return -1 def custom_get_data(client, client_name, key): # NOTE - this function has no extra arguments in jacklib # Instead, data and size will be passed in return value # in form of (int ret, void* data, size_t size) if jlib.jack_custom_get_data: data = c_void_p(0) size = c_size_t(0) ret = jlib.jack_custom_get_data(client, _e(client_name), _e(key), pointer(data), pointer(size)) return ret, data, size return -1, None, 0 def custom_unpublish_data(client, key): if jlib.jack_custom_unpublish_data: return jlib.jack_custom_unpublish_data(client, _e(key)) return -1 def custom_get_keys(client, client_name): if jlib.jack_custom_get_keys: return jlib.jack_custom_get_keys(client, _e(client_name)) return None def custom_set_data_appearance_callback(client, custom_callback, arg): if jlib.jack_custom_set_data_appearance_callback: global _custom_appearance_callback _custom_appearance_callback = JackCustomDataAppearanceCallback(custom_callback) return jlib.jack_custom_set_data_appearance_callback(client, _custom_appearance_callback, arg) return -1 # ------------------------------------------------------------------------------------------------- # Meta data Property = namedtuple('Property', ('key', 'value', 'type')) try: jlib.jack_free_description.argtypes = [POINTER(jack_description_t), c_int] jlib.jack_free_description.restype = None jlib.jack_get_all_properties.argtypes = [POINTER(POINTER(jack_description_t))] jlib.jack_get_all_properties.restype = c_int jlib.jack_get_properties.argtypes = [jack_uuid_t, POINTER(jack_description_t)] jlib.jack_get_properties.restype = c_int jlib.jack_get_property.argtypes = [jack_uuid_t, c_char_p, POINTER(c_char_p), POINTER(c_char_p)] jlib.jack_get_property.restype = c_int jlib.jack_remove_all_properties.argtypes = [POINTER(jack_client_t)] jlib.jack_remove_all_properties.restype = c_int jlib.jack_remove_properties.argtypess = [POINTER(jack_client_t), POINTER(jack_uuid_t)] jlib.jack_remove_properties.restype = c_int jlib.jack_remove_property.argtypes = [POINTER(jack_client_t), POINTER(jack_uuid_t), c_char_p] jlib.jack_remove_property.restype = c_int jlib.jack_set_property.argtypes = [ POINTER(jack_client_t), jack_uuid_t, c_char_p, c_char_p, c_char_p ] jlib.jack_set_property.restype = c_int jlib.jack_set_property_change_callback.argtypes = [ POINTER(jack_client_t), JackPropertyChangeCallback, c_void_p ] jlib.jack_set_property_change_callback.restype = c_int except AttributeError: jlib.jack_free_description = None jlib.jack_get_properties = None jlib.jack_get_property = None jlib.jack_remove_all_properties = None jlib.jack_remove_properties = None jlib.jack_remove_property = None jlib.jack_set_property = None jlib.jack_set_property_change_callback = None def free_description(description, free_description_itself=0): jlib.jack_free_description(description, free_description_itself) def _decode_property(prop, encoding=ENCODING): key, value, type_ = prop.key, prop.data, prop.type decode_value = True try: key = _d(key, encoding) except UnicodeDecodeError: pass if type_: try: type_ = _d(type_, encoding) except UnicodeDecodeError: pass else: decode_value = type_.startswith('text/') if decode_value: try: value = _d(value, encoding) except UnicodeDecodeError: pass return Property(key, value, type_) def get_all_properties(encoding=ENCODING): descriptions = POINTER(jack_description_t)() ret = jlib.jack_get_all_properties(byref(descriptions)) results = {} if ret != -1: for p_idx in range(ret): description = descriptions[p_idx] if description.property_cnt: results[description.subject] = [ _decode_property(description.properties[p_idx], encoding) for p_idx in range(description.property_cnt) ] jlib.jack_free_description(description, 0) free(descriptions) return results def get_properties(subject, encoding=ENCODING): description = jack_description_t() ret = jlib.jack_get_properties(subject, byref(description)) results = [] if ret != -1: for p_idx in range(description.property_cnt): results.append(_decode_property(description.properties[p_idx], encoding)) jlib.jack_free_description(byref(description), 0) return results def get_client_properties(client, client_uuid, encoding=ENCODING): if isinstance(client_uuid, str): client_uuid = get_uuid_for_client_name(client, client_uuid) return get_properties(uuid_parse(client_uuid), encoding) def get_port_properties(client, port, encoding=ENCODING): if not isinstance(port, POINTER(jack_port_t)): port = port_by_name(client, port) return get_properties(port_uuid(port), encoding) def get_property(subject, key, encoding=ENCODING): # FIXME: how to handle non-null terminated data in value? # We wouldn't know the length of the data in the value buffer. # This seems to be an oversight in the JACK meta data API. value_c = c_char_p() type_c = c_char_p() ret = jlib.jack_get_property(subject, _e(key), byref(value_c), byref(type_c)) value = value_c.value if ret != -1: decode_value = True if type_c: try: type_ = _d(type_c.value, encoding) except UnicodeDecodeError: # If type can't be decoded, we assume it's neither a mimetype # nor a URI, so we don't know how to interpret it and won't use # it to decide whether to decode the property value. type_ = type_c.value else: decode_value = type_.startswith('text/') free(type_c) else: type_ = None if decode_value: try: value = _d(value_c.value, encoding) except UnicodeDecodeError: pass free(value_c) return Property(key, value, type_) def get_client_property(client, client_uuid, key, encoding=ENCODING): if isinstance(client_uuid, str): client_uuid = get_uuid_for_client_name(client, client_uuid) return get_property(uuid_parse(client_uuid), key, encoding) def get_port_property(client, port, key, encoding=ENCODING): if not isinstance(port, POINTER(jack_port_t)): port = port_by_name(client, port) return get_property(port_uuid(port), key, encoding) def get_port_pretty_name(client, port, encoding=ENCODING): prop = get_port_property(client, port, JACK_METADATA_PRETTY_NAME, encoding) return prop.value if prop else None def remove_all_properties(client): return jlib.jack_remove_property(client) def remove_properties(client, subject): return jlib.jack_remove_property(client, subject) def remove_client_properties(client, client_uuid): if isinstance(client_uuid, str): client_uuid = get_uuid_for_client_name(client, client_uuid) return remove_properties(client, uuid_parse(client_uuid)) def remove_port_properties(client, port): if not isinstance(port, POINTER(jack_port_t)): port = port_by_name(client, port) return remove_properties(client, port_uuid(port)) def remove_property(client, subject, key, encoding=ENCODING): return jlib.jack_remove_property(client, subject, _e(key, encoding)) def remove_client_property(client, client_uuid, key, encoding=ENCODING): if isinstance(client_uuid, str): client_uuid = get_uuid_for_client_name(client, client_uuid) return remove_property(client, uuid_parse(client_uuid), key, encoding) def remove_port_property(client, port, key, encoding=ENCODING): if not isinstance(port, POINTER(jack_port_t)): port = port_by_name(client, port) return remove_property(client, port_uuid(port), key, encoding) def set_property(client, subject, key, value, type=None, encoding=ENCODING): if value is not None and encoding: value = _e(value, encoding) if type is not None and encoding: type = _e(type, encoding) return jlib.jack_set_property(client, subject, _e(key, encoding), value, type) def set_client_property(client, client_uuid, key, value, type=None, encoding=ENCODING): if isinstance(client_uuid, str): client_uuid = get_uuid_for_client_name(client, client_uuid) uuid = uuid_parse(client_uuid) return set_property(client, uuid, key, value, type, encoding) if uuid != -1 else -1 def set_port_property(client, port, key, value, type=None, encoding=ENCODING): if not isinstance(port, POINTER(jack_port_t)): port = port_by_name(client, port) uuid = port_uuid(port) return set_property(client, uuid, key, value, type, encoding) if uuid != -1 else -1 def set_port_pretty_name(client, port, value, encoding=ENCODING): return set_port_property(client, port, JACK_METADATA_PRETTY_NAME, value, 'text/plain', encoding) def set_property_change_callback(client, callback, arg=None): if jlib.jack_set_property_change_callback: global _property_change_callback _property_change_callback = JackPropertyChangeCallback(callback) return jlib.jack_set_property_change_callback(client, _property_change_callback, arg) return -1 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/jacklib/helpers.py0000644000175000017500000001047700000000000016673 0ustar00nilsnils#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Helper functions for extra jacklib functionality # Copyright (C) 2012-2013 Filipe Coelho # 2016-2021 Christopher Arndt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # 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. # # For a full copy of the GNU General Public License see the COPYING file # ------------------------------------------------------------------------------------------------- # Try Import jacklib from __future__ import absolute_import, print_function, unicode_literals from . import api as jacklib # ------------------------------------------------------------------------------------------------- # Get JACK error status as string def get_jack_status_error_string(cStatus): status = cStatus.value if status == 0x0: return "" errorString = [] if status == jacklib.JackFailure: # Only include this generic message if no other error status is set errorString.append("Overall operation failed") if status & jacklib.JackInvalidOption: errorString.append("The operation contained an invalid or unsupported option") if status & jacklib.JackNameNotUnique: errorString.append("The desired client name was not unique") if status & jacklib.JackServerStarted: errorString.append("The JACK server was started as a result of this operation") if status & jacklib.JackServerFailed: errorString.append("Unable to connect to the JACK server") if status & jacklib.JackServerError: errorString.append("Communication error with the JACK server") if status & jacklib.JackNoSuchClient: errorString.append("Requested client does not exist") if status & jacklib.JackLoadFailure: errorString.append("Unable to load internal client") if status & jacklib.JackInitFailure: errorString.append("Unable to initialize client") if status & jacklib.JackShmFailure: errorString.append("Unable to access shared memory") if status & jacklib.JackVersionError: errorString.append("Client's protocol version does not match") if status & jacklib.JackBackendError: errorString.append("Backend Error") if status & jacklib.JackClientZombie: errorString.append("Client is being shutdown against its will") return ";\n".join(errorString) + "." # ------------------------------------------------------------------------------------------------- # Convert C char** -> Python list def c_char_p_p_to_list(c_char_p_p, encoding=jacklib.ENCODING, errors='ignore'): i = 0 retList = [] if not c_char_p_p: return retList while True: new_char_p = c_char_p_p[i] if not new_char_p: break retList.append(new_char_p.decode(encoding=encoding, errors=errors)) i += 1 jacklib.free(c_char_p_p) return retList # ------------------------------------------------------------------------------------------------- # Convert C void* -> string def voidptr2str(void_p): char_p = jacklib.cast(void_p, jacklib.c_char_p) string = str(char_p.value, encoding="utf-8") return string # ------------------------------------------------------------------------------------------------- # Convert C void* -> jack_default_audio_sample_t* def translate_audio_port_buffer(void_p): return jacklib.cast(void_p, jacklib.POINTER(jacklib.jack_default_audio_sample_t)) # ------------------------------------------------------------------------------------------------- # Convert a JACK midi buffer into a python variable-size list def translate_midi_event_buffer(void_p, size): if not void_p: return () elif size == 1: return (void_p[0],) elif size == 2: return (void_p[0], void_p[1]) elif size == 3: return (void_p[0], void_p[1], void_p[2]) elif size == 4: return (void_p[0], void_p[1], void_p[2], void_p[3]) else: return () ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/jacklib/version.py0000644000175000017500000000002600000000000016703 0ustar00nilsnils__version__ = "0.1.0" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/nsmservercontrol.py0000644000175000017500000025155600000000000017264 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) The Non-Session-Manager by Jonathan Moore Liles : http://non.tuxfamily.org/nsm/ New Session Manager, by LinuxAudio.org: https://github.com/linuxaudio/new-session-manager With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library import struct import socket from os import getenv #to get NSM env var from shutil import rmtree as shutilrmtree from shutil import copytree as shutilcopytree from multiprocessing import Process from urllib.parse import urlparse #to convert NSM env var import subprocess import atexit import pathlib import json from uuid import uuid4 from datetime import datetime from sys import exit as sysexit from time import sleep #Our files from .comparedirectories import md5_dir def nothing(*args, **kwargs): pass class _IncomingMessage(object): """Representation of a parsed datagram representing an OSC message. An OSC message consists of an OSC Address Pattern followed by an OSC Type Tag String followed by zero or more OSC Arguments. """ def __init__(self, dgram): #NSM Broadcasts are bundles, but very simple ones. We only need to care about the single message it contains. #Therefore we can strip the bundle prefix and handle it as normal message. if b"#bundle" in dgram: bundlePrefix, singleMessage = dgram.split(b"/", maxsplit=1) dgram = b"/" + singleMessage # / eaten by split self.LENGTH = 4 #32 bit self._dgram = dgram self._parameters = [] self.parse_datagram() def get_int(self, dgram, start_index): """Get a 32-bit big-endian two's complement integer from the datagram. Args: dgram: A datagram packet. start_index: An index where the integer starts in the datagram. Returns: A tuple containing the integer and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: if len(dgram[start_index:]) < self.LENGTH: raise ValueError('Datagram is too short') return ( struct.unpack('>i', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def get_string(self, dgram, start_index): """Get a python string from the datagram, starting at pos start_index. We receive always the full string, but handle only the part from the start_index internally. In the end return the offset so it can be added to the index for the next parameter. Each subsequent call handles less of the same string, starting further to the right. According to the specifications, a string is: "A sequence of non-null ASCII characters followed by a null, followed by 0-3 additional null characters to make the total number of bits a multiple of 32". Args: dgram: A datagram packet. start_index: An index where the string starts in the datagram. Returns: A tuple containing the string and the new end index. Raises: ValueError if the datagram could not be parsed. """ #First test for empty string, which is nothing, followed by a terminating \x00 padded by three additional \x00. if dgram[start_index:].startswith(b"\x00\x00\x00\x00"): return "", start_index + 4 #Otherwise we have a non-empty string that must follow the rules of the docstring. offset = 0 try: while dgram[start_index + offset] != 0: offset += 1 if offset == 0: raise ValueError('OSC string cannot begin with a null byte: %s' % dgram[start_index:]) # Align to a byte word. if (offset) % self.LENGTH == 0: offset += self.LENGTH else: offset += (-offset % self.LENGTH) # Python slices do not raise an IndexError past the last index, # do it ourselves. if offset > len(dgram[start_index:]): raise ValueError('Datagram is too short') data_str = dgram[start_index:start_index + offset] return data_str.replace(b'\x00', b'').decode('utf-8'), start_index + offset except IndexError as ie: raise ValueError('Could not parse datagram %s' % ie) except TypeError as te: raise ValueError('Could not parse datagram %s' % te) def get_float(self, dgram, start_index): """Get a 32-bit big-endian IEEE 754 floating point number from the datagram. Args: dgram: A datagram packet. start_index: An index where the float starts in the datagram. Returns: A tuple containing the float and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: return (struct.unpack('>f', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def parse_datagram(self): try: self._address_regexp, index = self.get_string(self._dgram, 0) if not self._dgram[index:]: # No params is legit, just return now. return # Get the parameters types. type_tag, index = self.get_string(self._dgram, index) if type_tag.startswith(','): type_tag = type_tag[1:] # Parse each parameter given its type. for param in type_tag: if param == "i": # Integer. val, index = self.get_int(self._dgram, index) elif param == "f": # Float. val, index = self.get_float(self._dgram, index) elif param == "s": # String. val, index = self.get_string(self._dgram, index) else: logger.warning("Unhandled parameter type: {0}".format(param)) continue self._parameters.append(val) except ValueError as pe: #raise ValueError('Found incorrect datagram, ignoring it', pe) # Raising an error is not ignoring it! logger.warning("Found incorrect datagram, ignoring it. {}".format(pe)) @property def oscpath(self): """Returns the OSC address regular expression.""" return self._address_regexp @staticmethod def dgram_is_message(dgram): """Returns whether this datagram starts as an OSC message.""" return dgram.startswith(b'/') @property def size(self): """Returns the length of the datagram for this message.""" return len(self._dgram) @property def dgram(self): """Returns the datagram from which this message was built.""" return self._dgram @property def params(self): """Convenience method for list(self) to get the list of parameters.""" return list(self) def __iter__(self): """Returns an iterator over the parameters of this message.""" return iter(self._parameters) class _OutgoingMessage(object): def __init__(self, oscpath): self.LENGTH = 4 #32 bit self.oscpath = oscpath self._args = [] def write_string(self, val): dgram = val.encode('utf-8') diff = self.LENGTH - (len(dgram) % self.LENGTH) dgram += (b'\x00' * diff) return dgram def write_int(self, val): return struct.pack('>i', val) def write_float(self, val): return struct.pack('>f', val) def add_arg(self, argument): t = {str:"s", int:"i", float:"f"}[type(argument)] self._args.append((t, argument)) def build(self): dgram = b'' #OSC Path dgram += self.write_string(self.oscpath) if not self._args: dgram += self.write_string(',') return dgram # Write the parameters. arg_types = "".join([arg[0] for arg in self._args]) dgram += self.write_string(',' + arg_types) for arg_type, value in self._args: f = {"s":self.write_string, "i":self.write_int, "f":self.write_float}[arg_type] dgram += f(value) return dgram class NsmServerControl(object): """ The ServerControl can be started in three modes, regarding nsmd. We expect that starting our own nsmd will be the majority of cases. SessionRoot parameter is only honored if we start nsmd ourselves. Ascending lookup priority: 1) Default is to start our own nsmd. A single-instance watcher will prevent multiple programs on the same system. 2) When $NSM_URL is found as environment we will connect to that nsmd. 3) When hostname and portnumber are given explicitely as instance variables we will first test if a server is running at that URL, if not we will start our own with these parameters. This is not only a pure implemenation of the protocol. It is extended by us reacting to and storing incoming data. This data can be interpreted and enhanced by looking at the session dir ourselves. However, we don't do anything that is not possible by the original nsmd + human interaction. 100% Compatibility is the highest priority. The big problems are the async nature of communication, message come out of order or interleaved, and nsm is not consistent in its usage of osc-paths. For example it starts listing sessions with /nsm/gui/server/message, but sends the content with /reply [/nsm/server/list, nsmSessionName] and then ends it with /nsm/server/list [0, Done] (no reply!). So three message types, three callbacks for one logically connected process. To update our internal session information we therefore need to split the functionality into severall seemingly unconnected callbacks and you need to know how the protocol works to actually know the order of operations. Switch logging to info to learn more. We have a mix between NSM callbacks and our own functions. Most important there is a watchdog that looks at the session directory and creates its own callbacks if something changes. A typical operation, say sessionDelete or sessionCopy looks like this: * Ask (blocking) nsmd for a current list of sessions, update our internal state * Perform a file operation, like copy or delete or lift a lock * Let our watchdog discover the changes in the file system and trigger another (non-blocking) request for a list of sessions to adjust our internal state to reality. Granted, we could just call our blocking query again at the end, but we would still need to let the watchdog run for operations that the user does with a filemanager, which would end up in redundant calls. Bottom line: _updateSessionListBlocking is called at the beginning of a function, but not at the end. Docs: http://non.tuxfamily.org/nsm/ http://non.tuxfamily.org/wiki/Non%20Session%20Manager http://non.tuxfamily.org/wiki/ApplicationsSupportingNsm http://non.tuxfamily.org/nsm/API.html """ def __init__(self, sessionOpenReadyHook, sessionOpenLoadingHook, sessionClosedHook, clientStatusHook, singleInstanceActivateWindowHook, dataClientNamesHook, dataClientDescriptionHook, dataClientTimelineMaximumDurationChangedHook, parameterNsmOSCUrl=None, sessionRoot=None, startupSession=None, useCallbacks=True): """If useCallbacks is False you will see every message in the log. This is just a development mode to see all messages, unfiltered. Normally we have special hook functions that save and interpret data, so they don't show in the logs""" #Deactivate hooks for now. During init no hooks may be called, #but some functions want to do that already. We setup the true hooks at the end of init self.sessionOpenReadyHook= self.sessionOpenLoadingHook= self.sessionClosedHook= self.clientStatusHook= self.singleInstanceActivateWindowHook= self.dataClientNamesHook= self.dataClientDescriptionHook= nothing self._queue = list() #Incoming OSC messages are buffered here. #Status variables that are set by our callbacks self.internalState = { "sessions" : set(), #nsmSessionNames:str . We use set for unqiue, just in case. But we also clear that on /nsm/gui/server/message ['Listing sessions'] to handle deleted sessions "currentSession" : None, "port" : None, #Our GUI port "serverPort" : None, #nsmd port "nsmUrl" : None, #the environment variable "clients" : {}, #clientId:dict see self._initializeEmptyClient . Gets replaced with a new dict instance on session changes. "broadcasts" : [], #in the order they appeared "datastorage" : None, #URL, if present in the session } self.dataStorage = None #Becomes DataStorage() every time a datastorage client does a broadcast announce. self._addToNextSession = [] #A list of executables in PATH. Filled by new, waits for reply that session is created and then will send clientNew and clear the list. if useCallbacks: self.callbacks = { "/nsm/gui/session/name" : self._reactCallback_activeSessionChanged, #"/nsm/gui/session/root" #Session root is an active blocking call in init "/nsm/gui/client/label" : self._reactCallback_ClientLabelChanged, "/nsm/gui/client/new" : self._reactCallback_ClientNew, "/nsm/gui/session/session" : self._reactCallback_SessionSession, "/nsm/gui/client/status" : self._reactCallback_statusChanged, #handles multiple status keywords "/reply" : self._reactCallback_reply, #handles multiple replies "/error" : self._reactCallback_error, "/nsm/gui/client/has_optional_gui" : self._reactCallback_clientHasOptionalGui, "/nsm/gui/client/gui_visible" : self._reactCallback_clientGuiVisible, "/nsm/gui/client/pid" : self._reactCallback_clientPid, "/nsm/gui/client/dirty" : self._reactCallback_clientDirty, "/nsm/gui/server/message" : self._reactCallback_serverMessage, "/nsm/gui/gui_announce" : self._reactCallback_guiAnnounce, #we rarely receive that, especially not in init. "/nsm/server/list" : self._reactCallback_serverList, "/nsm/server/broadcast" : self._reactCallback_broadcast, } else: #This is just a development mode to see all messages, unfiltered self.callbacks = set() #empty set is easiest to check #Networking and Init for our control part, not for the server self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp self.sock.bind(('', 0)) #pick a free port on localhost. self.sock.setblocking(False) self.internalState["port"] = self.sock.getsockname()[1] #only happens once, ports don't change during runtime #self.sock.close() Do not close, this runs until the end of the program ###Testing of existing servers, starting and connecting #First handle the NSM URL, or generate on. #self.nsmOSCUrl must be a tuple compatible to the result of urlparse. (hostname, port) self.singleInstanceSocket = None if parameterNsmOSCUrl: o = urlparse(parameterNsmOSCUrl) #self.nsmOSCUrl = (o.hostname, o.port) #this forces lowercase. in rare circumstances this is not correct and we must be case sensitive. fix: self.nsmOSCUrl = o.netloc.split(":")[0], o.port else: envResult = self._getNsmOSCUrlFromEnvironment() if envResult: self.nsmOSCUrl = envResult else: #This is the default case. User just starts the GUI. The other modes are concious decisions to either start with URL as parameter or in an NSM environment. #But now we need to test if the user accidentaly opened a second GUI, which would start a second server. self._setupAndTestForSingleInstance() #This might quit the whole program and we will never see the next line. self.nsmOSCUrl = self._generateFreeNsmOSCUrl() assert self.nsmOSCUrl self.internalState["serverPort"] = self.nsmOSCUrl[1] #only happens once, ports don't change during runtime self.internalState["nsmUrl"] = f"osc.udp://{self.nsmOSCUrl[0]}:{self.nsmOSCUrl[1]}/" #only happens once, ports don't change during runtime #Then check if a server is running there. If not start one. self.ourOwnServer = None #Might become a subprocess handle if self._isNsmdRunning(self.nsmOSCUrl): #serverport = self.nsmOSCUrl[1] #No further action required. GUI announce below this testing. pass else: self._startNsmdOurselves(sessionRoot, startupSession) #Session root can be a commandline parameter we forward to the server if we start it ourselves. startupSession is an autoloader. Both are usually None. assert type(self.ourOwnServer) is subprocess.Popen, (self.ourOwnServer, type(self.ourOwnServer)) #Wait for the server, or test if it is reacting. self._waitForPingResponseBlocking() logger.info("nsmd is ready @ {}".format(self.nsmOSCUrl)) #Tell nsmd that we are a GUI and want to receive general messages async, not only after we request something self.sessionRoot = self._initial_announce() #Triggers "hi" and session root self.internalState["sessionRoot"] = self.sessionRoot self._forceProcessOnceToEmptyQueue() #process any leftover messages. atexit.register(self.quit) #mostly does stuff when we started nsmd ourself #Activate hooks for api callbacks, now that we are finished here. #Otherwise the hooks will get called from our functions (e.g. new client) while we are still during init self.sessionOpenReadyHook = sessionOpenReadyHook #self.sessionAsDict(nsmSessionName) as parameter self.sessionOpenLoadingHook = sessionOpenLoadingHook #self.sessionAsDict(nsmSessionName) as parameter self.sessionClosedHook = sessionClosedHook #no parameter. This is also "choose a session" mode self.clientStatusHook = clientStatusHook #all client status is done via this single hook. GUIs need to check if they already know the client or not. self.dataClientNamesHook = dataClientNamesHook self.dataClientDescriptionHook = dataClientDescriptionHook self.dataClientTimelineMaximumDurationChangedHook = dataClientTimelineMaximumDurationChangedHook self.singleInstanceActivateWindowHook = singleInstanceActivateWindowHook #added to self.processSingleInstance() to listen for a message from another wannabe-instance self._receiverActive = True logger.info("nsmservercontrol init is complete. Ready for event loop") #Now an external event loop can add self.process #Internal Methods def _setupAndTestForSingleInstance(self): """on program startup trigger this if there is already another instance of us running. This socket is only """ self.singleInstanceSocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) logger.info("Testing if another non-specific Agordejo is running.") try: ## Create an abstract socket, by prefixing it with null. # this relies on a feature only in linux, when current process quits, the # socket will be deleted. self.singleInstanceSocket.bind('\0' + "agordejo") self.singleInstanceSocket.listen(1) self.singleInstanceSocket.setblocking(False) logger.info("No other non-specific Agordejo found. Starting GUI") #Continue in self.processSingleInstance() return True except socket.error: logger.error("GUI for this nsmd server already running. Informing the existing application to show itself.") self.singleInstanceSocket.connect('\0' + "agordejo") self.singleInstanceSocket.send("agordejoactivate".encode()); self.singleInstanceSocket.close() sysexit(1) #triggers atexit #print ("not executed") return False def processSingleInstance(self): """Tests our unix socket for an incoming signal. if received forward to the engine->gui Can be added to a slower event loop, so it is not in self.process""" if self.singleInstanceSocket: try: connection, client_address = self.singleInstanceSocket.accept() #This blocks and waits for a message incoming = connection.recv(1024) if incoming and incoming == b"agordejoactivate": self.singleInstanceActivateWindowHook() except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. In fact: this happens when in non-blocking mode. pass except socket.timeout: pass def _setPause(self, state:bool): """Set both the socket and the thread into waiting mode or not. With this we can wait for answers until we resume async operation""" if state: self.sock.setblocking(True) #explicitly wait. self.sock.settimeout(0.5) self._receiverActive = False logger.info("Suspending receiving async mode.") else: self.sock.setblocking(False) self._receiverActive = True logger.info("Resuming receiving async mode.") def _forceProcessOnceToEmptyQueue(self): """Sometimes we want to make sure everything is processed until we continue. For example in our init. Initial usecase was connecting to a running nsmd with session. The api first callback to export sessions to the GUI was freezing because listSession was chocking on leftover messages from gui_announce to a running session, which sends the session name and a list of clients. The latter is not happening when starting the server ourselves, so we weren't expecting this. To be honest, this is really a patch to work around a design flaw and we hope this is a one-off corner case.""" logger.info("Force processing queue") #First gather all osc messages still in the pipe while True: try: data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if msg: self._queue.append(msg) except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. break except socket.timeout: break #Now process them all. This is different than normal self.process(). for msg in self._queue: if msg.oscpath in self.callbacks: self.callbacks[msg.oscpath](msg.params) else: logger.warning(f"Unhandled message with path {msg.oscpath} and parameters {msg.params}") self._queue.clear() logger.info("Ended force processing queue") def process(self): """Use this in an external event loop""" if self._receiverActive: while True: try: data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if msg: self._queue.append(msg) except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. break except socket.timeout: break for msg in self._queue: if msg.oscpath in self.callbacks: self.callbacks[msg.oscpath](msg.params) else: logger.warning(f"Unhandled message with path {msg.oscpath} and parameters {msg.params}") self._queue.clear() def _getNsmOSCUrlFromEnvironment(self): """Return the nsm osc url or None""" nsmOSCUrl = getenv("NSM_URL") if not nsmOSCUrl: return None else: #osc.udp://hostname:portnumber/ o = urlparse(nsmOSCUrl) return o.hostname, o.port def _generateFreeNsmOSCUrl(self): #Instead of reading out the NSM port we get a free port ourselves and set up nsmd with that tempServerSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp tempServerSock.bind(('', 0)) #pick a free port on localhost. address, tempServerSockPort = tempServerSock.getsockname() tempServerSock.close() #We need to close it because nsmd will open it right away. nsmOSCUrl = ("0.0.0.0", tempServerSockPort) #compatible to result of urlparse logger.info("Generated our own free NSM_URL to start a server @ {}".format(nsmOSCUrl)) return nsmOSCUrl def _isNsmdRunning(self, nsmOSCUrl): """Test if the port is open or not""" logger.info(f"Testing if a server is running @ {nsmOSCUrl}") hostname, port = nsmOSCUrl tempServerSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp try: tempServerSock.bind((hostname, port)) logger.info(f"No external nsmd found (we tested if port is closed) @ {nsmOSCUrl}") return False except: logger.info(f"External nsmd found (we tested if port is closed) @ {nsmOSCUrl}") return True finally: tempServerSock.close() def _startNsmdOurselves(self, sessionRoot:str, startupSession:str): assert self.nsmOSCUrl hostname, port = self.nsmOSCUrl arguments = ["nsmd","--osc-port", str(port)] if sessionRoot: arguments += ["--session-root", sessionRoot] if startupSession: logger.info(f"Got start-session as command line parameter. Fowarding to nsmd command line: {startupSession}") arguments += ["--load-session", startupSession] #nsmd allows all executables in $PATH. For technical reasons our GUI extends this PATH before we start the server. #This is a convenience service for fellow developers, that does not belong in the server control. #However, if you wonder why there are are more applications from unknown PATHs check qtgui/settings.py self.ourOwnServer = subprocess.Popen(arguments) def _blockingRequest(self, path:str, arguments:list, answerPath:str, answerArguments:list, repeat=False)->list: """During start-up we need to wait for replies. Also some operations only make sense if we got data back. This is an abstraction that deals with messages that may come out-of-order and keeps them for later, but at least prevents our side from sending messages out-of-order itself. Default is: send once, wait for answer. repeat=True sends multiple times until an answer arrives. Returns list of arguments, can be empty. """ assert not self._queue, [(m.oscpath, m.params) for m in self._queue] logger.info(f"[wait for answer]: Sending {path}: {arguments}") self._setPause(True) out_msg = _OutgoingMessage(path) for arg in arguments: out_msg.add_arg(arg) if not repeat: self.sock.sendto(out_msg.build(), self.nsmOSCUrl) #Wait for answer ready = False while not ready: if repeat: #we need to send multiple times. self.sock.sendto(out_msg.build(), self.nsmOSCUrl) try: data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if answerArguments and msg.oscpath == answerPath and msg.params == answerArguments: result = msg.params logger.info(f"[wait from {path}] Received {answerPath}: {result}") ready = True elif msg.oscpath == answerPath: result = msg.params logger.info(f"[wait from {path}] Received {answerPath}: {result}") ready = True else: logger.warning(f"Waiting for {answerPath} from nsmd, but got: {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. continue except socket.timeout: continue self._setPause(False) return result def _waitForPingResponseBlocking(self): self._blockingRequest(path="/osc/ping", arguments=[], answerPath="/reply", answerArguments=["/osc/ping",], repeat=True) def _initial_announce(self)->pathlib.Path: """nsm/gui/gui_announce triggers a multi-stage reply. First we get "hi", then we get the session root. We wait for session root and then clean 'hi' from the queue. When we connect to a running nsmd we also receive /nsm/gui/session/name with the current session (or empty string for no current). If in a session we will receive a list of clients which ends the gui_announce stage. Returns session root as pathlib-path.""" resultArguments = self._blockingRequest(path="/nsm/gui/gui_announce", arguments=[], answerPath="/nsm/gui/session/root", answerArguments=[]) if len(self._queue) == 1 and self._queue[0].oscpath == "/nsm/gui/gui_announce" and self._queue[0].params == ["hi"]: logger.info("Got 'hi'. We are now the registered nsmd GUI as per our initial /nsm/gui/gui_announce") self._queue.clear() #this is safe because we tested above that there is exactly the hi message in the queue. else: logging.error(f"For ValueError below: {[(m.oscpath, m.params) for m in self._queue]}") raise ValueError("We were expecting a clean _queue with only 'hi' as leftover, but instead there were unhandled messages. see print above. Better abort than a wrong program state") #all ok return pathlib.Path(resultArguments[0]) #General Commands def send(self, arg): """ Intended for a text input / command line interface. Sends anything to nsmd, separated by semicolon. First part is the message address, the rest are string-parameters.""" args = arg.split() msg = _OutgoingMessage(args[0]) for p in args[1:]: msg.add_arg(p) self.sock.sendto(msg.build(), self.nsmOSCUrl) def gui_announce(self): """This is just the announce without any answer. This is a last-resort method if another GUI "stole" our slot. For our own initial announce we use self._initial_announce()""" msg = _OutgoingMessage("/nsm/gui/gui_announce") self.sock.sendto(msg.build(), self.nsmOSCUrl) def ping(self): msg = _OutgoingMessage("/osc/ping") self.sock.sendto(msg.build(), self.nsmOSCUrl) def list(self): msg = _OutgoingMessage("/nsm/server/list") self.sock.sendto(msg.build(), self.nsmOSCUrl) def _updateSessionListBlocking(self): """To ensure correct data on session operations we manage ourselves, like copy, rename and delete. Ask nsmd for projects in session root and update our internal state. This will return None without doing anything when we are already in a session. This will wait for an answer and block all other operations. First is /nsm/gui/server/message ['Listing sessions'] Then session names come one reply at a time such as /reply ['/nsm/server/list', 'test3'] Finally /nsm/server/list [0, 'Done.'] , not a reply """ #In the past we only regenerated if we are not in a session. However, that was overzealous. #Some GUI functions did not work. Better regenerate that list as often as we want. logger.info("Requesting project list from session server in blocking mode") self._setPause(True) msg = _OutgoingMessage("/nsm/server/list") self.sock.sendto(msg.build(), self.nsmOSCUrl) #Wait for /reply ready = False while True: try: data, addr = self.sock.recvfrom(1024) except socket.timeout: continue msg = _IncomingMessage(data) if not ready and msg.oscpath == "/nsm/gui/server/message" and msg.params == ["Listing sessions"]: self.internalState["sessions"].clear() # new clients are added as /reply /nsm/server/list callbacks ready = True else: if len(msg.params) != 2: logger.warning(f"Expected project but got path {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) continue #This is what we want: elif msg.oscpath == "/reply" and msg.params[0] == "/nsm/server/list": #/reply ['/nsm/server/list', 'test3'] for a real session or #/reply ['/nsm/server/list', ''] as "list ended" marker if msg.params[1]: self.internalState["sessions"].add(msg.params[1]) logger.info(f"Received session name: {msg.params[1]}") else: #empty string break elif msg.params[0] == 0 and msg.params[1] == "Done.": # legacy nsmd sent the wrong message. Fixed in new-session-manager june 2020 break else: logger.warning(f"Expected project but got path {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) continue self._setPause(False) def quit(self): """Called through atexit. Thanks to start.py sys exception hook this will also trigger on PyQt crash""" if self.ourOwnServer: msg = _OutgoingMessage("/nsm/server/quit") self.sock.sendto(msg.build(), self.nsmOSCUrl) returncode = self.ourOwnServer.wait() logger.info("Stopped our own server with return code {}".format(returncode)) def broadcast(self, path:str, arguments:list): """/nsm/server/broadcast s:path [arguments...] http://non.tuxfamily.org/nsm/API.html 1.2.7.1 /nsm/server/broadcast s:path [arguments...] /nsm/server/broadcast /tempomap/update "0,120,4/4:12351234,240,4/4" All clients except the sender recive: /tempomap/update "0,120,4/4:12351234,240,4/4" """ logger.info(f"Sending broadcast with path {path} and args {arguments}") message = _OutgoingMessage("/nsm/server/broadcast") message.add_arg(path) for arg in arguments: message.add_arg(arg) #type autodetect self.sock.sendto(message.build(), self.nsmOSCUrl) #Primarily Without Session def open(self, nsmSessionName:str): if nsmSessionName in self.internalState["sessions"]: msg = _OutgoingMessage("/nsm/server/open") msg.add_arg(nsmSessionName) #s:project_name self.sock.sendto(msg.build(), self.nsmOSCUrl) else: logger.warning(f"Session {nsmSessionName} not found. Not forwarding to nsmd.") def new(self, newName:str, startClients:list=[])->str: """Saves the current session and creates a new session. Only works if dir does not exist yet. """ basePath = pathlib.Path(self.sessionRoot, newName) if basePath.exists(): return None self._addToNextSession = startClients msg = _OutgoingMessage("/nsm/server/new") msg.add_arg(newName) #s:project_name self.sock.sendto(msg.build(), self.nsmOSCUrl) #Only with Current Session def save(self): msg = _OutgoingMessage("/nsm/server/save") self.sock.sendto(msg.build(), self.nsmOSCUrl) def close(self, blocking=False): if not blocking: msg = _OutgoingMessage("/nsm/server/close") self.sock.sendto(msg.build(), self.nsmOSCUrl) else: msg = _OutgoingMessage("/nsm/server/close") self.sock.sendto(msg.build(), self.nsmOSCUrl) #Drive the process loop ourselves. This will still trigger updates but the mainloop will wait. while self.internalState["currentSession"]: self.process() def abort(self, blocking=False): if not blocking: msg = _OutgoingMessage("/nsm/server/abort") self.sock.sendto(msg.build(), self.nsmOSCUrl) else: msg = _OutgoingMessage("/nsm/server/abort") self.sock.sendto(msg.build(), self.nsmOSCUrl) #Drive the process loop ourselves. This will still trigger updates but the mainloop will wait. while self.internalState["currentSession"]: self.process() def duplicate(self, newName:str)->str: """Saves the current session and creates a new session. Requires an open session and uses nsmd to do the work. If you want to do copy of any session use our owns self.sessionCopy""" msg = _OutgoingMessage("/nsm/server/duplicate") msg.add_arg(newName) #s:project_name self.sock.sendto(msg.build(), self.nsmOSCUrl) #Client Commands for Loaded Session def clientAdd(self, executableName:str): """Adds a client to the current session. executable must be in $PATH. We do not trust NSM to perform the right checks. It will add an empty path or wrong path. """ if not pathlib.Path(executableName).name == executableName: logger.warning(f"{executableName} must be just an executable file in your $PATH. We expected: {pathlib.Path(executableName).name} . We will not ask nsmd to add it as client") return False allPaths = getenv("PATH") assert allPaths, allPaths binaryPaths = allPaths.split(":") #TODO: There is a corner case that NSMD runs in a different $PATH environment. executableInPath = any(pathlib.Path(bp, executableName).is_file() for bp in binaryPaths) if executableInPath: msg = _OutgoingMessage("/nsm/server/add") msg.add_arg(executableName) #s:executable_name self.sock.sendto(msg.build(), self.nsmOSCUrl) return True else: logger.warning("Executable {} not found. We will not ask nsmd to add it as client".format(executableName)) return False def clientStop(self, clientId:str): msg = _OutgoingMessage("/nsm/gui/client/stop") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientResume(self, clientId:str): """Opposite of clientStop""" msg = _OutgoingMessage("/nsm/gui/client/resume") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientRemove(self, clientId:str): """Client needs to be stopped already. We will do that and wait for an answer. Remove from the session. Will not delete the save-files, but make them inaccesible. There is never a point in nsmservercontrol where self.internalState["clients"] is emptied. nsmd actually sends a clientRemove for every client at session stop. """ #We have a blocking operation in here so we need to be extra cautios that the client exists. if not clientId in self.internalState["clients"]: return False self.clientStop(clientId) #We need to wait for an answer. #Drive the process loop ourselves. This will still trigger updates but the mainloop will wait. logger.info(f"Waiting for {clientId} to be status 'stopped'") while not self.internalState["clients"][clientId]["lastStatus"] == "stopped": self.process() msg = _OutgoingMessage("/nsm/gui/client/remove") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) #Flood lazy lagging nsmd until it removed the client. #We will receive a few -10 "No such client." errors but that is ok. while True: if not clientId in self.internalState["clients"]: break if self.internalState["clients"][clientId]["lastStatus"] == "removed": break self.sock.sendto(msg.build(), self.nsmOSCUrl) self.process() def clientSave(self, clientId:str): """Saves only the given client""" msg = _OutgoingMessage("/nsm/gui/client/save") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientHide(self, clientId:str): """Hides the client. Works only if client announced itself with this feature""" msg = _OutgoingMessage("/nsm/gui/client/hide_optional_gui") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientShow(self, clientId:str): """Shows the client. Works only if client announced itself with this feature""" msg = _OutgoingMessage("/nsm/gui/client/show_optional_gui") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) #Callbacks def _reactCallback_guiAnnounce(self, parameters:list): """This should not happen, but let's keep it in in case of edge-case multi GUI scenarios""" assert parameters == ["hi"], parameters logger.info("We got an unexpected 'hi', as if requesting gui_announce. Our own initial GUI announce as received and processed silently earlier already.") def _reactCallback_error(self, parameters:list): logger.error(parameters) def _reactCallback_reply(self, parameters:list): """This is a difficult function because replies arrive for many unrelated things, like status. We do our best to send all replies on the right way""" success = False l = len(parameters) if l == 2: originalMessage, data = parameters logger.info(f"Got reply for {originalMessage} with {data}") reply = { "/nsm/server/list" : self._reactReply_nsmServerList, "/nsm/server/new" : self._reactReply_nsmServerNew, "/nsm/server/close" : self._reactReply_nsmServerClose, "/nsm/server/open" : self._reactReply_nsmServerOpen, "/nsm/server/save" : self._reactReply_nsmServerSave, "/nsm/server/abort" : self._reactReply_nsmServerAbort, "/nsm/server/duplicate" : self._reactReply_nsmServerDuplicate, "/nsm/server/add" : self._reactReply_nsmServerAdd, } if originalMessage in reply: reply[originalMessage](data) success = True elif l == 3: originalMessage, errorCode, answer = parameters logger.info(f"Got reply for {originalMessage} with code {errorCode} saying {answer}") if originalMessage == "/nsm/server/add": assert errorCode == 0, parameters self._reactReply_nsmServerAdd(answer) success = True elif l == 1: singleMessage = parameters[0] """For unknown reasons these replies do not repeat the originalMessage""" if singleMessage == "/osc/ping": logger.info(singleMessage) success = True elif singleMessage == "Client removed.": self._reactReply_nsmServerRemoved() success = True elif singleMessage == "Client stopped.": self._reactReply_nsmServerStopped() success = True #After all these reactions and checks the function will eventually return here. if not success: raise NotImplementedError(parameters) def _reactCallback_serverMessage(self, parameters:list): """Messages are normally harmless and uninteresting. Howerver, we need to use some of them for actual tasks. In opposite to reply and status this all go in our function for now, until refactoring""" if parameters == ["Listing session"]: #this feels bad! A simple message is not a reliable state token and could change in the future. #we cannot put that into our own /list outgoing message because other actions like "new" also trigger this callback self.internalState["sessions"].clear() # new clients are added as /reply /nsm/server/list callbacks if parameters[0].startswith("Opening session"): #This gets send only when an existing session starts loading. It will not trigger on new sessions, be it really new or duplicate. #e.g. /nsm/gui/server/message ["Opening session FOO"] nsmSessionName = parameters[0].replace("Opening session ", "") logger.info(f"Starting to load clients of session: {nsmSessionName}") self.sessionOpenLoadingHook(self.sessionAsDict(nsmSessionName)) #notify the api->UI else: logger.info("/nsm/gui/server/message " + repr(parameters)) def _reactCallback_broadcast(self, parameters:list): """We have nothing to do with broadcast. But we save them, so they can be shown on request parameters[0] is an osc path:str without naming constraints the rest is a list of arguments. Attention: a broadcast is not saved by the server. You either are in the session to receive it or you will miss it. If we run Agordejo as attached GUI (incl. --load-session) a broadcast after the session was loaded, where programs announce themselves to all other clients, will not be received here. Such is the case with our data-client. """ logger.info(f"Received broadcast. Saving in internal state: {parameters}") self.internalState["broadcasts"].append(parameters) #Our little trick. We know and like some clients better than others. #If we detect our own data-storage we remember our friends. #It is possible that another datastorage broadcasts, then we overwrite the URL. if parameters and parameters[0] == "/agordejo/datastorage/announce": path, clientId, messageSizeLimit, url = parameters assert "osc.udp" in url logger.info(f"Got announce from agordejo datastorage clientId {clientId} @ {url}") o = urlparse(url) self.dataStorage = DataStorage(self, clientId, messageSizeLimit, (o.hostname, o.port), self.sock) def _reactCallback_serverList(self, parameters:list): """This finalizes a new session list. Here we send new data to the GUI etc.""" l = len(parameters) if l == 2: errorCode, message = parameters assert errorCode == 0, errorCode assert message == "Done.", message #don't miss the dot after Done logger.info("/nsm/server/list is done and has transmitted all available sessions to us") else: raise NotImplementedError(parameters) def _reactCallback_activeSessionChanged(self, parameters:list): """We receive this trough /nsm/gui/session/name This is called when the session has already changed. This also happens when you connect to a headless nsmd with a running session. We expect two parameters: [session name, session path] both of which could be "". If we start nsmd ourselves into an empty state we expect session name to be empty Session path is the subdirectory relative to session root. The session root is not included. !The unqiue name is the session path, not the name! Shortly before we received /nsm/gui/session/session which indicates the attempt to create a new one, I guess! :) If you want to react to the attempt to open a session you need to use /nsm/gui/server/message ["Opening session FOO"] OR creating a new session, after which nsmd will open that session without a message. Empty string is "No session" or "Choose A Session" mode. """ l = len(parameters) if l == 2: nsmSessionName, sessionPath = parameters if not nsmSessionName and not sessionPath: #No session loaded. We are in session-choosing mode. logger.info("Session closed or never started. Choose-A-Session mode.") self.internalState["currentSession"] = None #sessionCloseHooked triggers rebuilding of the session list, which will not work when there is a current session. self.sessionClosedHook() else: #Session path is the subdirectory relative to session root. The session root is not included. sessionPath = sessionPath.lstrip("/") #we strip for technical reasons. logger.info(f"Current Session changed. We are now {nsmSessionName} in {sessionPath}") self.internalState["currentSession"] = sessionPath #This is after the session, received after all programs have loaded. #We have a counterpart-message reaction that signals the attempt to load. self.sessionOpenReadyHook(self.sessionAsDict(sessionPath)) #notify the api->UI for autoClientExecutableInPath in self._addToNextSession: self.clientAdd(autoClientExecutableInPath) self._addToNextSession = [] #reset elif l == 0: #Another way of "no session". self.internalState["currentSession"] = None #sessionCloseHooked triggers rebuilding of the session list, which will not work when there is a current session. self.sessionClosedHook() else: raise NotImplementedError(parameters) def _initializeEmptyClient(self, clientId:str): """NSM reuses signals. It is quite possible that this will be called multiple times, e.g. after opening a session. This is not a reaction callback, we call this ourselves only in _reactCallback_ClientNew """ #if not self.internalState["currentSession"]: # logger.warning(f"We received a clientNew for ID {clientId} but no session open was received." # "This would happen in an old nsmd version. If you see the GUI with an open session and a client list you can ignore this warning") if clientId in self.internalState["clients"]: return logger.info(f"Creating new internal entry for client {clientId}") client = { "clientId":clientId, #for convenience, included internally as well "dumbClient":True, #Bool. Real nsm or just any old program? status "Ready" switches this. "executable":None, #Every client announces to the GUI with the exectuable name. True nsm clients later overwrite with a pretty name which we save as "reportedName" "reportedName":None, #str . The reported name is first the executable name, for status started. But for NSM clients it gets replaced with a reported name. "label":None, #str "lastStatus":None, #str "statusHistory":[], #list "hasOptionalGUI": False, #bool "visible": None, # bool "dirty": None, # bool } self.internalState["clients"][clientId] = client def _setClientData(self, clientId:str, parameter:str, value): if clientId in self.internalState["clients"]: self.internalState["clients"][clientId][parameter] = value return True else: logger.warning(f"Client {clientId} not found in internal status storage. If the session was just closed this is most likely a known race condition. Everything is fine in this case.") return False def _reactCallback_ClientLabelChanged(self, parameters:list): """osc->add_method( "/nsm/gui/client/label", "ss", osc_handler, osc, "path,display_name" ); """ l = len(parameters) if l == 2: clientId, label = parameters logger.info(f"Label for client {clientId} changed to {label}") self._setClientData(clientId, "label", label) self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientPid(self, parameters:list): clientId, pid = parameters self._setClientData(clientId, "pid", pid) def _reactCallback_SessionSession(self, parameters:list): """This is received only when a new session gets created and followed by /nsm/gui/client/new and then a reply for /reply /nsm/server/new Session created""" #This is the counterpart to Message "Opening Session", but for really new or freshly duplicated session. logger.info(f"Attempt to create session: {parameters}") self.sessionOpenLoadingHook(self.sessionAsDict(parameters[0])) #notify the api->UI def _reactCallback_ClientNew(self, parameters:list): """/nsm/gui/client/new ['nBAVO', 'jackpatch'] This is both add client or open. The message comes twice. Once when you add a client, then parameters will contain the executable name. If the client reports itself as NSM compatible through announce we will also get the Open message through this function. Then the name changes from executableName to a reportedName, which will remain for the rest of the session. Executable name is still important to look up icons in the GUI. This message is usually followed by /nsm/gui/client/status """ l = len(parameters) if l == 2: clientId, name = parameters if not clientId in self.internalState["clients"]: self._initializeEmptyClient(clientId) self._setClientData(clientId, "executable", name) logger.info(f"Client started {name}:{clientId}") else: self._setClientData(clientId, "reportedName", name) logger.info(f"Client upgraded to NSM-compatible: {name}:{clientId}") self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientDirty(self, parameters:list): """/nsm/gui/client/dirty ['nMAJH', 1] """ l = len(parameters) if l == 2: clientId, dirty = parameters dirty = bool(dirty) self._setClientData(clientId, "dirty", dirty) logger.info(f"Client {clientId} save status dirty is now: {dirty}") self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientGuiVisible(self, parameters:list): """/nsm/gui/client/gui_visible ['nMAJH', 0] """ l = len(parameters) if l == 2: clientId, visible = parameters visible = bool(visible) self._setClientData(clientId, "visible", visible) logger.info(f"Client {clientId} visibility is now: {visible}") self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientHasOptionalGui(self, parameters:list): """/nsm/gui/client/has_optional_gui ['nFDBK'] nsmd sends us this as reaction to a clients announce capabilities list """ l = len(parameters) if l == 1: clientId = parameters[0] self._setClientData(clientId, "hasOptionalGUI", True) logger.info(f"Client {clientId} supports optional GUI") else: raise NotImplementedError(parameters) def _reactCallback_statusChanged(self, parameters:list): """ Handles all status messages. Some changes, like removed and quit, are only available as status. This means that status removed is the opposite of /nsm/gui/client/new, even if it doesn't read like it. /nsm/gui/client/status ['nFDBK', 'open'] /nsm/gui/client/status ['nMAJH', 'launch'] /nsm/gui/client/status ['nLUPX', 'ready'] /nsm/gui/client/status ['nLUPX', 'save'] /nsm/gui/client/status ['nFHLB', 'quit'] /nsm/gui/client/status ['nLUPX', 'removed'] /nsm/gui/client/status ['nLUPX', 'stopped'] /nsm/gui/client/status ['nLUPX', 'noop'] #For dumb clients! no nsm support! /nsm/gui/client/status ['nLUPX', 'switch'] /nsm/gui/client/status ['nLUPX', 'error'] """ l = len(parameters) if l == 2: clientId, status = parameters logger.info(f"Client status {clientId} now {status}") r = self._setClientData(clientId, "lastStatus", status) if r: #a known race condition at quit may delete this in between calls self.internalState["clients"][clientId]["statusHistory"].append(status) if status == "ready": #we need to check for this now. Below in actions is after the statusHook and too late. self._setClientData(clientId, "dumbClient", False) self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) #Now handle our actions. For better readability in separate functions. actions = { "open": self._reactStatus_open, "launch": self._reactStatus_launch, "ready": self._reactStatus_ready, "save": self._reactStatus_save, "quit": self._reactStatus_quit, "removed": self._reactStatus_removed, "stopped": self._reactStatus_stopped, "noop": self._reactStatus_noop, "switch": self._reactStatus_switch, "error": self._reactStatus_error, }[status](clientId) actions #pylint does not like temporary dicts for case-switch def _reactStatus_removed(self, clientId:str): """Remove the client entry from our internal state. This also covers crashes.""" if clientId in self.internalState["clients"]: #race condition at quit del self.internalState["clients"][clientId] if self.dataStorage and clientId == self.dataStorage.ourClientId: #We only care about the current data-storage, not another instance that was started before it. self.dataClientNamesHook(None) self.dataClientDescriptionHook(None) self.dataClientTimelineMaximumDurationChangedHook(None) self.dataStorage = None def _reactStatus_stopped(self, clientId:str): """The client has stopped and can be restarted. The status is not saved. NSM will try to open all clients on session open and end in "ready" """ if self.dataStorage and clientId == self.dataStorage.ourClientId: #We only care about the current data-storage, not another instance that was started before it. self.dataClientNamesHook(None) self.dataClientDescriptionHook(None) self.dataClientTimelineMaximumDurationChangedHook(None) self.dataStorage = None def _reactStatus_launch(self, clientId:str): """ Launch is a transitional status for NSM clients but the terminal status for dumb clients """ pass def _reactStatus_open(self, clientId:str): """ """ pass def _reactStatus_ready(self, clientId:str): """ This is sent after startup but also after every save. It signals that the client can react to nsm signals, not that it is ready for something else. Note that this is *After* the clientStatusHook, so any data changed here is not submitted to the api/GUI yet. E.g. you can't change dumbClient to True here if that is needed directly after start by the GUI. """ pass def _reactStatus_save(self, clientId:str): """ """ pass def _reactStatus_quit(self, clientId:str): """ """ pass def _reactStatus_noop(self, clientId:str): """ Dumb clients, or rather nsmd, react with noop on signals they cannot understand, like saving. """ pass def _reactStatus_switch(self, clientId:str): """ """ pass def _reactStatus_error(self, clientId:str): """ """ logger.error(f"{clientId} has error status!") def _reactReply_nsmServerOpen(self, answer:str): assert answer == "Loaded.", answer def _reactReply_nsmServerSave(self, answer:str): assert answer == "Saved.", answer def _reactReply_nsmServerClose(self, answer:str): assert answer == "Closed.", answer def _reactReply_nsmServerAbort(self, answer:str): assert answer == "Aborted.", answer def _reactReply_nsmServerAdd(self, answer:str): """Reaction to add client""" assert answer == "Launched.", answer def _reactReply_nsmServerRemoved(self): pass def _reactReply_nsmServerStopped(self): pass def _reactReply_nsmServerDuplicate(self, answer:str): """There are a lot of errors possible here, reported through nsmd /error, because we are dealing with the file system. Our own GUI and other safeguards should protect us from most though Positive answers are 'Duplicated.' when nsmd finished copying and 'Loaded.' when the new session is loaded. Or so one would think... the messages arrive the other way around. Anyway, both are needed to signify a succesful duplication. """ assert answer == "Loaded." or answer == "Duplicated.", answer #We don't need any callbacks here, nsmd sends a session change on top of the duplicate replies. def _reactReply_nsmServerNew(self, answer:str): """Created. arrives when a new session is created for the first time and directory is mkdir Session created arrives when a session was opened and nsm created its internal "session". We do not need to react to the new signal because we watch the dir for new sessions ourselves and the currently active session is send through "/nsm/gui/session/name" : self._reactCallback_activeSessionChanged, """ assert answer == 'Created.' or answer == "Session created", answer def _reactReply_nsmServerList(self, nsmSessionName:str): """Session names come one reply at a time. We reacted to the message /nsm/gui/server/message ['Listing sessions'] by clearing our internal session status and will save the new ones here /reply ['/nsm/server/list', 'test3'] Do not confuse reply server list with the message /nsm/server/list [0, 'Done.'] The latter is a top level message :( """ self.internalState["sessions"].add(nsmSessionName) #Our own functions def allClientsHide(self): for clientId, clientDict in self.internalState["clients"].items(): if clientDict["hasOptionalGUI"]: self.clientHide(clientId) def allClientsShow(self): for clientId, clientDict in self.internalState["clients"].items(): if clientDict["hasOptionalGUI"]: self.clientShow(clientId) def clientToggleVisible(self, clientId:str): if self.internalState["clients"][clientId]["hasOptionalGUI"]: if self.internalState["clients"][clientId]["visible"]: self.clientHide(clientId) else: self.clientShow(clientId) #data-storage / nsm-data def clientNameOverride(self, clientId:str, name:str): """An agordejo-specific function that requires the client nsm-data in the session. If nsm-data is not present this function will write nothing, not touch any data. It will still send a callback to revert any GUI changes back to the original name. We accept empty string as a name to remove the name override """ if self.dataStorage: assert clientId in self.internalState["clients"], self.internalState["clients"] self.dataStorage.setClientOverrideName(clientId, name) #triggers callback #data-storage / nsm-data def setDescription(self, text:str): if self.dataStorage: self.dataStorage.setDescription(text) #data-storage / nsm-data def setTimelineMaximumDuration(self, minutes:int): if self.dataStorage: self.dataStorage.setTimelineMaximumDuration(minutes) def _checkDirectoryForSymlinks(self, path)->bool: for p in path.rglob("*"): if p.is_symlink(): return True return False def _checkIfLocked(self, nsmSessionName:str)->bool: basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() lockFile = pathlib.Path(basePath, ".lock") return lockFile.exists() def forceLiftLock(self, nsmSessionName:str): """Removes lockfile, no matter if session is actually open or just a remainder from a crash. If no lock exist it does nothing.""" self._updateSessionListBlocking() if self._checkIfLocked(nsmSessionName): basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() #implied by _checkIfLocked lockFile = pathlib.Path(basePath, ".lock") lockFile.unlink(missing_ok=True) logger.info(f"{nsmSessionName} was forced to unlock by us.") else: logger.info(f"Tried to unlock, but {nsmSessionName} is not locked") def getSessionFiles(self, nsmSessionName:str)->list: """Return all session files, useful to present to the user, e.g. as warning before deletion""" self._updateSessionListBlocking() basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() return [f.as_posix() for f in basePath.rglob("*")] #Includes directories themselves #Only files, no directories themselves. #result = [] #for path, dirs, files in walk(basePath): # for file in files: # result.append(pathlib.Path(path, file).as_posix()) #return result def deleteSession(self, nsmSessionName:str): """Delete project directory with all data. No undo. Only if session is not locked""" self._updateSessionListBlocking() if not nsmSessionName in self.internalState["sessions"]: logger.warning(f"{nsmSessionName} is not a session") return False basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() if not self._checkIfLocked(nsmSessionName): try: logger.info(f"Deleting session {nsmSessionName}: {self.getSessionFiles(nsmSessionName)}") shutilrmtree(basePath) except PermissionError: logger.warning(f"Tried to delete {basePath} but permission was denied.") else: logger.warning(f"Tried to delete {basePath} but it is locked") self._updateSessionListBlocking() #if we don't update our internal representation the watchdog will go mad. def renameSession(self, nsmSessionName:str, newName:str): """Only works if session is not locked and dir does not exist yet""" self._updateSessionListBlocking() newPath = pathlib.Path(self.sessionRoot, newName) oldPath = pathlib.Path(self.sessionRoot, nsmSessionName) assert oldPath.exists() if self._checkIfLocked(nsmSessionName): logger.warning(f"Can't rename {nsmSessionName} to {newName}. {nsmSessionName} is locked.") return False elif newPath.exists(): logger.warning(f"Can't rename {nsmSessionName} to {newName}. {newName} already exists.") return False else: logger.info(f"Renaming {nsmSessionName} to {newName}.") tmp = pathlib.Path(oldPath.name+str(uuid4())) #Can't move itself into a subdir in itself. move to temp first. We don't use tempdir because that could be on another partition. we already know we can write here. oldPath.rename(tmp) pathlib.Path(newPath).mkdir(parents=True, exist_ok=True) tmp.rename(newPath) assert newPath.exists() def copySession(self, nsmSessionName:str, newName:str, progressHook=None): """Copy a whole tree. Keep symlinks as symlinks. Lift lock. If progressHook is provided (e.g. by a GUI) it will be called at regular intervals to inform of the copy process, or at least that it is still running. """ self._updateSessionListBlocking() source = pathlib.Path(self.sessionRoot, nsmSessionName) destination = pathlib.Path(self.sessionRoot, newName) if destination.exists(): logger.warning(f"Can't copy {nsmSessionName} to {newName}. {newName} already exists.") return False elif not nsmSessionName in self.internalState["sessions"]: logger.warning(f"{nsmSessionName} is not a session") return elif not source.exists(): logger.warning(f"Can't copy {nsmSessionName} because it does not exist.") return False #All is well. try: def mycopy(): shutilcopytree(source, destination, symlinks=True, dirs_exist_ok=False) #raises an error if dir already exists. But we already test above. if progressHook: def waiter(copyProcess): """Compare the final size with the current size and generate a percentage from it, which we send as progress""" sourceDirectorySize = sum(f.stat().st_size for f in source.glob('**/*') if f.is_file()) - 2048 #padded so we don't create an infinite loop from a rounding error destinationDirectorySize = sum(f.stat().st_size for f in destination.glob('**/*') if f.is_file()) #destinationDirectorySize does not start at 0. the copy() function might already by running before waiter() starts. while destinationDirectorySize < sourceDirectorySize: if not copyProcess.is_alive(): break percentString = str( int((destinationDirectorySize / sourceDirectorySize) * 100)) + "%" progressHook(percentString) sleep(0.5) #don't send too much. two times a second is plenty. #For next round destinationDirectorySize = sum(f.stat().st_size for f in destination.glob('**/*') if f.is_file()) """ #This moves both processes away from the main thread. It works, but Qt will not update anymore #We need a way to just spawn one extra process and wait/processHook in the main process processes = [] for function in (waiter, mycopy): proc = Process(target=function) proc.start() processes.append(proc) for proc in processes: proc.join() """ proc = Process(target=mycopy) proc.start() waiter(proc) #has the while loop to wait and check proc proc.join() #finish #Do a check if both dirs are equal progressHook("Veryfying file-integrity. This may take a while...") #string gets translated in qt gui mainwindow. Don't change just this here. sourceHash = md5_dir(source) desinationHash = md5_dir(destination) if not sourceHash == desinationHash: logger.error("ERROR! Copied session data is different from source session. Please check you data!") progressHook("ERROR! Copied session data is different from source session. Please check you data!") #ERROR! is a keyword for the gui wait dialog to not switch away. This gets translated in the Qt GUI mainwindow. Don't change this string else: mycopy() self.forceLiftLock(newName) except Exception as e: #we don't want to crash if user tries to copy to /root or so. logger.error(e) return False #Export to the User Interface def sessionAsDict(self, nsmSessionName:str)->dict: assert self.sessionRoot entry = {} entry["nsmSessionName"] = nsmSessionName entry["name"] = pathlib.Path(nsmSessionName).name basePath = pathlib.Path(self.sessionRoot, nsmSessionName) sessionFile = pathlib.Path(basePath, "session.nsm") if not sessionFile.exists(): #This is a reason to let the program exit. logger.error("Got wrong session directory from nsmd. Race condition after delete? In any case a breaking error (please report). Quitting. Project was: " + repr(sessionFile)) sysexit() #return None switch to return None to let it crash and see the python traceback timestamp = datetime.fromtimestamp(sessionFile.stat().st_mtime).isoformat(sep=" ", timespec='minutes') entry["lastSavedDate"] = timestamp entry["sessionFile"] = sessionFile entry["lockFile"] = pathlib.Path(basePath, ".lock") entry["fullPath"] = str(basePath) #No generator expression for the next one. We need to watch out for PermissionError (sudo chmod 000) sizeInBytes = 0 for f in basePath.glob('**/*'): try: if f.exists() and f.is_file(): sizeInBytes += f.stat().st_size except PermissionError: logger.error(f"PermissionError for {f}. It is possible that the file is read-protected. Trying to load the session anyway, please be careful.") entry["sizeInBytes"] = sizeInBytes entry["numberOfClients"] = len(open(sessionFile).readlines()) entry["hasSymlinks"] = self._checkDirectoryForSymlinks(basePath) entry["parents"] = basePath.relative_to(self.sessionRoot).parts[:-1] #tuple of each dir between NSM root and nsmSessionName/session.nsm, exluding the actual project name. This is the tree entry["locked"] = self._checkIfLocked(nsmSessionName) #not for direct display return entry def exportSessionsAsDicts(self)->list: """Return a list of dicts of projects with additional information: """ logger.info("Exporting sessions to dict. Will call blocking list sessions next") results = [] #assert not self.internalState["currentSession"], self.internalState["currentSession"] #Do not request session list while in active session self._updateSessionListBlocking() for nsmSessionName in self.internalState["sessions"]: result = self.sessionAsDict(nsmSessionName) results.append(result) return results class DataStorage(object): """Interface to handle the external datastorage client url is pre-processed (host, port) Our init is the same as announcing the nsm-data client in the session. That means everytime nsm-data sends a new/open reply we get created. Thus we will send all our data to parent and subsequently to GUI-callbacks in init. Keys are strings, While nsmd OSC support int, str and float we use json exclusively. We send json string and parse the received data. Try to use only ints, floats, strin gs, lists and dicts. Client pretty names are limited to 512 chars, depending on our OSC message size. nsm-data will just cut to 512 chars. So a GUI should better protect that limit. """ def __init__(self, parent, ourClientId, messageSizeLimit:int, url:tuple, sock): logger.info("Create new DataStorage instance") self.parent = parent self.messageSizeLimit = messageSizeLimit # e.g. 512 self.ourClientId = ourClientId self.clients = parent.internalState["clients"] #shortcut. Mutable, persistent dict, until instance gets deleted. self.url = url self.sock = sock self.ip, self.port = self.sock.getsockname() #Get initial data. Directly send to the api->GUI. self.data = self.getAll() #blocks. our local copy. = {"clientOverrideNames":{clientId:nameOverride}, "description":"str", "timelineMaximumDuration":"minutes in int"} self.namesToParentAndCallbacks() self.descriptionToParentAndCallbacks() self.timelineMaximumDurationToParentAndCallbacks() def namesToParentAndCallbacks(self): self.parent.dataClientNamesHook(self.data["clientOverrideNames"]) def descriptionToParentAndCallbacks(self): """Every char!!!""" self.parent.dataClientDescriptionHook(self.data["description"]) def timelineMaximumDurationToParentAndCallbacks(self): self.parent.dataClientTimelineMaximumDurationChangedHook(self.data["timelineMaximumDuration"]) def _waitForMultipartMessage(self, pOscpath:str)->str: """Returns a json string, as if the message was sent as a single one. Can consist of only one part as well.""" logger.info(f"Waiting for multi message {pOscpath} in blocking mode") self.parent._setPause(True) jsonString = "" chunkNumberOfParts = float("+inf") #zero based currentPartNumber = float("-inf") #zero based while True: if currentPartNumber >= chunkNumberOfParts: break try: data, addr = self.sock.recvfrom(1024) except socket.timeout: break msg = _IncomingMessage(data) if msg.oscpath == pOscpath: currentPartNumber, l, jsonChunk = msg.params jsonString += jsonChunk chunkNumberOfParts = l #overwrite infinity the first time and redundant afterwards. else: self.parent._queue.append(msg) self.parent._setPause(False) logger.info(f"Message complete with {chunkNumberOfParts} chunks.") return jsonString def getAll(self): """Mirror everything from nsm-data""" msg = _OutgoingMessage("/agordejo/datastorage/getall") msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) jsonString = self._waitForMultipartMessage("/agordejo/datastorage/reply/getall") return json.loads(jsonString) def setTimelineMaximumDuration(self, minutes:int): msg = _OutgoingMessage("/agordejo/datastorage/settimelinemaximum") msg.add_arg(json.dumps(minutes)) self.sock.sendto(msg.build(), self.url) self.getTimelineMaximumDuration() def getTimelineMaximumDuration(self): msg = _OutgoingMessage("/agordejo/datastorage/gettimelinemaximum") msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) #Wait in blocking mode self.parent._setPause(True) while True: try: data, addr = self.sock.recvfrom(1024) except socket.timeout: break msg = _IncomingMessage(data) if msg.oscpath == "/agordejo/datastorage/reply/gettimelinemaximum": jsonMinutes = msg.params[0] #list of one answerMinutes = json.loads(jsonMinutes) break else: self.parent._queue.append(msg) self.parent._setPause(False) #Got answer assert type(answerMinutes) is int, (answerMinutes, type(answerMinutes)) self.data["timelineMaximumDuration"] = answerMinutes self.timelineMaximumDurationToParentAndCallbacks() def setClientOverrideName(self, clientId:str, value): """We accept empty string as a name to remove the name override""" assert clientId in self.clients, self.clients msg = _OutgoingMessage("/agordejo/datastorage/setclientoverridename") msg.add_arg(clientId) msg.add_arg(json.dumps(value)) self.sock.sendto(msg.build(), self.url) self.getClientOverrideName(clientId) #verifies data and triggers callback def getClientOverrideName(self, clientId:str): msg = _OutgoingMessage("/agordejo/datastorage/getclientoverridename") msg.add_arg(clientId) msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) #Wait in blocking mode self.parent._setPause(True) while True: try: data, addr = self.sock.recvfrom(1024) except socket.timeout: break msg = _IncomingMessage(data) if msg.oscpath == "/agordejo/datastorage/reply/getclient": replyClientId, jsonName = msg.params assert replyClientId == clientId, (replyClientId, clientId) break else: self.parent._queue.append(msg) self.parent._setPause(False) #Got answer answer = json.loads(jsonName) if answer: self.data["clientOverrideNames"][clientId] = answer else: #It is possible that a client not present in our storage will send an empty string. Protect. if clientId in self.data["clientOverrideNames"]: del self.data["clientOverrideNames"][clientId] self.namesToParentAndCallbacks() def _chunkstring(self, string): return [string[0+i:self.messageSizeLimit+i] for i in range(0, len(string), self.messageSizeLimit)] def setDescription(self, text:str): """This most likely arrives one char at time with the complete text""" chunks = self._chunkstring(text) descriptionId = str(id(text))[:8] for index, chunk in enumerate(chunks): msg = _OutgoingMessage("/agordejo/datastorage/setdescription") msg.add_arg(descriptionId) msg.add_arg(index) msg.add_arg(chunk) msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) #No echo answer. #We cheat a bit and inform parents with the new text directly. self.data["description"] = text self.descriptionToParentAndCallbacks() #and back #Generic Functions. Not in use and not ready. def _test(self): self.readAll() self.setDescription("Ein Jäger aus Kurpfalz,\nDer reitet durch den grünen Wald,\nEr schießt das Wild daher,\nGleich wie es ihm gefällt.") self.read("welt") self.create("welt", "world") self.read("welt") self.create("str", "bar") self.create("int", 1) self.create("list", [1, 2, 3]) self.create("tuple", (1, 2, 3)) #no tuples, everything will be a list. self.create("dict", {1:2, 3:4, 5:6}) self.update("str", "rolf") self.delete("str") def read(self, key:str): """Request one value""" msg = _OutgoingMessage("/agordejo/datastorage/read") msg.add_arg(key) msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) def readAll(self): """Request all data""" msg = _OutgoingMessage("/agordejo/datastorage/readall") msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) def create(self, key:str, value): """Write/Create one value.""" msg = _OutgoingMessage("/agordejo/datastorage/create") msg.add_arg(key) msg.add_arg(json.dumps(value)) self.sock.sendto(msg.build(), self.url) def update(self, key:str, value): """Update a value, but only if it exists""" msg = _OutgoingMessage("/agordejo/datastorage/update") msg.add_arg(key) msg.add_arg(json.dumps(value)) self.sock.sendto(msg.build(), self.url) def delete(self, key:str): """Delete a key/value completely""" msg = _OutgoingMessage("/agordejo/datastorage/delete") msg.add_arg(key) self.sock.sendto(msg.build(), self.url) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9711657 agordejo-0.3.1/engine/resources/gen_grepexcluded.sh0000644000175000017500000000142100000000000021117 0ustar00nilsnils#!/bin/sh sudo pacman -Fy pacman -Fx "/usr/bin/([A-Z])" | cut -d " " -f1 | uniq | sort > allexe.txt pacman -Fl $(pacman -Sg pro-audio |cut -d " " -f2) | cut -d " " -f2 | grep "usr/bin" | uniq | sort > audioexe.txt sed -i -e 's/usr\/bin\///g' audioexe.txt #strip usr/bin yes, the initial / from usr is missing sed '/^[[:space:]]*$/d' -i audioexe.txt #remove empty lines grep -vFf audioexe.txt allexe.txt > grepexcluded2.txt grep '^usr\/bin\/' grepexcluded2.txt > grepexcluded.txt #only keep lines that start with usr/bin. There are some false positives in there sed -i -e 's/usr\/bin\///g' grepexcluded.txt #strip usr/bin yes, the initial / from usr is missing sed '/^[[:space:]]*$/d' -i grepexcluded.txt #remove empty lines rm grepexcluded2.txt rm allexe.txt rm audioexe.txt ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/resources/grepexcluded.txt0000644000175000017500000061241000000000000020501 0ustar00nilsnilsa2ping a2ps a2x a2x.py a52dec a5toa4 a68g aa-audit aa-autodep aa-cleanprof aa-complain aa-decode aa-disable aa-easyprof aa-enabled aa-enforce aa-exec aafire aa-genprof aainfo aalib-config aa-logprof aa-mergeprof aa-notify aarch64-linux-gnu-addr2line aarch64-linux-gnu-ar aarch64-linux-gnu-as aarch64-linux-gnu-c++ aarch64-linux-gnu-c++filt aarch64-linux-gnu-cpp aarch64-linux-gnu-dwp aarch64-linux-gnu-elfedit aarch64-linux-gnu-g++ aarch64-linux-gnu-gcc aarch64-linux-gnu-gcc-10.1.0 aarch64-linux-gnu-gcc-ar aarch64-linux-gnu-gcc-nm aarch64-linux-gnu-gcc-ranlib aarch64-linux-gnu-gcov aarch64-linux-gnu-gcov-dump aarch64-linux-gnu-gcov-tool aarch64-linux-gnu-gdb aarch64-linux-gnu-gdb-add-index aarch64-linux-gnu-gfortran aarch64-linux-gnu-gprof aarch64-linux-gnu-ld aarch64-linux-gnu-ld.bfd aarch64-linux-gnu-ld.gold aarch64-linux-gnu-lto-dump aarch64-linux-gnu-nm aarch64-linux-gnu-objcopy aarch64-linux-gnu-objdump aarch64-linux-gnu-ranlib aarch64-linux-gnu-readelf aarch64-linux-gnu-size aarch64-linux-gnu-strings aarch64-linux-gnu-strip aa-remove-unknown aasavefont aa-status aa-teardown aatest aa-unconfined ab abc2ly abcconvert abcde abcde-musicbrainz-tool abcdiff abcecho abcechobounds abcls abctree abduco abidump abigen abiword abook abrecovery abuse abuse.elf abuse-tool abw2html abw2raw abw2text acat acceleration_speed accept_check accerciser accessdb accountwizard accuraterip-checksum ace acetoneiso aclocal aclocal-1.16 acme acme acme-redirect acme.sh acme-tiny aconnect acorn acpi acpibin acpid acpidump acpiexamples acpiexec acpihelp acpi_listen acpisrc acpixtract activate-global-python-argcomplete activity-log-manager actmerge actsync actsyncd acyclic ada_count adb addcomputer.py adddebug addedgeg addftinfo addgnupghome addld addpart addpass addr2line addr2name address6 adf2hdf adhocfilelist adie Adie.stx adiff adjacency adjacency_gmp admin.sld adnmz adnsheloex adnshost adnslogres adnsresfilter adrdox adriconf advdef adventure advmng advpng advtest advzip aec aegisub-3.2 aem2john aeson-pretty afew affixcompress afl-analyze afl-clang afl-clang++ afl-clang-fast afl-clang-fast++ afl-cmin afl-collect afl-cron afl-fuzz afl-g++ afl-gcc afl-gotcpu afl-minimize afl-multicore afl-multikill afl-plot afl-showmap afl-stats afl-sync afl-tmin afl-vcrash afl-whatsup afm2afm afm2pl afm2tfm afmtodit afp_client afpcmd afpfs afpfsd afpgetstatus afterimage-config afterimage-libs aft-mtp-cli aft-mtp-mount ag ag-backup agda agda-mode agentxtrap agetty aggregate-avatars aggregate_profile.pl agrep ag-tool aiksaurus airbase-ng aircrack-ng airdecap-ng airdecloak-ng airdrop-ng aireplay-ng airgraph-ng airmon-ng airodump-join airodump-ng airolib-ng airserv-ng airspy_gpio airspy_gpiodir airspy_info airspy_lib_version airspy_r820t airspy_rx airspy_si5351c airspy_spiflash airtun-ng airventriloquist-ng aix2john akaidump akaiextract akit akonadi2xml akonadi_agent_launcher akonadi_agent_server akonadi_akonotes_resource akonadi_archivemail_agent akonadi_birthdays_resource akonadiconsole akonadi_contacts_resource akonadi_control akonadictl akonadi_davgroupware_resource akonadi_ewsmta_resource akonadi_ews_resource akonadi_followupreminder_agent akonadi_googlecalendar_resource akonadi_googlecontacts_resource akonadi_icaldir_resource akonadi_ical_resource akonadi_imap_resource akonadiimportwizard akonadi_indexing_agent akonadi_kalarm_dir_resource akonadi_kalarm_resource akonadi_knut_resource akonadi_kolab_resource akonadi_maildir_resource akonadi_maildispatcher_agent akonadi_mailfilter_agent akonadi_mbox_resource akonadi_migration_agent akonadi_mixedmaildir_resource akonadi_newmailnotifier_agent akonadi_notes_agent akonadi_notes_resource akonadi_openxchange_resource akonadi_pop3_resource akonadi_rds akonadiselftest akonadi_sendlater_agent akonadiserver akonaditest akonadi_tomboynotes_resource akonadi_unifiedmailbox_agent akonadi_vcarddir_resource akonadi_vcard_resource akregator akregatorstorageexporter al al2 alacarte alacritty albert alc alcc alembic alertmanager alevtd alex alffplay alhrtf alicloud-vault align align_image_stack alive2map.sh alive6 aliyun allatency allcm allec allegro-config allfaces allfaces_gmp allneeded alloopback alltray alltray-ctt-helper almanah almultireverb alot alplay alreverb als alsabat alsabat-test.sh alsactl alsa_delay alsa_in alsa_in alsa-info.sh alsaloop alsa_loopback alsa_out alsa_out alsatplg alsaucm alsoft-config alstream alt_getopt alt_getopt5.1 alt_getopt5.2 altonegen alurecdplay alureplay alurestream amavisd amavisd-agent amavisd-milter amavisd-nanny amavisd-release amavisd-signer amavisd-snmp-subagent amavisd-snmp-subagent-zmq ambiguous_words amd_hdmi_compliance amidi amm amptest amqp-consume amqp-declare-queue amqp-delete-queue amqp-get amqp-publish amrnb-dec amrnb-enc amrwb-dec amtog amtool amuFormat.sh amule amulecmd amuled amulegui amuleweb anacron analyseplugin analyze analyze-pv-structure andotp2john androidbackup2john androidfde2john android-file-transfer angband animate anjuta anjuta-launcher anjuta-tags anki annexBbytecountStatic annotate ansi2html ansible ansible2john ansible-bender ansible-config ansible-connection ansible-console ansible-doc ansible-galaxy ansible-inventory ansible-lint ansible-playbook ansible-pull ansible-test ansible-vault ant anthy-agent anthy-dic-tool anthy-morphological-analyzer antiword antlr4 any2djvu anything-sync-daemon anytopnm anytovcd.sh aomdec aomenc aoss AP100 AP210 apachectl apack apcaccess apctest apcupsd apex2john apirst2html.py apitrace apitrace32 apl aplay aplaymidi aplist apm apparmor_parser apparmor_status apper applenotes2john applycal applygeo applygnupgdefaults applyplugin appres appstream-builder appstreamcli appstream-compose appstream-generator appstream-util apr-1-config apricots apropos APserver apu-1-config apxs apython aqbanking-cli aqbanking-config aqhbci-tool4 aqpaypal-tool ar ar86 arandr arara arbfplight arbfslight arbocclude arbocclude2 arch-audit archboot-svn.sh archboot-tarball-helper.sh archboot-testing-package.sh archboot-uefi-cd.sh archboot-x86_64-iso.sh archbuild arch-chroot archco archey3 archive archive-cleaner archive.inn archlinux-java arch-meson arch-nspawn archrelease arcverify arduino arduino-builder arduino-cli arduino-ctags aread arepack argbash argbash-1to2 argbash-init argon2 argparse-manpage aria2c aria_chk aria_dump_log aria_ftdump aria_pack aria_read_log ario arithmetic arj arjdisp arj-register ark arlatex armagetronad armagetronad_bin arm-none-eabi-addr2line arm-none-eabi-ar arm-none-eabi-as arm-none-eabi-c++ arm-none-eabi-c++filt arm-none-eabi-cpp arm-none-eabi-dwp arm-none-eabi-elfedit arm-none-eabi-g++ arm-none-eabi-gcc arm-none-eabi-gcc-10.1.0 arm-none-eabi-gcc-ar arm-none-eabi-gcc-nm arm-none-eabi-gcc-ranlib arm-none-eabi-gcov arm-none-eabi-gcov-dump arm-none-eabi-gcov-tool arm-none-eabi-gdb arm-none-eabi-gdb-add-index arm-none-eabi-gprof arm-none-eabi-ld arm-none-eabi-ld.bfd arm-none-eabi-ld.gold arm-none-eabi-lto-dump arm-none-eabi-nm arm-none-eabi-objcopy arm-none-eabi-objdump arm-none-eabi-ranlib arm-none-eabi-readelf arm-none-eabi-size arm-none-eabi-strings arm-none-eabi-strip arp arpaname arp-fingerprint arping arp-scan arpsnmp arpspoof arptables arptables arptables-nft arptables-nft arptables-nft-restore arptables-nft-restore arptables-nft-save arptables-nft-save arptables-restore arptables-restore arptables-save arptables-save arpwatch array artikulate artikulate_editor artist_to_albumartist as as10k1 as2gbmap as86 asapcat asar ascheckttf ascii asciidoc asciidoc.py asciidoctor asciinema asciinema.sh asciiportal asciiquarium asciitopgm ascii-xfr ascompose asd aseqdump aseqnet aserver asflip asg-hevc asgrad asi18n asl-acousticWaves asl-bus_wind asl-compressor asl-cubeGravity asl-cubeIncompressibleGravity asl-cubePoroelasticGravity asl-flow asl-flow2 asl-flow3 asl-flowKDPGrowth asl-flowRotatingCylinders asl-hardware asl-jumpingBox asl-levelSetBasic asl-levelSetFacetedGrowth asl-levelSetNormalGrowth asl-locomotive asl-locomotive_laminar asl-locomotive_stability asl-multicomponent_flow asl-multiphase_flow asl-pitot_tube_ice asl-poroelastic asl-surfaceFlux asl-testSMDiff asl-testSMDiff3C asl-testSMPhi asl-testSMPhiBV asm2wasm asm_count asmerge asn1Parser asoundconf asoundconf-gtk asp aspell aspell-import assimp assistant assistant-qt5 astext astile astraceroute astromenace astyle asunder asvector asview asy at at51 atari-convert-dir atari-hd-image atc atd atexec.py atftp atktopbm atmaddr atmarp atmdiag atmdump atmloop atmsigd atmswitch atmtcp atoc_conv atom AtomicParsley atomix atool atop atop-2.5.0 atopacctd atopconvert atopgpud atopsar atopsar-2.5.0 atq ATR_analysis atril atril-previewer atril-thumbnailer atrm atrun attr aubio aubiocut aucat audacious audaconvert audaplay audaremap audex audio-convert audisp-remote auditbeat auditctl auditd audtool augenrules augmatch augparse augtool aulast aulastlog aunpack aureport aurphan aurpublish ausearch austro ausyscall auth/ authorindex auth/passwd/ auth/passwd/auth_krb5 auth/passwd/ckpasswd auth/passwd/radius auth/resolv/ auth/resolv/domain auth/resolv/ident authvar autob5 autoconf autoconf-2.13 autocutsel autoexpect autogb autogen autogsdoc autoheader autoheader-2.13 autoinst autom4te automake automake-1.16 automat-visualize automat-visualize2 automount auto-multiple-choice autooptimiser autoopts-config autopasswd autopep8 autopep8-python2 autopoint autorandr autoreconf autoreconf-2.13 autoscan autoscan-2.13 autosp autossh autrace auvirt avahi-bookmarks avahi-browse avahi-browse-domains avahi-daemon avahi-discover avahi-discover-standalone avahi-dnsconfd avahi-publish avahi-publish-address avahi-publish-service avahi-resolve avahi-resolve-address avahi-resolve-host-name avahi-set-host-name avbtool avcenc average avfs-config avfsd avidemux3_cli avidemux3_jobs_qt5 avidemux3_qt5 avifix aviindex avimerge avinfo avisplit avisync avivotool avr-addr2line avr-ar avr-as avr-c++ avr-c++filt avr-cpp avrdude avr-elfedit avr-g++ avr-gcc avr-gcc-10.1.0 avr-gcc-ar avr-gcc-nm avr-gcc-ranlib avr-gcov avr-gcov-dump avr-gcov-tool avr-gdb avr-gdb-add-index avr-gprof avr-ld avr-ld.bfd avr-lto-dump avr-man avr-nm avr-objcopy avr-objdump avr-ranlib avr-readelf avr-size avr-strings avr-strip avstopam avtest awesome awesome-client awk awk_count awrite aws aws_completer aws_zsh_completer.sh awx axcrypt2john axel axfer axfr-reverse.sh axfr.sh axohelp b1ff b2 b2sum b43-fwcutter babe babel babeld babel-external-helpers babeltrace babeltrace2 babeltrace-log backgammon badblocks badtouch badvpn-client badvpn-flooder badvpn-ncd badvpn-ncd-request badvpn-server badvpn-tun2socks badvpn-tunctl badvpn-udpgw bak2db.pl baka-mplayer baksmali balooctl baloo_file baloosearch balooshow balsa balsa-ab balsam bam bandit bandit-baseline bandit-config-generator bandwhich banner baobab barcode barrage barrier barrierc barriers base32 base64 base64conv basename basenc bash bashbrew bashbug bashburn bash-language-server bashrun bashtop basket bass bat batch batch_crop batcher batch_image_convert batch_rubber_sheet batctl bats battlestar bazel bazel-real bbe bbl2bib bbox bbpager bc bcat bcc bccmd bcd bchscheme bchunk bcmfw bcomps bcrelay bdaddr bdftogd bdftopcf bdftruncate bd_info bearoffdump beaver be.contacts beep beet bemenu bemenu-run bench_bitcoin benchcmp benchmark benzene besside-ng besside-ng-crawler bestcrypt2john bettercap bezier bf_compact bf_copy bfgminer bfgminer-rpc bfort bf_tar bg5conv bg5+latex bg5latex bglafile bgldepend bgldepend.sh bgljas bgljas.sh bgljfile bgljfile.sh bglmake bglmake.sh bglmco bglmco.sh bglmem bglmemrun bglpp bglpp.sh bglprof bglprof.sh bgltags bgltags.sh bgp_btoa bib2gls bib2html bibdoiadd bibexport bibhva bibletime biblex bibmradd bibparse biburl2doi bibzbladd bigloo bigloo4.3f bijiben bin2ecm bin2fex bin2hex.py bin2iso bin2obj binaural bindgen binlm2arpa binwalk bioauth bioctl biodrvctl bioradtopgm biosdecode biplabg bird birdc birdcl birdfont birdfont-autotrace birdfont-export birdfont-import bison bitcoin2john bitcoin-cli bitcoind bitcoin-qt bitcoin-tx bitlbee bitlocker2john bitmap bitmap2component bitshares2john bitwarden2john bjam bjoentegaard bks2john black blackbox blackd black-hole-solve bladeRF-cli bladeRF-fsk bleachbit blender blender-softwaregl blender-thumbnailer.py blescan blind_id blinken blinking-teapot blinksocks bliss blkcalc blkcat blkdeactivate blkdiscard blkid blkls blkstat blkzone blobby blobby-server blobwars blockchain2john blockdev blockdiag block-rate-estim blocks blood-elf blueberry blueberry-tray bluedevil-sendfile bluedevil-wizard bluefish bluegriffon blueman-adapters blueman-applet blueman-assistant blueman-manager blueman-report blueman-sendto blueman-services blueman-tray bluemoon bluetooth bluetoothctl bluetooth-player bluetooth-sendto blur_image bmake bmon bmptopnm bmptoppm bnamazu bneptest bochs boggle bogofilter bogolexer bogotune bogoupgrade bogoutil boinc boinc boinc_client boinc_client boinccmd boinccmd boincmgr boinctui boltctl bombardment bomber bond2team bonnie++ bonzomatic boobands boobank boobathon boobcoming boobill boobmsg boobooks boobsize boobtracker bookman boomoney bootctl bootnode borg borgfs borgmatic botan bottle.py bottle.py2 bounce bovo bower bowler boxdumper bpf_asm bpfc bpf_dbg bpf_jit_disasm bpftool bpython bpython-curses bpython-urwid br2684ctl brasero brctl break_filelist breathe-apidoc breeze-settings5 brick bridge brightnessctl brltty brltty-atb brltty-cldr brltty-config brltty-ctb brltty-ktb brltty-lscmds brltty-lsinc brltty-morse brltty-trtxt brltty-ttb brltty-tune bro-0.6 broadwayd brook brooklyn broot brotli browserify browserpass brushtopbm brz bs1770gain bs2bconvert bs2bstream bsatool bscalc bsdcat bsdcpio bsdiff bsdl2jtag bsdtar bsetbg bsetroot bshell bspatch bspc bspwm bsqldb bsqlodbc bssh bstyleconvert bt747 bt-adapter bt-agent btattach btcflash btconfig bt-device btfdiff btfs btfsstat btgatt-client btgatt-server bt_get bt_get2 bti btinfo btiotest bti-shrink-urls btmgmt btmon bt-network bt-obex btpclient btplay btproxy btrfs btrfsck btrfs-convert btrfs-extent-same btrfs-find-root btrfs-image btrfs-map-logical btrfs-select-super btrfstune btsnoop buckle buckygen buddy-ng budgie-daemon budgie-desktop budgie-desktop-settings budgie-extras-daemon budgie-panel budgie-polkit-dialog budgie-run-dialog budgie-wm buffchan buffindexed_d bugpoint bugpoint buildah buildbot buildbot-worker build_docbook_index build_filelist build_firefox.sh buildhash build-progress.sh build_tmdb buildxpi.py bullet3_examplebrowser bully bumblebee-bugreport bumblebeed bump bundle bundledoc bundler bunzip2 buoh bup burnBX burnK6 burnK7 burnMMX burnP5 burnP6 bus busctl busybox bvnc bwm-ng bwrap bximage byacc byobu byobu-config byobu-ctrl-a byobu-disable byobu-disable-prompt byobu-enable byobu-enable-prompt byobu-export byobu-janitor byobu-launch byobu-launcher byobu-launcher-install byobu-launcher-uninstall byobu-layout byobu-prompt byobu-quiet byobu-reconnect-sockets byobu-screen byobu-select-backend byobu-select-profile byobu-select-session byobu-shell byobu-silent byobu-status byobu-status-detail byobu-tmux byobu-ugraph byobu-ulevel byuu byzanz-playback bzadmin bzcat bzdiff bzflag bzfs bzgrep bzip2 bzip2recover bzmore bzr bzr-receive-pack bzr-upload-pack bzz c++ c1541 c2dec c2enc c2hs c44 c++-8 c89 c99 cabal cacaclock caca-config cacademo cacafire cacaplay cacaserver cacaview cache_check cache_dump cache_metadata_size cachepic cache_repair cache_restore cache_writeback cadaver caddy caesar caff cage caiksaurus cairo-dock cairo-sphinx cairosvg cairo-trace caja caja-autorun-software caja-connect-server caja-file-management-properties caja-sendto cake cal calc calc-prorate calc-prorate2 calc_stat calc_tickadj calculator calcurse calcurse-caldav calcurse-upgrade calcurse-vdir calendarjanitor calgebra calibrate_lens_gui calibre calibre-alternatives calibre-complete calibre-customize calibredb calibre-debug calibre-parallel calibre-server calibre-smtp calidns callgraph callgrind_annotate callgrind_control calligra calligraconverter calligragemini calligrageminithumbnailhelper calligraplan calligraplanwork calligrasheets calligrastage calligrawords calmjs cameratopam camlp4 camlp4boot camlp4o camlp4of camlp4of.opt camlp4oof camlp4oof.opt camlp4o.opt camlp4orf camlp4orf.opt camlp4prof camlp4r camlp4rf camlp4rf.opt camlp4r.opt camlp5 camlp5o camlp5o.opt camlp5r camlp5r.opt camlp5sch canberra-boot canberra-gtk-play canbusutil cancel canfield canorus cantata canto-curses canto-daemon cantor canto-remote cantor_juliaserver cantor_pythonserver cantor_rserver cantor_scripteditor cap2hccapx caph capinfos capnp capnpc capnpc-c++ capnpc-capnp caprine capsh captest captoinfo captype card cardos-tool cargo cargo cargo-add cargo-audit cargo-cbuild cargo-cinstall cargo-clippy cargo-clippy cargo-crev cargo-fmt cargo-fmt cargo-fuzz cargo-miri cargo-outdated cargo-rm cargo-upgrade cargo-watch caribou-preferences cartconv cas cas_help caspol castget cat cataclysm cataclysm-tiles catatonit catchsegv catdoc catfish catg catimg catman catppt catstomp cauchydeclgen cauchymake cauchymc cawbird cb2ti3 cbatticon cbc cb_console_runner cbindgen cb_share_config cc cc-8 ccache ccache2john ccache-swig cccheck ccd2iso cchardetect ccl cclive ccls ccmake ccmakedep ccomps cconfig c++-copy-class-and-file c_count ccrewrite cct cctiff ccxxmake ccze cdb cd-convert cd-create-profile cdda2mp3 cdda2ogg cdda2wav cdda-player cddb_query cddb-tool cddexec cddexec_gmp cd-discid cd-drive cdemu cdemu-daemon cdfdiff cd-fix-profile cd-iccdump cdiff cd-info cd-it8 cd-paranoia cdparanoia cdr2raw cdr2text cdr2xhtml cdrdao cd-read cdrskin cdrwtool cdxa2mpeg cecc-client cecc-client-5.0.0 cec-client cec-client-5.0.0 cec-compliance cec-ctl cec-follower cef5conv cef5latex cefconv ceflatex cefsconv cefslatex celery celeste_standalone celestia celluloid celtdec celtdec051 celtenc celtenc051 ceph ceph-authtool ceph_bench_log ceph-bluestore-tool ceph-client-debug ceph-clsinfo ceph-conf ceph-coverage ceph-crash ceph-create-keys ceph-debugpack cephdeduptool ceph-dencoder ceph_erasure_code ceph_erasure_code_benchmark cephfs-data-scan cephfs-journal-tool cephfs-shell cephfs-table-tool ceph-fuse ceph_kvstorebench ceph-kvstore-tool ceph-mds ceph-mgr ceph-mon ceph-monstore-tool ceph_multi_stress_watch ceph_objectstore_bench ceph-objectstore-tool ceph_omapbench ceph-osd ceph-osdomap-tool ceph_perf_local ceph_perf_msgr_client ceph_perf_msgr_server ceph_perf_objectstore ceph-post-file ceph_psim ceph_radosacl ceph-rbdnamer ceph_rgw_jsonparser ceph_rgw_multiparser ceph-run ceph_scratchtool ceph_scratchtoolpp ceph-syn ceph-volume ceph-volume-systemd cert2spc certbot certmgr cert-sync cert-to-efi-hash-list cert-to-efi-sig-list certtool certutil cervisia cfcomp cfdisk cfftot1 c++filt cfn-lint cfscores cftp cftp3 cgal_create_CMakeLists cgal_create_cmake_script cgal_make_macosx_app cg_annotate cgc cgconfig cgdb cg_diff cgdisk cgi-fcgi cgi-wrapper cgm cgmanager cg_merge cgminer cgnscalc cgnscheck cgnscompress cgnsconvert cgnsdiff cgnslist cgnsnames cgnsnodes cgnsplot cgnstools/ cgnstools/aflr3_to_cgns cgnstools/calcwish cgnstools/cgiowish cgnstools/cgns_to_aflr3 cgnstools/cgns_to_fast cgnstools/cgns_to_plot3d cgnstools/cgns_to_tecplot cgnstools/cgns_to_vtk cgnstools/convert_dataclass cgnstools/convert_location cgnstools/convert_variables cgnstools/extract_subset cgnstools/fast_to_cgns cgnstools/interpolate_cgns cgnstools/patran_to_cgns cgnstools/plot3d_to_cgns cgnstools/plotwish cgnstools/tecplot_to_cgns cgnstools/tetgen_to_cgns cgnstools/vgrid_to_cgns cgnsview cgoban cgproxy cgps cgrep cgroup_event_listener chacl chafa chage changestool change_tz chardetect chardetect-py2 charon-cmd charon-systemd chartread chat chattr chcon chcp chcpu cheapskate cheatmake checkap check_api checkbandwidth checkbashisms checkcites checkers checkgid check_hd check_isa checkisomd5 checklistings checkmk checkpkg checkpoint-admin checkpto checkrad check-regexp checksctp checksec check-selftest checkXML5 cheese chef chem chemtool chemtoolbg cheroot cheroot2 cherryd cherryd2 chewing-editor chezdav chezmoi chfn chg chgpasswd chgrp chicken chicken-do chicken-install chicken-profile chicken-status chicken-uninstall chinese-calendar chinese-calendar-autostart chkdvifont chkfont chklref chkstow chktex chktrust chkweb ch_lab chmcmd chmem chm_http chmls chmod chmorph chntpw choom chop choptest choqok chown chpasswd chromedriver chrome-gnome-shell chromium chromium-bsu chronic chronyc chronyd chroot chroot.fakechroot chrootuid chrpath chrt chsh chsh.ldap cht ch_track ch_utt chvt ch_wave ci cifscreds cifsdd cifs.idmap cifsiostat cifs.upcall cigloo cinder c-index-test cineoob cinnamon cinnamon2d cinnamon-desktop-editor cinnamon-file-dialog cinnamon-install-spice cinnamon-json-makepot cinnamon-killer-daemon cinnamon-launcher cinnamon-looking-glass cinnamon-menu-editor cinnamon-preview-gtk-theme cinnamon-screensaver cinnamon-screensaver-command cinnamon-screensaver-lock-dialog cinnamon-session cinnamon-session-cinnamon cinnamon-session-cinnamon2d cinnamon-session-quit cinnamon-settings cinnamon-settings-users cinnamon-slideshow cinnamon-subprocess-wrapper cinnamon-xlet-makepot circo circuits cisco2john cisco-decrypt cistopbm cjb2 cjdroute cjk-gs-integrate cjpeg cjs cjs-console ckermit ckermit.ini ckermit-sshsub ckeygen ckeygen3 cksfv cksum cl clamav-config clamav-milter clambc clamconf clamd clamdscan clamdtop clamonacc clamtk clang clang++ clang-10 clang-apply-replacements clang-change-namespace clang-check clang-cl clang-cpp clangd clang-doc clang-extdef-mapping clang-format clang-import-test clang-include-fixer clang-move clang-offload-bundler clang-offload-wrapper clang-query clang-refactor clang-rename clang-reorder-fields clang-scan-deps clang-tidy clash clasp class-11d.x class-4d.x class-5d.x class-6d.x classifier_tester class.x clawsker claws-mail clazy clazy-standalone clddb-tool cldrparser cl-dump cl-dump.pl cleanallruv.pl cleanlinks cleanup_digikamdb cleanup-rules clear clear-color clear-fbo clear-fbo-scissor clear-fbo-tex clear-random clear-repeat clear-scissor clear-undefined clef clementine clementine-tagreader clevis clevis-decrypt clevis-decrypt-sss clevis-decrypt-tang clevis-decrypt-tpm2 clevis-encrypt-sss clevis-encrypt-tang clevis-encrypt-tpm2 clevis-luks-bind clevis-luks-common-functions clevis-luks-list clevis-luks-unbind clevis-luks-unlock cli4 clientsession-generate clinfo clipctl clipfsck clipgrab clipmenu clipmenud clipnotify clippy-driver clippy-driver clisp clisp-link clit clj cllualatex cloc clockdiff clojure clonezilla cloud-id cloud-init cloud-init-per cloud-localds cloud-publish-image cloud-publish-tarball cloud-publish-ubuntu clp clpeak cltest cluster clusterdb cluttex clxelatex cm256_rx cm256_test cm256_tx cm2html cm2latex cm2man cm2pseudoxml cm2rem.tcl cm2xetex cm2xml cmail cmake cmake-fedora-fedpkg cmake-fedora-koji cmake-fedora-newprj cmake-fedora-pkgdb cmake-fedora-reset cmake-fedora-zanata cmake-gui cmark cmark-gfm cmark-py3 cmatrix cmis-client cmp cmpp cmsd cmst cmsutil cmucl cmus cmus-remote cmuwmtopbm cmx2raw cmx2text cmx2xhtml cnfsheadconf cnfsstat cntraining co cobalt cobc cob-config cobcrun cobol_count cockatrice cockney cockpit-bridge code code2color codeblocks code-oss codepage codespell codiff coffee cog cog.py col col1 colcrt collateindex.pl collectd collectdctl collectdmon collectd-nagios collectd-tg collectl2pcp collink colorcet colordiff colord-kde-icc-importer colorgcc colorhug-backlight colorhug-ccmx colorhug-cmd colorhug-flash colorhug-refresh colorit colormap colormgr colorsvn colprof colrm column columns colverify com combinator combinator3 combine combinediff combine_lang_model combine_tessdata combipow com.github.babluboy.bookworm com.github.davidmhewitt.torrential com.github.johnfactotum.Foliate com.github.needleandthread.vocal com.github.robertsanseries.ciano comicthumb comm commitpkg community2extra communityco communitypkg community-stagingpkg community-testingpkg compare comparoob comp_err compface compilebench compile_et complement complg composeglyphs composer composite compress compsize compton compton-trans compute_all compute_sloc_lang comsatd comtool conch conch3 configure-printer conflict conf.py conjure conky conky-manager conmon connect6 connexion connmanctl connmand connmand-wait-online connman-vpnd connsplit6 connsplit6.sh conntrack conntrackd conplay construct_mohawk consul consul-template contactprintthemeeditor contactthemeeditor containerd container-diff containerd-shim containerd-shim-runc-v1 containerd-shim-runc-v2 containerd-stress context contextjit contractor control/ controlbatch controlchan control/checkgroups.pl control/ihave.pl control/newgroup.pl ControlPanel control/rmgroup.pl control/sendme.pl control/sendsys.pl control/senduuname.pl control/version.pl convbkmk convdate converseen converseg convert convert-caffe2-to-onnx convert-caffe2-to-onnx convert-caffe2-to-onnx convert-caffe2-to-onnx ConvertCDDextToLatte ConvertCDDineToLatte convert-dtsv0 convertfilestops convertformat convertgls2bib convert_hd convert-json convert-ly convert-mans convert_NtoMbit_YCbCrStatic convert-onnx-to-caffe2 convert-onnx-to-caffe2 convert-onnx-to-caffe2 convert-onnx-to-caffe2 convertquota convertsegfilestops converttops convmv convolution convolutions cookboob cookbook cookiecutter cool-retro-term copydatabase copy_files_verbose.py copy-from-journal copyg copy-mountpoint.sh copypix copyq copytex copy-to-journal coqc coqchk coqdep coqdoc coqide coqidetop coqidetop.opt coq_makefile coqpp coqproofworker.opt coqqueryworker.opt coqtacticworker.opt coq-tex coqtop coqtop.opt coqwc coqworkmgr cordova coredumpctl core_perl/ core_perl/corelist core_perl/cpan core_perl/enc2xs core_perl/encguess core_perl/h2ph core_perl/h2xs core_perl/instmodsh core_perl/json_pp core_perl/libnetcfg core_perl/perlbug core_perl/perldoc core_perl/perlivp core_perl/perlthanks core_perl/piconv core_perl/pl2pm core_perl/pod2html core_perl/pod2man core_perl/pod2text core_perl/pod2usage core_perl/podchecker core_perl/prove core_perl/ptar core_perl/ptardiff core_perl/ptargrep core_perl/shasum core_perl/splain core_perl/streamzip core_perl/xsubpp cork-hash corkscrew corrade-rc corrupt_mpeg2 couchdb-dump couchdb-load couchdb-load-design-doc couchdb-replicate couchpy count count count_extensions countg count-linear-forms-from-polynomial countmail count_unknown_ext cover coverage coverage2 coverage-2.7 coverage3 coverage-3.8 covert_send6 covert_send6d cowfortune cowpatty cowsay cowthink coxeter cozy-desktop cozy-stack cp cpack cpaldjvu cpclean cpfind cpio cpnt cpp cpp2html cpp-8 cppcheck cppcheck-gui cppcheck-htmlreport cpphs cpplint cprepair cpspc cpufreq-bench cpufreq-bench_plot.sh cpufreq-selector cpuid cpuinfo cpulimit cpupower cpustat cqtest cracf2john cracklib-check cracklib-format cracklib-packer cracklib-unpacker crackNum cram crash crawl crawl-tiles crda create_ap create-cracklib-dict create_cvsignore createdb create-debian-live CreateDOMDocument create-drbl-live create-drbl-live-by-pkg createfp create-gparted-live create-image create_makefile create_makefiles create-native-map create_network_map.sh createPYMB create_svnignore create-ubuntu-live createuser createwin c_rehash c++-rename-class-and-file cribbage criterion-report critter crlutil croc croco-0.6-config crond cronnext cron-pacmatic crontab crun crushtool cryfs cryfs-unmount cryptdir cryptoflex-tool cryptol cryptol-html cryptominisat5 cryptominisat5_simple cryptsetup cryptsetup-reencrypt crystal ct3_to_ntlm ctags ctail ctanbib ctangle ctanify ctan-o-mat ctanupload ctdb ctdbd ctdbd_wrapper ctdb_local_daemons ctdb_run_cluster_tests ctdb_run_tests ctest ctie ctlinnd ctpl ctr ctracer ctrlaltdel ct_run ct_run ctstat ctwill ctwill-refsort ctwill-twinx cu cu2 cubemap cuberender cubex cubhamg cue2toc cuebreakpoints cueconvert cueprint cuetag.sh cuneiform cupsaccept cups-browsed cups-calibrate cups-config cupsctl cupsd cupsdisable cupsenable cupsfilter cupsreject cura CuraEngine curator curator_cli curl curl-config curlftpfs curse curseofwar curve_keygen curvetun custom_storage cut cutb cutsel Cutter cuttlefish cuyo cvc4 cvlc cvs cvsaddcurrentdir cvsaskpass cvsbackport cvsblame cvsbug cvscheck cvs-clean cvsco.tdf cvsdiff.tdf cvsforwardport cvslastchange cvslastlog cvsps cvsrevertlast cvsservice5 cvsup.tdf cvsversion cvt cvtbatch cvtenc cvtsudoers cweave cwebp cws-11d.x cws-4d.x cws-5d.x cws-6d.x cws.x cx18-ctl cxf2ti3 cxfreeze cxfreeze-quickstart cxpm cxxmetric cxxtestgen cxx_to_docbook cycfx2prog cyclicdeadline cyclictest cygdb cygdb2 cygdb3 cysignals-CSI cython cython2 cython3 cythonize cythonize2 cythonize3 dab danmaQ darkstat darktable darktable-chart darktable-cli darktable-cltest darktable-cmstest darktable-generate-cache darktable-rs-identify dart dart2js dart2native dartanalyzer dartaotruntime dartdevc dartdoc dartfmt dash DashCast dashlane2john dat dat2c dat2s data2inc datacopy datamash datastore-service date dateadd dateconv datediff dategrep dateround dateseq datesort datetest datezone dav1d davpass dawg2wordlist daxctl dazzle-list-counters db2bak.pl db2dvi db2html db2index.pl db2ldif.pl db2man db2ps db2rtf db2tex db2texi db2txt db2x_manxml db2x_texixml db2x_xsltproc db4-entities.pl dbar db_archive db_checkpoint dbclient dbconverter db_deadlock db_dump dbeaver dbfadd dbfcat dbfcreate dbfdump dbfinfo dbgen.pl db_hotbackup dbl2png dblatex db_load db_log_verify dbmail-deliver dbmail-export dbmail-pop3d dbmail-sievecmd dbmail-timsieved dbmail-users dbmail-util dbm-fc-solver dbmmanage dbpmda db_printlog db_recover db_replicate dbscan db_stat db_tuner db_upgrade dbus-broker dbus-broker-launch dbus-cleanup-sockets dbus-daemon dbus-hslogger-client dbus-launch dbus-monitor dbus-run-session dbus-send dbus-test-tool dbus-uuidgen dbusxx-introspect dbusxx-xml2cpp db_verify dbwrap_tool dbxtool dc dc1394_reset_bus dcadec dcd-client dcd-server dchroot dcomexec.py dconf dconf-editor dcraw dcraw_emu dcraw_half dctool dd ddbugtopbm ddclient ddcmon ddcutil dde-calendar dde-clipboard dde-clipboardloader dde-control-center dde-desktop dde-dock dde-file-manager dde-file-manager-daemon dde-file-manager-pkexec dde-launcher dde-license-dialog dde-lock ddemangle dde-open dde-pixmix dde-printer dde-property-dialog dde-shutdown dde-switchtogreeter dde-wm-chooser ddjvu ddns-confgen dd_rescue ddrescue ddrescuelog deadbeef deadline_test deallocvt debootstrap debugapp debugclient debugfs debugfs.reiserfs debuginfod debuginfod-find debugreiserfs dec265 decine decode-dimms decode-edid decode_tm6000 decode-vaio decomp decryptdir dedit deepin-album deepin-anything-monitor deepin-anything-tool deepin-anything-tool-ionice deepin-boot-maker deepin-calculator deepin-clone deepin-clone-honest deepin-clone-ionice deepin-clone-pkexec deepin-compressor deepin-desktop-ts-convert deepin-draw deepin-editor deepin-fix-xauthority-perm deepin-generate-mo deepin-greeter deepin-image-viewer deepin-menu deepin-metacity deepin-metacity-message deepin-metacity-theme-viewer deepin-metacity-window-demo deepin-movie deepin-music deepin-mutter deepin-picker deepin-policy-ts-convert deepin-reader deepin-screensaver deepin-screenshot deepin-shortcut-viewer deepin-system-monitor deepin-terminal deepin-topbar deepin-turbo-invoker deepin-turbo-single-instance deepin-wm deepin-wm-dbus deepsound2john dee-tool defaults deflatehd defncopy defrag.f2fs deghosting_mask degob deheader dehtmldiff dehydrated deja-dup dekyra delaunay deledgeg delp delpart delptg deluge deluge-console deluged deluge-gtk deluge-web delv de-macro denial6 dep dephell depmod deprince depth-dbm-fc-solver depythontex derb deriv descumm designer designer-qt5 design_filter deskey_to_ntlm desktop-file-edit desktop-file-install desktop-file-validate desktoptojson desmume desmume-cli desword2 detect-new-ip6 detect_sniffer6 determine_maximum_mpps.sh detex detox deurlname devdump devede_ng.py devhelp devilspie devlink devmon devnag devp2p deweb dex df dfa_determinize dfa_minimize dfc d-feet dfmt dfrotz dfrs dfu-prefix dfu-programmer dfu-suffix dfu-tool dfu-util dgen dgen_tobin dhall dhall-to-bash dhall-to-json dhall-to-yaml dhclient dhclient-script dhcpcd dhcpclient dhcping dhcrelay dhex DHT_bootstrap dhtchat dhtcluster dhtnode dht_put dhtscanner dia diadia diagtool dialog dialog-config dialtest dialyzer dialyzer diatheke dict dictd dictdplugin-config dictfmt dictfmt_index2suffix dictfmt_index2word dictl dict_lookup dictunformat dictzip diesel diet diff diff3 diff-cover diffeet diffimg diffoscope diffpp diff-quality diff-so-fancy diffstat diff_tpl_auto_escape diffuse dig digikam digitaglinktree digraph dijkstra dikcube dillo dillo-install-hyphenation dir dircolors directg dirmngr dirmngr-client dirname dis86 disable-paste disco discord disdvi diskcryptor2john diskus dislocate disorderfs dispcal display display-buttons displaycal displaycal-3dlut-maker displaycal-apply-profiles displaycal-curve-viewer displaycal-eecolor-to-madvr-converter displaycal-profile-info displaycal-scripting-client displaycal-synthprofile displaycal-testchart-editor displaycal-vrml-to-x3d-converter displaycal-vrml-to-x3d-converter-console display-coords dispread dispwin dissolve distcc distccd distccmon-gnome distccmon-text distro distro2 ditaa django-admin django-admin3 django-admin3.py django-admin.py djpeg djview djview4 djvm djvmcvt djvudigital djvudump djvuextract djvumake djvups djvused djvuserve djvutoxml djvutxt djvuxmlparser dkfilter.in dkfilter.out dkimsign dkimverify dkms dknewkey dk_responder.pl dkverify.pl dl10k1 dlist-begin-call-end dlist-dangling dlist-degenerate dlist-edgeflag dlist-edgeflag-dangling dlist-flat-tri dlist-mat-tri dlist-recursive-call dlist_test dlist-tri-flat-tri dlist-tri-mat-tri DllPlugInTester dlsdump dltest dlv dm dmd dmemory-warning-dialog dmenu dmenu_path dmenu_run dmesg dmeventd dmevent_tool dmg2img dmg2john dmidecode dmraid dmsetup dmstats dmtcp_command dmtcp_discover_rm dmtcp_launch dmtcp_nocheckpoint dmtcp_restart dmtcp_rm_loclaunch dmtcp_srun_helper dmtcp_ssh dmtcp_sshd dm-tool dmypy dnet dnet-config dnie-tool dnsbulktest dnscrypt-proxy dnscrypt-wrapper dnsdict6 dnsdomainname dnsget dnsgram dnsmasq dnspcap2calidns dnspcap2protobuf dnsreplay dnsrevenum6 dnsrevenum6.sh dnsscan dnsscope dnssec-cds dnssec-checkds dnssec-coverage dnssec-dsfromkey dnssec-importkey dnssec-keyfromlabel dnssec-keygen dnssec-keymgr dnssec-revoke dnssec-settime dnssec-signzone dnssec-verify dnssecwalk dnssecwalk.sh dnssort dnsspoof dnstcpbench dnstracer dnswasher doas doc2lt docbook2dvi docbook2html docbook2man docbook2man docbook2ps docbook2rtf docbook2tex docbook2texi docbook2texi docbook2txt docfdisk docheckgroups docker docker docker-compose dockerd docker-init docker-machine docker-proxy doc_loadbios doctest doctext doctl docx2txt doge doh-client doh-server doit dolphin dolphin-emu dolphin-emu-nogui DOMCount DOMPrint dontpanic dopewars dos2unix dosbox dosdebug dosemu dosemu.bin dosepsbin dosfsck dosfslabel dos_mld6.sh dos-new-ip6 dot dot2gxl dot2tex dot_builtins dotenv dotlock dotlockfile dotnet dotty double doublecmd doveadm doveconf dovecot dovecot-sysreport doxygen doxywizard dp DPAPImk2john dpapi.py dpcd_reg dpid dpidc dpipe dpkg dpkg-architecture dpkg-buildflags dpkg-buildpackage dpkg-checkbuilddeps dpkg-deb dpkg-distaddfile dpkg-divert dpkg-genchanges dpkg-gencontrol dpkg-gensymbols dpkg-maintscript-helper dpkg-mergechangelogs dpkg-name dpkg-parsechangelog dpkg-query dpkg-scanpackages dpkg-scansources dpkg-shlibdeps dpkg-source dpkg-split dpkg-statoverride dpkg-trigger dpkg-vendor dpl4hydra.sh dprof2calltree dqtile-cmd dqtool dracut dracut-catimages dragon draw2arrays drawarrays DRAWEXE DRAWEXE-7.4.0 drawing drawl draw_lib_dependencies drawoverhead drawpix drawrange drbl-3n-conf drbl4imp drbl-add-vir-netdev drbl-all-service drbl-aoe-img-dump drbl-aoe-serv drbl-autologin-env-reset drbl-autologin-home-reset drbl-bug-report drbl-clean-autologin-account drbl-client-reautologin drbl-client-root-passwd drbl-client-service drbl-client-switch drbl-client-system-select drbl-collect-mac drbl-cp drbl-cp-host drbl-cp-user drbl-doit drbl-find-dhcp-srv drbl-fuh drbl-fuh-get drbl-fuh-put drbl-fuh-rm drbl-fuu drbl-fuu-get drbl-fuu-put drbl-fuu-rm drbl-gen-grub-efi-nb drbl-get-host drbl-get-user drbl-host-cp drbl-host-get drbl-host-rm drbl-live drbl-live-boinc drbl-live-hadoop drbl-login-switch drbl-netinstall drbl-proxy-dhcp drblpush drbl-pxelinux-passwd drbl-rm-host drbl-rm-user drbl-run-parts drbl-sl drblsrv drblsrv-offline drbl-swapfile drbl-syslinux-efi-pxe-sw drbl-syslinux-netinstall drbl-uriparse drbl-usb-netinstall drbl-useradd drbl-useradd-file drbl-useradd-list drbl-useradd-range drbl-user-cp drbl-userdel drbl-userdel-file drbl-userdel-list drbl-userdel-range drbl-user-env-reset drbl-user-get drbl-user-rm drbl-wakeonlan dreadnaut dretodot dretog drill driverless dropbear dropbearconvert dropbearkey dropuser drracket drs232 drs232_ldpc drumstick-drumgrid drumstick-dumpmid drumstick-dumpove drumstick-dumpsmf drumstick-dumpwrk drumstick-guiplayer drumstick-metronome drumstick-playsmf drumstick-sysinfo drumstick-vpiano dscanner dsconf dscreate dsctl dsdccx dsdp5 dselect dsidm ds-logpipe.py dsniff ds-replcheck dssi_analyse_plugin dssi_list_plugins dssi_osc_send dstat dsymutil dsymutil dsync dt2dv dtagnames dtc dtd2rng dtd2xsd dt-danechk dtdiff dt-getaddr dt-gethost dt-getname dt-getquery dt-getrrset dt-libval_check_conf dtools-catdoc dtools-ddemangle dtools-detab dtools-dget dtools-dustmite dtools-rdmd dtools-tolf dtsdec dt-validate dtxgen du dua dub dubdv dubya ducktype dul-receive-pack dul-upload-pack dulwich dumpcap dump_dhcp6 dumpe2fs dumpelf dumpexfat dump.f2fs dumpfeats dumpimage dumpiso dumpiso_mod dumpkeys dumpmscat dumpnames dump_nl80211 dump_osc dump_router6 dump_rtnetlink dumpseg dumpsexp dump_torrent dump-typelib dump_xsettings dumresp dune dunst dunstify duperemove duplicity durmeanstd dustmite dv2dt dvb-fe-tool dvb-format-convert dvbv5-daemon dvbv5-scan dvbv5-zap dvconnect dvcont dvdauthor dvdbackup dvddirdel dvd-ram-control dvd+rw-booktype dvd+rw-format dvd+rw-mediainfo dvdunauthor dvdxchap dvgrab dvi2fax dvi2tty dviasm dvibook dvidvi dvigif dvihp dviinfox dvilj dvilj2p dvilj4 dvilj4l dvilj6 dvilualatex dviluatex dvipng dvipos dvips dvired dviselect dvispc dvisvgm dvitodvi dvitomp dvitype dvtest dvtm dvtm-status dwarfdump dwarffortress dwdiff dwebp dwp dxf2idf dyecmd dynamicmusic dzen2 e2freefrag e2fsck e2fsdroid e2image e2label e2mmpstatus e2pall e2scrub e2scrub_all e2undo e3 e3em e3ne e3pi e3vi e3ws e4crypt e4defrag eapmd5tojohn eapol_test earlyoom easside-ng easy_install easy_install-2.7 easy_install-3.8 easyjson easyrsa easytag eatmydata ebb ebfc ebook2html ebook2raw ebook2text ebook-convert ebook-device ebook-edit ebook-meta ebook-polish ebook-viewer ebrowse ebrowse ebtables ebtables ebtables-nft ebtables-nft ebtables-nft-restore ebtables-nft-restore ebtables-nft-save ebtables-nft-save ebtables-restore ebtables-restore ebtables-save ebtables-save ec2metadata echo echoping echo_supervisord_conf ecj ecl ecl-config ecm ecm2bin ecnf ecore_evas_convert ecpg ecppack ecppll ecpunpack ecryptfs2john ecryptfs-add-passphrase ecryptfsd ecryptfs-find ecryptfs-insert-wrapped-passphrase-into-keyring ecryptfs-manager ecryptfs-migrate-home ecryptfs-mount-private ecryptfs-recover-private ecryptfs-rewrap-passphrase ecryptfs-rewrite-file ecryptfs-setup-private ecryptfs-setup-swap ecryptfs-stat ecryptfs-umount-private ecryptfs-unwrap-passphrase ecryptfs-verify ecryptfs-wrap-passphrase ed ed2k ed2k-link edac-ctl edac-util edbrowse eddystone edgepaint edit editcap editdiff edit-faxcover editorconfig editorconfig-0.12.3 editorconfig3 edje_cc edje_codegen edje_decc edje_external_inspector edje_inspector edje_pick edje_player edje_recc edje_watch edonr256-hash edonr512-hash edquota eeprom eepromer eeschema eet eetpack eeze_disk_ls eeze_mount eeze_scanner eeze_scanner_monitor eeze_umount efibootdump efibootmgr efikeygen efi-readvar efisiglist efitool-mkusb efivar efivar-tester efl_debug efl_debugd efl_wl_test efl_wl_test_stack efreetd eg egk-tool eglgears_x11 eglinfo eglinfo32 eglkms eglretrace egltri_x11 egrep ehrhart ehrhart3 eidenv eina_btlog einfo ejabberd2john ejabberdctl eject eldbus-codegen electron electron5 electron6 electron7 electrum electrum2john elf2dmp elf2dmp elf2hex elfedit elfls elftoc elinks elisa elixir elixirc elm_prefs_cc elua email_validator embryo_cc emby-server emelfm2 emoji-picker em-proxy EmptyEpsilon emptyvss emveepee enblend enc265 enca en_cas_help encfs encfs2john encfsctl encfssh enchant-2 enchant-lsmod-2 encodedv encode_keychange enconv enfuse engine engrampa enhance enigma enitune enjarify enlightenment enlightenment_askpass enlightenment_filemanager enlightenment_imc enlightenment_open enlightenment_remote enlightenment_start enpass2john enscript entr enum_chmLib enumdir_chmLib EnumVal env env.fakechroot env_parallel env_parallel.ash env_parallel.bash env_parallel.dash env_parallel.fish env_parallel.ksh env_parallel.mksh env_parallel.sh env_parallel.zsh envsubst envvars envvars-std envy24control eo_debug eog eolian_gen eolie eom epic epic-EPIC4-2.10.7 epiphany eplain epmd epmd eps2eps epsffit epsg_tr.py eptex epydoc epydocgui epylint epylint2 eqn eqn2graph era_check era_dump era_invalidate era_restore erb erb-2.6 eric6 eric6_api eric6_browser eric6_compare eric6_configure eric6_diff eric6_doc eric6_editor eric6_hexeditor eric6_iconeditor eric6_plugininstall eric6_pluginrepository eric6_pluginuninstall eric6_qregexp eric6_qregularexpression eric6_re eric6_shell eric6_snap eric6_sqlbrowser eric6_tray eric6_trpreviewer eric6_uipreviewer eric6_unittest erl erl erlc erlc erl-guestfs errno es2gears_wayland es2gears_wayland32 es2gears_x11 es2gears_x1132 es2_info es2_info32 es2tri es2tri32 escapesrc es_cas_help escp2topbm escputil escript escript esentutl.py esi ESingular eslint esmska espeak espeak espeak-ng espeakup espefuse.py esponja espsecure.py esptool.py es_repo_mgr esri2wkt.py estimator_ckpt_converter estimator_ckpt_converter estimator_ckpt_converter estimator_ckpt_converter etags etags etckeeper etex etf2ly etherape ethereum2john etherwall ethkey ethstore ethtool ethumb ethumbd ethumbd_client ETL-config etr ettercap ettercap ettercap-pkexec etterfilter etterfilter etterlog etterlog eu-addr2line eu-ar eu-elfclassify eu-elfcmp eu-elfcompress eu-elflint eu-findtextrel eu-make-debug-archive eu-nm eu-objdump euptex eu-ranlib eu-readelf eu-size eu-stack eu-strings eu-strip eutp eu-unstrip evcd2vcd evdev-joystick evdev-joystick evemu-describe evemu-device evemu-event evemu-play eventlogadm event_rpcgen.py eview evilginx evim evince evince-previewer evince-thumbnailer evm evolution evolution-import-rss evtest ewfacquire ewfacquirestream ewfdebug ewfexport ewfinfo ewfmount ewfrecover ewfverify ex exa examples example_to_doc++ exceltex exedat exempi exfalso exfatfsck exfatlabel exicyclog exifautotran exiftran exigrep exim exim_checkaccess exim_dbmbuild exim_fixdb exim_lock eximstats exim_tidydb exinext exipick exiqgrep exiqsumm exiv2 exiwhat exo-desktop-item-edit exo-open exo-preferred-applications expac expand expander exp_count expect expire expireover expirerm expiry exploit6 exportfs export_signing_key expr exr2aces ex-rdns exrenvmap exrheader exrmakepreview exrmaketiled exrmultipart exrmultiview exrstdattr ext2simg ext3grep ext4magic extconv extend_dmalloc external-ip extlinux extra2community extract extract_a52 extractattr extractbb extract_chmLib extract_dca extract_dts extract_hosts6.sh extracticc extract_mohawk extract_mpeg2 extract_networks6.sh extractrc extractres extractttag extrapkg extra-x86_64-build extresso extundelete eyeD3 eyuvtoppm f2cpsp f2fscrypt f2fstat f2py f2py2 f2py2.7 f2py3 f2py3.8 f90_count f95 f95-8 faac faad fab facter factor fadot fail2ban-client fail2ban-python fail2ban-regex fail2ban-server fail2ban-testcases faillog fake_advertise6 fakechroot fakeCMY faked fake_dhcps6 fake_dns6d fake_mipv6 fake_mld26 fake_mld6 fake_mldrouter6 fake_pim6 faker fakeread fakeroot fake_router26 fake_router6 fake_solicitate6 faketime falcon-bench falcon-print-routes falkon fallocate false fancontrol fanficfare fasd fasm fasm-listing fasm-prepsrc fasm-symbols fastboot fastd fast-import-filter fast-import-info fast-import-query fastjar fastjet-config fastrm fatlabel fatresize fatsort faucet fax2ps fax2tiff faxabort faxaddmodem faxadduser faxalter faxanswer faxconfig faxcover faxcron faxdeluser faxinfo faxlock faxmail faxmodem faxmsg faxq faxqclean faxquit faxrm faxsetup faxsetup.bsdi faxsetup.irix faxsetup.linux faxstat faxstate faxwatch fb fbc fb_config fbdump fbgrab fbgs fbi fbobind fbo_firecube fbotexture FBReader fbrun fbset fbsetbg fbsetroot fbtv fbv fbxine fcat fc-cache fc-cache-32 fc-cat fc-conflist fceux fceux-net-server fcgistarter fcgiwrap fcitx fcitx4-config fcitx5 fcitx5-config-qt fcitx5-configtool fcitx5-remote fcitx-autostart fcitx-config-gtk3 fcitx-configtool fcitx-dbus-watcher fcitx-diagnose fcitx-remote fcitx-skin-installer fc-list fc-match fc-pattern fc-query fcrackzip fcron fcrondyn fcronsighup fcrontab fc-scan fc-solve fc_solve_find_index_s2ints.py fc-validate fd fd2c fd2pascal fdformat fdisk fdkaac fdm fdmdv_demod fdmdv_get_test_bits fdmdv_mod fdmdv_put_test_bits fdp fdtdump fdtget fdtoverlay fdtput fdupes feathers feedreader feh ferm festival festival_client fetch-ebook-metadata fex2bin ffado-bridgeco-downloader ffado-dbus-server ffado-diag ffado-dice-firmware ffado-fireworks-downloader ffado-set-nickname ffado-test ffado-test-isorecv ffado-test-isoxmit ffado-test-streaming ffado-test-streaming-ipc ffado-test-streaming-ipcclient ffcfstress ffcfstress ffdetect-emby fff ffind fflas-ffpack-config ffmpeg ffmpeg2theora ffmpeg-emby ffmpeg_harvid ffmpegthumbnailer ffmsindex ffmvforce ffmvforce ffplay ffprobe ffprobe-emby ffprobe_harvid ffset ffset fftest fftest fftwf-wisdom fftwl-wisdom fftwq-wisdom fftw-wisdom fftw-wisdom-to-conf fgconsole fgrep fh2raw fh2svg fh2text fibmap.f2fs fidentify fido2-assert fido2-cred fido2-token fig2dev fig2ps fig2ps2tex fig4latex figlet figlist filan file file65 filebeat filecap filechan FileCheck FileCheck filedumper filedumper-slim filedumperx filefrag fileinfo filelight file-roller filesnarf filezilla fill fillets filter/ filterdiff filter/filter_innd.pl filter/filter_innd.py filter/INN.py filter/startup_innd.pl finch fincore find findaffix find-all-symbols findbits.py findDelegation.py finddeps find-freecell-deal-index.py findfs findhyph find-libdeps find-libprovides findmissingcrystal findmnt findmyhash findomain findsmb findssl.sh fingerterm fio fio2gnuplot fio-btrace2fio fio-dedupe fio_generate_plots fio-genzipf fiologparser_hist.py fiologparser.py fio-verify-state fire firecfg firefox firefox-developer-edition firejail firejail-ui firemon firetools firewall6 firewall-applet firewall-cmd firewall-config firewalld firewall-offline-cmd firrtl fish fish_indent fish_key_reader fisql fitscopy fitshdr fitstopnm five-or-more fiwalk fixcvsdiff fiximports fix-include.sh fixkdeincludes fixnt fixparts fixproc fixps fix-qdf fixqt4headers.pl fixqt4headers.pl-qt5 fixuifiles fixup-linkedattrs.pl fixup-memberof.pl flac flac123 flake8 flake8-python2 flameshot flashcp flash_erase flash_eraseall flash_lock flash_otp_dump flash_otp_info flash_otp_lock flash_otp_write flash-player-properties flashrom flash_unlock flash-var flask flatboob flatc flat-clip flatpak flatpak-bisect flatpak-builder flatpak-coredumpctl flatxml2po flawfinder flea flex flex++ flickcurl flickcurl-config flickrdf flit flite flite_cmu_us_kal16 flite_cmu_us_slt flobopuyo flock flood_advertise6 flood_dhcpc6 flood_mld26 flood_mld6 flood_mldrouter6 flood_redir6 flood_router26 flood_router6 flood_rs6 flood_solicitate6 flood_unreach6 floppyd floppyd_installtest flowtop fls flterm fltk-config fluid fluid-demo fluxbox fluxbox-generate_menu fluxbox-remote fluxctl fma-config-tool fm_demod fmt fmtutil fmtutil-sys fmtutil-user fn fnteditfs fogcoord fold foliate folks-import folks-inspect fontforge fontimage fontinst fontlint fonttools foomatic-addpjloptions foomatic-combo-xml foomatic-compiledb foomatic-configure foomatic-datafile foomatic-extract-text foomatic-fix-xml foomatic-getpjloptions foomatic-kitload foomatic-nonumericalids foomatic-perl-data foomatic-preferred-driver foomatic-printermap-to-gutenprint-xml foomatic-printjob foomatic-replaceoldprinterids foomatic-rip foomatic-searchprinter fop Forcevideo-drbl-live foremost formail fortran_count fortune fortune.pl fossil four2six fourier fourier_gmp four-in-a-row fox-config fp fpack fp.ans fpc fpcalc fpcjres fpclasschart fpcmake fpcmkcfg fpcres fping fplight fplll fppkg fprcp fprintd-delete fprintd-enroll fprintd-list fprintd-verify fractal fragcoord fragmaster fragmentation6 fragments fragrouter6 fragrouter6.sh fr_cas_help free freealut-config freebcp freecell-solver-fc-pro-range-solve freecell-solver-multi-thread-solve freecell-solver-range-parallel-solve freeciv-gtk3 freeciv-manual freeciv-mp-gtk3 freeciv-server freecol freedroid freedroidRPG freehdl-config freehdl-gennodes freehdl-v2cc freemind freerdp-proxy freerdp-shadow-cli freshclam fribidi frm frm_admin frm_purged frm_xfragent frm_xfrd frogatto from frotz fsadm fsarchiver fsc fsck fsck.btrfs fsck.cramfs fsck.exfat fsck.ext2 fsck.ext3 fsck.ext4 fsck.f2fs fsck.fat fsck.jfs fsck.minix fsck.msdos fsck.reiserfs fsck.vfat fsck.xfs fscrypt fsfreeze fsk_mod fslight fsraytrace fsstat fst2vcd fstminer fstopgm fs-tri fstrim fstrm_capture fstrm_dump fstrm_replay fs-uae fs-uae-device-helper fs-uae-launcher fsview ftbench ftchkwd ftdi_eeprom ftdiff ftdump ftgamma ftgrid ftl_check ftl_format ftlint ftmemchk ftmulti ftp ftpatchk ftpbench ftppass ftp-rfc ftstring fttimer ftvalid ftview fuck fudd fuidshift fulla fullcircle fullgen function_grep.pl function_grep.pl funetak funpack funzip fuseiso fuse-overlayfs fuser fusermount fusermount3 fusermount-glusterfs fusesoc futatabi futurize futurize2 fuzz_dhcpc6 fuzz_dhcps6 fuzz_ip6 fuzz_option fv3_impulser_jack fv3_jack_earlyr fv3_jack_earlyr2 fv3_jack_gd_lroom fv3_jack_impulser2 fv3_jack_mbcomp fv3_jack_noise1 fv3_jack_nrev fv3_jack_progenitor fv3_jack_revmodel fv3_jack_stenh fv3_jack_strev fvwm fvwm2 fvwm-bug FvwmCommand fvwm-config fvwm-convert-2.6 fvwm-menu-desktop fvwm-menu-directory fvwm-menu-headlines fvwm-menu-xlock fvwm-perllib fvwm-root fwbedit fwb_iosacl fwb_ipf fwb_ipfw fwb_ipt fwb_junosacl fwb_nxosacl fwb_pf fwb_pix fwb_procurve_acl fwbuilder fwknop fw_printenv fw_setenv fxa-client fzf fzf-tmux fzipinfo fzputtygen fzsftp fzy g++ g15daemon g2root g2root g3topbm g++-8 gacutil gacutil2 gaf gaiksaurus gajim gajim-history-manager gajim-remote gala gala-daemon galculator galera_new_cluster galera_recovery galleroob gambas3 gambas3.gambas gambcomp-C gambcomp-java gambcomp-js gambcomp-php gambcomp-python gambcomp-ruby gambdoc gambitc gameconqueror gamma gamma4scanimage gammaray gammu gammu-config gammu-detect gammu-smsd gammu-smsd-inject gammu-smsd-monitor gandi ganglia2pcp ganv_bench gap gapi2-codegen gapi2-fixup gapi2-parser gapi3-codegen gapi3-fixup gapi3-parser gapplication garbd garchive gate gattrib gatt-service gauche-cesconv gauche-config gauche-install gauche-package gaupol gawk gawk-5.1.0 gba3 gbc3 gbi3 gbklatex gbr3 gbrainy gbs3 gbs3.gambas gbw3 gbx3 gc gcab gcalccmd gcc gcc-8 gcc-ar gcc-ar-8 gccgo gccmakedep gcc-nm gcc-nm-8 gcc-ranlib gcc-ranlib-8 gcin gcin2tab gcin-exit gcin-gb-toggle gcin-kbm-toggle gcin-message gcin-tools gcm-import gcm-inspect gcm-picker gcm-viewer gcnmz gcolor2 gcolor3 gcompris-qt gcore gcov gcov-dump gcovr gcov-tool gcp-devrel-py-tools gcps2vec.py gcps2wld.py gcpubar gcron.py gcr-viewer gct-tool gd2copypal gd2i gd2togif gd2topng gda-browser-5.0 gda-control-center-5.0 gdal2tiles.py gdal2xyz.py gdaladdo gdal_auth.py gdalbuildvrt gdal_calc.py gdalchksum.py gdalcompare.py gdal-config gdal_contour gdaldem gdal_edit.py gdalenhance gdal_fillnodata.py gdal_grid gdalident.py gdalimport.py gdalinfo gda-list-config gda-list-config-5.0 gda-list-jdbc-providers-5.0 gda-list-server-op gda-list-server-op-5.0 gdallocationinfo gdalmanage gdal_merge.py gdalmove.py gdal_pansharpen.py gdal_polygonize.py gdal_proximity.py gdal_rasterize gdal_retile.py gdalserver gdal_sieve.py gdalsrsinfo gdaltindex gdaltransform gdal_translate gdalwarp gda-sql gda-sql-5.0 gda-test-connection-5.0 gdaui-demo-5.0 gdb gdb-add-index gdbar gdbm_dump gdbm_load gdbmtool gdb.py gdbserver gdbus gdbus-codegen gdc gdcmpgif gdialog gdiffmk gdisk gdk_pixbuf_mlsource gdk_pixbuf_mlsource3 gdk-pixbuf-pixdata gdk-pixbuf-query-loaders gdk-pixbuf-query-loaders-32 gdk-pixbuf-thumbnailer gdm gdmap gdmflexiserver gdm-screenshot gdnc gdnsd gdnsdctl gdnsd_geoip_test gdomap gdome-config gdparttopng gdtopng geany gearadmin gearbox gearman gearmand gears geartrain geary geckodriver gedit geeqie gegl gegl-imgcmp gegps geli2john gem gem-2.6 gemtopbm gemtopnm genbg genbgL gen_binary_files genbrk gencat genccode gencfu gencmn gencnval gendarme gendarme-wizard gendesk gendict gendiff gen-enc-table generate generate-borgmatic-config generate_config generate-domains-blacklist generate_log_config generate-ngram generate-pxe-menu generate_randfile generate-rules generate_signing_key.py generic_count genfasm genfio genflagcodes genfstab geng gengetopt gen-grub-efi-nb-menu genhash genisoimage genius genl genl-ctrl-list genlinfo gen-loadpulses genmipmap genmkvpwd genmod genmodel gen-multiple-pysol-layouts gennorm2 genpmda genpmk genpyt genquarticg genrang genrb genreflex genreflex genresscript gens gens2shp gensiot genspecialg gensprep gensymm gentest gen-torrent-from-ptcl gentourng gentreeg gen_unigram GenX_IR genXrdPattern genxs geocode geocpset geod geogebra geoiplookup geoiplookup6 geolooc geom-outlining-130 geom-outlining-150 geom-sprites geom-stipple-lines geom-wide-lines geos-config geotag geotifcp gephi geqn gerbv gerbview ges-launch-1.0 GetADUsers.py getArch.py getcap get_character.py getcifsacl get_company.py getconf get_cpuinfo_mhz.sh getc_putc getc_putc_helper get_cyclictest_snapshot getdefs get_device getdns_query getdns_server_mon getdpi get-drbl-conf-param get_driver get-edid getent getent.ldap getfacl getfattr get_first_character.py get_first_company.py get_first_movie.py get_first_person.py geth gethostip get-iab getkeycodes get_keyword.py getlist get_machine get_module get_movie.py get_moz_enUS.py GetNPUsers.py getopt get-oui getPac.py getpcaps get_person.py get_sloc get_sloc_details getST.py getsysinfo gettext gettextize gettext.sh getTGT.py get_top_bottom_movies.py GetUserSPNs.py get-versions getwordfreq gf2pbm gfan gfan_bases gfan_buchberger gfan_combinerays gfan_doesidealcontain gfan_fancommonrefinement gfan_fanhomology gfan_fanisbalanced gfan_fanlink gfan_fanproduct gfan_fansubfan gfan_genericlinearchange gfan_groebnercone gfan_groebnerfan gfan_homogeneityspace gfan_homogenize gfan_initialforms gfan_interactive gfan_ismarkedgroebnerbasis gfan_krulldimension gfan_latticeideal gfan_list gfan_markpolynomialset gfan_minkowskisum gfan_minors gfan_mixedvolume gfan_overintegers gfan_padic gfan_polynomialsetunion gfan_render gfan_renderstaircase gfan_resultantfan gfan_saturation gfan_secondaryfan gfan_stats gfan_substitute gfan_symmetries gfan_tolatex gfan_topolyhedralfan gfan_tropicalbasis gfan_tropicalbruteforce gfan_tropicalcurve gfan_tropicalevaluation gfan_tropicalfunction gfan_tropicalhypersurface gfan_tropicalintersection gfan_tropicallifting gfan_tropicallinearspace gfan_tropicalmultiplicity gfan_tropicalrank gfan_tropicalstartingcone gfan_tropicaltraverse gfan_tropicalweildivisor gfan_version gf_attach gfcombine gfind_missing_files gfio gflags2man.py gflags_completions.sh gfortran gfortran-8 gfsplit gftodvi gftopk gftp gftp-gtk gftp-text gftype ghb ghc ghc-8.10.1 ghci ghci-8.10.1 ghc-pkg ghc-pkg-8.10.1 ghdl ghdl ghdl ghdl1-llvm ghex gh_fsb_decrypt ghi ghidra ghidra-analyzeHeadless ghp-import ghwdump gh_xen_decrypt giac giblib-config gidmigrator gids-tool gie gif2dbl gif2h5 gif2h5 gif2mask gif2png gif2rgb gif2webp gifbuild gifclrmp gifdiff giffix gifsicle gifski giftext giftogd2 giftool giftopnm gifview gig2mono gig2stereo gigdump gigextract giggle gigmerge gilt gimagereader-gtk gimagereader-qt5 gimp gimp-2.10 gimp-console gimp-console-2.10 gimp-debug-tool-2.0 gimp-test-clipboard-2.0 gimptool gimptool-2.0 ginsh gio gio-querymodules gio-querymodules-32 g-ir-annotation-tool g-ir-compiler g-ir-doc-tool g-ir-generate g-ir-inspect g-ir-scanner girtod gist git gitaly gitaly-blackbox gitaly-debug gitaly-hooks gitaly-ssh gitaly-wrapper git-annex git-annex-shell git-clang-format git-crypt git-cvsserver gitea git-filter-repo gitg gitk gitlab gitlab-runner gitlab-workhorse gitlab-zip-cat gitlab-zip-metadata git-latexdiff git-lfs gitolite gitprompt-rs git-receive-pack git-remote-bzr git-remote-keybase git-remote-tor-annex git-repair git-review git-shell git-upload-archive git-upload-pack givaro-config givaro-makefile gixy gjs gjs-console gkbd-keyboard-display gkrellm gkrellmd glabels-3 glabels-3-batch glacier2router glade glade-previewer glance glances glav glava glcontexts glewinfo glewinfo glfsheal glfunctions glhack glib-compile-resources glib-compile-schemas glib-genmarshal glib-gettextize glib-mkenums glider glilypond glinfo glisdeprecated glmeta glmovie glob2 global globaltime globash glom glom_create_from_example glom_export_po glom_export_po_all glom_import_po_all glom_test_connection gloobus-preview gloobus-preview-configuration gloobus-sushi gloox-config gloss glpsol glqueries glreadtest glretrace glslangValidator glslc glsl-compile-time glslstateschange glsync glsync32 gltestperf glthreads glthreads32 gluster glusterd gluster-eventsapi glustereventsd glusterfind glusterfs glusterfsd gluster-georep-sshkey gluster-mountbroker gluster-setgfid2path glxcontexts glxcontexts32 glxdemo glxdemo32 glxgears glxgears32 glxgears_fbconfig glxgears_fbconfig32 glxgears_pixmap glxgears_pixmap32 glxheads glxheads32 glxinfo glxinfo32 glxpbdemo glxpbdemo32 glxpixmap glxpixmap32 glxsnoop glxsnoop32 glxspheres32 glxspheres64 glxswapcontrol glxswapcontrol32 glyrc gm gmenudbusmenuproxy gmic gmic_krita_qt gmic_qt gml2gv gmock-config.in gmp3info gmrun gmtp gn gnac gnat gnatbind gnatchop gnatclean gnatfind gnatgcc gnatkr gnatlink gnatls gnatmake gnatname gnatprep gnatxref gnc-fq-check gnc-fq-dump gnc-fq-helper gnetlist gnetlist-legacy gn-m76 gnmanalyse gnmmanage gnokii gnokiid gnome-2048 gnome-autogen.sh gnome-books gnome-boxes gnome-break-timer gnome-break-timer-service gnome-builder gnome-calculator gnome-calendar gnome-characters gnome-chess gnome-clocks gnome-contacts gnome-control-center gnome-dictionary gnome-disk-image-mounter gnome-disks gnome-documents gnome-epub-thumbnailer gnome-extensions gnome-extensions-app gnome-firmware gnome-flashback gnome-font-viewer gnome-games gnome-genius gnome-help gnome-keyring gnome-keyring-3 gnome-keyring-daemon gnome-klotski gnome-latex gnome-logs gnome-mahjongg gnome-maps gnome-mines gnome-mobi-thumbnailer gnome-mplayer gnome-multi-writer gnome-music gnome-nettool gnome-nibbles gnome-panel gnome-passwordsafe gnome-phone-manager gnome-photos gnome-pie gnome-podcasts gnome-recipes gnome-robots gnome-screensaver gnome-screensaver-command gnome-screenshot gnome-search-tool gnome-session gnome-session-custom-session gnome-session-inhibit gnome-session-quit gnome-shell gnome-shell-extension-prefs gnome-shell-extension-tool gnome-shell-perf-tool gnome-software gnome-subtitles gnome-sudoku gnome-system-log gnome-system-monitor gnome-taquin gnome-terminal gnome-tetravex gnome-tetravex-cli gnome-thumbnail-font gnome-todo gnome-tweaks gnome-unstablepkg gnome-unstable-x86_64-build gnome-usage gnome-weather gnomon gnote gnubg gnubiff gnucash gnuchess gnuchessu gnuchessx gnugo gnujump gnu-mail gnumeric gnumeric-1.12.46 gnunet-arm gnunet-ats gnunet-auto-share gnunet-bcd gnunet-bugreport gnunet-cadet gnunet-config gnunet-consensus-profiler gnunet-conversation gnunet-conversation-test gnunet-core gnunet-datastore gnunet-dht-get gnunet-dht-monitor gnunet-dht-put gnunet-directory gnunet-download gnunet-ecc gnunet-fs gnunet-gns gnunet-gns-proxy-setup-ca gnunet-identity gnunet-namecache gnunet-namestore gnunet-nat gnunet-nat-auto gnunet-nat-server gnunet-nse gnunet-peerinfo gnunet-peerstore gnunet-publish gnunet-resolver gnunet-revocation gnunet-scalarproduct gnunet-scrypt gnunet-search gnunet-secretsharing-profiler gnunet-set-profiler gnunet-suidfix gnunet-testbed-profiler gnunet-testing gnunet-transport gnunet-transport-certificate-creation gnunet-unindex gnunet-uri gnunet-vpn gnunet-zoneimport gnuplot gnuplot_qt gnuplot_x11 gnuradio-companion gnuradio-config-info gnurl gnurl-config gnustep-config gnustep-tests gnutls-cli gnutls-cli-debug gnutls-serv go go goaccess gob2 gobby-0.5 go-bindata go-bindata go-bindata-assetfs gobject-query gob_loadcalc gocode go-contrib-init gocr gocr.tcl gocryptfs gocryptfs-xray godex godoc go-fish gofmt gofmt gogglesmm goid-tool goimports golangide goldendict goldenPac.py go-md2man gomoku gomvpkg goobox google-authenticator google-oauthlib-tool gopass gopls gorename gosh gost gost12-256-hash gost12-512-hash gotools gottengeography gotype gouldtoppm gource gox goyacc gozilla gp gp-2.11 gpa gpart gparted gpartedbin gpasm gpasswd gpaste-client gpcl6 gpcl6c gperf gperl gpg gpg2 gpg2john gpg-agent gpgconf gpg-connect-agent gpg-crypter gpgdir gpg-error gpg-error-config gpg-key2latex gpg-key2ps gpglist gpg-mailkeys gpgme-config gpgme-json gpgme-tool gpgparsemail gpgparticipants gpgparticipants-prefill gpgrt-config gpgscm gpgsigs gpgsm gpgtar gpgv gpgv2 gpg-wks-server gpgwrap gphelp gphoto2 gphoto2-config gphoto2-port-config gpick gpicview gpinyin gpk-application gpk-log gpk-prefs gplc gplib gplink gplprog.pt gplunit.pt gpm gpm-root gpo gpodder gpodder-migrate2tres gprename gprof gprolog gps2udp gpsbabel gpsbabelfe gpscat gpscorrelate gpscorrelate-gui gpsctl gpsd gpsdctl gpsdecode gpsfake gpsim gpsinit gpsmon gpspipe gpsprof gpsprune gpsrinex gpstrip gpvc gpvo gpxlogger gpxsee gpx-viewer gqrx gqrx-alsa gr2fonttest grabber grab_cgoban gracket gracket-text gradle grafana-cli grafana-server grafx2 grails gram2sapixml.pl grammalecte-cli grammalecte-server gramps granatier granite-demo grant-ops-in-room.py grap2graph grape graph graphml2gv graphtoprob graver grc grcat grcc grc_setup_freedesktop gr-ctrlport-monitor greet gregorio grenum grep grep6.pl grephistory grepjar grep.tdf gresource greytiff gr_filter_design grilo-test-ui-0.3 grim grl-inspect-0.3 grl-launch-0.3 gr_modtool grn grodvi groebner groff groffer grog grolbp grolj4 groovy groovyc groovyConsole groovydoc groovy.ico groovysh grops grotty groupadd groupmems groupmod groups growisofs growpart grpc_cli grpc_cpp_plugin grpck grpc_node_plugin grpc_objective_c_plugin grpconv grpc_php_plugin grpc_python_plugin grpc_ruby_plugin gr-perf-monitorx gr_plot gr_plot_const gr_plot_fft gr_plot_iq gr_plot_psd gr_plot_qt grpunconv gr_read_file_metadata grsync grsync-batch grub-bios-setup grub-customizer grub-editenv grub-file grub-fstest grub-glue-efi grub-install grub-kbdcomp grub-macbless grub-menulst2cfg grub-mkconfig grub-mkfont grub-mkimage grub-mklayout grub-mknetdir grub-mkpasswd-pbkdf2 grub-mkrelpath grub-mkrescue grub-mkstandalone grub-mount grub-ofpathname grub-probe grub-reboot grub-render-label grub-script-check grub-set-default grub-sparc64-setup grub-syslinux2cfg grun grunt gs gsa gsad gsasl gsbj gsc gsch2pcb gschem gschlas gscriptor gsc-script gsdj gsdj500 gsettings gsf gsf-office-thumbnailer gsftopk gsf-vba-dump gsharp gsi gsignond gsimplecal gsi-script gsl-config gsl-histogram gslj gslp gsl-randist gsmartcontrol gsmartcontrol_polkit gsmartcontrol-root gsound-play gspath gspell-app1 gsraytrace gss-client gssdp-device-sniffer gssdp-discover gsso-example gssproxy gss-server gst gst-blox gst-browser gst-config gst-convert gst-device-monitor-1.0 gst-discoverer-1.0 gst-doc gst-inspect-1.0 gst-inspect-1.0-32 gst-launch-1.0 gst-launch-1.0-32 gst-load gst-package gst-play-1.0 gst-profile gst-reload gst-remote gs-tri gst-stats-1.0 gst-stats-1.0-32 gst-sunit gst-transcoder-1.0 gst-typefind-1.0 gst-typefind-1.0-32 gsx gsymcheck gsymfix gtab-db-gen gtab-merge gtags gtbl gtest-config.in gtester gtester-report gtf gthumb gtk2fontsel gtk2hsC2hs gtk2hsHookGenerator gtk2hsTypeGen gtk3-demo gtk3-demo-application gtk3-icon-browser gtk3-widget-factory gtk-builder-convert gtk-builder-tool gtk-chtheme gtk-demo gtkdoc-check gtkdoc-depscan gtkdoc-fixxref gtkdocize gtkdoc-mkdb gtkdoc-mkhtml gtkdoc-mkhtml2 gtkdoc-mkman gtkdoc-rebase gtkdoc-scan gtkdoc-scangobj gtk-encode-symbolic-svg gtk-launch gtk-lshw gtk-query-immodules-2.0 gtk-query-immodules-2.0-32 gtk-query-immodules-3.0 gtk-query-immodules-3.0-32 gtk-query-settings gtkwave gtlssh gtlsshd gtlssh-keygen gtop gtranslator gts2dxf gts2oogl gts2stl gts2xyz gtscheck gtscompare gts-config gtstemplate guake guake-toggle guards gucharmap guestfish guestmount guestunmount guetzli gufw gufw-pkexec gui-compare guild guild2.0 guile guile1.8 guile2.0 guile-config guile-config1.8 guile-config2.0 guile-snarf guile-snarf1.8 guile-snarf2.0 guile-tools guile-tools1.8 guile-tools2.0 gulp gummi gunicorn gunicorn_pecan gunzip gupnp-av-cp gupnp-dlna-info-2.0 gupnp-dlna-ls-profiles-2.0 gupnp-network-light gupnp-universal-cp gupnp-upload guru gusbcmd guvcview guvcview-qt gv gv2gml gv2gxl gvcolor gvedit gvgen gvhdl gview gvim gvimdiff gvimtutor gvmap gvmap.sh gvnccapture gvncviewer gvpack gvpr gwakeonlan gwenhywfar-config gwenview gwenview_importer gwget gxditview gxkb gxl2dot gxl2gv gxmessage gxps gxpsc gxyrs gyp gzexe gzip h1bsd h1bsdcurisog h1clist h1curve h1first h264encode h2load h2pas h2paspp h2root h2root h52gif h52gif h5c++ h5c++ h5cc h5clear h5clear h5copy h5copy h5debug h5debug h5diff h5diff h5dump h5dump h5fc h5format_convert h5format_convert h5import h5import h5jam h5jam h5ls h5ls h5mkgrp h5mkgrp h5pcc h5perf h5perf_serial h5perf_serial h5pfc h5redeploy h5redeploy h5repack h5repack h5repart h5repart h5stat h5stat h5unjam h5unjam h5watch h5watch habak hacha hack hackbench hackrf_cpldjtag hackrf_debug hackrf_info hackrf_spiflash hackrf_transfer hacksaw hadd hadd haddock haddock-ghc-8.10.1 hakyll-init half_mt halog halt hamheuristic HandBrakeCLI handjoob hangman hangul happrox happy haproxy hardlink harvid has160-hash hashcat hashdeep hash_password hashstats hash-to-efi-sig-list haskell_count haskell-mustache hasktags hass hatari hatari-prg-args havedate haveged haxe haxelib hbf2gf hb-ot-shape-closure hb-shape hb-subset hb-view hccap2john hccapx2john hcieventmask hcisecfilter hcloud hcxdumptool hcxessidtool hcxhash2cap hcxhashcattool hcxhashtool hcxmactool hcxpcapngtool hcxpcaptool hcxpioff hcxpmkidtool hcxpsktool hcxwltool hdajackretask hdajacksensetest hdapsd hdav hda-verb hddtemp hdf2adf hd-idle hdifftopam hdparm hdrcopy hdspconf hdsploader head headerthemeeditor healthd heaptrack heaptrack_gui heaptrack_print heartbeat hedgewars hedgewars-server hediag hefurctl hefurd heif-convert heif-enc heif-info heif-thumbnailer heimdall heimdall-frontend helm help2man helpviewer helpviewer2 HepMC3-config herbstclient herbstluftwm hersheydemo hevcencode hevea hevea2mml hex2 hex2bin.py hex2dump.py hex2hcd hexchat hexchat-text hexdiff.py hexdump hexedit hexer hexgplc hexinfo.py hexmerge.py hextoraw hexyl hey hfind hg hgk hg-ssh hhp hiawatha hid2hci hid-decode hid-replay hiera higan highlight highlight-gui hilbert hilbert-from-rays hilbert-from-rays-symm hipstopgm hist2workspace hist2workspace hitch hitori hivexget hivexml hivexregedit hivexsh hjsmin hkt hledger hledger-ui hledger-web hlint hltest hmac256 hmanage hmaptool hmsa hocon hokey homebank homectl hoogle hop horst host hostid hostname hostnamectl hot hotshot2calltree howl howl-spec hp2ps hpack hp-align hpc hpcdtoppm hp-check hp-clean hp-colorcal hp-config_usb_printer hp-devicesettings hp-diagnose_plugin hp-diagnose_queues hp-doctor hp-fab hp-faxsetup hp-firmware hpftodit hp-info hping hping2 hping3 hp-levels hp-linefeedcal hp-logcapture hp-makecopies hp-makeuri hp-pkservice hp-plugin hp-pqdiag hp-print hp-printsettings hp-probe hp-query hp-scan hp-sendfax hp-setup hp-systray hp-testpage hp-timedate hp-toolbox hp-uiscan hp-uninstall hp-unload hp-upgrade hp-wificonfig HPXcvt hq hsc2hs HsColour hsetroot hslock hspec-discover hspec-meta-discover hspell hspell-i hsr-outputs hs-sha256sum ht htags htags-server htc htcacheclean htcontext htdbm htdigest htdigest2john htlatex htmex html2article html2po html2text html2text2 htmldiff htmldoc htmlhint HTMLLinker htmlmin htop htpasswd hts htsserver httex httexi http httpbin httpcfg httpclient httping https httrack httxt2dbm htxelatex htxetex hub hugin hugin_executor hugin_hdrmerge hugin_lensdb hugin_stacker hugin_stitch_project hugo hunspell hunt huntd hunter-trace hunzip hv_fcopy_daemon hv_kvp_daemon hv_vss_daemon hwasan_symbolize hwclock hwdb hwdetect hwengine hwinfo hwlatdetect hwloc-annotate hwloc-bind hwloc-calc hwloc-compress-dir hwloc-diff hwloc-distrib hwloc-dump-hwdata hwloc-gather-cpuid hwloc-gather-topology hwloc-info hwloc-ls hwloc-patch hwloc-ps hwmixvolume hwsim hxput hxsel hy hy2py hy2py3 hy3 hyc hyc3 hydra hydra-wizard.sh hylafax hypercorn hyperfine hyperiond hyperion-framebuffer hyperion-remote hyperion-v4l2 hyperion-x11 hzip i2cdetect i2cdump i2cget i2c-stub-from-dump i2ctransfer i3 i3 i386 i3bar i3bar i3blocks i3-config-wizard i3-config-wizard i3-dmenu-desktop i3-dmenu-desktop i3-dump-log i3-dump-log i3-input i3-input i3lock i3-migrate-config-to-v4 i3-migrate-config-to-v4 i3-msg i3-msg i3-nagbar i3-nagbar i3-save-tree i3-save-tree i3-sensible-editor i3-sensible-editor i3-sensible-pager i3-sensible-pager i3-sensible-terminal i3-sensible-terminal i3status i3-with-shmlog i3-with-shmlog i686-pc-linux-gnu-pkg-config i7z i7z-gui i915-perf-configs i915-perf-control i915-perf-reader ia iagno iasecc-tool iasl ibanchk ibd2sdi ibeacon ibmiscanner2john ibus ibus-daemon ibus-setup ibus-setup-hangul ibus-table-createdb ibus-ui-emojier-plasma ical2po icalendar icarus icas icat iccdump iccgamut icclu iceauth icebox icebox++11 iceboxadmin icebridge icecast icedax icegridadmin icegriddb icegridnode icegridregistry icehelp icepatch2calc icepatch2server icesh icesound icestormadmin icestormdb icewm icewmbg icewmhint icewm-menu-fdo icewm-menu-xrandr icewm-session icewm-set-gnomewm icmake icmbuild icmd icmstart icns2png icombine icontainer2icns icontopbm iconv iconvconfig iconvert icotool icp icpfind icu-config icu-config-32 icuinfo icupkg id id3convert id3cp id3info id3tag id3v2 ida idea idectl ident identify identify-cli identity idevicebackup idevicebackup2 idevicecrashreport idevicedate idevicedebug idevicedebugserverproxy ideviceenterrecovery idevice_id ideviceimagemounter ideviceinfo idevicename idevicenotificationproxy idevicepair ideviceprovision idevicescreenshot idevicesetlocation idevicesyslog idf2vrml idfcyl idfrect idiag-socket-details idiff idl2wrs idle idle2 idle3 idle3.8 idle3ctl idml2po idn idn2 idngram_merge idris idris-codegen-c idris-codegen-javascript idris-codegen-node ids2ngram iec16022 iex ifcfg ifconfig ifdata ifenslave ifind ifmap.py ifnames ifnames-2.13 ifne ifpline ifplugd ifplugd-daemon ifplugstatus ifpps ifrename ifstat iftop iga64 igmpproxy igraph igrep igt_results igt_resume igt_runner igt_stats iinfo iio_adi_xflow_check iio_attr iiod iio_genxml iio_info iio_readdev iio_reg iio-sensor-proxy iio_stresstest iio_writedev iis2apache.pl ijoin ijs_client_example ijs-config ijs_server_example ikdasm ikescan2john ikhal ilasm ilbmtoppm illinkanalyzer illumread ilmid ilmidiag ils imagen imagetops imake imap4d imapfeed imcopy imdbpy imdbpy2sql.py img2png img2png2 img2py img2py2 img2srec img2txt img2webp img2xpm img2xpm2 img_cat imgcmp imginfo img_stat imgtoppm imlib2_bumpmap imlib2_colorspace imlib2-config imlib2_conv imlib2_grab imlib2_load imlib2_poly imlib2_show imlib2_test imlib2_view imlib-config imp2gbs imp2ld imp2vs implantisomd5 implementation6 implementation6d import import-ferm import_interpolation imv imv imvirt imvirt-report imv-msg imv-wayland imv-x11 imwheel includeres incrond incrontab indent indep_test indi_aaf2_focus indi_arduinost4 indi_astromech_lpm indi_astrometry indi_baader_dome indicator-china-weather indi_celestron_gps indi_celestron_sct_focus indi_deepskydad_af1_focus indi_deepskydad_af2_focus indi_deepskydad_af3_focus indi_dmfc_focus indi_domepro2_dome indi_dsc_telescope indi_efa_focus indi_eq500x_telescope indi_eval indi_fcusb_focus indi_flipflat indi_gemini_focus indi_getprop indi_gpusb indi_hid_test indi_hitecastrodc_focus indi_ieqlegacy_telescope indi_ieq_telescope indi_imager_agent indi_integra_focus indi_ioptronHC8406 indi_ioptronv3_telescope indi_joystick indi_lacerta_mfoc_focus indi_lakeside_focus indi_lx200_10micron indi_lx200_16 indi_lx200ap indi_lx200ap_experimental indi_lx200ap_gtocp2 indi_lx200autostar indi_lx200basic indi_lx200classic indi_lx200fs2 indi_lx200gemini indi_lx200generic indi_lx200gotonova indi_lx200gps indi_lx200_OnStep indi_lx200pulsar2 indi_lx200ss2000pc indi_lx200_TeenAstro indi_lx200zeq25 indi_lynx_focus indi_manual_wheel indi_mbox_weather indi_meta_weather indi_microtouch_focus indi_moonlitedro_focus indi_moonlite_focus indi_myfocuserpro2_focus indi_nfocus indi_nightcrawler_focus indi_nstep_focus indi_onfocus_focus indi_openweathermap_weather indi_optec_wheel indi_paramount_telescope indi_pegasus_ppb indi_pegasus_upb indi_perfectstar_focus indi_planewave_deltat indi_pmc8_telescope indi_pyxis_rotator indi_qhycfw1_wheel indi_qhycfw2_wheel indi_qhycfw3_wheel indi_quantum_wheel indi_rainbow_telescope indi_rigel_dome indi_robo_focus indi_rolloff_dome indi_scopedome_dome indi_script_dome indi_script_telescope indiserver indi_sestosenso2_focus indi_sestosenso_focus indi_setprop indi_siefs_focus indi_simulator_ccd indi_simulator_dome indi_simulator_focus indi_simulator_gps indi_simulator_guide indi_simulator_spectrograph indi_simulator_sqm indi_simulator_telescope indi_simulator_weather indi_simulator_wheel indi_skycommander_telescope indi_skysafari indi_skywatcherAltAzMount indi_skywatcherAltAzSimple indi_smartfocus_focus indi_snapcap indi_sqm_weather indi_star2000 indi_steeldrive2_focus indi_steeldrive_focus indi_synscanlegacy_telescope indi_synscan_telescope indi_tcfs3_focus indi_tcfs_focus indi_temma_telescope indi_trutech_wheel indi_usbdewpoint indi_usbfocusv3_focus indi_v4l2_ccd indi_vantage_weather indi_watchdog indi_watcher_weather indi_weather_safety_proxy indi_wunderground_weather indi_xagyl_wheel indxbib inetcat inews infect infinoted-0.7 inflatehd influx influxd influx_inspect influx_stress influx_tools influx_tsm info infocmp infotocap infotopam ini2po inimf init init_audigy init_audigy_eq10 initdb initex init_live init.lxc inject_alive6 inkscape inkview inline-detox innbind inncheck innconfval innd inndf innfeed innmail innochecksum innochecksum innoextract innotop innreport innstat innupgrade innwatch innxbatch innxmit inotifywait inotifywatch inputattach inputattach inputattachctl insert_errors insmod inspectrum install install-catalog installfont-tl install-info installmgr install-printerdriver installvst instantfpc integrate integritysetup intel_audio_dump intel_backlight intel_bios_dumper intel_display_crc intel_display_poller intel_dp_compliance intel_dump_decode intel_error_decode intel_firmware_decode intel_forcewaked intel_framebuffer_dump intel_gem_info intel-gen4asm intel-gen4disasm intel_gpu_abrt intel_gpu_frequency intel-gpu-overlay intel_gpu_time intel_gpu_top intel_gtt intel_guc_logger intel_gvtg_test intel_infoframes intel_l3_parity intel_lid intel_opregion_decode intel_panel_fitter intel_perf_counters intel_reg intel_reg_checker intel_residency intel_stepping intel_vbt_decode intel-virtual-output intel_watermark interdiff intltool-extract intltoolize intltool-merge intltool-prepare in-toto-keygen in-toto-mock in-toto-run in-toto-sign in-toto-verify inv inverse_lookup6 invgeod invoke invprofcheck invproj io iodbc-config iodbctest iodbctestw ioflo ioflo3 ionice ioping ios7tojohn iosock.pl iostat iostat2pcp io_static iotop ip ip6range ip6tables ip6tables ip6tables-apply ip6tables-apply ip6tables-legacy ip6tables-legacy ip6tables-legacy-restore ip6tables-legacy-restore ip6tables-legacy-save ip6tables-legacy-save ip6tables-nft ip6tables-nft ip6tables-nft-restore ip6tables-nft-restore ip6tables-nft-save ip6tables-nft-save ip6tables-restore ip6tables-restore ip6tables-restore-translate ip6tables-restore-translate ip6tables-save ip6tables-save ip6tables-translate ip6tables-translate ipcalc ipcmk ipcrm iperf iperf3 ipers ipf-mod.pl ipfs ipguard ipmaddr ipmicmd ipmievd ipmilan ipmish ipmi_sim ipmitool ipmi_ui ipod-read-sysinfo-extended ipop2d ipop3d ippeveprinter ippfind ipptool iprange iproxy ipsec ipset ipsetbuild ipsetcat ipsetdot iptables iptables iptables-apply iptables-apply iptables-legacy iptables-legacy iptables-legacy-restore iptables-legacy-restore iptables-legacy-save iptables-legacy-save iptables-nft iptables-nft iptables-nft-restore iptables-nft-restore iptables-nft-save iptables-nft-save iptables-restore iptables-restore iptables-restore-translate iptables-restore-translate iptables-save iptables-save iptables-translate iptables-translate iptables-xml iptables-xml iptc iptest iptest2 iptest3 iptraf-ng iptstate iptunnel ipv6calc ipv6loganon ipv6logconv ipv6logstats ipvsadm ipvsadm-restore ipvsadm-save ipython ipython2 ipython3 iqshell irb irb-2.6 ircat irccat ir-ctl irdb-get irexec irk irkerd irkerhook.py ir-keytable irpipe irpty irqbalance irqbalance-ui irsend irsimreceive irsimsend irssi irtestcase irtext2udp irw irxevent isadump isaset iscan iscan-registry isdv4-serial-debugger isdv4-serial-inputattach isisd isnsadm isnsd isnsdd isodebug isodump isohybrid isohybrid.pl iso-info isoinfo iso-read isort isort2 isosize isosurf isovfy ispc ispell ispellaff2myspell isql istat isutf8 isympy isync itg_pck itox itstool itunes_backup2john itweb-settings itw-modularjdk.args iucode_tool iusql iv iverilog iverilog-vpi ivshmem-client ivshmem-client ivshmem-server ivshmem-server ivstools ivy iw iwconfig iwctl iwevent iwgetid iwhich iwlist iwmon iwork2john iwpriv iwspy ixion-formula-tokenizer ixion-parser ixion-sorter ixplore jack_alias jack_alias jack_bufsize jack_bufsize jack_connect jack_connect jack_control jack_cpu jack_cpu_load jackd jackd jackdbus jack_disconnect jack_disconnect jack-dssi-host jack_evmon jack_evmon jack_freewheel jack_freewheel jack_impulse_grabber jack_iodelay jack_iodelay jack_latent_client jack_latent_client jack_load jack_load jack_load_test jack_lsp jack_lsp jack_metro jack_metro jack_midi_dump jack_midi_dump jack_midi_latency_test jack_midiseq jack_midiseq jack_midisine jack_midisine jack_monitor_client jack_monitor_client jack_multiple_metro jack_net_master jack_net_slave jack_netsource jack_netsource jack_property jack_property jack_rec jack_rec jack_samplerate jack_samplerate jack_server_control jack_server_control jack_session_notify jack_session_notify jack_showtime jack_showtime jack_simdtests jack_simple_client jack_simple_client jack_simple_session_client jack_simple_session_client jack_test jack_thru jack_transport jack_transport jack_transport_client jack_unload jack_unload jack_wait jack_wait jack_zombie jad jade jadetex jadmaker jadx jadx-gui jake jam jami jami-gnome jamo-normalize jar jarsigner jasper java java2groovy java2html javac java_count javadoc javah javap javaws jbgtopbm jbgtopbm85 jbig2dec jbigtopnm jcat jcat-tool jclient.pl jcmd jconsole jcontrol jdb jdiff jedit jeditbg jemalloc-config jemalloc.sh jeprof jethro jetring-accept jetring-apply jetring-build jetring-checksum jetring-diff jetring-explode jetring-gen jetring-review jetring-signindex jffs2dump jffs2reader jfmutil jfs_debugfs jfs_fsck jfs_fscklog jfs_logdump jfs_mkfs jfs_tune jgem jgmenu jgmenu_run jhead jinfo jira-wiki-markup jirb jirb_swing jiv jive jlpm jls jmap jmol jnettop joe john join jose josm journalbeat journalctl jp2a jpeg2ktopam jpeg2yuv jpeg-archive jpeg-compare jpegenc jpegexiforient jpeg_extract jpeg-hash jpegoptim jpeg-recompress jpegtopnm jpegtran jpgicc jpico jp.py jps jq jruby jrubyc js js52 js52-config js60 js60-config js68 js68-config js-beautify jscal jscal jscal-restore jscal-restore jscal-store jscal-store js-config jshon json2po json-diff jsondiff json-glib-format json-glib-validate json-patch jsonpatch jsonpointer json_reformat jsonschema json-to-dhall json_verify jsp_count jstack jstar jstat jstatd jstest jstest jsvc jtag jtagconsole juk julia julius junitmsgfmt jupyter jupyter-bundlerextension jupyter-console jupyter-kernel jupyter-kernelspec jupyter-lab jupyter-labextension jupyter-migrate jupyter-nbconvert jupyter-nbextension jupyter-notebook jupyter-qtconsole jupyter-run jupyter-serverextension jupyter-troubleshoot jupyter-trust juyin-learn jw jwm jws JxrDecApp JxrEncApp k3b k5srvutil k9s kaccess kacpimon kactivities-cli kaddressbook kadmin kadmind kadmin.local kaem kaeru kaffeine kajongg kajonggserver kak kakasi kakasi-config kak-lsp kalarm kalarmautostart kalgebra kalgebramobile kalzium kamene kamoso kanagram kanji-fontmap-creator kanshi kantiword kapidox_generate kapman kapplymousetheme kapptemplate kaptan karbon karmaSMB.py karplong katalyzer kate katedec kateenc kate-syntax-highlighter katomic kaudiocreator kbackup kbd-layout-viewer kbd-layout-viewer5 kbd_mode kbdrate kbfsfuse kblackbox kblocks kbnm kbookmarkmerger kbounce kbreakout kbroadcastnotification kbruch kbuildsycoca5 kbxutil kcachegrind kcadm kcalc kccachetest kcdirmgr kcdirtest kcforestmgr kcforesttest kcgrasstest kcharselect kchashmgr kchashtest kchmviewer kclangctest kcminit kcminit_startup kcmshell5 kcm-touchpad-list-devices kcolorchooser kcolorschemeeditor kconfig-hardened-check kcookiejar5 kcov kcov-system-daemon kcpolymgr kcpolytest kcprototest kcptun-client kcptun-server kcreg kctreemgr kctreetest kcutilmgr kcutiltest kdb3_sqlite3_dump kdb5_ldap_util kdb5_util kdbg kdcdump2john kde-add-printer kdebugdialog5 kdebugsettings kdeconnect-app kdeconnect-cli kdeconnect-handler kdeconnect-indicator kdeconnect-settings kdeconnect-sms kdecp5 kded5 kdedoc kde_generate_export_header kdeinit5 kdeinit5_shutdown kdeinit5_wrapper kdekillall kdelnk2desktop.py kdemangen.pl kdemv5 kdenlive kdenlive_render kde-open5 kde-print-queue kdestroy kdesu kdesvn kdesvnaskpass kde-systemsettings-tree.py kde-unstablepkg kde-unstable-x86_64-build kdev_dbus_socket_transformer kdevelop kdevelop! kdev_format_source kdev_includepathsconverter kdev-pg-qt kdevplatform_shell_environment.sh kde_wacom_tabletfinder kdf kdialog kdialog_progress_helper kdiamond kdiff3 kdig kdwsdl2cpp keditbookmarks keditfiletype5 keepalived keepass keepass2john keepassxc keepassxc-cli keepassxc-proxy kernel-install ketcindy kexec kexi-3.2 key2raw key2text key2xhtml keyanalyze keyart keybase keybase-gui keychain keychain2john keyctl key.dns_resolver keylookup keymgr keynav keyring keyring2john keyspace keystore2john keytab-lilo keytool kf5-config kf5kross kfind kfloppy kfmclient kfontinst kfontview kfourinline kfourinlineproc kgendesignerplugin kgeography kget kglobalaccel5 kgoldrunner kgpg kgraphviewer khal khangman khard khelpcenter khost kibitz kicad kicad2step kicad-ogltest kiconfinder5 kid3 kid3-cli kid3-qt kig kigo kile kill killall killbots kill_router6 kimagemapeditor kimtoy kinfocenter kinit kintercept.py kioclient5 kirbi2john kirigami2gallery kiriki kismet kismet_cap_freaklabs_zigbee kismet_cap_kismetdb kismet_cap_linux_bluetooth kismet_cap_linux_wifi kismet_cap_nrf_51822 kismet_cap_nrf_mousejack kismet_cap_nxp_kw41z kismet_cap_pcapfile kismet_cap_sdr_rtl433 kismet_cap_sdr_rtladsb kismet_cap_sdr_rtlamr kismet_cap_ti_cc_2531 kismet_cap_ti_cc_2540 kismetdb_clean kismetdb_dump_devices kismetdb_strip_packets kismetdb_to_gpx kismetdb_to_kml kismet_server kiten kitengen kitenkanjibrowser kitenradselect kitty kjots kjournalprint kjs5 kjscmd5 kjsconsole kjumpingcube kkc kkc-package-data klavaro klayout kleopatra klettres klickety klines klipper klist kmag kmahjongg kmail kmail_antivir.sh kmail_clamav.sh kmail_fprot.sh kmail-refresh-settings kmail_sav.sh kmenuedit kmimetypefinder5 kmines kmix kmixctrl kmixremote kmod kmousetool kmouth kmplayer kmplot kmscon kmymoney knavalbattle knetattach knetwalk knights knock knockd knotc knotd knotes known_hosts2john knsec3hash kobodl kodak2ti3 kodi kodi-ps3remote kodi-send kodi-standalone koi8rxterm koji-build-scratch kolf kollision kolourpaint komkindex kompare konqueror konquest konsole konsolekalendar konsoleprofile kontact konversation kopete korg2gig korgac korganizer korgdump kotlin kotlinc kotlinc-js kotlinc-jvm kpa-backup.sh kpackagelauncherqml kpackagetool5 kpartloader kpartx kpasswd kpat kphononplayer kphotoalbum kprop kproplog kpseaccess kpsepath kpsereadlink kpsestat kpsetool kpsewhere kpsewhich kpsexpand kquitapp5 kramdown kraut krazy-licensecheck krb2john krb5-config krb5kdc krb5-send-pr krdb krdc kreadconfig5 krename kresus kreversi krfb krita kritarunner kronometer kruler krunner krusader ksba-config kscreen-console kscreen-doctor ksh ksh93 kshell5 kshisen kshutdown ksirk ksirkskineditor ksmserver ksnakeduel kspaceduel ksplashqml ksquares ksshaskpass kstars kstart5 kstats kstatsviewer kstool ksu ksudoku ksvgtopng5 kswitch ksysguard ksysguardd ksystemlog ksystemstats kteatime ktelnetservice5 ktikz ktimer ktimetracker ktmagnetdownloader ktnef ktoblzcheck ktorrent ktouch ktp-contactlist ktp-debugger ktp-dialout-ui ktp-log-viewer ktp-send-file ktraderclient5 ktrash5 ktuberling ktupnptest kturtle ktutil kube kubectl kubectx kubens kubrick kuickshow kuiviewer kup-daemon kupfer kupfer-exec kup-filedigger kustomize kvantummanager kvantumpreview kvazaar kvirc kvirc-config kvno kwallet2john kwalletd5 kwalletmanager5 kwallet-query kwatchgnupg kwave kwikdisk kwin_wayland kwin_x11 kwnmz kwordquiz kwrapper5 kwrite kwriteconfig5 kylin-nm kzonecheck l20n2po l2ping l2test l3afpad l3build labelg lablgladecc2 lablgladecc3 lablgtk2 labplot2 lacheck lal lame language-specific.sh languagetool laptop-detect lasem-render-0.4 last lastb lastlog lastpass2john latest latex latex2html latex2man latex2nemeth latex2png latex2rtf latexdef latexdiff latexdiff-vc latexfileversion latex-git-log latexindent latexmk latexpand latex-papersize latexrevise latextags latsieve latte2ext latte2ine latte-count latte-dock latte-maximize latte-minimize latticegen launchSysDbus launchy lav2avi.sh lav2mpeg lav2wav lav2yuv lavaddwav lavinfo lavpipe lavplay lavtc.sh lavtrans lazarus lazarus lazbuild lbreakout2 lbreakout2server lbunzip2 lbzcat lbzip2 lc lcalc lca-tool lcdd LCDd lcdd_gmp lcdexec lcdgps lcdident.pl lcdmetar.pl lcdproc lcdvc lckdo lconvert lconvert-qt5 ld ld10k1 ld10k1d ld64.lld ld86 ldapadd ldap-agent ldapcompare ldapexop ldapmodify ldapmodrdn ldappasswd ldapsearch ldapurl ldapwhoami ldattach ldb ldbadd ldbdel ldbedit ld.bfd ldbmodify ldbrename ldbsearch ldc ldc2 ldc-build-runtime ldclt ldconfig ldc-profdata ldc-prune-cache ldd lddd ldd.fakechroot lddtree ld.gold ldif2db.pl ldif2john ldiff ld.lld ldmd ldmd2 ldmtool ld.musl-clang ldns-chaos ldns-compare-zones ldns-config ldnsd ldns-dane ldns-dpa ldns-gen-zone ldns-key2ds ldns-keyfetcher ldns-keygen ldns-mx ldns-notify ldns-nsec3-hash ldns-read-zone ldns-resolver ldns-revoke ldns-rrsig ldns-signzone ldns-test-edns ldns-testns ldns-verify-zone ldns-version ldns-walk ldns-zcat ldns-zsplit ldo65 ldoc ldoc.lua ldrdf leafpad leaftoppm ledger leet lefh lefty leg lein len lensfun-add-adapter lensfun-convert-lcp les less lessc lessecho lesskey lesspipe.sh less_trivial_synth lex lex_count lexcount1 lexgrog lexicon lf-mkmovixiso lftp lftpget lha libassuan-config libcdaudio-config libesmtp-config libevdev-tweak-device libfm-pref-apps libfm-pref-apps libftdi1-config libftdi-config libgcrypt-config libglade-convert libgtop_daemon2 libgtop_server2 libguestfs-make-fixed-appliance libguestfs-test-tool libIDL-config-2 libieee1284_test libime_history libime_pinyindict libime_prediction libime_slm_build_binary libime_tabledict libinput libjingle-call libjulius-config libmcrypt-config libmikmod-config libnet-config libnovaconfig libotf-config libpng16-config libpng16-config-32 libpng-config libpng-config-32 libpolys-config libquicktime_config librados-config librecad libreoffice libreoffice libreoffice2john libressl-openssl libsent-config libsingular-config libusb-config libval-config libvirtd libvirt-dbus libwacom-list-local-devices libwmf-config libwmf-fontmap libyosys.so liferea liferea-add-feed light light_correct lightdm lightdm-deepin-greeter lightdm-gtk-greeter lightdm-gtk-greeter-settings lightdm-gtk-greeter-settings-pkexec lightdm-webkit2-greeter light-locker light-locker-command lightsoff lilv-bench lily-glyph-commands lily-image-commands lilymidi lilypond lilypond-book lilypond-invoke-editor lilysong LimeQuickTest LimeSuiteGUI LimeUtil linbox-config lincity-ng line line-clip line-cull linefind line-flat linegraphg lineloop lineloop-clip lineloop-elts line-smooth line-stipple-wide linestrip linestrip-clip linestrip-flat-stipple linestrip-stipple linestrip-stipple-wide line-userclip line-userclip-clip line-userclip-nop line-userclip-nop-clip line-wide line-xor linguist linguist-qt5 link LinkHut linkicc link-parser links linktest linssid linssid-pkexec linux32 linux64 linux-boot-prober linuxdoc lion2john lion2john-alt lirc-config-tool lircd lircd-setup lirc-init-db lirc-lsplugins lirc-lsremotes lircmd lircrcd lirc-setup lire liri-calculator liri-text lisp lisp_count lispmtopgm listaction listaction_d listbib listfdb listg listgeo listings-ext.sh listjpeg listmp3 listplugins listswf listswf_d lit lit lit2epub litedram_gen liteeth_gen liteide litepcie_gen litex_read_verilog litex_server litex_sim litex_simple litex_term livewallpaper livewallpaper-config livewallpaper-indicator lkbib llc llc lld lldb lldb-argdumper lldb-instr lldb-server lldb-vscode lld-link lldpcli lldpctl lli lli lli-child-target lli-child-target llpp llppac llpphtml llpp.inotify llvm-addr2line llvm-addr2line llvm-ar llvm-ar llvm-as llvm-as llvm-bcanalyzer llvm-bcanalyzer llvm-cat llvm-cat llvm-cfi-verify llvm-cfi-verify llvm-config llvm-config llvm-config32 llvm-cov llvm-cov llvm-c-test llvm-c-test llvm-cvtres llvm-cvtres llvm-cxxdump llvm-cxxdump llvm-cxxfilt llvm-cxxfilt llvm-cxxmap llvm-cxxmap llvm-diff llvm-diff llvm-dis llvm-dis llvm-dlltool llvm-dlltool llvm-dwarfdump llvm-dwarfdump llvm-dwp llvm-dwp llvm-elfabi llvm-elfabi llvm-exegesis llvm-exegesis llvm-extract llvm-extract llvm-ifs llvm-install-name-tool llvm-jitlink llvm-jitlink llvm-lib llvm-lib llvm-link llvm-link llvm-lipo llvm-lipo llvm-lto llvm-lto llvm-lto2 llvm-lto2 llvm-mc llvm-mc llvm-mca llvm-mca llvm-modextract llvm-modextract llvm-mt llvm-mt llvm-nm llvm-nm llvm-objcopy llvm-objcopy llvm-objdump llvm-objdump llvm-opt-report llvm-opt-report llvm-PerfectShuffle llvm-PerfectShuffle llvm-profdata llvm-profdata llvm-ranlib llvm-ranlib llvm-rc llvm-rc llvm-readelf llvm-readelf llvm-readobj llvm-readobj llvm-reduce llvm-rtdyld llvm-rtdyld llvm-size llvm-size llvm-spirv llvm-split llvm-split llvm-stress llvm-stress llvm-strings llvm-strings llvm-strip llvm-strip llvm-symbolizer llvm-symbolizer llvm-tblgen llvm-tblgen llvm-undname llvm-undname llvm-xray llvm-xray lm32-elf-addr2line lm32-elf-ar lm32-elf-as lm32-elf-c++ lm32-elf-c++filt lm32-elf-cpp lm32-elf-elfedit lm32-elf-g++ lm32-elf-gcc lm32-elf-gcc-10.1.0 lm32-elf-gcc-ar lm32-elf-gcc-nm lm32-elf-gcc-ranlib lm32-elf-gcov lm32-elf-gcov-dump lm32-elf-gcov-tool lm32-elf-gdb lm32-elf-gdb-add-index lm32-elf-gprof lm32-elf-ld lm32-elf-ld.bfd lm32-elf-lto-dump lm32-elf-nm lm32-elf-objcopy lm32-elf-objdump lm32-elf-ranlib lm32-elf-readelf lm32-elf-size lm32-elf-strings lm32-elf-strip ln lndir lneato lnnmz lnstat lo10k1 loadjpeg loadkeys loadndisdriver loadunimap lobase lobase localc localc local_discovery6.sh locale localectl localedef locale-gen locate lockfile lockfile-check lockfile-create lockfile-remove lockfile-touch locktest lockutils-wrapper lodbias lodraw lodraw loffice loffice lofromtemplate lofromtemplate log4cpp-config logcli logconv.pl loggen logger logilab-pytest login loginctl logname logo logo.elf logresolve logrotate logsave logstash logwatch loimpress loimpress lokalize loki lolcat lollipop lollypop lomath lomath long-fixed-func look lookandfeelexplorer lookandfeeltool lookbib lookupsid.py losetup lostfiles lotus2john lou_allround lou_checkhyphens lou_checktable lou_debug lou_maketable lou_maketable.d/ lou_maketable.d/export_chunked_words.py lou_maketable.d/generate_alphabet.py lou_maketable.d/lou_maketable.mk lou_maketable.d/make_suggestions.py lou_maketable.d/submit_rows.py lou_maketable.d/submit_rows.sh lou_maketable.d/submit_rules.py lou_maketable.d/submit_rules.sh lou_maketable.d/substrings.pl lou_maketable.d/utils.py lou_maketable.d/wrap_patgen.sh lout lou_tableinfo lou_trace lou_translate love loweb loweb lowntfs-3g lowriter lowriter lp lpadmin lpass lpc lpconvert lpinfo lpmove lpoptions lpq lpr lprm lprodump lprodump-qt5 lprsetup.sh lp_solve lpstat lpunlock lqtplay lqtremux lqt_transcode lr_anondump lr_anonymize lrcalc lr_cron lr_deanonymize lr_desyslog lrelease lrelease-pro lrelease-pro-qt5 lrelease-qt5 lr_env lrf2lrs lrfviewer lr_log2mail lr_log2report lr_rawmail2mail lr_report_cfg2xml lr_run lrs lrs2lrf lr_setup_responder lrsnash lr_spec2pot lr_spoold lrunzip lr_vendor_cron lr_xml2mail lr_xml2report lrz lrzcat lrzip lrztar lrzuntar ls ls2ti3 lsar lsattr lsblk lsb_release lscollection lscp lscpu lsd lsdev lsdiff lsdistcc lsdvd lsgpu lshw lsinitcpio lsinitrd lsipc lskat lslocks lslogins lsmem lsmod lsmon lsmtd lsns lsof lsoptdepends lspci lspst lss16toppm lssu lstimed lstmeval lstmtraining lstopo lsusb lsusb.py lsw lswallpaper ltags ltdbtool lto-dump ltrace ltris lttng-gen-tp ltxfileinfo ltximg lua lua5.1 lua5.2 lua5.3 luac luac5.1 luac5.2 luac5.3 luahbtex luajit luajit-2.0.5 luajithbtex luajittex luakit lualatex luaotfload-tool luarocks luarocks-admin luatex luatools luit luks2john luksmeta luminance-hdr luminance-hdr-cli lur-command lutris lv2apply lv2bench lv2info lv2lint lv2ls lv2specgen.py lv2_validate lvchange lvconvert lvcreate lvdisplay lvextend lvm lvm-cache-stats lvmconf lvmconfig lvmdiskscan lvmdump lvmetad lvmpolld lvmsadc lvmsar lvreduce lvremove lvrename lvresize lvs lvscan lwarpmk lw-generate-schema lwm lwtrace lxappearance lxc lxc-attach lxc-autostart lxc-cgroup lxc-checkconfig lxc-checkpoint lxc-config lxc-console lxc-copy lxc-create lxc-destroy lxc-device lxc-execute lxc-freeze lxcfs lxc-info lxclipboard lxclipboard lxc-ls lxc-monitor lxc-snapshot lxc-start lxc-stop lxc-to-lxd lxc-top lxc-unfreeze lxc-unshare lxc-usernsexec lxc-wait lxd lxd-agent lxd-benchmark lxde-logout lxdm lxdm lxdm-binary lxdm-binary lxdm-config lxdm-config lxd-p2c lxhotkey lximage-qt lxinput lxlauncher lxlock lxlock lxmusic lxpanel lxpanel lxpanelctl lxpanelctl lxpolkit lxpolkit lxqt-about lxqt-admin-time lxqt-admin-user lxqt-admin-user-helper lxqt-archiver lxqt-backlight_backend lxqt-config lxqt-config-appearance lxqt-config-brightness lxqt-config-file-associations lxqt-config-globalkeyshortcuts lxqt-config-input lxqt-config-locale lxqt-config-monitor lxqt-config-notificationd lxqt-config-powermanagement lxqt-config-session lxqt-globalkeysd lxqt-leave lxqt-notificationd lxqt-openssh-askpass lxqt-panel lxqt-policykit-agent lxqt-powermanagement lxqt-runner lxqt-session lxqt-sudo lxrandr lxserver lxsession lxsession lxsession-db lxsession-db lxsession-default lxsession-default lxsession-default-apps lxsession-default-apps lxsession-default-terminal lxsession-default-terminal lxsession-edit lxsession-edit lxsession-logout lxsession-logout lxsession-xdg-autostart lxsession-xdg-autostart lxsettings-daemon lxsettings-daemon lxshortcut lxshortcut lxsim lxsplit lxsu lxsudo lxt2miner lxt2vcd lxtask lxterm lxterminal ly lynis lynx ly-server lyx lyxclient lz lz4 lz4c lz4cat lzcat lzcmp lzdiff lzegrep lzf lzfgrep lzgrep lzip lzless lzma lzmadec lzmainfo lzmore lzocompress lzodecompress lzop M1 m17n-conv m17n-date m17n-db m17n-dump m17n-edit m17n-view m2r m2r-python2 m4 ma2asm mac mac2john mac2john-alt mac2unix macchanger machinectl macof macpack macptopbm mactime mag magick Magick++-config MagickCore-config magick-script MagickWand-config magnet-link magnum-al-info magnum-distancefieldconverter magnum-fontconverter magnum-gl-info magnum-imageconverter mail mailgraph.pl mail-lock mailmail mailmail3 mailman mailnag mailnag-config mailodf mailpost mailq mailq mailq mailq mailsnarf mailstat mail-touchlock mail-unlock mailutils mailutils-config mailutime mailx mairix make make4ht makebearoff makebin makeboot makecert makechrootpkg makeconv makedb makedbz makedepend makedtx makefdb makefile_count make_filelists makeg makeglossaries makeglossaries-lite makehistory makehyper makeindex makeinfo makeivs-ng makejvf makemap make_metadata.py makemhr makeobj makepage makepkg makepkg-template make_pysol_freecell_board.py makerepropkg makeskel makeskel.rsj make_strings makeswf make_torrent make_tpl_varnames_h maketx make_utts make_wagon_desc makeweb makeweights mako makoctl mako-render malcontent-client malcontent-control mamba mame mame-castool mame-chdman mame-floptool mame-imgtool mame-jedutil mame-ldresample mame-ldverify mame-nltool mame-nlwav mame-pngcmp mame-regrep mame-romcmp mame-split mame-srcclean mame-testkeys mame-unidasm mamlTest mamlTestMessageSwarm man man2html manaplus man.cgi mandb mandelbrot manette-test manhole-cli manifest manpath man-recode manweb manywin manywin32 mapnames mapnik-config mapnik-index mapnik-render mapscrn map_unit marble marble-qt marco marco-message marco-theme-viewer marco-window-demo mari0 mariabackup mariadb mariadb-access mariadb-admin mariadb-backup mariadb-binlog mariadb-check mariadb-client-test mariadb-client-test-embedded mariadb_config mariadb-convert-table-format mariadbd mariadbd-multi mariadbd-safe mariadbd-safe-helper mariadb-dump mariadb-dumpslow mariadb-embedded mariadb-find-rows mariadb-fix-extensions mariadb-hotcopy mariadb-import mariadb-install-db mariadb-ldb mariadb-plugin mariadb-secure-installation mariadb-service-convert mariadb-setpermission mariadb-show mariadb-slap mariadb-test mariadb-test-embedded mariadb-tzinfo-to-sql mariadb-upgrade mariadb-waitpid marisa-benchmark marisa-build marisa-common-prefix-search marisa-dump marisa-lookup marisa-predictive-search marisa-reverse-lookup markdown markdown-calibre markdown_py markdown_py2 markdown-unlit marked marked-man markov maruku marutex masktest masscan massif-visualizer master mat2 match_parens matdump mate-about mate-about-me mate-appearance-properties mate-at-properties mate-autogen mate-calc mate-calc-cmd mate-calculator mate-color-select mate-control-center mate-cpufreq-selector mate-default-applications-properties mate-desktop-item-edit mate-dictionary mate-disk-image-mounter mate-disk-usage-analyzer mate-display-properties mate-display-properties-install-systemwide mate-doc-common mate-file-share-properties mate-font-viewer mate-keyboard-properties mate-maximus mate-mouse-properties mate-network-properties mate-notification-properties mate-panel mate-panel-screenshot mate-panel-test-applets mate-power-backlight-helper mate-power-manager mate-power-preferences mate-screensaver mate-screensaver-command mate-screensaver-preferences mate-screenshot mate-search-tool mate-session mate-session-inhibit mate-session-properties mate-session-save mate-system-log mate-system-monitor mate-terminal mate-terminal.wrapper mate-thumbnail-font mate-time-admin mate-typing-monitor mate-volume-control mate-volume-control-status-icon mate-window-properties mate-wm mathmlps mathmlsvg matho mathomatic matho-mult matho-pascal matho-primes matho-sum matho-sumsq math-readline mathspic matrix_decrypt matrix_sso_helper matrix_upload matteblend.flt matterbridge mattrib maturin maude mausezahn mautil maxima maxima mayavi2 mb2md.pl mb2org mb2txt mbadblocks mbedtls_aescrypt2 mbedtls_benchmark mbedtls_cert_app mbedtls_cert_req mbedtls_cert_write mbedtls_crl_app mbedtls_crypt_and_hash mbedtls_dh_client mbedtls_dh_genprime mbedtls_dh_server mbedtls_dtls_client mbedtls_dtls_server mbedtls_ecdh_curve25519 mbedtls_ecdsa mbedtls_gen_entropy mbedtls_generic_sum mbedtls_gen_key mbedtls_gen_random_ctr_drbg mbedtls_gen_random_havege mbedtls_hello mbedtls_key_app mbedtls_key_app_writer mbedtls_mini_client mbedtls_mpi_demo mbedtls_pem2der mbedtls_pk_decrypt mbedtls_pk_encrypt mbedtls_pk_sign mbedtls_pk_verify mbedtls_query_compile_time_config mbedtls_req_app mbedtls_rsa_decrypt mbedtls_rsa_encrypt mbedtls_rsa_genkey mbedtls_rsa_sign mbedtls_rsa_sign_pss mbedtls_rsa_verify mbedtls_rsa_verify_pss mbedtls_selftest mbedtls_ssl_client1 mbedtls_ssl_client2 mbedtls_ssl_fork_server mbedtls_ssl_mail_client mbedtls_ssl_server mbedtls_ssl_server2 mbedtls_strerror mbedtls_udp_proxy mbedtls_udp_proxy_wrapper.sh mbedtls_zeroize mbimcli mbim-network mboximporter mbstream mbsync mbsync-get-cert mc mcabber mcafee_epo2john mcaptest mcat mcd mcdiff mcdp mcedit mcheck mclasserase mcli mco mcollectived mcomix mcomp mconfig mcookie mcopy mcpp mcqd mc-tool mcube mcview mc-wait-for-name md2html md5deep md5pass md5sum mda mdadm mdassembler mdatopbm mdb_copy mdb_dump mdb_load mdbrebase mdb_stat mdconvert mdel mdeltree mdexport.py mdf2iso mdfind mdig mdir mdk3 mdmon mdns-scan mdoc mdoc-assemble mdoc-export-html mdoc-export-msxdoc mdoc-validate mdp mdp_test mdu mdvalidater media-ctl MediaElch mediainfo mediainfo-gui mediastream mediastreamer2_tester mediathekview mednafen medusa meek-client meek-server megaglest megaglest_editor megaglest_g3dviewer meilisearch meinproc5 meld melt memcached memcached-tool memcapable memcat memconf memcp memdiskfind memdump memerror memexist memflush memhog mem_image memparse MemParse memping memprobe memprobe memprof2calltree memrm memslap memstat memtester memtouch memusage memusagestat mencoder mendex merge mergecap Merge_dimPCBPS mergelib merge_metadata.py MergePCBPS merge_unicharsets merkaartor mesa-overlay-control.py mesg meshbird mesh-cfgclient meshctl meshdebug meson messages metacity metacity-message metacity-theme-viewer metacity-window-demo metaflac metalog metapixel metapixel-prepare metricbeat mev mex mf mf2pt1 mfkey32 mfkey32v2 mfkey64 mflua mfluajit mfluajit-nowin mflua-nowin mf_nonce_brute mf-nowin mfoc mformat mfplain mfsappendchunks mfsbdev mfscgiserv mfscheckfile mfschkarchive mfschunkserver mfschunktool mfscli mfsclrarchive mfscopyeattr mfscopygoal mfscopyquota mfscopysclass mfscopytrashtime mfsdeleattr mfsdelquota mfsdirinfo mfsfileinfo mfsfilepaths mfsfilerepair mfsgeteattr mfsgetgoal mfsgetquota mfsgetsclass mfsgettrashtime mfslistsclass mfsmakesnapshot mfsmaster mfsmetadirinfo mfsmetadump mfsmetalogger mfsmetarestore mfsmount mfsnetdump mfsrgetgoal mfsrgettrashtime mfsrmsnapshot mfsrsetgoal mfsrsettrashtime mfsscadmin mfssetarchive mfsseteattr mfssetgoal mfssetquota mfssetsclass mfssettrashtime mfsstatsdump mfstools mfsxchgsclass mft mftrace mftraining mg mgba mgba-qt mgnokiidev mgrtopbm mib2c mibcopy mibdump mid3cp mid3iconv mid3v2 midi2ly midicat midid midori migrate migrate-ds.pl migratepages migrate-repository migrate-users.py migspeed mii-tool milkytracker mill mille mimeview mimikatz.py min minetest minetestserver minews minfo ming-config minicom minidlnad minieap minikube minimize minio minisign miniterm2.py miniterm.py minitest minitube min-nbd-client MinOZW minuet minvoke mirb mispipe mitmdump mitmproxy mitmweb mit-scheme mit-scheme-native mit-scheme-x86-64 mix mixartloader mjpeg_simd_helper mk4ht mkafmmap mkarchiso mkarchroot mkarmins mkbingram mkbinhmm mkbinhmmlist mkbitmap mkbootimg mkbundle mkcamlp4 mkcamlp5 mkcamlp5.opt mkcert mk_cmds mkcp mkcycl mkd2html mkdfa.pl mkdir mkdirhier mkdiskimage mkdosfs mkdotl mke2fs mke2fs.android mkenvimage mkexfatfs mkfa mkfastmod mkfatimage mkfatimage16 mkfifo mkfontdir mkfs mkfs.bfs mkfs.btrfs mkfs.cramfs mkfs.exfat mkfs.ext2 mkfs.ext3 mkfs.ext4 mkfs.f2fs mkfs.fat mkfs.jffs2 mkfs.jfs mkfs.minix mkfs.msdos mkfs.nilfs2 mkfs.ntfs mkfs.reiserfs mkfs.ubifs mkfs.udf mkfs.vfat mkfs.xfs mkgraph mkgraticule.py mkgrkindex mkgshmm mkhom mkhomedir_helper mkhom_old mkhtmlindex mkhybrid mkimage mkinc mkindex mkinitcpio mkinitrd mkinsadd mk_isdnhwdb mkisofs mkisolinux mkjobtexmf mkkanwa mklistdoc mkl_link_tool mklost+found mkluatexfontdb mklvars.sh mkmanifest mkmovixiso mknic-nbi mknmz mknod mkntfs mkocp mkoctfile mkoctfile-5.2.0 mkofm mkpasswd mkpic mkpxelinux mkreiserfs mkrfc2734 mkrlconf mksh mksquashfs mkss mksub mksubvolume mkswap mksyslinux mkt1font mktemp mktexfmt mktexlsr mktexmf mktexpk mktextfm mktorrent mktorrent-archboot mktree mkudffs mkvcalcproba mkvextract mkvinfo mkvmerge mkvpropedit mkvstream mkvtoolnix-gui mkwinpeimg mkx86ins mlabel mlbt mlbt+gui ml_count mldc mldc+gui mldonkey mldonkey+gui mldonkey_gui mlgnut mlgnut+gui mlgui mli2 mlitenotificationtool mllatex mllex mlnet mlnet+gui mlnlffigen mlprof mlslsk mlslsk+gui mltex mlton mlyacc mm2gv mmafm mmaker mmcat mmcli mm-common-get mm-common-prepare mmc-tool mmd mmdblookup mmdbresolve mmex mmls mmount mmove mmpfb mmroff mmseg mmsrip mmstat moc mocp moc-qt5 mod mod2imp mod2osis mod2vpl mod2zmod mod-active modclean mode2 ModemManager modem-manager-gui modhex modprobe modsec-rules-check modula3_count modularize modutil moggsplit mogrify molecule molequeue monboob monero2john monero-blockchain-ancestry monero-blockchain-depth monero-blockchain-export monero-blockchain-import monero-blockchain-mark-spent-outputs monero-blockchain-prune monero-blockchain-prune-known-spent-data monero-blockchain-stats monero-blockchain-usage monerod monero-gen-ssl-cert monero-gen-trusted-multisig monero-wallet-cli monero-wallet-gui monero-wallet-rpc money2john mongoc-stat monit monitor-sensor monkeytype monmaptool mono mono-api-html mono-api-info mono-boehm mono-cil-strip mono-configuration-crypto monodis monodoc monodocer mono-find-provides mono-find-requires mono-gdb.py mono-heapviz monolinker monop monop2 mono-package-runtime mono-service mono-service2 mono-sgen mono-sgen-gdb.py mono-shlib-cop mono-symbolicate mono-test-install mono-xmltool montage mopidy mopidyctl more moreap mori-11d.x mori-4d.x mori-5d.x mori-6d.x mori.x morph morph3d morse moserial mosh mosh-client mosh-server mosquitto mosquitto_passwd mosquitto_pub mosquitto_rr mosquitto_sub most motion moto_server mount mount_afp mountavfs mount.ceph mount.cifs mount.crypt mount.crypt_LUKS mount.crypto_LUKS mount.davfs mount.ecryptfs mount.ecryptfs_private mount.exfat mount.exfat-fuse mount.fuse mount.fuse3 mount.fuse.ceph mount.fuse.sshfs mount.glusterfs mount-image-callback mount.lowntfs-3g mount.moosefs mount.nfs mount.nfs4 mount.nilfs2 mount.ntfs mount.ntfs-3g mountpoint mount.smb3 mount.sshfs mountstats mount.unionfs mount.vmhgfs mouse-dpi-tool mousepad mouse-test mousetweaks movemail move_remote_media_to_new_store.py movix-conf movix-files movix-version moz2po mozcerts-qt5 mozlang2po mozo mozroots mp2enc mp3info mp3splt mp3splt-gtk mp3unicode mp3wrap MP42TS mp4art MP4Box mp4chaps MP4Client mp4extract mp4file mp4info mp4subtitle mp4tags mp4track mp4trackdump mpartition mpathpersist mpc mpc2sv8 mpcchap mpccut mpcd mpcdec mpcenc mpcgain mpeg2dec mpeg2desc mpeg2enc MPEG2TransportStreamIndexer mpeg2vaenc mpeg2vldemo mpegtranscode mperfmon mpg123 mpg123-id3dump mpg123-strip mpgcat mpgdemux mpginfo mpgjoin mpgsplit mpgtx mpic++ mpicalc mpicc mpiCC mpicxx mpiexec mpif77 mpif90 mpifort mpirun mplayer mplex mpop mpost mppcheck mpplu mppprof mpris-proxy mprof mprof-decoder mprof-heap-viewer mprof-report mpstat mpsyt mpv mpxstats mqtt_check.py mrbc mrd mred mred-text mren mrftopbm mrtg2pcp mruby mruby-strip msbuild msfconsole msfd msfdb msf-json-rpc.ru msfrpc msfrpcd msfvenom msf-ws.ru msgattrib msgcat msgcmp msgcomm msgconv msgen msgexec msgfilter msgfmt msggrep msginit msgmerge msgsnarf msgunfmt msguniq mshortname mshowfat msidb msidb msiexec msiexec msmtp ms_print msql2mysql mss mssqlclient.py mssqlinstance.py msxlint mtcp_restart mtd_debug mtdev-test mtdpart mtools mtoolstest mtpaint mtp-albumart mtp-albums mtp-connect mtp-delfile mtp-detect mtp-emptyfolders mtp-files mtp-filetree mtp-folders mtp-format mtpfs mtp-getfile mtp-getplaylist mtp-hotplug mtp-newfolder mtp-newplaylist mtp-playlists mtp-reset mtp-sendfile mtp-sendtr mtp-thumb mtp-tracks mtp-trexist mtr mtr mtrace mtr-packet mtr-packet mtvtoppm m-tx mtxrun mtxrunjit mtype mujs multiarb multibibliography multibit2john multiblend.flt multictx multictx32 multig multilib-build multilibpkg multilib-staging-build multilib-stagingpkg multilib-testing-build multilib-testingpkg multimon-ng multinoise multipath multipathd multirender_test multispell multitail multitex multixterm mumble mumble-overlay mu-mh/ mu-mh/ali mu-mh/anno mu-mh/burst mu-mh/comp mu-mh/fmtcheck mu-mh/folder mu-mh/folders mu-mh/forw mu-mh/inc mu-mh/install-mh mu-mh/mark mu-mh/mhl mu-mh/mhn mu-mh/mhparam mu-mh/mhpath mu-mh/mhseq mu-mh/msgchk mu-mh/next mu-mh/pick mu-mh/prev mu-mh/prompter mu-mh/refile mu-mh/repl mu-mh/rmf mu-mh/rmm mu-mh/scan mu-mh/send mu-mh/show mu-mh/sortm mu-mh/whatnow mu-mh/whom munch munchlist munin-check munin-cron munindoc munin-get munin-node munin-node-configure munin-run mupen64plus muraster murmurd musicxml2ly musixflx musixtex musl-clang musl-gcc mustache mutagen-inspect mutagen-pony mutool mutt muttbug mutter mutt_pgpring muxer mv mvn mvnDebug mvrefind mvxattr mwaw2html mwaw2raw mwaw2svg mwaw2text mwawFile mwawZip mwm mwrank mx-create-image-cache mxtar mygpo-bpsync mygpo-list-devices mygpo-simple-client myisamchk myisamchk myisam_ftdump myisam_ftdump myisamlog myisamlog myisampack myisampack mypaint mypaint-ora-thumbnailer my_print_defaults my_print_defaults mypy mypyc myrocks_hotbackup mysql mysql mysqlaccess mysqladmin mysqladmin mysqlbinlog mysqlbinlog mysqlcheck mysqlcheck mysql_client_test mysql_client_test mysql_client_test_embedded mysql_config mysql_config_editor mysql_convert_table_format mysqld mysqld mysqld_multi mysqld_pre_systemd mysqld_safe mysqld_safe mysqld_safe_helper mysqldump mysqldump mysqldumpslow mysqldumpslow mysql_embedded mysql_find_rows mysql_fix_extensions mysqlhotcopy mysqlimport mysqlimport mysql_install_db mysql_ldb mysql_ldb mysql_plugin mysqlpump mysqlrouter mysqlrouter_keyring mysqlrouter_passwd mysqlrouter_plugin_info mysql_secure_installation mysql_secure_installation mysql_setpermission mysqlshow mysqlshow mysqlslap mysqlslap mysql_ssl_rsa_setup mysqltest mysqltest mysqltest_embedded mysqltest_safe_process mysql_tzinfo_to_sql mysql_tzinfo_to_sql mysql_upgrade mysql_upgrade mysql_waitpid mysql-workbench mysql-workbench-bin mysqlxtest mytop mzc mzip mzpp mzscheme mztext naev nageru namazu namcap name2addr named named-checkconf named-checkzone named-compilezone named-journalprint named-nzd2nzf named-rrchecker namei nameif nanddump nandtest nandwrite nano nanocat na_play nasm natpmpc natsort nautilus nautilus-autorun-software nautilus-sendto navit nawk nbd-client nbd-server nbd-trdump nbtscan nc nc nc3tonc4 nc3tonc4 nc4tonc3 nc4tonc3 ncat ncb nc-config nc-config nccopy nccopy ncdu ncdump ncdump ncftool ncftp ncftpbatch ncftpbookmarks ncftpget ncftpls ncftpput ncftpspooler ncgen ncgen ncgen3 ncgen3 ncinfo ncinfo ncmpc ncmpcpp ncmpidiff ncmpidump ncmpigen ncoffsets ncrack ncu ncursesw6-config ncvalidator ncxx4-config ndctl ndg_httpclient ndg_httpclient2 ndiff ndisasm ndisc6 ndiswrapper ndiswrapper-buginfo ndpexhaust26 ndpexhaust6 ndptool ndrdump nds32le-elf-addr2line nds32le-elf-ar nds32le-elf-as nds32le-elf-c++ nds32le-elf-c++filt nds32le-elf-cpp nds32le-elf-elfedit nds32le-elf-g++ nds32le-elf-gcc nds32le-elf-gcc-10.1.0 nds32le-elf-gcc-ar nds32le-elf-gcc-nm nds32le-elf-gcc-ranlib nds32le-elf-gcov nds32le-elf-gcov-dump nds32le-elf-gcov-tool nds32le-elf-gprof nds32le-elf-ld nds32le-elf-ld.bfd nds32le-elf-lto-dump nds32le-elf-nm nds32le-elf-objcopy nds32le-elf-objdump nds32le-elf-ranlib nds32le-elf-readelf nds32le-elf-size nds32le-elf-strings nds32le-elf-strip nearblack neato nebula nebula-cert nebula-service nedit nedit-client nef-11d.x nef-4d.x nef-5d.x nef-6d.x nef.x neko nekoc nekoml nekotools nemiver nemo nemo-autorun-software nemo-connect-server nemo-desktop nemo-open-with nemo-preview nemo-seahorse-tool nemo-terminal-prefs neo2john neofetch neomutt neon-config neotoppm neqn net netaddr netbeans netbrake netcap netcat netcat netconsole netctl netctl-auto netdata netdatacli nethack nethogs netkey-tool netntlm netperf netplan netresolv netscreen netserver netsniff-ng net-snmp-cert net-snmp-config net-snmp-create-v3-user netstat netstat-nat netsurf netsurf-gtk3 nettest nettle-hash nettle-lfib-stream nettle-pbkdf2 netview.py netwatch network2john networkctl NetworkManager newaliases newaliases newaliases newedgeg newgidmap newgrp news2mail newsboat news.daily newuidmap newusers nextcloud nextcloudcmd nextword nfacct nfbpf_compile nfbpf_compile nfc-anticol nfc-barcode nfc-dep-initiator nfc-dep-target nfc-emulate-forum-tag2 nfc-emulate-forum-tag4 nfc-emulate-tag nfc-emulate-uid nfc-jewel nfc-list nfc-mfclassic nfc-mfsetuid nfc-mfultralight nf-config nfcount nfc-poll nfc-read-forum-tag3 nfc-relay nfc-relay-picc nfc-scan-device nfct nf-ct-add nf-ct-events nf-ct-list nf-exp-add nf-exp-delete nf-exp-list nfhpcurve nfhpmcurve nf-log nf-monitor nfnl_osf nfnl_osf nfoview nf-queue nfs-cat nfsconf nfs-cp nfsdcld nfsdcltrack nfsidmap nfsiostat nfs-ls nfsstat nft nftldump nftl_format ngettext nghttp nghttpx nginx nginx ngircd ngmakeidx ngmultidec ngnutmeg ngproc2mod ngram_build ngram_test ngrep ngsconvert ngspice nice niceload nick2ldif nickle niftest nih-dbus-tool nikola nikon-curve nikto nilfs-clean nilfs_cleanerd nilfs-resize nilfs-tune nim nimble nimblepkg nimfind nim-gdb nim-gdb.bash nim-gdb.bat nimgrep nimpretty nimsuggest ninfod ninja ninpaths nitrogen nitrokey-app nl nl-addr-add nl-addr-delete nl-addr-list nl-class-add nl-class-delete nl-classid-lookup nl-class-list nl-cls-add nl-cls-delete nl-cls-list nl-fib-lookup nl-link-enslave nl-link-ifindex2name nl-link-list nl-link-name2ifindex nl-link-release nl-link-set nl-link-stats nl-list-caches nl-list-sockets nl-monitor nl-neigh-add nl-neigh-delete nl-neigh-list nl-neightbl-list nload nl-pktloc-lookup nl-qdisc-add nl-qdisc-delete nl-qdisc-list nl-route-add nl-route-delete nl-route-get nl-route-list nl-rule-list nl-tctree-list nltk nl-util-addr nm nm86 nmap nmapAnswerMachine.py nm-applet nmbd nmblookup nmcli nm-connection-editor nmigen-rpc nmon nm-online nmtui nmtui-connect nmtui-edit nmtui-hostname nmzcat nmz-config nmzegrep nmzgrep nmzmerge nnn nntpget nntpsend node node nodeenv node-gyp node_query6 nodm nohup noise noise2 nokfw nologin nona nonce2key noncvslist nonsvnlist nop noping normalform normaliz nose2 nose2-3.8 nosetests nosetests2 nosetests-2.7 nosetests3 nosetests-3.8 not not notepad notepad notepadqq notify-send notion notionflux notmuch notmuch-mutt nova nox npa-tool nping npm nproc nproxy npth-config npx nrg2iso nroff nrpe NRswitchg ns-accountstatus.pl ns-activate.pl nscd nsd nsd-checkconf nsd-checkzone nsd-control nsd-control-setup nsec3dig nsec3hash nsenter nsgenbind nsgmls ns-inactivate.pl nsjail nslcd nslookup nsm-legacy-gui ns-newpwpolicy.pl nspluginplayer nspluginwrapper nspr-config nss-config nstat ntfs-3g ntfs-3g.probe ntfscat ntfsclone ntfscluster ntfscmp ntfscp ntfsdecrypt ntfsfix ntfsinfo ntfslabel ntfsls ntfs-read.py ntfsrecover ntfsresize ntfssecaudit ntfstruncate ntfsundelete ntfsusermap ntfswipe ntk-chtheme ntk-fluid ntlm_auth ntlmrelayx.py ntop ntpctl ntp-keygen ntpq ntpshmmon ntptime ntptrace ntp-wait ntsc-cc nufraw nufraw-batch nuget nuitka3 nuitka3-run nullmailer-dsn nullmailer-inject nullmailer-queue nullmailer-send numactl numademo numastat number numbers2raw numbers2text numfmt numlockx nuspell nut-scanner nvchecker nvcmp nvidia-bug-report.sh nvidia-cuda-mps-control nvidia-cuda-mps-server nvidia-debugdump nvidia-modprobe nvidia-persistenced nvidia-settings nvidia-sleep.sh nvidia-smi nvidia-xconfig nvim nvim-qt nvlc nvme nvtake nvtop nxagent nxproxy nyancat nyx nzbget oathtool obabel obconf obconformer obconf-qt obdistgen obenergy obex-client-tool obexctl obex-server-tool obfit obfitall obfs-local obfs-server obgen obgrep obgui obj2yaml obj2yaml objc_count objcopy objdump objdump86 objres obminimize obmm obprobe obprop obrms obrotamer obrotate obs obs-ffmpeg-mux obspectrophore obsym obtautomer obthermo obxprop ocaml ocamlbuild ocamlbuild.byte ocamlbuild.native ocamlc ocamlc.byte ocamlcmt ocamlc.opt ocamlcp ocamlcp.byte ocamlcp.opt ocamldebug ocamldep ocamldep.byte ocamldep.opt ocamldoc ocamldoc.opt ocamlfind ocamlfind_opt ocaml-instr-graph ocaml-instr-report ocamllex ocamllex.byte ocamllex.opt ocamlmklib ocamlmklib.byte ocamlmklib.opt ocamlmktop ocamlmktop.byte ocamlmktop.opt ocamlobjinfo ocamlobjinfo.byte ocamlobjinfo.opt ocamlopt ocamlopt.byte ocamlopt.opt ocamlprof ocamlprof.byte ocamlprof.opt ocamlrun ocamlrund ocamlruni ocamlyacc ociobakelut ociocheck ocloc oclock ocount ocpp5 ocrad ocrfeeder ocrfeeder-cli octave octave-5.2.0 octave-cli octave-cli-5.2.0 octave-config octave-config-5.2.0 od odbc_config odbcinst odbx-sql ode ode-config odf2mht odf2xhtml odf2xliff odf2xml odfimgimport odflint odfmeta odfoutline odfuserfield odt2txt odvitype oeminst office2john offlineimap offload-build offroad offset offset32 ofx2qif ofxconnect ofxdump ogg123 oggdec oggenc ogginfo oggsplt oggz oggz-chop oggz-comment oggz-diff oggz-dump oggz-info oggz-merge oggz-rip oggz-scan oggz-sort oggz-validate ogmcat ogmdemux ogminfo ogmmerge ogmsplit ogonkify ogr2ogr OgreMeshTool ogrinfo ogrlineref ogrmerge.py ogrtindex oid2name oidentd oidnBenchmark oidnDenoise oidnTest oiiotool okteta okular ol ols oLschema2ldif ols_test omfonts ompi-clean ompi_info ompi-server omshell omxregister-bellagio onboard onboard-settings onig-config onionshare onionshare-gui onlinequoteseditor5 onnode onsgmls oo2po oo2xliff oobtest op2calltree opal_wrapper opam opam-installer opannotate oparchive op-check-perfevents openaldevices openal-info openapi-generator openapi-generator-cli openapi-spec-validator openapp openbox openbox-kde-session openbox-lxde openbox-session open-browser-example openbsd_softraid2john openbve opencc opencc_dict opencc_phrase_extract openconnect opencv_annotation opencv_interactive-calibration opencv_version opencv_visualisation opencv_waldboost_detector opendkim opendkim-genkey opendkim-genzone opendkim-testkey opendkim-testmsg opendmarc opendmarc-check opendmarc-expire opendmarc-import opendmarc-importstats opendmarc-reports openethereum openethereum-ethkey openethereum-evm openfortivpn openipmicmd openipmi_eventd openipmish openjade openmdao openmw openmw-essimporter openmw-iniimporter openmw-launcher openmw-wizard openocd openpgp-tool openra-cnc openra-cnc-server openra-d2k openra-d2k-server openra-ra openra-ra-server open-raw.pl openRTSP openscad opensc-asn1 opensc-explorer opensc-notify opensc-tool opensearch-discover opensearch-genquery openshot-audio-test-sound openshot-qt opensips openssl openssl-1.0 openssl2john openstack openstack-inventory opentool opentoonz OpenToonz openttd openvpn openvt opera operf operon opgprof ophcrack ophelp opimport oping opj_compress opj_decompress opj_dump opjitconv opl2ofm opreport opt opt optimal optipng optirun opusdec opusenc opusinfo or1k-elf-addr2line or1k-elf-ar or1k-elf-as or1k-elf-c++ or1k-elf-c++filt or1k-elf-cpp or1k-elf-elfedit or1k-elf-g++ or1k-elf-gcc or1k-elf-gcc-10.1.0 or1k-elf-gcc-ar or1k-elf-gcc-nm or1k-elf-gcc-ranlib or1k-elf-gcov or1k-elf-gcov-dump or1k-elf-gcov-tool or1k-elf-gdb or1k-elf-gdb-add-index or1k-elf-gprof or1k-elf-ld or1k-elf-ld.bfd or1k-elf-lto-dump or1k-elf-nm or1k-elf-objcopy or1k-elf-objdump or1k-elf-ranlib or1k-elf-readelf or1k-elf-size or1k-elf-strings or1k-elf-strip oracle orage orbd orbrep orca orc-bugreport orcc orcus-detect orcus-gnumeric orcus-json orcus-mso-encryption orcus-ods orcus-styles-ods orcus-xlsx orcus-xls-xml orcus-xml orcus-yaml orcus-zip-dump ortecc orte-clean orted orte-info orterun orte-server osage oscdump osdbattery osd_cat osdemo osdemo16 osdemo1632 osdemo32 osdemo3232 osdmaptool osgarchive osgconv osgfilecache osgmlnorm osgversion osgviewer osinfo-db-export osinfo-db-import osinfo-db-path osinfo-db-validate osinfo-detect osinfo-install-script osinfo-query osipsconfig osirrox osis2mod oslc oslinfo oslo-config-generator oslo-config-validator oslo_debug_helper oslo_run_cross_tests oslo_run_pre_release_tests osmo osmocom_fft osmocom_siggen_nogui osmocom_spectrum_sense osmo_sdr ospam ospBenchmark ospcat ospCvtParaViewTfcn ospDistribViewerDemo ospent ospExampleViewer ospf6d ospfclient ospfd ospRandSciVisTest ospRandSphereTest ospRawToAmr ospRawToVtu ospray_mpi_worker ospray_test_suite os-prober ospTutorialBouncingSpheres ospTutorialSpheres ospTutorialStructuredVolume ospTutorialSubdivision ospTutorialUnstructuredVolume osql osqueryctl osqueryd osqueryi ossp-alsap ossp-padsp ostra-cg ostree osx ot2kpx otangle otfdump otfinfo otflist otftobdf otftotfm otfview otp2ocp otr_mackey otr_modify otr_parse otr_readforge otr_remac otr_sesskeys ots otter-browser out123 outocp output ovdb_init ovdb_monitor ovdb_server ovdb_stat over overchan overlay overlay32 ovf2ovp ovm ovp2ovf ovs-appctl ovs-bugtool ovsdb-client ovsdb-server ovsdb-tool ovs-docker ovs-dpctl ovs-dpctl-top ovs-l3ping ovs-ofctl ovs-parse-backtrace ovs-pcap ovs-pki ovs-tcpundump ovs-test ovs-testcontroller ovs-vlan-test ovs-vsctl ovs-vswitchd owncloud owncloudcmd ownership oxipng oxygen-demo5 oxygen-gtk-demo oxygen-settings5 ozw_config p0f p0f-analyzer.pl p0f-client p0f-sendsyn p0f-sendsyn6 p11-kit p11tool p2psim pacat paccache paccapability paccheck pacconf pacdiff pacfile pacgraph pacgraph-tk pacinfo pacini pacinstall pack pack200 package_crystalsvg packer Packer packetbeat packetforge-ng packETH packihx paclist paclock paclog paclog-pkglist pacman pacman-conf pacman-db-upgrade pacman-key pacmatic pacmd pacredir pacremove pacrepairdb pacrepairfile pacreport pacrunner pactester pactl pactrans pactree padlock2john padsp pageant pageedit pages2html pages2raw pages2text pahole pa-info pal2rgb palapeli palmtopnm paltex pamarith pambackground pambayer pamchannel pamcomp pamcrater pamcut pamdeinterlace pamdepth pamdice pamditherbw pamedge pamendian pamenlarge pamexec pamfile pamfix pamfixtrunc pamflip pamfunc pamgauss pamgradient pamlookup pammasksharpen pammixinterlace pammosaicknit pamoil pamon pampaintspill pamperspective pamphletangler pampick pampop9 pamrecolor pamrgbatopng pamrubber pam_tally pam_tally2 pamthreshold pamtilt pam_timestamp_check pamtoavs pamtodjvurle pamtofits pamtogif pamtohdiff pamtohtmltbl pamtojpeg2k pamtompfont pamtooctaveimg pamtopam pamtopfm pamtopng pamtopnm pamtosrf pamtosvg pamtotga pamtotiff pamtouil pamtowinicon pamtoxvmini pamu2fcfg pamundice pamunlookup pamvalidate pamwipeout pan pandoc pandoc-citeproc panelctl pango-list pango-view panoinfo pano_modify pano_trafo pansy pantheon-parental-controls-daemon paper paperconf paperconfig paperkey paperwork paperwork-shell paplay paprefs paps par2 par2create par2repair par2verify parallel parallel-moreutils parasite6 paraview paraview.conf paraview-config parcat parcellite parceloob parec parley parole parse-edid parse.f2fs parsepkgbuild parse_srcinfo parset parse_xsd2.py partclone.btrfs partclone.chkimg partclone.dd partclone.exfat partclone.ext2 partclone.ext3 partclone.ext4 partclone.ext4dev partclone.extfs partclone.f2fs partclone.fat partclone.fat12 partclone.fat16 partclone.fat32 partclone.hfs+ partclone.hfsp partclone.hfsplus partclone.imager partclone.info partclone.minix partclone.nilfs2 partclone.ntfs partclone.ntfsfixboot partclone.ntfsreloc partclone.reiserfs partclone.restore partclone.vfat partclone.xfs parted partimage partimaged partimaged-gencrt partitionmanager partprobe partx pas2fpm pas2jni pas2ut pascal_count pass pass_gen passive_discovery6 passmass passmenu passwd paste pastebinit pasteurize pasteurize2 pastoob pasuspender pasystray pat2dat pat2ppm patch patchage patchelf patchwork patextract patgen pathchk PathFinder pathoc pathod pathological paver pavucontrol pavucontrol-qt pax pax11publish paxcpio paxtar paxtest pbdemo pbdemo32 pbinfo pbinfo32 pbmclean pbmlife pbmmake pbmmask pbmminkowski pbmpage pbmreduce pbmtext pbmtextps pbmto10x pbmto4425 pbmtoascii pbmtoatk pbmtobbnbg pbmtocis pbmtocmuwm pbmtodjvurle pbmtoepsi pbmtoepson pbmtoescp2 pbmtog3 pbmtogem pbmtogo pbmtoibm23xx pbmtoicon pbmtojbg pbmtojbg85 pbmtolj pbmtoln03 pbmtolps pbmtomacp pbmtomatrixorbital pbmtomda pbmtomgr pbmtomrf pbmtonokia pbmtopgm pbmtopi3 pbmtopk pbmtoplot pbmtoppa pbmtopsg3 pbmtoptx pbmtosunicon pbmtowbmp pbmtox10bm pbmtoxbm pbmtoybm pbmtozinc pbmupc pbpst pbr pbr2 pbunzip2 pbzcat pbzip2 pc pc1toppm pcap2john pcap-config pcb pcb_backannotate pcb_calculator pcbnew pcdovtoppm pcf2vpnc pchrt pcmanfm pcmanfm-qt pcp pcp2graphite pcp2json pcp2spark pcp2xml pcp2zabbix pcprofiledump pcre2-config pcre2grep pcre2test pcre-config pcregrep pcretest PCSX2 PCSX2-linux.sh pct2rgb.py pcurses pcurve pcxhrloader pcxtoppm peas-demo pecan pedigree pedump pee peek peekfd peg peglgears pekwm pelican pelican-import pelican-quickstart pelican-themes pem2john pencil2d peony peony-qt-desktop percol perconaserver_config perf perfdhcp performous perl perl_count perl-nocem perl-rename perltex permute permute_exist permview perror perror persepolis persist-tool pesigcheck pesign pesign-client petcat pethtool peverify pew pf2afm pfarrei pfb2pfa pfbtopfa pfbtops pfc pfd pflags pflogsumm pfmtopam pfunct pfx2john pgadmin4 pgadmin4-server pg_archivecleanup pg_basebackup pgbench pgbouncer pg_checksums pg_config pg_controldata pg_ctl pg_dump pg_dumpall pgiac pg_isready pglobal pgmabel pgmbentley pgmcrater pgmdeshadow pgmedge pgmenhance pgmhist pgmkernel pgmmake pgmmedian pgmminkowski pgmmorphconv pgmnoise pgmnorm pgmoil pgmramp pgmslice pgmtexture pgmtofs pgmtolispm pgmtopbm pgmtopgm pgmtoppm pgmtosbig pgmtost4 pgmtoy4m pgon-mode pgp-clean pgpewrap pgp-fixkey pgpring pgpverify pgpwde2john pg_receivewal pg_recvlogical pgrep pg_resetwal pg_restore pg_rewind pgsql2shp pg_standby pg_test_fsync pg_test_timing pg_upgrade pg_waldump ph5diff phantasia phar phar.phar phoa2d phod2a phoenix_info phonegap phononsettings photoflare photorec php php2po php-cgi php-config php_count php-fpm phpize phppo2pypo physlock pi pi1toppm pi3topbm pia pianobar pianoctl pic pic2graph pic2plot pic2tpic picard pickg picmi picocom picom picom-trans pid1 pidgin pidof pidproxy pidstat piep pifconfig pifpaf pig pigar pigz pi-make-microsoft-freecell-board pimd pimdataexporter pimdataexporterconsole pinentry pinentry-curses pinentry-gnome3 pinentry-gtk-2 pinentry-qt pinentry-tty pinfo ping ping6.py ping_pong ping.py pingus pingus.bin pinky pint-convert pip pip2 pip2.7 pip3 pip3.8 pipenv pipenv-resolver piper pipesocks pipewire pipewire-media-session pipreqs pip_stress pipx pirate pi_stress pitchmark pitivi pius pius-keyring-mgr pius-party-worksheet pius-report pivot_root piv-tool pixeltest pixeltool pixeltool-qt5 pixiewps pixz pjtoppm pk12util pk2bm pkaction pkcheck pkcon pk-example-frobnicate pkexec pkfix pkfix-helper pkgconf pkg-config pkgdata pkgdiff pkgfile pkginfo pkgstats pki pkill pkmon pktogf pktopbm pktsetup pkttyagent pktype pl pl2link pl2wam planarg planarity planetsplitter planetsplitter-slim plank plantri plantuml plasma-browser-integration-host plasma-discover plasmaengineexplorer plasmapkg2 plasma_session plasmashell plasma-shutdown plasmathemeexplorer plasma_waitforname plasmawindowed plasmoidviewer platex play playbackmanager play_cell Player playerctl playerctld plaympeg playonlinux playonlinux-check_dd playonlinux-pkg playonlinux-url_handler playout play.sh playSIP playsound playsound_simple pldd pldes pl_editor plex plget plink plipconfig plistutil plmerge plog plot plotfont plowdel plowdown plowlist plowmod plowprobe plowup plparse plser plt-games plt-help pltotf plt-r5rs plt-r6rs plt-web-server plugctl pluginviewer plugreport pluma plv pm pm2 pm2-dev pm2-docker pm2-runtime pm3 pm3_eml2lower.sh pm3_eml2mfd.py pm3_eml2upper.sh pm3-flash pm3-flash-all pm3-flash-bootrom pm3-flash-fullimage pm3_mfd2eml.py pm3_mfdread.py pmafm pmap pmchart pmclient pmclient_fg pmconfirm pmd2raw pmd2svg pmd2text pmdate pmdbg pmdefaults pmdiff pmdumplog pmdumptext pmerr pmevent pmfind pmgenmap pmie pmie2col pmieconf pminfo pmiostat pmjson pmlc pmlogcheck pmlogconf pmlogextract pmlogger pmloglabel pmlogmv pmlogsize pmlogsummary pmmessage pmpost pmprobe pmpython pmqtest pmquery pmrep pmsocks pmstat pmstore pmt-ehd pmtime pmtrace pmval pmvarrun pmxab pmxchords pn53x-diagnose pn53x-sam pn53x-tamashell pnetcdf-config pnetcdf_version png2dbl png2icns png2mng.pl png2pat png2pnm png2yuv pngcrush pngfix pngfix-32 png-fix-itxt png-fix-itxt-32 pngquant pngtogd pngtogd2 pngtopam pngtopnm pnm2png pnmalias pnmarith pnmcat pnmcolormap pnmcomp pnmconvol pnmcrop pnmcut pnmdepth pnmenlarge pnmfile pnmflip pnmgamma pnmhisteq pnmhistmap pnmindex pnminterp pnminvert pnmmargin pnmmercator pnmmontage pnmnlfilt pnmnoraw pnmnorm pnmpad pnmpaste pnmpsnr pnmquant pnmquantall pnmremap pnmrotate pnmshear pnmsmooth pnmsplit pnmstitch pnmtile pnmtoddif pnmtofiasco pnmtofits pnmtojbig pnmtojpeg pnmtopalm pnmtopclxl pnmtoplainpnm pnmtopng pnmtopnm pnmtops pnmtorast pnmtorle pnmtosgi pnmtosir pnmtotiff pnmtotiffcmyk pnmtoxwd pnmtoy4m po2flatxml po2html po2ical po2idml po2ini po2json po2l20n po2oo po2php po2prop po2rc po2resx po2sub po2symb po2tiki po2tmx po2ts po2txt po2web2py po2wordfast po2xliff po2xmi5 po2xml po2yaml poclcc poclean pocommentclean pocompendium pocompile poconflicts pocount pod2texi podboat podebug podman podman-remote podofobox podofocolor podofocountpages podofocrop podofoencrypt podofogc podofoimgextract podofoimpose podofomerge podofopages podofosign podofotxtextract podofouncompress podofoxmp poedit poetry poff pofilter pogrep point pointblast point-clip pointcoord point-param points point_search point-sprite point-wide point-wide-smooth polar_channel_construction polari policyeditor polipo poly poly poly-11d.x poly-4d.x poly-5d.x poly-6d.x polyc poly-flat poly-flat-clip poly-flat-unfilled-clip polyhedron-to-cones polyimport polymake polymake-config poly-unfilled poly.x pom pomerge pomigrate2 pon ponyc ponymix ponysay ponysay-tool ponythink pooltype pop3d popauth popeye popuretext poreencode porestructure pork portablectl posegment posplit postalias postcat postconf postdrop posterazor postfix postfix-collate.pl postfwd postfwd1 postfwd2 postfwd3 postfwd-script.sh postgres postgresql-check-db-dir postgrest postgrey postgreyreport post-grohtml postkick postlock postlog postmap postmaster postmodern postmulti postprocessing_benchmark postqueue postsuper posttls-finger postw32 poswap pot2po potcheck poterminology potrace pound poundctl povray powerline powerline-config powerline-daemon powerline-lint powerline-render poweroff powertop powmeanstd PParse ppc64le-elf-addr2line ppc64le-elf-ar ppc64le-elf-as ppc64le-elf-c++filt ppc64le-elf-dwp ppc64le-elf-elfedit ppc64le-elf-embedspu ppc64le-elf-gdb ppc64le-elf-gdb-add-index ppc64le-elf-gprof ppc64le-elf-ld ppc64le-elf-ld.bfd ppc64le-elf-ld.gold ppc64le-elf-nm ppc64le-elf-objcopy ppc64le-elf-objdump ppc64le-elf-ranlib ppc64le-elf-readelf ppc64le-elf-size ppc64le-elf-strings ppc64le-elf-strip ppcx64 pphs ppi ppl-config ppl_lcdd ppl_lpsol ppl_pips ppltotf ppm2tiff ppm3d ppmbrighten ppmchange ppmcie ppmcolormask ppmcolors ppmdcfont ppmddumpfont ppmdim ppmdist ppmdither ppmdmkfont ppmdraw ppmfade ppmflash ppmforge ppmglobe ppmhist ppmlabel ppmmake ppmmix ppmnorm ppmntsc ppmpat ppmquant ppmquantall ppmrainbow ppmrelief ppmrough ppmshadow ppmshift ppmspread ppmtoacad ppmtoapplevol ppmtoarbtxt ppmtoascii ppmtobmp ppmtoeyuv ppmtogif ppmtoicr ppmtoilbm ppmtojpeg ppmtoleaf ppmtolj ppmtolss16 ppmtomap ppmtomitsu ppmtompeg ppmtoneo ppmtopcx ppmtopgm ppmtopi1 ppmtopict ppmtopj ppmtopjxl ppmtoppm ppmtopuzz ppmtorgb3 ppmtosixel ppmtospu ppmtoterm ppmtotga ppmtouil ppmtowinicon ppmtoxpm ppmtoy4m ppmtoyuv ppmtoyuvsplit ppmtv ppmwheel pppoe pppoe-connect pppoe-discovery pppoe-relay pppoe-server pppoe-setup pppoe-sniff pppoe-start pppoe-status pppoe-stop pppstats pprof pprof2calltree pprofile ppscheck ppsh PPSSPPHeadless PPSSPPQt PPSSPPSDL ppt pptp pptpctrl pptpsetup pp-trace ppudump ppufiles ppumove pr praefect pragha prance pr-downloader precat pre-commit pre-commit-validate-config pre-commit-validate-manifest precond preconv prefcnt pre-grohtml premake premake4 prepare prepareHistFactory prepareHistFactory preparetips5 prepmx present present3D pretranslate prettier prettyping preunzip prezip prezip-bin prg2lout primecount prime-run primes primesieve primorial primusrun primus_vk_diag printafm printcal printcbm printemf printenv printf print_sum printtarg privoxy prlimit probemodem procan procbatch process_keys procinfo procmail profanity profcheck profile2mat.pl profile-cleaner profiles profile-sync-daemon program.pt progress proj projection projection_gmp projectM-pulseaudio projectMSDL projinfo projtex Projucer prometheus prometheus-blackbox-exporter prometheus-memcached-exporter prometheus-node-exporter promtail promtool pronto2lirc proofexecv proofexecv proofserv proofserv proofserv.exe proofserv.exe prop2po propwatch proselint prosody prosody2john prosodyctl prosody-migrator protege protoc protoc-c protoc-gen-c protoc-gen-mypy protoc_gen_mypy.bat protocoltojson proxmark3 proxy proxychains proxychains4 proxytrack proxytunnel prtstat prune pruneemptydirs prunehistory prusa-slicer ps ps2ascii ps2eps ps2epsi ps2frag ps2pk ps2ps ps2ps2 ps-admin psbook psc pscap pscp psd psd-overlay-helper psd-suspend-sync pse2john psensor psensor-server pseudochop psexec.py psfaddtable psfgettable psfstriptable psftp psfxtable psi psicc psiconv psiconv-config psidtopgm psjoin psk pskctool psktool psl pslatex pslist pslog psmandup ps_mem ps_mysqld_helper psnup pspax pspell-config psql psresize pss psselect psset pst2ldif pstat pstoedit pstogif pstoimg ps_token2john pstopnm pstops pstotext pstoxbm pstree pstree.x11 PSVIWriter pt2to3 ptable ptaskset PTBatcherGUI PTblender PTcrop ptdump pterm ptex ptftopl pth-config PTinfo PTmasker PTmender pt-mongodb-query-digest pt-mongodb-summary pto_gen pto_lensstack pto_mask pto_merge pto_move ptop ptop.rsj PToptimizer pto_template pto_var pt-pg-summary ptrepack PTroller pt-secure-collect ptsematest PTtiff2psd PTtiffdump pttree PTuncrop ptunnel ptx ptxinfo pub pub2raw pub2xhtml pudb3 pullnews pulseaudio pulseaudio-equalizer pulseaudio-equalizer-gtk pulseeffects pulseview pump puppet puppeth purge purge-old-kernels purifyeps purple-client-example purple-remote purple-send purple-send-async purple-url-handler putmail putsurface putsurface_wayland putty putty2john puttygen puttytel puzzles-blackbox puzzles-bridges puzzles-cube puzzles-dominosa puzzles-fifteen puzzles-filling puzzles-flip puzzles-flood puzzles-galaxies puzzles-guess puzzles-inertia puzzles-keen puzzles-lightup puzzles-loopy puzzles-magnets puzzles-map puzzles-mines puzzles-net puzzles-netslide puzzles-palisade puzzles-pattern puzzles-pearl puzzles-pegs puzzles-range puzzles-rect puzzles-samegame puzzles-signpost puzzles-singles puzzles-sixteen puzzles-slant puzzles-solo puzzles-tents puzzles-towers puzzles-tracks puzzles-twiddle puzzles-undead puzzles-unequal puzzles-unruly puzzles-untangle pv pvbatch pvchange pvck pvcreate pvdataserver pvdisplay pvkrun pvmove pvpython pvremove pvrenderserver pvresize pvs pvscan pvserver pvtkpython pw-cat pwck pw-cli pwconv pwd pwdhash pw-dot pwdx pwgen pwhash pw-inspector pwiz.py pw-jack pwkond pwmake pwmconfig pw-metadata pw-mididump pw-midiplay pw-mon pwn pw-play pw-profiler pw-pulse pwsafe2john pwscore pwunconv pxelinux-options py3-cmd py3status pyacc pyalacarte2 pyalamode2 pyannotate pybabel pybabel2 pybtex pybtex-convert pybtex-format pycharm pychecker pychess pycman-database pycman-deptest pycman-query pycman-remove pycman-sync pycman-upgrade pycman-version pycodestyle pycodestyle2 pycrust pycrust2 pydb pydf pydiff pydoc pydoc2 pydoc3 pydoc3.8 pyenv pyenv-install pyenv-uninstall pyfiglet pyflakes pyflakes3k pyflakes-python2 pyftmerge pyftsubset pygal_gen.py pygmentex pygmentize pygobject-codegen-2.0 pyhtmlizer pyhtmlizer3 pyjson5 pyjwt pykig.py pykwalify pylama pylint pylint2 pyls pymol pyperf pyperf2 pypo2phppo pyprof2calltree pypugjs pypy pypy3 pyqt-bundle pyrcc5 pyreverse pyreverse2 pyrit pyrogenesis pyroute2-cli pyrsa-decrypt pyrsa-encrypt pyrsa-keygen pyrsa-priv2pub pyrsa-sign pyrsa-verify pysemver pyshell pyshell2 pyside_tool.py pyslices pyslicesshell pysol pytddmon.py py.test pytest py.test2 pytest2 py.test-benchmark pytest-benchmark pythia8-config python python2 python2.7 python2.7-config python2-config python2-pyrcc5 python2-pyuic5 python3 python3.8 python3.8-config python3-config python-argcomplete-check-easy-install-script python-build python-build python-config python_count pythontex pyuic5 pywrap2 pywxrc pywxrc2 pyzo pzstd qalc qalculate-gtk qapitrace qb-blackbox qbittorrent qbittorrent-nox qbs qbs-config qbs-config-ui qbs-create-project qbs-setup-android qbs-setup-qt qbs-setup-toolchains qcachegrind qcad qcatool-qt5 qcmd qcollectiongenerator qcollectiongenerator-qt5 qconf qconvex qcp qdarkstyle qdbus qdbuscpp2xml qdbuscpp2xml-qt5 qdbus-qt5 qdbusviewer qdbusviewer-qt5 qdbusxml2cpp qdbusxml2cpp-qt5 qd-config qdelaunay qdistancefieldgenerator qdistancefieldgenerator-qt5 qdoc qdoc-qt5 qemu-aarch64 qemu-aarch64 qemu-aarch64_be qemu-aarch64_be qemu-alpha qemu-alpha qemu-arm qemu-arm qemu-armeb qemu-armeb qemu-cris qemu-cris qemu-edid qemu-edid qemu-ga qemu-hppa qemu-hppa qemu-i386 qemu-i386 qemu-img qemu-img qemu-io qemu-io qemu-keymap qemu-keymap qemu-m68k qemu-m68k qemu-microblaze qemu-microblaze qemu-microblazeel qemu-microblazeel qemu-mips qemu-mips qemu-mips64 qemu-mips64 qemu-mips64el qemu-mips64el qemu-mipsel qemu-mipsel qemu-mipsn32 qemu-mipsn32 qemu-mipsn32el qemu-mipsn32el qemu-nbd qemu-nbd qemu-nios2 qemu-nios2 qemu-or1k qemu-or1k qemu-ppc qemu-ppc qemu-ppc64 qemu-ppc64 qemu-ppc64abi32 qemu-ppc64abi32 qemu-ppc64le qemu-ppc64le qemu-pr-helper qemu-pr-helper qemu-riscv32 qemu-riscv32 qemu-riscv64 qemu-riscv64 qemu-s390x qemu-s390x qemu-sh4 qemu-sh4 qemu-sh4eb qemu-sh4eb qemu-sparc qemu-sparc qemu-sparc32plus qemu-sparc32plus qemu-sparc64 qemu-sparc64 qemu-storage-daemon qemu-storage-daemon qemu-system-aarch64 qemu-system-aarch64 qemu-system-alpha qemu-system-alpha qemu-system-arm qemu-system-arm qemu-system-cris qemu-system-cris qemu-system-hppa qemu-system-hppa qemu-system-i386 qemu-system-i386 qemu-system-lm32 qemu-system-lm32 qemu-system-m68k qemu-system-m68k qemu-system-microblaze qemu-system-microblaze qemu-system-microblazeel qemu-system-microblazeel qemu-system-mips qemu-system-mips qemu-system-mips64 qemu-system-mips64 qemu-system-mips64el qemu-system-mips64el qemu-system-mipsel qemu-system-mipsel qemu-system-moxie qemu-system-moxie qemu-system-nios2 qemu-system-nios2 qemu-system-or1k qemu-system-or1k qemu-system-ppc qemu-system-ppc qemu-system-ppc64 qemu-system-ppc64 qemu-system-riscv32 qemu-system-riscv32 qemu-system-riscv64 qemu-system-riscv64 qemu-system-rx qemu-system-rx qemu-system-s390x qemu-system-s390x qemu-system-sh4 qemu-system-sh4 qemu-system-sh4eb qemu-system-sh4eb qemu-system-sparc qemu-system-sparc qemu-system-sparc64 qemu-system-sparc64 qemu-system-tricore qemu-system-tricore qemu-system-unicore32 qemu-system-unicore32 qemu-system-x86_64 qemu-system-x86_64 qemu-system-xtensa qemu-system-xtensa qemu-system-xtensaeb qemu-system-xtensaeb qemu-tilegx qemu-tilegx qemu-x86_64 qemu-x86_64 qemu-xtensa qemu-xtensa qemu-xtensaeb qemu-xtensaeb qexp qgis qgis_bench qgis_mapserver qgis_mapserv.fcgi qgit qgltf qgo qhalf qhelpgenerator qhelpgenerator-qt5 qhull qiniupy qiv qjournalctl qlalr qlalr-qt5 QLinkHut QLinkHutSilent qmake qmake-qt5 qmapshack qmaptool qmc2 qmc2-sdlmame qmicli qmi-network qml qmlcachegen qmlcachegen-qt5 QmlComponentGallery5 qmleasing qmleasing-qt5 qmlformat qmlformat-qt5 qmlimportscanner qmlimportscanner-qt5 qmllint qmllint-qt5 qmlmin qmlmin-qt5 QMLPlayer qmlplugindump qmlplugindump-qt5 qmlpreview qmlpreview-qt5 qmlprofiler qmlprofiler-qt5 qml-qt5 qmlscene qmlscene-qt5 qmltestrunner qmltestrunner-qt5 qmltyperegistrar qmltyperegistrar-qt5 qmmp qmqp-sink qmqp-source qmt_map2jnx qmt_rgb2pct qmv qopenvpn qpaeq qpress qprint qqwing qr qrazercfg qrazercfg-applet qreator qrencode qrttoppm qscxmlc qshape qshell qsolve qspectrumanalyzer qstardict qstopmotion qt2text qt4-ssh-askpass qt5ct qtattributionsscanner qtattributionsscanner-qt5 qtcreator qtcreator.sh qtdechunk qtdiag qtdiag-qt5 qtdoc qtdump qterminal qt-faststart qtikz qtile qtile-cmd qtile-run qtile-top qtinfo qtox qtpass qtpaths qtpaths-qt5 qtplugininfo qtplugininfo-qt5 qtrechunk qtstreamize qtwaylandscanner qtxdg-mat qtyuv4toyuv quad quad-clip quad-clip-all-vertices quad-clip-nearplane quad-degenerate quad-flat quad-offset-factor quad-offset-unfilled quad-offset-units quadrapassel QuadraticSieve quads quadstrip quadstrip-clip quadstrip-cont quadstrip-flat quad-tex-2d quad-tex-3d quad-tex-alpha quad-tex-pbo quad-tex-sub quad-unfilled quad-unfilled-clip quad-unfilled-stipple quart quartic_points quassel quasselclient quasselcore quest queuelat quickchar quilt quiterss quiz quodlibet quota quotacheck quota_nld quotaoff quotaon quotastats quotasync qutebrowser qv4l2 qvidcap qvkbd qvkgen qvkgen-qt5 qvlc qvoronoi qwebengine_convert_dict qxp2raw qxp2svg qxp2text R r2 r2agent r2pm r2r rabbitmqadmin rabbitmqctl rabbitmq-defaults rabbitmq-env rabbitmq-plugins rabbitmq-queues rabbitmq-server rabbitmq-upgrade rabin2 racc racc2y racer racf2john racket racket rackup raco raco rad radare2 radattr radclient rad_counter radcrypt raddebug radeapclient radeonreg radeontool radicale radiff2 radio radioob radius2john radiusd radlast radmin rados radosgw radosgw-admin radosgw-es radosgw-object-expirer radosgw-token radsniff radsqlrelay radtest radvd radvdump radwho radzap raetflo rafind2 ragel ragg2 rahash2 rain raiseChild.py rake rake-2.6 rake-compiler rand_graph randicmp6 random randpkt randsounds ranger rankmirrors ranlabg ranlib rapid-photo-downloader rapper rapydscript rar2john rarp rarun2 rasm2 rasqal-config raster2pgsql rasttopnm ratbagctl ratbagd ratpoints ratpoison rav1e raven raw raw2adpcm raw2dyna raw2tiff rawdog raw-identify rawshark rawtherapee rawtherapee-cli raw-thumbnailer rawtopgm rawtoppm raw_to_xgraph rax2 ray rays razercfg razerd razer-gamewrapper rb rbd rbd-fuse rbdmap rbd-mirror rbd-nbd rbd-replay rbd-replay-many rblcheck rbox rc2po rcc rcc-qt5 rclone rc.news rcp rc.radiusd rctest rcvboxdrv rd-curves rdesktop rdesktop-vrdp rdf2bin rdf2com rdf2dot rdf2ihx rdf2ith rdf2srec rdfdump rdfgraphisomorphism rdflib rdfpipe rdfproc rdfs2dot rdic_conv rdiff rdiff-backup rdiff-backup-delete rdiffdir rdisc rdisc6 rdiscount rdjpgcom rdma rdmd rdmsr rdnssd rdoc rdoc-2.6 rdp_check.py rds-ctl rdx re2c react-native-debugger read_arch readcd readelf readelf.py readlink readmsg readnsstate readom readpix readpixels readprofile readpst readPYBase readPYMB readtags realpath rearj reaver rebar rebind reboot rebuildctl rebuilderd rebuilderd-worker rebuildpkgs rec rec_control recode recode-sr-latin recoll recollindex recountdiff recover_glhack recv_image recvstats red redcheck redcheck_gmp rediff redir6 Redirect redirsniff6 redis-benchmark redis-check-aof redis-check-rdb redis-cli redis-sentinel redis-server redland-config redland-db-upgrade redneck redshift redshift-gtk refdes_renum refer refind-install refind-mkdefault refind-mkfont refine reflect reflector regdbdump regdiff reged regedit regedit regexxer register_new_matrix_user register-python-argcomplete registerRTSPStream registry-read.py regpatch reg.py regshell regsvr32 regsvr32 regtree reindexdb reiserfsck reiserfstune reloc65 rem rem2ps remaining remake remind remmina remmina-file-wrapper remotectl remote-viewer remotinator remove-ds.pl remove-idle-users.py remove-user.py remuxer rename renice renormal renpy reordercap repc replace repl-monitor repl-monitor.pl repo repo-add repo-elephant repo-remove repose repquota reprepro repro reprotest reptyr req-exclude req-include request-key reset resgen resgen2 resize resize2fs resizecons resize.f2fs resizepart resize-part-image resize_reiserfs resolvconf resolvconf resolvectl resolveip resolve_stack_dump restcli restic restore-tool reswrap resx2po resynth retext retroarch retroarch-cg2glsl retro-demo rev revfix reviewboard-am revpath rexgen2rules rfddump rfdformat rfdump rfidtest.pl rfkill rfnmz rftp rg rgb2pat rgb2pct.py rgb3toppm rgbmap rgview rgvim rhash rhino rhino-debugger rhino-idswitch rhino-jsc rhythmbox rhythmbox-client ri ri-2.6 rifftree rifle rigctl rigctld rigmem rigsmtr rigswr rime_deployer rime_dict_manager rink riot-desktop ripngd riscv32-elf-addr2line riscv32-elf-ar riscv32-elf-as riscv32-elf-c++filt riscv32-elf-elfedit riscv32-elf-gdb riscv32-elf-gdb-add-index riscv32-elf-gprof riscv32-elf-ld riscv32-elf-ld.bfd riscv32-elf-nm riscv32-elf-objcopy riscv32-elf-objdump riscv32-elf-ranlib riscv32-elf-readelf riscv32-elf-size riscv32-elf-strings riscv32-elf-strip riscv64-elf-addr2line riscv64-elf-ar riscv64-elf-as riscv64-elf-c++ riscv64-elf-c++filt riscv64-elf-cpp riscv64-elf-elfedit riscv64-elf-g++ riscv64-elf-gcc riscv64-elf-gcc-10.1.0 riscv64-elf-gcc-ar riscv64-elf-gcc-nm riscv64-elf-gcc-ranlib riscv64-elf-gcov riscv64-elf-gcov-dump riscv64-elf-gcov-tool riscv64-elf-gdb riscv64-elf-gdb-add-index riscv64-elf-gprof riscv64-elf-ld riscv64-elf-ld.bfd riscv64-elf-lto-dump riscv64-elf-nm riscv64-elf-objcopy riscv64-elf-objdump riscv64-elf-ranlib riscv64-elf-readelf riscv64-elf-size riscv64-elf-strings riscv64-elf-strip riscv64-linux-gnu-addr2line riscv64-linux-gnu-ar riscv64-linux-gnu-as riscv64-linux-gnu-c++ riscv64-linux-gnu-c++filt riscv64-linux-gnu-cpp riscv64-linux-gnu-elfedit riscv64-linux-gnu-g++ riscv64-linux-gnu-gcc riscv64-linux-gnu-gcc-10.1.0 riscv64-linux-gnu-gcc-ar riscv64-linux-gnu-gcc-nm riscv64-linux-gnu-gcc-ranlib riscv64-linux-gnu-gcov riscv64-linux-gnu-gcov-dump riscv64-linux-gnu-gcov-tool riscv64-linux-gnu-gdb riscv64-linux-gnu-gdb-add-index riscv64-linux-gnu-gprof riscv64-linux-gnu-ld riscv64-linux-gnu-ld.bfd riscv64-linux-gnu-lto-dump riscv64-linux-gnu-nm riscv64-linux-gnu-objcopy riscv64-linux-gnu-objdump riscv64-linux-gnu-ranlib riscv64-linux-gnu-readelf riscv64-linux-gnu-size riscv64-linux-gnu-strings riscv64-linux-gnu-strip ristretto rjoe rkhunter rkill rkpng2c rkward rlatopam rletopnm rli rli2 rlm_ippool_tool rlog rlogin rlogin-cwd rlogind rls rls rltraceroute6 rlwrap rm rmail rmath rmaxima rmaxima rmcp rmcp_ping rmcvsdir rmdir rmedigicontrol rmic rmid rmiregistry rmkdepend rmkdepend rmlint rmmod r-mpost rnano rndc rndc-confgen rnews rnews.libexec/ rnews.libexec/bunbatch rnews.libexec/c7unbatch rnews.libexec/decode rnews.libexec/encode rnews.libexec/gunbatch rngd rngtest robotfindskitten robots RockboxUtility roff2dvi roff2html roff2ps roff2text roff2x rofi rofiles-fuse rofimoji rofi-pass rofi-sensible-terminal rofi-theme-selector rogue ronn root root rootbrowse rootbrowse rootcint rootcint rootcling rootcling root-config root-config rootcp rootcp rootdrawtree rootdrawtree rooteventselector rooteventselector root.exe root.exe rootls rootls rootmkdir rootmkdir rootmv rootmv rootnb.exe rootnb.exe rootn.exe rootn.exe rootprint rootprint rootrm rootrm roots roots roots.exe roots.exe rootslimtree rootslimtree rootv ROPgadget ropper roqet rot rot13 rotatelogs rotctl rotctld rougify roundtrip route routef routel router router+lib router+lib-slim router-slim rox rpcbind rpcclient rpcdebug rpcdump.py rpcgen rpc.gssd rpcinfo rpc.mountd rpc.nfsd rpc.rquotad rpc.statd rpm rpm2archive rpm2cpio rpm2cpio rpmbuild rpmdb rpmextract.sh rpmextract.sh rpmgraph rpmkeys r-pmpost rpmquery rpmsign rpmspec rpmverify rpws rrdcached rrdcgi rrdcreate rrdtool rredtool rrenice Rscript rsf rsh rshd rshijack rsibreak rsmtp rsmurf6 rsnapreport.pl rsnapshot rsnapshot-diff rspamadm rspamadm-2.5 rspamc rspamc-2.5 rspamd rspamd-2.5 rspamd_stats rspec rssguard rst2html rst2html2 rst2html2.py rst2html4 rst2html42 rst2html42.py rst2html4.py rst2html5 rst2html52 rst2html52.py rst2html5.py rst2html.py rst2latex rst2latex2 rst2latex2.py rst2latex.py rst2man rst2man2 rst2man2.py rst2man.py rst2odt rst2odt2 rst2odt2.py rst2odt_prepstyles rst2odt_prepstyles2 rst2odt_prepstyles2.py rst2odt_prepstyles.py rst2odt.py rst2pseudoxml rst2pseudoxml2 rst2pseudoxml2.py rst2pseudoxml.py rst2s5 rst2s52 rst2s52.py rst2s5.py rst2xetex rst2xetex2 rst2xetex2.py rst2xetex.py rst2xml rst2xml2 rst2xml2.py rst2xml.py rstcheck rstconv rstpep2html rstpep2html2 rstpep2html2.py rstpep2html.py rsvg-convert rsync rsync-backup.sh rsync-ssl rtacct rtcwake rtf2rtf rtirq rtkitctl rtl_adsb rtlbrowse rtl_eeprom rtl_fm rtlfw rtl_number rtl_power rtl_sdr rtl_tcp rtl_test rt-migrate-test rtmon rtmpgw rtmpsrv rtmpsuck rtorrent rtpr rtstat rtv rubber rubberband rubber-info rubber-pipe rubikrotation ruby ruby-2.6 ruby_count rules_optimize rulestack rumakeindex runantlr2 runc runcon run_erl run_erl run-festival-script runghc runghc-8.10.1 rungs runhaskell runlevel runner run_om_test run-on-ac run-on-bat run-parts runq runscript runtest runuser run-with-aspell runxlrd.py r-upmpost rust-analyzer rustc rustc rustdoc rustdoc rustfmt rustfmt rust-gdb rust-gdb rust-gdbgui rust-lldb rust-lldb rustup rview rview rvim rvim rvlc rvnamed-ng rw rx rx11vnc rx_fm rx_power rx_sdr rygel rygel-preferences rz s32imdbpy.py s3cmd s3fs s51 saaldump sadf safe_finger safe_yaml sage sage-cachegrind sage-callgrind sage-cleaner sage-coverage sage-coverageall sage-cython sage-eval sage-fixdoctests sage-grep sage-inline-fortran sage-ipynb2rst sage-ipython sage-massif sage-maxima.lisp sage-native-execute sagenb-export sage-notebook sage-num-threads.py sage-omega sage-open sage-preparse sage-python sage-run sage-run-cython sage-runtests sage-startuptime.py sage-valgrind sage-version.sh saidar sail sakura salt salt-api salt-call salt-cloud salt-cp salt-key salt-master salt-minion salt-proxy salt-run salt-ssh salt-syndic salt-unity samba samba_downgrade_db samba_kcc sambaPipe.py samba-regedit samba-tool samba_upgradedns sample samplers samplers_array samrdump.py samu sancov sancov sane-config saned sane-find-scanner sanstats sanstats sap2john sapic sapWatch sar sar2pcp sarg sarg-2.4.0. sas_disk_blink saslauthd sasldblistusers2 saslpasswd2 sass sassc sass-convert sasteroids sasteroids.elf sauerbraten-client sauerbraten-server saved_model_cli saved_model_cli saved_model_cli saved_model_cli SAX2Count SAX2Print SAXCount saxfr SAXPrint saytime sb sbattach sbcdec sbcenc sbcinfo sbcl sbigtopgm sbiload sbkeysync sbsiglist sbsign sbt sbvarsign sbverify sbxkb sc scala scalac scaladoc scalap scanadf scan-build scan-devreg scanelf scanimage scanin scanlogs scanmacho scanmem scanspool scantailor scantailor-cli scantv scan-view scapy sccmap scdd scdd_gmp scdoc scel2org scel2org5 scfg_make scfg_parse scfg_parse_text scfg_test scfg_train scgcheck scgskeleton schdiff schedtool schemagen schema-reload.pl scheme scheme-ieee-1178-1990 scheme-r4rs scheme-r5rs scheme-srfi-0 schroot schroot-sbuild sc-hsm-tool schubmult scim scim-config-agent scim-im-agent scim-setup sclient SCMPrint scmp_sys_resolver scncopy scons scons-3.1.2 scons-configure-cache scons-configure-cache-3.1.2 sconsign sconsign-3.1.2 scons-time scons-time-3.1.2 scor2prt scour scp scp scp-dbus-service scponly scponlyc scqref scrapy scratch screen screen-4.8.0 screenfetch screengrab screenkey scribble scribus script scriptlive scriptor scriptreplay scrot scrotwm scrypt sctp_darn sctp_status sctp_test scummvm scummvm-tools scummvm-tools-cli sd sd2raw sd2svg sd2text sdar sdas390 sdas6808 sdas8051 sdasgb sdasrab sdasstm8 sdasz80 sdcc sdcdb sdcdb.el sdcdbsrc.el sdcpp sdcv sddm sddm-greeter sddmthemeinstaller sdedit sdiff sdig sdl2-config sdl-config sdl-config-32 sdld sdld6808 sdldgb sdldstm8 sdldz80 sdnm sdobjcopy sdparm sdpscanner sdranlib sdrcomp sdw2html seahorse seahorse-daemon seahorse-tool seamonkey search_character.py search_company.py search_keyword.py search_movie.py search_person.py searchsploit secretsdump.py secret-tool secutil sed sed_count sem semver sendfax send-ihave sendiso sendmail sendmail sendmail sendmail sendmail send-nntp send_osc sendpage sendpees6 sendpeesmp6 sendsms send-uucp sendxbatches sensord sensors sensors-conf-convert sensors-detect sensortag SEnumVal seprule seq seq2bseq seqdiag ser2net serdi serialver servatrice serve_image servertool servicefw servicemenuinstaller services.py session_log_alerts setarch setcap setcifsacl setcollection setconf set-default-router-config-dice-eap setfacl setfattr setfont setkeycodes setleds setmetamode setpci setpriv setquota setrandom setreg setsid setterm settimed set_unicharset_properties setup-ds.pl setup-plt setup-vfio setvtrgb setwallpaper set-wireless-regdom setxkbmap setxrd.sh setxrd.sh sexp-conv sf2dump sf2extract sfconvert sfddiff sfdisk sfdp sfinfo sfparse sfrotz sftp sfv-hash sfwlisten sg sg_bg_ctl sg_compare_and_write sg_copy_results sg_dd sg_decode_sense sgdisk sg_emc_trespass sgen sgen-grep-binprot sg_format sg_get_config sg_get_elem_status sg_get_lba_status sg_ident sginfo sg_inq sgitopnm sg_logs sg_luns sg_map sg_map26 sgm_dd sgml2html sgml2info sgml2latex sgml2lyx sgml2rtf sgml2txt sgml2xml sgml2xml-isoent sgmlcheck sgmldiff sgmlnorm sgmlpre sgmlsasp sgmlspl sgmlwhich sg_modes sg_opcodes sgp_dd sg_persist sg_prevent sg_raw sg_rbuf sg_rdac sg_read sg_read_attr sg_read_block_limits sg_read_buffer sg_readcap sg_read_long sg_reassign sg_referrals sg_rep_zones sg_requests sg_reset sg_reset_wp sg_rmsn sg_rtpg sg_safte sg_sanitize sg_sat_identify sg_sat_phy_event sg_sat_read_gplog sg_sat_set_features sg_scan sg_seek sg_senddiag sg_ses sg_ses_microcode sg_start sg_stpg sg_stream_ctl sg_sync sg_test_rwbuf sg_timestamp sg_turs sg_unmap sg_verify sg_write_buffer sg_write_long sg_write_same sg_write_verify sg_write_x sg_wr_mode sg_xcopy sg_zone sh sh2-elf-addr2line sh2-elf-ar sh2-elf-as sh2-elf-c++filt sh2-elf-elfedit sh2-elf-gdb sh2-elf-gdb-add-index sh2-elf-gprof sh2-elf-ld sh2-elf-ld.bfd sh2-elf-nm sh2-elf-objcopy sh2-elf-objdump sh2-elf-ranlib sh2-elf-readelf sh2-elf-size sh2-elf-strings sh2-elf-strip sh4-elf-addr2line sh4-elf-ar sh4-elf-as sh4-elf-c++filt sh4-elf-elfedit sh4-elf-gprof sh4-elf-ld sh4-elf-ld.bfd sh4-elf-nm sh4-elf-objcopy sh4-elf-objdump sh4-elf-ranlib sh4-elf-readelf sh4-elf-size sh4-elf-strings sh4-elf-strip sha1deep sha1pass sha1sum sha224sum sha256deep sha256sum sha384sum sha512sum shadow_sampler shadowsocks-libqss shadowtex sha-dump shairplay shairport-sync shake shake shape shape32 shapeclustering shapeindex Shape_PointInPoly shar shards sharedtex sharedtex32 sharedtex_mt sharedtex_mt32 sharesec sharkd sha-test shc08 shcomp sh_count sheet2pcp shellcheck shellharden sherlock265 shfmt shfsmount shfsumount shiboken2 shiboken_tool.py shlibsign shlock shmidcat shncat shncmp shnconv shncue shnfix shngen shnhash shninfo shnjoin shnlen shnpad shnsplit shnstrip shntool shntrim shopoob shorewall shorewall6 shortg shotcut shotgun shotwell showbatt showconsolefont showdb showfigfonts show_filecount showfoto showg showjournal showkey showmount showqt showriff show-shared-extents showstat4 showwal shp2pgsql shpadd shpcat shpcentrd shpcreate shpfix shpinfo shprewind shpsort shptreedump shputils shpwkb shred shrinkfile shrink_width shtest shuf shuffle shutdown shutterbug siege siege.config sieve sievec sieve-dump sieveeditor sieve-filter sieve-test sig2dot sig2fv sigal SigDigger sigfilter sigfind sigil sig-list-to-certs signal2john signal-desktop signalgen signaltest signcode signcontrol sign-efi-sig-list signify signoff signond signonpluginprocess signon-ui signtool signver sigrok-cli sigtool sigwaittest sim2trad simavr sim_client simh-altair simh-eclipse simh-gri simh-h316 simh-hp2100 simh-hp3000 simh-i1401 simh-i1620 simh-i7094 simh-id16 simh-id32 simh-lgp simh-nova simh-s3 simh-sds simh-sigma simh-uc15 simh-vax simh-vax780 simple_dcraw simpleexpand simpleftp simpleindex simple-scan simplesearch simple_web_server simplex-noise sim_server simutrans singlebuffer Singular singularity sinksh sink_smtp_test sink_synchronizer siod sip sip5 sip-build sipcalc sip-distinfo SIPdump sip-install sip-module sip-sdist sip-wheel sirtopnm site_perl/ six six2four.sh six-script size size222 size86 sjisconv sjislatex sk skanlite skdump skinning skk skkdic-count skkdic-expr skkdic-expr2 skkdic-sort skopeo skrooge skroogeconvert sktest sk-tmux skylighting-extract sl sl slabtop slapacl slapadd slapauth slapcat slapindex slappasswd slapschema slaptest slatex slattach slcli sldtoppm sleep slencheck slib slice2confluence slice2cpp slice2html slice2java slice2js slice2matlab slice2objc slice2php slice2rb slice2swift sliceprint slideshow slim slimevolley slimit slimlock slirp4netns slirpvde slmbuild slminfo slmpack slmprune slmseg slmthread sln sload.f2fs slocate sloccount slock slogimport slogkey slogverify slop slowhttptest slsh slugify slurp sm smali smartctl smartd smartdns smartypants smb2-quota smb4k smbcacls smbclient smbclient.py smbcontrol smbcquotas smbd smbencrypt smbexec.py smbget smbinfo smbios-battery-ctl smbios-get-ut-data smbios-keyboard-ctl smbios-lcd-brightness smbios-passwd smbios-state-byte-ctl smbios-sys-info smbios-sys-info-lite smbios-thermal-ctl smbios-token-ctl smbios-upflag-ctl smbios-wakeup-ctl smbios-wireless-ctl smbnetfs smbpasswd smbrelayx.py smbserver.py smbspool smbstatus smbtar smbtorture smbtree smem smemcap smfsh smime_keys smlnj sm-notify smokeinfo smokeping smokeping_cgi smpeg-config smproxy smsd smTestDriver-pv5.8 smtp smtp smtpctl smtp-sink smtp-source smtube smurf6 sn sn0int snake snapper snapperd snappy snap_scheduler.py snap-sync snarf snes9x snes9x-gtk sniffer.py sniffglue sniffit sniff.py snmp-bridge-mib snmpbulkget snmpbulkwalk snmpcheck snmpconf snmpget snmpgetnext snmpinform snmpnetstat snmppcap snmpping snmpps snmpset snmpstatus snmptable snmptest snmptop snmptranslate snmptrap snmpusm snmpvacm snmpwalk snowball snscore sntp soapcpp2 soapsuds soapy_power SoapySDRServer SoapySDRUtil soc socat sockd socklist socksify soelim soffice soffice softhsm2-dump-file softhsm2-keyconv softhsm2-migrate softhsm2-util sogrep sol solaar solaar-cli solc solid-action-desktop-gen solid-hardware5 solidity-upgrade solid-power solr solterm solve_conic solve_legendre sonetdiag sopel sopel-config sopel-plugins sordi sord_validate sort sorter sotruss soundconverter sound-juicer soundkonverter source-highlight source-highlight-esc.sh source-highlight-settings sow sox soxi spa-inspect spam spa-monitor sparkleshare spawn-fcgi spcat spctoppm speak speaker-test speak-ng speakupconf spec2cie specplot spectacle spectest-interp spectex spectgen spectrwm spectrwm-no-preload speech-dispatcher speed speedcrunch speedtest speedtest-cli speexdec speexenc spellout spent spfd spfquery sphinx-apidoc sphinx-apidoc2 sphinx-autogen sphinx-autogen2 sphinx-build sphinx-build2 sphinx-quickstart sphinx-quickstart2 spice-client-glib-usb-acl-helper spice-vdagent spice-vdagentd spice-webdavd spicy spicy-screenshot spicy-stats spike spike-dasm spirv-as spirv-cfg spirv-dis spirv-lesspipe.sh spirv-link spirv-opt spirv-reduce spirv-remap spirv-val spline splint split split2po splitdiff splitimg splitindex splitlen split.py splitti3 spm sponge spotread spottopgm spring spring-dedicated springgraph spring-headless springlobby spriteblast sprof spumux sputoppm spuunmux spyder sql sql2subunit sqlcipher sql_count sqldiff sqlformat sqlite2s4 sqlite3 sqlite3_analyzer sqlitebrowser sqlmap sqlmapapi sqlmetal sqlobject-admin sqlobject-convertOldURI sqlsharp squashfuse squashfuse_ll squeak squeak.sh squid squidclient sr src2man src-hilite-lesspipe.sh srch_strings srcredact srftopam srptool srsenb srsenb srsepc srsepc srsepc_if_masq.sh srsepc_if_masq.sh srslte_install_configs.sh srslte_install_configs.sh srsmbms srsmbms srsue srsue srt srt-ffplay srt-file-transmit srt-live-transmit srt-test-file srt-test-live srt-test-multiplex srt-test-relay srt-tunnel ss ss2 ss_adpcm_decode ssadump ss_archive_extract sscape_ctl ss_chc_decode ssconvert ss_cover_conv ssdd ssdeep ssdiff sserver ss_extract ssgrep ssh ssh2john ssh-add ssh-agent ssh-copy-id sshd sshfs sshguard ssh-keygen ssh-keyscan sshmitm sshow sshpass sshtunnel sshuttle ssi-cgi ssindex ss_ipu_conv sslh sslh-fork sslh-select ss-local sslocal sslscan sslsplit sslstrip ssltap ss-manager ss-nat ss_pak_extract sspr2john ss-qt5 ss-redir sss_cache sssctl sssd sss_debuglevel ss-server ssserver sss_obfuscate sss_override sss_seed sss_ssh_authorizedkeys sss_ssh_knownhostsproxy sst_dump sst_dump sstm8 sstpc sstrip ss-tunnel st4topgm stable stack stagingpkg staging-x86_64-build stalonetray stardict staroffice2john start-bfgminer.sh startdde startfluxbox startGroovy startlazarus startlazarus startlxde startlxqt startplasma-wayland startplasma-x11 start-pulseaudio-x11 start-statd startx startxfce4 stat states statgrab statgrab-make-mrtg-config statgrab-make-mrtg-index staticcheck stdbuf StdInParse steadyflow steam steamdeps steam-native steam-runtime stella stellarium stemwords step stest stestr stex3d st-flash st-info stk-demo stl2gts stlink-gui stlink-gui.ui stoken stoken-gui stompcat stow strace strace-graph strace-log-merge strace.py stratis stratisd strawberry strawberry-tagreader stream streamer streamlink stress strfile stringer strings strip strip2john strip-bsn strip-bsr strip_duplicates.py strptime struct2osd stty stubby stubgen stubquery stubtest s-tui stunbdc stund stunnel stunnel3 st-util sty2dtx stylelint stylish-haskell stylus su sub2po subbrute subdivideg subdl subdownloader suboob substrings.pl subsurface subsurface.debug sub-tex subtitleeditor subtitles subunit-1to2 subunit2disk subunit2gtk subunit2junitxml subunit2pyunit subunit2sql subunit2sql-db-manage subunit2sql-graph subunit-2to1 subunit-filter subunit-ls subunit-notify subunit-output subunit-stats subunit-tags sudo sudoedit sudoers-add sudoku sudo_logsrvd sudoreplay sudo_sendlog suexec sugar sugar-activity3 sugar-activity-web sugar-backlight-helper sugar-backlight-setup sugar-control-panel sugar-erase-bundle sugar-install-bundle sugar-launch sugar-runner sugar-serial-number-helper sulogin sum sumtool sunicontopnm sunpinyin-dictgen sunxi-bootinfo sunxi-fel sunxi-fexc sunxi-nand-image-builder sunxi-nand-part sunxi-pio supermin supertux2 supertuxkart supervisorctl supervisord surfraw suscan.status sushi svc_recv svc_send svcutil svgcleaner svgtopam svlc svm-checkdata.py svm-easy.py svm-grid.py svm-predict svm-subset.py svm-toy svm-train svn svnadmin svnbackport svnbench svnchangesince svn-clean svndumpfilter svnforwardport svnfsfs svngettags svnintegrate svnlastchange svnlastlog svnlook svnmucc svn-multi svnrdump svnrevertlast svnserve svnsync svnversion svnversions svsematest SvtAv1DecApp SvtAv1EncApp SvtHevcEncApp SvtVp9EncApp swagger-flex swaks swanctl swapbuffers swaplabel swapoff swapon swappo sway swaybar swaybg swayidle swaylock swaymsg swaynag sweethome3d swell-foop swftocxx swftoperl swftophp swftopython swftotcl swift swiften-config swift-im swift-open-uri swig swindle swipl swipl-ld swipl-win switcher switcher switch_root sx sx sxhkd sxiv sxpm sxw2txt sylpheed symb2po symcryptrun symilar symilar2 symkeyutil symnew sympol sympow symtree synapse synapse_port_db synbak sync synclient syncqt.pl syncqt.pl-qt5 sync_room_to_group.pl synctex syncthing syncthing-gtk syncthing-relaysrv synctl syndaemon synergy synergyc synergyd synergys synfig synfig-config synfigstudio syntax-validate.pl synthcal synthread syntool sysbench sysctl sysdig sysdig-probe-loader syslinux syslinux2ansi syslog-ng syslog-ng-ctl syslog-ng-debun syslogtocern sysprof sysprof-cli systemadm systembus-notify system-config-printer system-config-printer-applet systemctl systemd-analyze systemd-ask-password systemd-cat systemd-cgls systemd-cgtop systemd-delta systemd-detect-virt systemd-escape systemd-firstboot systemdgenie systemd-gnome-ask-password-agent systemd-hwdb systemd-id128 systemd-inhibit systemd-machine-id-setup systemd-mount systemd-notify systemd-nspawn systemd-path systemd-repart systemd-resolve systemd-run systemd-socket-activate systemd-stdio-bridge systemd-swap systemd-sysusers systemd-tmpfiles systemd-tty-ask-password-agent systemd-umount systemmonitor systemsettings5 systool sz sz80 t1ascii t1asm t1binary t1disasm t1dotlessj t1lint t1mac t1rawafm t1reencode t1testpage t1unmac t4ht tabs tabulate tac tachyon taglib-config taglib-config-32 tagmp3 tagtest tail tail.pl tali talk talkd talkwith tally.control tamarin-prover tangle tang-show-keys tap tap2subunit tapestat TAppDecoderAnalyserHighBitDepthStatic TAppDecoderAnalyserStatic TAppDecoderHighBitDepthStatic TAppDecoderStatic TAppEncoderHighBitDepthStatic TAppEncoderStatic TAppMCTSExtractorHighBitDepthStatic TAppMCTSExtractorStatic tappy tar tarantool tarantoolctl tarcolor targen tarsnap tarsnap-keygen tarsnap-keymgmt tarsnap-keyregen tarsnap-recrypt task taskd taskdctl taskell taskset tastenbrett tasty-discover tbl tbx2po tc tca_correct tcat tcbench tcc tccat tcdecode tcdemux tcextract tcl_count tcleanup tclsh tclsh8.6 tcmp3cut tcomposer tcond tconverter tcpbridge tcpcapinfo tcp-cutter tcpflow tcpkill tcplay tcpliveplay tcpnice tcpprep tcpreplay tcpreplay-edit tcprewrite tcprobe tcpspray tcpspray6 tcptrace tcptraceroute6 tcxmlcheck tcxpm2rgb tcyait tdbbackup tdbdump tdbrestore tdbtool tdspool tdx-util teachgammon teamd teamdctl teamnl teapot teckit_compile tee teensy-loader-cli teeworlds teeworlds_srv tei2mod tek2plot telegram2john telegram-desktop telepathy-gabble-xmpp-console telinit tellico telnet telnetd template-converter temu tenshi tensorboard term_cdiff term_colortab term_decolor term_display termdown terminator terminator.wrapper terminology termios-xspike termite term_mandel term_snow termtosvg terraform terraform-provider-keycloak terrain terraingenerator tessdemo tesseract tesstrain.sh tesstrain_utils.sh test testament testAMRAudioStreamer test-avccmd test-bed test-bufferops testcdd1 testcdd1_gmp testcdd2 testcdd2_gmp test_chmLib test-clock_nanosleep test-connect-t test-cycle-time test-devicestringparser test-dice-eap testdisk testDVVideoStreamer testepsg testflo test-focusrite test_func test-fw410 testH264VideoStreamer testH264VideoToHLSSegments testH264VideoToTransportStream testH265VideoStreamer testH265VideoToTransportStream test-ieee1394service test_igmpv3_join testingpkg testing-x86_64-build test_io test-ipcringbuffer testlibraw testlp1 testlp1_gmp testlp2 testlp2_gmp testlp3 testlp3_gmp test-messagequeue testMKVSplitter testMKVStreamer testMP3Receiver testMP3Streamer testMPEG1or2AudioVideoStreamer testMPEG1or2ProgramToTransportStream testMPEG1or2Splitter testMPEG1or2VideoReceiver testMPEG1or2VideoStreamer testMPEG2TransportReceiver testMPEG2TransportStreamer testMPEG2TransportStreamSplitter testMPEG2TransportStreamTrickPlay testMPEG4VideoStreamer test_name testOggStreamer testOnDemandRTSPServer testparm testpattern testpkg testr testrb-2.6 testRelay testrender testReplicator testRTSPClient test-runner testsaslauthd testshade testshade_dso test-shm testshoot testshoot_gmp testssl test-streamdump test-sysload test-timestampedbuffer test-volume test-watchdog testWAVAudioStreamer tetrinet tetrinet-server tetris-bsd tex tex2aspc tex2lyx tex2mail tex2xindy tex4ebook tex4ht texaaline texconfig texconfig-dialog texconfig-sys texcount texcyl texdef texdemo1 texdiff texdirflatten texdoc texdoctk texenv texexec texexpand texfot texhash texi2any texi2dvi texi2dvi4a2ps texi2html teximage texindex texindy texlab texlinks texliveonfly texloganalyser texlua texluac texluajit texluajitc texmaker texmfstart texplate tex-quads texsis texstudio text2image text2pcap text2pos text2wave textconv tex_to_images texture_from_pixmap texture_from_pixmap32 TexturePacker textures textwidth texvc texworks tezos2john tfarmcontroller tfarmserver tflite_convert tflite_convert tflite_convert tflite_convert tfmtodit tftopl tftp tftpy_client tftpy_server tf_upgrade_v2 tf_upgrade_v2 tf_upgrade_v2 tf_upgrade_v2 tgatoppm tgz thc-ipv6-setup.sh thcping6 thefuck theme theta th_gen_idx.pl thin thin_check thin_delta thin_dump thingy52 thinkjettopbm thin_ls thin_metadata_size thin_repair thin_restore thin_rmap thin_trim thisroot.fish thisroot.fish thisroot.sh thisroot.sh thor thrift thunar Thunar thunar-settings thunar-volman thunar-volman-settings thunderbird tic tickadj ticketConverter.py ticketer.py tidy tie tiff2bw tiff2ps tiff2rgba tiffcheck tiffcmp tiffcp tiffcrop tiffdither tiffdump tiffgamut tiffgt tiffinfo tiffmedian tiffset tiffsplit tifftopnm tificc tig tigerdeep tiger-hash tiki2po tilda tiled tilix tilt tilt_analysis tilt_synthesis timage time timedatectl timed-read timed-run timelineeditor timeout timew timew-refresh-holidays tincd tint2 tint2conf tinygo tinyleaf tinyprog tinyproxy tinyssh-convert tinysshd tinysshd-makekey tinysshd-printkey tipc tipp10 tjbench tkconch tkconch3 tkiptun-ng tkmib tknewsbiff tkpasswd tkremind tldextract tldr tload tlp tlp-rdw tlp-stat tlsh_unittest tmate tmesis tmesis-dynamic tm_gs tmon tmserver tmux tmuxp tmxrasterizer tmxviewer tnameserv tnftp toast toastify toc2cddb toc2cue toc2mp3 toco toco toco toco toco_from_protos toco_from_protos toco_from_protos toco_from_protos todo toe to_erl to_erl tohtml tokei tokssh tokuftdump tokuftdump tokuft_logprint tokuft_logprint toluapp toobig6 toobigsniff6 toolame toolbox toolstash top top-ehrhart-knapsack topmorph tor torbrowser-launcher tor-gencert torify tor-print-ed-signing-cert tor-prompt tor-resolve torsocks totem totem-video-thumbnailer touch touchpad-edge-detector towncrier tox tox2 tox2-quickstart tox3 toxic tox-quickstart tox-to-nox toyball tpacpi-bat tpgrep.tdf tpm2-abrmd tpm2_activatecredential tpm2_certify tpm2_certifycreation tpm2_changeauth tpm2_changeeps tpm2_changepps tpm2_checkquote tpm2_clear tpm2_clearcontrol tpm2_clockrateadjust tpm2_create tpm2_createak tpm2_createek tpm2_createpolicy tpm2_createprimary tpm2_dictionarylockout tpm2_duplicate tpm2_encryptdecrypt tpm2_eventlog tpm2_evictcontrol tpm2_flushcontext tpm2_getcap tpm2_getekcertificate tpm2_getrandom tpm2_gettestresult tpm2_gettime tpm2_hash tpm2_hierarchycontrol tpm2_hmac tpm2_import tpm2_incrementalselftest tpm2_load tpm2_loadexternal tpm2_makecredential tpm2_nvcertify tpm2_nvdefine tpm2_nvextend tpm2_nvincrement tpm2_nvread tpm2_nvreadlock tpm2_nvreadpublic tpm2_nvsetbits tpm2_nvundefine tpm2_nvwrite tpm2_nvwritelock tpm2_pcrallocate tpm2_pcrevent tpm2_pcrextend tpm2_pcrread tpm2_pcrreset tpm2_policyauthorize tpm2_policyauthorizenv tpm2_policyauthvalue tpm2_policycommandcode tpm2_policycountertimer tpm2_policycphash tpm2_policyduplicationselect tpm2_policylocality tpm2_policynamehash tpm2_policynv tpm2_policynvwritten tpm2_policyor tpm2_policypassword tpm2_policypcr tpm2_policyrestart tpm2_policysecret tpm2_policysigned tpm2_policytemplate tpm2_policyticket tpm2_print tpm2_ptool tpm2_quote tpm2_rc_decode tpm2_readclock tpm2_readpublic tpm2_rsadecrypt tpm2_rsaencrypt tpm2_selftest tpm2_send tpm2_setclock tpm2_setprimarypolicy tpm2_shutdown tpm2_sign tpm2_startauthsession tpm2_startup tpm2_stirrandom tpm2_testparms tpm2-totp tpm2tss-genkey tpm2_unseal tpm2_verifysignature tpm_server t-prot tput tqdm tr trace trace6 trace62list.sh tracegen tracegen-qt5 tracepath traceroute tracert6 tracker trad2sim traefik trafgen tragesym trans transcode transfig transform transicc transist.flt translaboob transmageddon transmission-cli transmission-create transmission-daemon transmission-edit transmission-gtk transmission-qt transmission-remote transmission-remote-gtk transmission-show transpose-freecell-board.py traptoemail trash trash-empty trash-list trash-put trash-restore trash-rm traveloob trayer tred tree treeify trek tri tri-2101010 tri-2101010-dlist trial trial3 tri-alpha tri-alpha-tex triangulate tri-array-interleaved tri-blend tri-blend-color tri-blend-max tri-blend-min tri-blend-revsub tri-blend-sub tribler tri-clear tri-clip tri-cull tri-cull-both tri-dlist tri-edgeflag tri-edgeflag-array tri-edgeflag-pv trietool trietool-0.2 trifan trifan-flat trifan-flat-clip trifan-flat-unfilled-clip trifan-unfilled tri-fbo tri-fbo-tex tri-fbo-tex-mip tri-flat tri-flat-clip tri-fog tri-fp tri-fp-const-imm tri-lit tri-lit-material tri-logicop-none tri-logicop-xor tri-mask-tri tri-multitex-vbo tri-orig tri-point-line-clipped tri-query trirast tri-repeat tri-rotate tri-scissor-tri tri-square tri-stencil tri-stipple tristrip tristrip-clip tristrip-flat tri-tex tri-tex-1d tri-tex-3d tri-tex-stipple tri-tri tri-unfilled tri-unfilled-clip tri-unfilled-edgeflag tri-unfilled-fog tri-unfilled-point tri-unfilled-smooth tri-unfilled-tri tri-unfilled-tri-lit tri-unfilled-userclip tri-unfilled-userclip-stip tri-userclip trivial_sampler trivial_synth tri-viewport tri-z tri-z-9 tri-z-clip tri-z-eq troff trojan trojita true truecrypt truecrypt2john truncate trust tryaffix try-from ts ts2po ts3server tsa2d32 tsc ts_calibrate ts_conf ts-contribute ts-contribute-en tsd2a32 tsdnsserver ts-edit ts-edit-en tset ts_finddev tshark ts_harvest tsig-keygen tsin2gtab-phrase TSingular tsitest tsk_comparedir tsk_gettimes tsk_loaddb tsk_recover tslearn tslmendian tslminfo tSmoke ts-node ts-node-script ts-node-transpile-only tsocks tsort ts_print ts_print_mt ts_print_raw tsql tss2_authorizepolicy tss2_changeauth tss2_createkey tss2_createnv tss2_createseal tss2_decrypt tss2_delete tss2_encrypt tss2_exportkey tss2_exportpolicy tss2_getcertificate tss2_getdescription tss2_getinfo tss2_getplatformcertificates tss2_getrandom tss2_gettpmblobs tss2_import tss2_list tss2_nvextend tss2_nvincrement tss2_nvread tss2_nvsetbits tss2_nvwrite tss2_pcrextend tss2_pcrread tss2_provision tss2_quote tss2_setcertificate tss2_setdescription tss2_sign tss2_unseal tss2_verifyquote tss2_verifysignature tss2_writeauthorizenv ts-script tsserver ts_test ts_test_mt tst_plugin ts_uinput ts_verify tt ttcp_atm ttdebug ttf2afm ttf2kotexfont ttf2lff ttf2pk ttf2tfm ttfdump ttfread ttftotype42 tth-hash ttl2c ttv ttx tty ttyload ttysolitaire tuc tuna tune2fs tunefs.reiserfs tunnel tunnel2 tuntox tup tupitube.bin tupitube.desk turbostat turnadmin turnserver turnutils_natdiscovery turnutils_oauth turnutils_peer turnutils_stunclient turnutils_uclient tuxcards tuxcmd tvtime tvtime-command tvtime-configure tvtime-scanner tvtk_doc twattach twcat twclip twclutter twcuckoo twdetach twdialog twdisplay twdm twevent twfindtwin twin twine twine3 twin_server twinwave twist twist3 twistd twistd3 twlsmsgport twlsobj twm twohamg twolame twopi twoside twsendmsg twsetroot twstart twsysmon twterm twthreadtest txt2gtab-phrase txt2man txt2mb txt2po txt2tags txt2ti3 tyalpha tybg tycat tyls type1afm typemaker typemaker2 typeoutfileinfo typer typer typespeed typetest typop tyq tysend tz_convert tzselect u2f-host u2f-server uaf2john ubiattach ubiblock ubicrc32 ubidetach ubiformat ubimkvol ubinfo ubinize ubirename ubirmvol ubirsvol ubuntu-cloudimg-query ubuntu-ec2-run ubxtool ucarp ucat uchardet uconv udevadm udevil udfinfo udflabel udiskie udiskie-info udiskie-mount udiskie-umount udisksctl udp2raw udpxrec udpxy ufw uget-gtk uget-gtk-1to2 uglifyjs uhd_cal_rx_iq_balance uhd_cal_tx_dc_offset uhd_cal_tx_iq_balance uhd_config_info uhd_fft uhd_find_devices uhd_image_loader uhd_images_downloader uhd_rx_cfile uhd_rx_nogui uhd_siggen uhd_siggen_gui uhd_usrp_probe uic uic-qt5 uil uim-chardict-qt5 uim-el-agent uim-el-helper-agent uim-fep uim-fep-tick uim-help uim-im-switcher-gtk uim-im-switcher-gtk3 uim-im-switcher-qt5 uim-input-pad-ja uim-input-pad-ja-gtk3 uim-m17nlib-relink-icons uim-module-manager uim-pref-gtk uim-pref-gtk3 uim-pref-qt5 uim-sh uim-toolbar-gtk uim-toolbar-gtk3 uim-toolbar-gtk3-systray uim-toolbar-gtk-systray uim-toolbar-qt5 uim-xim ukui-backgroundserver ukui-control-center ukui-desktopserver ukui-flash-disk ukui-fontserver ukui-greeter ukui-interfaceserver ukui-keyboardserver ukui-marcogeneralserver ukui-menu ukui-mouseserver ukui-panel ukui-power-backlight-helper ukui-power-manager ukui-power-preferences ukui-powerserver ukui-screensaver-backend ukui-screensaver-command ukui-screensaver-dialog ukui-screensaverserver ukui-session ukui-sessionserver ukui-session-tools ukui-sidebar ukui-touchpadserver ukui-volume-control ukui-volume-control-applet ukui-volume-control-applet-qt ukui-window-switch ukui-xkbgeneralserver ukwm ul ulockmgr_server ulogd ulqda ultrabayd umax_pp umbrello5 umockdev-run umockdev-wrapper umount umountavfs umount.crypt umount.crypt_LUKS umount.crypto_LUKS umount.davfs umount.ecryptfs umount.ecryptfs_private umount.nfs umount.nfs4 umount.nilfs2 umount.udisks2 umurmurd unace unafs uname uname26 unar unarj unattr unbound unbound-anchor unbound-checkconf unbound-control unbound-control-setup unbound-host unbuffer unclutter uncompface uncompress uncompress.real uncpk uncrustify uncrustify-kf5 undrop unexpand unflatten unhide unhide-linux unhide-posix unhide_rb unhide-tcp uni unicode_start unicode_stop uniconfd unidecode unifdef unifdefall unihelper unionfs unionfsctl uniq unique unison unison-fsmonitor unison-gtk2 unison-text unit2 unitconv unitdiff unit.pt units units_cur unix2dos unix2mac unix_chkpwd unixcmd unix-lpr.sh unixterm unlink unlz4 unlzf unlzma unmunch unmute-ozonic unoconv unopkg unopkg unp unpack200 unpack_bootimg unpaper unpigz unprocessed_raw unrar unrealircd unrtf unrule unshadow unshar unshare unshield unsquashfs unstr unterm untoast unxrandr unxz unzip unzip-mem unzipsfx unzstd unzzip unzzip-big unzzip-mem unzzip-mix upgrade-borgmatic-config uplatex upmendex upmpost upnpc upower uppltotf upsc upscmd upsd upsdrvctl upslog upsmon upsrw upssched upssched-cmd uptex uptftopl uptime uptimed upx uqm urh urh_cli uriparse urlbst urlscan urlsnarf urlwatch urxvt urxvtc urxvtd urxvt-tabbed us428control usbctl usb-devices usbguard usbguard-daemon usbguard-dbus usbguard-rule-parser usbhid-dump usbip usb_modeswitch usb_modeswitch_dispatcher usbmuxd usbredirserver usbview useradd userdbctl userdel UserFeedbackConsole userfeedbackctl usermod userpath users usleep usnjls usn-tombstone-cleanup.pl usrp2_card_burner usx2yloader utf8mex utf8trans UTkamene utox UTscapy utsushi uu-arch uu-base32 uu-base64 uu-basename uu-cat uu-chgrp uuchk uu-chmod uu-chown uu-chroot uucico uu-cksum uuclient uu-comm uuconv uu-cp uucp uu-cut uudecode uudeview uu-dircolors uu-dirname uu-du uu-echo uuencode uu-env uuenview uu-expand uu-expr uu-factor uu-false uufilter uu-fmt uu-fold uu-groups uu-hashsum uu-head uu-hostid uu-hostname uu-id uuidcdef uuidd uuidgen uuidparse uu-install uu-join uu-kill uu-link uu-ln uulog uu-logname uu-ls uu-mkdir uu-mkfifo uu-mknod uu-mktemp uu-more uu-mv uuname uu-nice uu-nl uu-nohup uu-nproc uu-numfmt uu-od uu-paste uu-pathchk uupick uu-pinky uu-printenv uu-printf uu-ptx uu-pwd uu-readlink uu-realpath uu-relpath uu-rm uu-rmdir uusched uu-seq uuserver uu-shred uu-shuf uu-sleep uu-sort uu-split uu-stat uustat uu-stdbuf uu-sum uu-sync uu-tac uu-tail uu-tee uu-test uu-timeout uuto uu-touch uu-tr uu-true uu-truncate uu-tsort uu-tty uu-uname uu-unexpand uu-uniq uu-unlink uu-uptime uu-users uu-wc uu-who uu-whoami uux uuxqt uu-yes uwsgi uwsgitop uxterm uz v2ctl v2ray v2ray-plugin v4l2-compliance v4l2-ctl v4l2-dbg v4l2loopback-ctl v4l2-sysfs-path v4l-conf v4lctl v4l-info vacuumdb vacuumlo vagrant vainfo vala vala-0.48 valabind valabind-cc valac valac-0.48 valadoc valadoc-0.48 vala-gen-introspect vala-gen-introspect-0.48 valgrind valgrind-di-server valgrind-listener validate-borgmatic-config valspeak vamps vao_demo vapigen vapigen-0.48 variety varnishadm varnishd varnishhist varnishlog varnishreload varnishstat varnishtest varnishtop vault vavpp vba_extract.py vbam vbc vbetool vbindiff vbo vbo-drawarrays vbo-drawarrays-2101010 vbo-drawrange vbo-noninterleaved vbo-tri VBox vboxballoonctrl VBoxBalloonCtrl VBoxClient VBoxClient-all VBoxControl VBoxControl vboxheadless VBoxHeadless vboximg-mount vboxmanage VBoxManage vboxreload vboxsdl VBoxSDL VBoxService VBoxService VBoxTunctl vboxwebsrv vcd2fst vcd2lxt vcd2lxt2 vcd2vzt vcdimager vcd-info vcdxbuild vcdxgen vcdxminfo vcdxrip vcolg vcut vdb_print vde_autolink vdecmd vde_cryptcab vdekvm vde_l3 vde_over_ns vde_pcapplug vde_plug vde_plug2tap vdeq vdeqemu vde_switch vdeterm vde_tunctl vdi2john vdir vdirsyncer vdpauinfo vedit vegeta vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ vendor_perl/ack vendor_perl/bdf2gdfont.pl vendor_perl/biber vendor_perl/binhex.pl vendor_perl/blinkenlights vendor_perl/borg-restore.pl vendor_perl/bubbles vendor_perl/buildrealms vendor_perl/ccon vendor_perl/check-zone-expiration vendor_perl/cleanarch vendor_perl/cleankrf vendor_perl/clusterssh_bash_completion.dist vendor_perl/colored_dmesg vendor_perl/colorfg vendor_perl/color_matrix vendor_perl/config_data vendor_perl/convertar vendor_perl/cpan2aur vendor_perl/cpan2dist vendor_perl/cpanm vendor_perl/cpanp vendor_perl/cpanp-run-perl vendor_perl/cpio-filter vendor_perl/crc32 vendor_perl/crsh vendor_perl/ctel vendor_perl/dbilogstrip vendor_perl/dbiprof vendor_perl/dbiproxy vendor_perl/dbish vendor_perl/debinhex.pl vendor_perl/djabberd vendor_perl/dm_date vendor_perl/dm_zdump vendor_perl/dnspktflow vendor_perl/donuts vendor_perl/donutsd vendor_perl/drawvalmap vendor_perl/dtck vendor_perl/dtconf vendor_perl/dtconfchk vendor_perl/dtdefs vendor_perl/dtinitconf vendor_perl/dtrealms vendor_perl/execflow vendor_perl/exiftool vendor_perl/expchk vendor_perl/fb-server vendor_perl/findrule vendor_perl/fixkrf vendor_perl/frozen-bubble vendor_perl/frozen-bubble-editor vendor_perl/gedi vendor_perl/genkrf vendor_perl/GET vendor_perl/getdnskeys vendor_perl/getds vendor_perl/grandvizier vendor_perl/gtk2-youtube-viewer vendor_perl/gtk3-youtube-viewer vendor_perl/gtk-youtube-viewer vendor_perl/HEAD vendor_perl/hexdump vendor_perl/htmltree vendor_perl/imgsize vendor_perl/ipcount vendor_perl/iptab vendor_perl/ipv4calc vendor_perl/json_xs vendor_perl/keyarch vendor_perl/keymod vendor_perl/krfcheck vendor_perl/l4p-tmpl vendor_perl/lights vendor_perl/ls++ vendor_perl/lsdnssec vendor_perl/lskrf vendor_perl/lsrealm vendor_perl/lsroll vendor_perl/lwp-download vendor_perl/lwp-dump vendor_perl/lwp-mirror vendor_perl/lwp-request vendor_perl/maketestzone vendor_perl/mapper vendor_perl/Markdown.pl vendor_perl/mech-dump vendor_perl/mimeopen vendor_perl/mimetype vendor_perl/modemtest vendor_perl/msguntypot vendor_perl/net-server vendor_perl/package-stash-conflicts vendor_perl/patchperl vendor_perl/perlbrew vendor_perl/perlcritic vendor_perl/perli11ndoc vendor_perl/perlsh vendor_perl/perltidy vendor_perl/perlver vendor_perl/pkg-config.pl vendor_perl/po4a vendor_perl/po4a-display-man vendor_perl/po4a-display-pod vendor_perl/po4a-gettextize vendor_perl/po4a-normalize vendor_perl/po4a-translate vendor_perl/pod_cover vendor_perl/podselect vendor_perl/podspell vendor_perl/POST vendor_perl/ppkg-config vendor_perl/pppusage vendor_perl/pt-align vendor_perl/pt-archiver vendor_perl/pt-config-diff vendor_perl/pt-deadlock-logger vendor_perl/pt-diskstats vendor_perl/pt-duplicate-key-checker vendor_perl/pt-fifo-split vendor_perl/pt-find vendor_perl/pt-fingerprint vendor_perl/pt-fk-error-logger vendor_perl/pt-heartbeat vendor_perl/pt-index-usage vendor_perl/pt-ioprofile vendor_perl/ptked vendor_perl/pt-kill vendor_perl/ptksh vendor_perl/pt-mext vendor_perl/pt-mysql-summary vendor_perl/pt-online-schema-change vendor_perl/pt-pmp vendor_perl/pt-query-digest vendor_perl/pt-show-grants vendor_perl/pt-sift vendor_perl/pt-slave-delay vendor_perl/pt-slave-find vendor_perl/pt-slave-restart vendor_perl/pt-stalk vendor_perl/pt-summary vendor_perl/pt-table-checksum vendor_perl/pt-table-sync vendor_perl/pt-table-usage vendor_perl/pt-upgrade vendor_perl/pt-variable-advisor vendor_perl/pt-visual-explain vendor_perl/razor-admin vendor_perl/razor-check vendor_perl/razor-client vendor_perl/razor-report vendor_perl/razor-revoke vendor_perl/realmchk vendor_perl/realmctl vendor_perl/realminit vendor_perl/realmset vendor_perl/rename vendor_perl/rollchk vendor_perl/rollctl vendor_perl/rollerd vendor_perl/rollinit vendor_perl/rolllog vendor_perl/rollrec-editor vendor_perl/rollset vendor_perl/sa-awl vendor_perl/sa-check_spamd vendor_perl/sa-compile vendor_perl/sa-learn vendor_perl/scandeps.pl vendor_perl/sdl-config.pl vendor_perl/sgmlspl.pl vendor_perl/shell-quote vendor_perl/show_all_colors vendor_perl/signset-editor vendor_perl/snmpkey vendor_perl/SOAPsh.pl vendor_perl/spamassassin vendor_perl/spamc vendor_perl/spamd vendor_perl/spfd vendor_perl/spfquery vendor_perl/spfquery vendor_perl/strip-nondeterminism vendor_perl/stubmaker.pl vendor_perl/tachk vendor_perl/test-yaml vendor_perl/tidy_changelog vendor_perl/timetrans vendor_perl/tkjpeg vendor_perl/tpage vendor_perl/translit vendor_perl/trustman vendor_perl/ttree vendor_perl/uncolor vendor_perl/use-devel-checklib vendor_perl/validjson vendor_perl/widget vendor_perl/xgettext.pl vendor_perl/xml_grep vendor_perl/xml_merge vendor_perl/xml_pp vendor_perl/xml-pretty vendor_perl/xml_spellcheck vendor_perl/xml_split vendor_perl/xpath vendor_perl/yapp vendor_perl/youtube-viewer vendor_perl/zonesigner veracrypt vercmp verdandi verify-db.pl verify-uselistorder verify-uselistorder verilator verilator_bin verilator_bin_dbg verilator_coverage verilator_coverage_bin_dbg verilator_gantt verilator_profcfunc veritysetup versuck-ng vertexrate vert-or-frag-only vert-tex vfdecrypt vfnmz vftovp vgcfgbackup vgcfgrestore vgchange vgck vgconvert vgcreate vgdb vgdisplay vgexport vgextend vgimport vgimportclone vglclient vglconfig vglconnect vglgenkey vgllogin vglrun vglserver_config vglxinfo vgmerge vgmknodes vgreduce vgremove vgrename vgs vgscan vgsplit vi videoob vidir vieet view viewgam viewgar viewnior vifm vifm-convert-dircolors vifm-pause vifm-screen-split vig_optimize vigpg vigr vigra-config viking vim vim vimb vimcat vimdiff vimdiff vimdot vimiv vimpager vimrunner vimtutor vimtutor vinagre vint vipe vips vips-8.9 vipsedit vipsheader vipsprofile vipsthumbnail vipw virgl_test_server virsh virt-admin virt-alignment-scan virt-builder virt-builder-repository virt-cat virt-clone virt-convert virt-copy-in virt-copy-out virt-customize virt-df virt-dib virt-diff virt-edit virt-filesystems virt-format virtfs-proxy-helper virtfs-proxy-helper virt-get-kernel virt-host-validate virt-index-validate virt-inspector virt-install virtinterfaced virtlockd virt-log virtlogd virt-login-shell virt-ls virtlxcd virt-make-fs virt-manager virtnetworkd virtnodedevd virtnwfilterd virt-pki-validate virtproxyd virtqemud virt-qemu-run virt-rescue virt-resize virtsecretd virt-sparsify virtstoraged virt-sysprep virt-tail virt-tar-in virt-tar-out virtualbox VirtualBox virtualenv virtualenv2 virtualenv3 virtualenv-clone virtualenvwrapper_lazy.sh virtualenvwrapper.sh virtvboxd virt-viewer virt-win-reg virt-xml virt-xml-validate vis vis-clipboard vis-complete vis-digraph visgrep visitors vis-menu vis-open visualboyadvance-m visualimagecompare visualinfo visualinfo visualvm visudo vit viterbi vivid vkconfig vkcube vkcubepp vkreplay vktrace vktracedump vktraceviewer vlc vlc-wrapper vlna vlock vmaf vmaf-moment vmaf-ms_ssim vmafossexec vmaf-psnr vmaf-ssim vmcore-dmesg vmhgfs-fuse vmmouse_detect vmprofshow vmstat vmtoolsd vmware-checkvm vmwarectrl vmware-hgfsclient vmware-namespace-cmd vmware-rpctool vmware-toolbox-cmd vmware-user vmware-user-suid-wrapper vmware-vmblock-fuse vmware-xdg-detect-de vmware-xferlogs vmx2john vncconfig vncpasswd vncpcap2john vncviewer vnstat vnstatd vnstati vobStreamer voikkogc voikkohyphenate voikkospell voikkovfstc volatility volk-config-info volk_modtool volk_profile vol.py volumeicon volume_key vorbiscomment vorbisgain votour vp8enc vp9enc vp-array vp-array-hf vp-array-int vp-clip vpe vpl2mod vpl2ovp vpl2vpl vp-line-clip vpnc vpnc-disconnect vppchromasitting vppscaling_n_out_usrptr vppsharpness vpr vptovf vp-tri vp-tri-cb vp-tri-cb-pos vp-tri-cb-tex vp-tri-imm vp-tri-invariant vp-tri-swap vp-tri-tex vp-unfilled vpxdec vpxenc vs2osisref vs2osisreftxt vsd2raw vsd2text vsd2xhtml vsid vspipe vsraytrace vss2raw vss2text vss2xhtml vstp vsyasm vte-2.91 vtep-ctl vterm-ctrl vterm-dump vtkEncodeString vtkParseJava vtkParseJava-pv5.8 vtkProbeOpenGLVersion-pv5.8 vtkProcessXML-pv5.8 vtkpython vtkWrapClientServer-pv5.8 vtkWrapHierarchy vtkWrapHierarchy-pv5.8 vtkWrapJava vtkWrapJava-pv5.8 vtkWrapPython vtkWrapPythonInit vtkWrapPythonInit-pv5.8 vtkWrapPython-pv5.8 vty-build-width-table vty-demo vty-mode-demo vtysh vulkaninfo vvp vwebp vxloader vym vzt2vcd vztminer w w3m w3mman waf wagon wagon_test wait41 waitress-serve waitress-serve2 wakatime wal walk wall wam2ma wammu wammu-configure wamp warez wargames warnquota warp warsow warzone2100 wash wasm2c wasm2js wasm2wat wasm-as wasm-ctor-eval wasm-dis wasm-emscripten-finalize wasm-interp wasm-ld wasm-metadce wasm-objdump wasm-opcodecnt wasm-opt wasm-reduce wasm-shell wasm-strip wasm-validate wast2json wat2wasm watch watchexec watchgnupg watchgod watchman watchman-make watchman-wait watchmedo watchquagga wat-desugar watercluster2 waterfall wavcmp wavegain wavemon wavpack waybar wayinfo wayland-scanner wayland-scanner++ wbcopytables wbcopytables-bin wbinfo wbmptopbm wc wcap-decode wcgrep wdctl wdf wdf-cat wdf-dump wdiff weather weave web2disk web2png web2py2po webcam webcontentedit webfsd webhook webhttrack WebKitWebDriver webmitm weboob weboob-cli weboob-config weboob-debug weboob-repos weboorrents webpinfo webpmux webpng webquiz webspy webview weechat weechat-curses weechat-headless wesnoth wesnothd wesside-ng westcos-tool weston weston-calibrator weston-clickdot weston-cliptest weston-confine weston-content_protection weston-debug weston-dnd weston-editor weston-eventdemo weston-flower weston-fullscreen weston-image weston-info weston-launch weston-multi-resource weston-presentation-shm weston-resizor weston-screenshooter weston-simple-damage weston-simple-dmabuf-egl weston-simple-dmabuf-v4l weston-simple-egl weston-simple-shm weston-simple-touch weston-smoke weston-stacking weston-subsurfaces weston-terminal weston-touch-calibrator weston-transformed wetboobs wfst_build wfst_run wfst_train wfuse wg wgdb wget wgetpaste wg-quick whatis wheel wheel2 when whereis which whipper whiptail whirlpooldeep whirlpool-hash who whoami whois whoismac whowatch widelands widl widl wifi wifi-menu wifi-status wifite wigwam wiiuseexample wikicurses wiki-search wiki-search-html wildmidi wimappend wimapply wimcapture wimdelete wimdir wimexport wimextract wiminfo wimjoin wimlib-imagex wimmount wimmountrw wimoptimize wimsplit wimunmount wimverify winbindd wincopy wincopy32 wine wine wine64 wine64 wine64-preloader wine64-preloader wineboot wineboot winebuild winebuild winecfg winecfg wineconsole wineconsole winecpp winecpp winedbg winedbg winedump winedump winefile winefile wineg++ wineg++ winegcc winegcc winemaker winemaker winemine winemine winepath winepath wine-preloader wine-preloader wineserver wineserver winetricks wingpanel wingproj wings3d winicontopam winicontoppm winpopup-install winpopup-send winpos winpr-hash winpr-makecert wipe wipefs wiper.sh wire-desktop wirefilter wires wireshark wish wish8.6 wishbone-tool wit withsctp WizNote wkhtmltoimage wks2raw wks2text wlancap2wpasec wlangenpmk wlangenpmkocl wlanhcx2john wlanhcx2ssid wlanhcxcat wlanhcxinfo wlanjohn2hcx wlanpmk2hcx wlanwkp2hcx wl-copy wlfreerdp wl_map_info wl_map_object_info wl-paste wmc wmc wmctrl wmf2eps wmf2fig wmf2gd wmf2svg wmf2x wmiexec.py wmipersist.py wmiquery.py wmname wms_metadata.xml wnckprop wnck-urgency-monitor wnode wodim woff2_compress woff2_decompress woff2_info wofi wol wol-bootptab wopl2ofm wordcount wordforms wordlist2dawg wordlist2hunspell word-list-compress wordview workmanir workrave worm worms wovf2ovp wovp2ovf wpaclean wpa_cli wpa_passphrase wpapcap2john wpa_supplicant wpg2raw wpg2svg wpg2svgbatch.pl wps2html wps2raw wps2text wpscan wrap wrc wrc wrestool write writelog write-mime-multipart writer2latex wrjpgcom wrmsr wrudf wsd wsdl wsdl2 wsdl2h wsdump.py wserv4 wsgen wsimport wsrep_sst_common wsrep_sst_mariabackup wsrep_sst_mysqldump wsrep_sst_rsync wsrep_sst_rsync_wan wsw-server wswtv-server wtf wump wvAbw wvCleanLatex wvConvert wvdial wvdialconf wvDocBook wvDVI wvgain wvHtml wvLatex wvMime wvPDF wvPS wvRTF wvSummary wvtag wvtestrun wvText wvunpack wvVersion wvWare wvWml wwan wwt wx2_conv wxcas wx-config wx-config32 wx-config-gtk3 wxdemo wxget wxmaxima wxrc wxrc-3.0 wxrc32 wxrc32-3.0 X x11amp.pl x11perf x11perfcomp x11vnc x128 x264 x265 x2goagent x2gobasepath x2gocleansessions x2goclient x2gocmdexitmessage x2godbadmin x2gofeature x2gofeaturelist x2gofm x2gogetapps x2gogetservers x2golistdesktops x2golistmounts x2golistsessions x2golistsessions_root x2golistshadowsessions x2gomountdirs x2gopath x2goprint x2goresume-desktopsharing x2goresume-session x2goruncommand x2goserver-run-extensions x2gosessionlimit x2gosetkeyboard x2goshowblocks x2gostartagent x2gosuspend-desktopsharing x2gosuspend-session x2goterminate-desktopsharing x2goterminate-session x2goumount-session x2goversion x64 x64dtv x64sc x86_64 x86_64-pc-linux-gnu-c++ x86_64-pc-linux-gnu-c++-8 x86_64-pc-linux-gnu-g++ x86_64-pc-linux-gnu-g++-8 x86_64-pc-linux-gnu-gcc x86_64-pc-linux-gnu-gcc-10.1.0 x86_64-pc-linux-gnu-gcc-8 x86_64-pc-linux-gnu-gcc-8.4.0 x86_64-pc-linux-gnu-gcc-ar x86_64-pc-linux-gnu-gcc-ar-8 x86_64-pc-linux-gnu-gccgo x86_64-pc-linux-gnu-gcc-nm x86_64-pc-linux-gnu-gcc-nm-8 x86_64-pc-linux-gnu-gcc-ranlib x86_64-pc-linux-gnu-gcc-ranlib-8 x86_64-pc-linux-gnu-gdc x86_64-pc-linux-gnu-gfortran x86_64-pc-linux-gnu-gfortran-8 x86_64-pc-linux-gnu-pkg-config x86_energy_perf_policy xa Xalan xandikos xaos xapian-check xapian-compact xapian-config xapian-delve xapian-metadata xapian-pos xapian-progsrv xapian-replicate xapian-replicate-server xapian-tcpsrv xarchiver xargs xasy xattr xauth xautolock xawtv xawtv-remote xbacklight xbacklight xbcloud xbcloud_osenv xbcrypt xbiff xbmtopbm xboard xbrlapi xbrnetwork xbstream xbuild xca xcalc xcalib xcam xcape xcas xcasnew xcbm2 xcbm5x0 xcf2png xcf2pnm xcffibgen xcfinfo xcfview xchm xcircuit xclip xclipboard xclip-copyfile xclip-cutfile xclip-pastefile xclock xcmsdb xcompmgr xconsole xconv.pl xcur2png xcurses-config xcursorgen xcutsel xdelta3 xdg-dbus-proxy xdg-desktop-icon xdg-desktop-menu xdg-email xdg-icon-resource xdg_menu xdg_menu_su xdg-mime xdg-open xdg-screensaver xdg-settings xdg-user-dir Xdialog xdiskusage xdm xdo xdosemu xdot xdotool xdpr xdpyinfo xdriinfo xdt-autogen Xdummy xdvi xdvi-xaw xed xedit xeglgears xeglthreads xelatex xembedsniproxy Xephyr xetex xev xeyes xfbinst_x32 xfbinst_x64 xfburn xfce4-about xfce4-accessibility-settings xfce4-appearance-settings xfce4-appfinder xfce4-clipman xfce4-clipman-history xfce4-clipman-settings xfce4-color-settings xfce4-dict xfce4-display-settings xfce4-find-cursor xfce4-keyboard-settings xfce4-kiosk-query xfce4-mime-settings xfce4-mouse-settings xfce4-notes xfce4-notes-settings xfce4-notifyd-config xfce4-panel xfce4-pm-helper xfce4-popup-applicationsmenu xfce4-popup-clipman xfce4-popup-clipman-actions xfce4-popup-directorymenu xfce4-popup-notes xfce4-popup-whiskermenu xfce4-popup-windowmenu xfce4-power-manager xfce4-power-manager-settings xfce4-screensaver xfce4-screensaver-command xfce4-screensaver-configure xfce4-screensaver-preferences xfce4-screenshooter xfce4-sensors xfce4-session xfce4-session-logout xfce4-session-settings xfce4-settings-editor xfce4-settings-manager xfce4-taskmanager xfce4-terminal xfconf-query xfd xfdesktop xfdesktop-settings xferfaxstats xflock4 xfmpc xfont xfont32 xfontsel xfpm-power-backlight-helper xfreerdp xfrun4 xfs_admin xfs_bmap xfs_copy xfs_db xfsdump xfs_estimate xfsettingsd xfs_freeze xfs_fsr xfs_growfs xfs_info xfsinvutil xfs_io xfs_logprint xfs_mdrestore xfs_metadump xfs_mkfile xfs_ncheck xfs_quota xfs_repair xfsrestore xfs_rtcp xfs_scrub xfs_scrub_all xfs_spaceman xfwm4 xfwm4-settings xfwm4-tweaks-settings xfwm4-workspace-settings xgamma xgap xgettext xglock xglyph xgnokii xgps xgpsspeed xhlatex xhost xhydra xicclu ximtoppm XInclude xindex xindy xindy.mem xindy.run xine xine-bugreport xine-check xine-config xine-list-1.2 xine-remote xinetd xinfo xinit xinput xjadeo xjc xjremote xkbbell xkbcomp xkbevd xkbsel xkbsel-aw xkbseldb xkbvleds xkbwatch xkcdpass xkeycaps xkibitz xkill xlet-about-dialog xlet-settings xliff2odf xliff2oo xliff2po xlinks xload xloadimage xlock xlogo xlsatoms xlsclients xlsfonts xmag xman xmaxima xmaxima xmbind xmessage xmi2pot5 xminicom xmkmf xml xml2ag xml2-config xml2gbs xml2guido xml2ly xml2odf xml2pot xml2stems xml2stw xmlcatalog xmldiff xmlif xmllint xmlmantohtml xmlmerge xmlock xmlparse xml_parser xmlpatch xmlpatterns xmlpatternsvalidator xmlread xmlrpc xml-rpc-api2cpp xml-rpc-api2txt xmlrpc-c-config xmlrpc_cpp_proxy xmlrpc_dumpserver xmlrpc_parsecall xmlrpc_pstream xmlrpc_transport xmlschema-json2xml xmlschema-validate xmlschema-xml2json xmlsec1 xmlsec1-config xmlstarlet xmltex xmlto xmltoman xmltranspose xmlversion xmlwf xmms2 xmms2d xmms2-et xmms2-find-avahi xmms2-launcher xmms2-mdns-avahi xmobar xmode2 xmodmap xmonad xmousepos Xnest xnoise xonotic-dedicated xonotic-glx xonotic-sdl xon.sh xonsh xonsh-cat xorcheck.py Xorg xorn xorriso xorrisofs xorriso-tcltk xortool xortool-xor xosd-config xournalpp xournal-thumbnailer xpad xpet xpl2gpl xplanet xplot xplus4 xpmroot xpmtoppm xpr xpra xpra_launcher xpra_signal_listener xpra_udev_product_version xpra_Xdummy xprep xprintidle xproofd xproofd xprop xpstat xpstojpeg xpstopng xpstops xpstosvg xq xqmstats xrandr xrced2 xrd xrdacctest xrdadler32 xrdb xrdcopy xrdcp xrdcp-old xrdfs xrdgsiproxy xrdgsitest xrdmapc xrdpfc_print xrdpwdadmin xrdshmap xrdsssadmin xrdstagetool xreader xreader-previewer xreader-thumbnailer xrefresh xrootd xrootd-config xrootdfs xrotfontdemo xrotfontdemo32 xsane xsane-gimp xscanimage xscpu64 xscreensaver xscreensaver-command xscreensaver-demo xscreensaver-getimage xscreensaver-getimage-file xscreensaver-getimage-video xscreensaver-gl-helper xscreensaver-systemd xscreensaver-text xsd xsdcxx xsecurelock xsel xsensors xset xsetbg xsetroot xsettingsd xsetwacom xslt-config xsltproc Xspice xspike xss-lock xssstate xtables-legacy-multi xtables-legacy-multi xtables-monitor xtables-monitor xtables-nft-multi xtables-nft-multi xte xterm xtmsplit xtotroff xtrabackup xtrace xtractprotos xtrlock Xvfb xvfb-run xvic xvidtune xvinfo xvminitoppm Xvnc xwallpaper xwax Xwayland xwd xwdtopnm xwininfo xwud xxd xxd xxh128sum xxh32sum xxh64sum xxhsum xxkb xz xzcat xzcmp xzdec xzdiff xzegrep xzfgrep xzgrep xzless xzmore y2racc y4mblack y4mcolorbars y4mdenoise y4mhist y4minterlace y4mivtc y4mshift y4mspatialfilter y4mstabilizer y4mtopnm y4mtoppm y4mtoyuv y4munsharp yabause yacc yad yad-icon-browser yaegi yakuake yamdi yaml2obj yaml2obj yaml2po yaml-bench yaml-bench yamllint yapf yara yarac yard yardoc yarn yarnpkg yasm yat2m yate yate-config yaws yaz-asncomp yaz-client yaz-config yaz-iconv yaz-icu yaz-illclient yaz-json-parse yaz-marcdump yaz-url yaz-ztest ybmtopbm ydcv yelp yelp-build yelp-check yelp-new yes yggdrasil yggdrasilctl ykchalresp ykclient ykfde-enroll ykfde-format ykfde-open ykgenerate ykinfo ykman ykman-gui ykpamcfg ykparse ykpersonalize yodl yodl2html yodl2latex yodl2man yodl2txt yodl2whatever yodl2xml yodlpost yodlstriproff yodlverbinsert yomi2voca.pl yosys yosys-abc yosys-config yosys-filterlib yosys-smtbmc you-get youtube-dl ypipe yplan yq yrd yri ytasm ytdl ytnef ytnefprint ytnefprocess yubikey-personalization-gui yubikey-touch-detector yubioath-desktop yul-phaser yuv2lav yuv4mpeg yuvcorrect yuvcorrect_tune yuvdeinterlace yuvdenoise yuv-distortion yuvfps yuvinactive yuvkineco yuvmedianfilter yuvplay yuvsplittoppm yuvtoppm yuy2topam yuyvtoy4m z3 zabbix_agent2 zabbix_agentd zabbix_get zabbix_proxy_mysql zabbix_proxy_postgresql zabbix_proxy_sqlite3 zabbix_sender zabbix_server_mysql zabbix_server_postgresql zad zanshin zanshin-migrator zaproxy zart zathura zaz zbarcam zbarcam-gtk zbarcam-qt zbarimg zbasis zbl zblacklist zcash-cli zcashd zcash-tx zcat zcav zcf zcl zcmp zconfig zconfig_schema2html zcp zct zcv zdiff zdump zebra zef zegrep zeisstopnm zeitgeist-daemon zeitgeist-datahub zeitgeist-explorer zenheiset zenity zeppelin zerk zerotier-cli zerotier-idtool zerotier-one zev zfgrep zforce zfr zgrep zic zig zile zim zinnia zinnia_convert zinnia_learn zint zint-qt zip zip2john zip2st zipcloak zipcmp zipgrep zipinfo zipmerge zipnote ziproxy ziproxylogtool zipsplit ziptool ziv zkd zless zlib_decompress zlib-flate zmap zmf2raw zmf2svg zmo zmore zmu zmw znc znc-buildmod znew zntune znu zone2json zone2ldap zone2sql zonetab2pot.py zoomsh zope-testrunner zope-testrunner2 zopfli zopflipng zor zpo zpr zpt zqt zramctl zresample zretune zro zrun zsc zsh zsh-5.8 zshdb zsi zsnes zsoelim zsolve zsp zssh zstd zstdcat zstdgrep zstdless zstdmt zsy zsync zsyncmake ztc zte ztee ztelnet ztr zts zuk zvbi-atsc-cc zvbi-chains zvbid zvbi-ntsc-cc zvp zzat zzcat zzdir zzuf zzxorcat zzxorcopy zzxordir ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/start.py0000644000175000017500000002411000000000000014754 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ #This is the first file in the program to be actually executed, after the executable which uses this as first instruction. """ We use a 'wrong' scheme of importing modules here because there are multiple exit conditions, good and bad. We don't want to use all the libraries, including the big Qt one, only to end up displaying the --version and exit. Same with the tests if jack or nsm are running. """ #Give at least some feedback when C libs crash. #Will still not work for the common case that PyQt crashes and ends Python. #But every bit helps when hunting bugs. import faulthandler; faulthandler.enable() from engine.config import * #includes METADATA only. No other environmental setup is executed. from qtgui.helper import setPaletteAndFont #our error boxes shall look like the rest of the program """ Check parameters first. It is possible that we will just --help or --version and exit. In this case nothing gets loaded. """ import os.path import argparse parser = argparse.ArgumentParser(description=f"""{METADATA["name"]} - Version {METADATA["version"]} - Copyright {METADATA["year"]} by {METADATA["author"]} - {METADATA["url"]}""") parser.add_argument("-v", "--version", action='version', version="{} {}".format(METADATA["name"], METADATA["version"])) parser.add_argument("-V", "--verbose", action='store_true', help="(Development) Switch the logger to INFO and give out all kinds of information to get a high-level idea of what the program is doing.") parser.add_argument("-u", "--url", action='store', dest="url", help="Force URL for the session. If there is already a running session we will connect to it. Otherwise we will start one there. Default is local host with random port. Example: osc.udp://myhost.localdomain:14294/") parser.add_argument("--nsm-url", action='store', dest="url", help="Same as --url.") parser.add_argument("-l", "--load-session", action='store', dest="session", help="Session to open on startup, must exist. Overrides --continue") parser.add_argument("-c", "--continue", action='store_true', dest="continueLastSession", help="Autostart last active session.") parser.add_argument("-i", "--hide", action='store_true', dest="starthidden", help="Start GUI hidden in tray, only if tray available on system.") parser.add_argument("--session-root", action='store', dest="sessionRoot", help="Root directory of all sessions. Defaults to '$HOME/NSM Sessions'") args = parser.parse_args() import logging if args.verbose: #development logging.basicConfig(level=logging.INFO, format='[' + METADATA["shortName"] + '] %(levelname)s %(asctime)s %(name)s: %(message)s',) #logging.getLogger().setLevel(logging.INFO) #development else: #production logging.basicConfig(level=logging.ERROR, format='[' + METADATA["shortName"] + '] %(levelname)s %(asctime)s %(name)s: %(message)s',) #logging.getLogger().setLevel(logging.ERROR) #production logger = logging.getLogger(__name__) logger.info("import") """set up python search path before the program starts We need to be earliest, so let's put it here. This is influence during compiling by creating a temporary file "compiledprefix.py". Nuitka complies that in, when make is finished we delete it. #Default mode is a self-contained directory relative to the uncompiled patroneo python start script """ import sys import os import os.path from PyQt5.QtWidgets import QApplication, QStyleFactory logger.info(f"Python Version {sys.version}") try: from compiledprefix import prefix compiledVersion = True logger.info("Compiled prefix found: {}".format(prefix)) except ModuleNotFoundError as e: compiledVersion = False logger.info("Compiled version: {}".format(compiledVersion)) if compiledVersion: PATHS={ #this gets imported "root": "", "bin": os.path.join(prefix, "bin"), "doc": os.path.join(prefix, "share", "doc", METADATA["shortName"]), "desktopfile": os.path.join(prefix, "share", "applications", METADATA["shortName"] + ".desktop"), #not ~/Desktop but our desktop file "share": os.path.join(prefix, "share", METADATA["shortName"]), "templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"), "sessionRoot": args.sessionRoot, "url": args.url, "startupSession": args.session, "startHidden": args.starthidden, #bool "continueLastSession": args.continueLastSession, #bool } _root = os.path.dirname(__file__) _root = os.path.abspath(os.path.join(_root, "..")) else: _root = os.path.dirname(__file__) _root = os.path.abspath(os.path.join(_root, "..")) PATHS={ #this gets imported "root": _root, "bin": _root, "doc": os.path.join(_root, "documentation", "out"), "desktopfile": os.path.join(_root, "desktop", "desktop.desktop"), #not ~/Desktop but our desktop file "share": os.path.join(_root, "engine", "resources"), "templateShare": os.path.join(_root, "template", "engine", "resources"), "sessionRoot": args.sessionRoot, "url": args.url, "startupSession": args.session, "startHidden": args.starthidden, #bool "continueLastSession": args.continueLastSession, #bool } if PATHS["startupSession"]: logger.warning("--continue ignored because --load-session was used.") PATHS["continueLastSession"] = None #just in case. See --help string logger.info("PATHS: {}".format(PATHS)) #Construct QAppliction before constantsAndCOnfigs, which has the fontDB #QApplication.setDesktopSettingsAware(False) #We need our own font so the user interface stays predictable QApplication.setDesktopFileName(PATHS["desktopfile"]) qtApp = QApplication(sys.argv) setPaletteAndFont(qtApp) QApplication.setStyle(QStyleFactory.create("Fusion")) setPaletteAndFont(qtApp) def exitWithMessage(message:str): title = f"""{METADATA["name"]} Error""" if sys.stdout.isatty(): sys.exit(title + ": " + message) else: from PyQt5.QtWidgets import QMessageBox #This is the start file for the Qt client so we know at least that Qt is installed and use that for a warning. QMessageBox.critical(qtApp.desktop(), title, message) sys.exit(title + ": " + message) def setProcessName(executableName): """From https://stackoverflow.com/questions/31132342/change-process-name-while-executing-a-python-script """ import ctypes, ctypes.util lib = ctypes.cdll.LoadLibrary(None) prctl = lib.prctl prctl.restype = ctypes.c_int prctl.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong] def set_proctitle(new_title): result = prctl(15, new_title, 0, 0, 0) if result != 0: raise OSError("prctl result: %d" % result) set_proctitle(executableName.encode()) libpthread_path = ctypes.util.find_library("pthread") if not libpthread_path: return libpthread = ctypes.CDLL(libpthread_path) if hasattr(libpthread, "pthread_setname_np"): _pthread_setname_np = libpthread.pthread_setname_np _pthread_self = libpthread.pthread_self _pthread_self.argtypes = [] _pthread_self.restype = ctypes.c_void_p _pthread_setname_np.argtypes = [ctypes.c_void_p, ctypes.c_char_p] _pthread_setname_np.restype = ctypes.c_int if _pthread_setname_np is None: return _pthread_setname_np(_pthread_self(), executableName.encode()) def _is_jack_running(): """Check for JACK""" import ctypes import os silent = os.open(os.devnull, os.O_WRONLY) stdout = os.dup(1) stderr = os.dup(2) os.dup2(silent, 1) #stdout os.dup2(silent, 2) #stderr cjack = ctypes.cdll.LoadLibrary("libjack.so.0") class jack_client_t(ctypes.Structure): _fields_ = [] cjack.jack_client_open.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)] #the two ints are enum and pointer to enum. #http://jackaudio.org/files/docs/html/group__ClientFunctions.html#gab8b16ee616207532d0585d04a0bd1d60 cjack.jack_client_open.restype = ctypes.POINTER(jack_client_t) ctypesJackClient = cjack.jack_client_open("probe".encode("ascii"), 0x01, None) #0x01 is the bit set for do not autostart JackNoStartServer try: ret = bool(ctypesJackClient.contents) except ValueError: #NULL pointer access ret = False cjack.jack_client_close(ctypesJackClient) os.dup2(stdout, 1) #stdout os.dup2(stderr, 2) #stderr return ret def checkJackOrExit(prettyName): import sys if not _is_jack_running(): exitWithMessage("JACK Audio Connection Kit is not running. Please start it.") checkJackOrExit(METADATA["name"]) try: #Only cosmetics setProcessName(METADATA["shortName"]) except: pass #Capture Ctlr+C / SIGINT and let @atexit handle the rest. import signal import sys def signal_handler(sig, frame): sys.exit(0) #atexit will trigger signal.signal(signal.SIGINT, signal_handler) #Catch Exceptions even if PyQt crashes. import sys sys._excepthook = sys.excepthook def exception_hook(exctype, value, traceback): """This hook purely exists to call sys.exit(1) even on a Qt crash so that atexit gets triggered""" #print(exctype, value, traceback) logger.error("Caught crash in execpthook. Trying too execute atexit anyway") sys._excepthook(exctype, value, traceback) sys.exit(1) sys.excepthook = exception_hook ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/engine/watcher.py0000644000175000017500000001707500000000000015270 0ustar00nilsnils#! /usr/bi n/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library from datetime import datetime import pathlib import os #Our Modules from engine.start import PATHS def fast_scandir(dir): """ Get all subdirectories recursively. https://stackoverflow.com/questions/973473/getting-a-list-of-all-subdirectories-in-the-current-directory""" try: subfolders= [f.path for f in os.scandir(dir) if f.is_dir()] except PermissionError: subfolders = [] for dir in list(subfolders): subfolders.extend(fast_scandir(dir)) return subfolders class Watcher(object): """ Initialize with the server controller The watcher will should only run when the program is in the mode to choose a session. If a session is loaded it will technically work, but will be a waste of resources. The watcher will also trigger the nsmController to redundantly query for a session list, for example when a new empty sessions gets created or duplicated. Once because nsmd sends the "new" signal, and then our watcher will notice the change itself. However, this happens only so often and can be accepted, so the program does not need more checks and exceptions. Inform you via callback when: a) a session dir was deleted b) a new directory got created. c) session directory renamed or moved d) session.nsm changed timestamp e) A lockfile appeared or disappeared We cannot poll nsmServerControl.exportSessionsAsDicts() because that triggers an nsmd response including log message and will flood their console. Instead this class only offers incremental updates. Another way is to watch the dir for changes ourselves in in some cases request a new project list from nsmd. Lockfile functionality goes beyond what NSM offers. A session-daemon can open only one project at a time. If you try to open another project with a second GUI (but same NSM-URL) it goes wrong a bit, at least in the new-session-manager GUI. They will leave a zombie lockfile. However, it still is possible to open a second nsmd instance. In this case the lockfile prevents opening a session twice. And we are reflecting the opened state of the project, no matter from which daemon. Horray for us :) Clients can only be added or removed while a session is locked. We do not check nor update number of clients or symlinks. Therefore we advice a GUI to deactivate the display of these two values while a session is locked (together with size) """ def __init__(self, nsmServerControl): self.active = True self._nsmServerControl = nsmServerControl assert self._nsmServerControl.sessionRoot self._directories = fast_scandir(self._nsmServerControl.sessionRoot) logger.info("Requestion our own copy of the session list. Don't worry about the apparent redundant call :)") self._lastExport = self._nsmServerControl.exportSessionsAsDicts() #list of dicts self._lastTimestamp = {d["nsmSessionName"]:d["lastSavedDate"] for d in self._lastExport} #str:str #Init all values with None will send the initial state via callback on program start, which is what the user wants to know. self._lastLockfile = {d["nsmSessionName"]:None for d in self._lastExport} #str:bool self.timeStampHook = None # a single function that gets informed of changes, most likely the api callback self.lockFileHook = None # a single function that gets informed of changes, most likely the api callback self.sessionsChangedHook = None # the api callback function api.callbacks._sessionsChanged. Rarely used. def resume(self, *args): """For api callbacks""" self.active = True #If we returned from an open session that will surely have changed. Trigger a single poll self.process() logger.info("Watcher resumed") def suspend(self, *args): """For api callbacks""" self.active = False logger.info("Watcher suspended") def _update(self): current_directories = fast_scandir(self._nsmServerControl.sessionRoot) if not self._directories == current_directories: self._directories = current_directories self._lastExport = self.sessionsChangedHook() #will gather its own data, send it to api callbacks, but also return for us. self._lastTimestamp = {d["nsmSessionName"]:d["lastSavedDate"] for d in self._lastExport} #str:str self._lastLockfile = {d["nsmSessionName"]:None for d in self._lastExport} #str:bool def process(self): """Add this to your event loop. We look for any changes in the directory structure. If we detect any we simply trigger a new NSM export and a new NSM generated project list via callback. We do not expect this to happen often. This will also trigger if we add a new session ourselves. This *is* our way to react to new Sessions. """ if not self.active: return if self.sessionsChangedHook: self._update() #Now check the incremental hooks. #No hooks, no reason to process if not (self.timeStampHook or self.lockFileHook): logger.info("No watcher-hooks to process") return for entry in self._lastExport: nsmSessionName = entry["nsmSessionName"] try: #Timestamp of session.nsm if self.timeStampHook: timestamp = datetime.fromtimestamp(entry["sessionFile"].stat().st_mtime).isoformat(sep=" ", timespec='minutes') #same format as server control export if not timestamp == self._lastTimestamp[nsmSessionName]: #This will only trigger on a minute-based slot, which is all we want and need. This is for relaying information to the user, not for advanced processing. self._lastTimestamp[nsmSessionName] = timestamp self.timeStampHook(nsmSessionName, timestamp) #Lockfiles if self.lockFileHook: lockfileState = entry["lockFile"].is_file() if not self._lastLockfile[nsmSessionName] == lockfileState: self._lastLockfile[nsmSessionName] = lockfileState self.lockFileHook(nsmSessionName, lockfileState) except PermissionError: logger.warning(f"File Permission error for {entry}") self._lastExport.remove(entry) #avoid stumbling upon this again self._update() except FileNotFoundError: logger.warning(f"File not found error for {entry}") self._lastExport.remove(entry) #avoid stumbling upon this again self._update() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/addclientprompt.py0000644000175000017500000001272500000000000016705 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library #Engine import engine.api as api #Third Party from PyQt5 import QtCore, QtWidgets class PromptWidget(QtWidgets.QDialog): wordlist = None minlen = None def __init__(self, parent): super().__init__(parent) layout = QtWidgets.QFormLayout() #layout = QtWidgets.QVBoxLayout() updateWordlist() #this is a fast index update, we can call that every time to be sure. Otherwise newly added executable-PATHs will not be reflected in the dialog without a program-database update, which takes too long to be convenient. self.setLayout(layout) self.setWindowFlag(QtCore.Qt.Popup, True) self.comboBox = QtWidgets.QComboBox(self) self.comboBox.setEditable(True) self.comboBox.currentTextChanged.connect(self.check) #not called when text is changed programatically if PromptWidget.wordlist: completer = QtWidgets.QCompleter(PromptWidget.wordlist) completer.setModelSorting(QtWidgets.QCompleter.CaseInsensitivelySortedModel) completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) completer.activated.connect(self.process) #To avoid double-press enter to select one we hook into the activated signal and trigger process self.comboBox.setCompleter(completer) self.comboBox.setMinimumContentsLength(PromptWidget.minlen) labelString = QtCore.QCoreApplication.translate("PromptWidget", "Type in the name of an executable file on your system.") else: labelString = QtCore.QCoreApplication.translate("PromptWidget", "No program database found. Please update through Control menu.") label = QtWidgets.QLabel(labelString) layout.addWidget(label) self.comboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLength) layout.addWidget(self.comboBox) errorString = QtCore.QCoreApplication.translate("PromptWidget", "Command not found or not accepted!
Parameters, --switches and relative paths are not allowed.
Use nsm-proxy or write a starter-script instead.") errorString = "" + errorString + "" self.errorLabel = QtWidgets.QLabel(errorString) layout.addWidget(self.errorLabel) self.errorLabel.hide() #shown in process or check self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self.process) self.buttonBox.rejected.connect(self.reject) layout.addWidget(self.buttonBox) self.exec() #blocks until the dialog gets closed def abortHandler(self): pass def check(self, currentText): """Called every keypress. We do preliminary error and collision checking here, so the engine does not have to throw an error """ self.errorLabel.hide() #start in good faith ok = self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok) if currentText in PromptWidget.wordlist: #this is taken literally. Any extra char or whitespace counts as false ok.setEnabled(True) else: ok.setEnabled(False) self.errorLabel.show() def process(self): """Careful! Calling this eats python errors without notice. Make sure your objects exists and your syntax is correct""" assert PromptWidget.wordlist assert PromptWidget.minlen curText = self.comboBox.currentText() if not curText or curText == " ": #TODO: qt weirdness. This is a case when focus is lost from a valid entry. The field is filled from a chosen value, from the list. But it says " ". #Do not show the errorLabel. This is a qt bug or so. return if curText in PromptWidget.wordlist: api.clientAdd(curText) logger.info(f"Prompt accepted {curText} and will send it to clientAdd") self.done(True) else: logger.info(f"Prompt did not accept {curText}.Showing info to the user.") self.errorLabel.show() def updateWordlist(): """in case programs are installed while the session is running the user can manually call a database update""" PromptWidget.wordlist = api.getUnfilteredExecutables() if PromptWidget.wordlist: PromptWidget.minlen = len(max(PromptWidget.wordlist, key=len)) else: logger.error("Executable list came back empty! Most likely an error in application database build. Not trivial!") def askForExecutable(parent): PromptWidget(parent) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/changelog.py0000644000175000017500000000401100000000000015430 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Lib #System Wide Modules from PyQt5 import QtCore, QtWidgets, QtGui #Local Modules from engine.config import * #imports METADATA from engine.start import PATHS class Changelog(QtWidgets.QWidget): """ A modal window that is hidden. Just shows the file CHANGELOG """ def __init__(self, mainWindow): super().__init__() self.mainWindow = mainWindow ourLayout = QtWidgets.QVBoxLayout() #ourLayout.setSpacing(0) #ourLayout.setContentsMargins(0,0,0,0) self.setLayout(ourLayout) self.setWindowTitle(METADATA["name"] + " " + QtCore.QCoreApplication.translate("TemplateChangelog", "Changelog")) introtext = QtCore.QCoreApplication.translate("TemplateChangelog", "The Changelog is only available in English.") ourLayout.addWidget(QtWidgets.QLabel(introtext)) textEdit = QtWidgets.QPlainTextEdit() textEdit.setReadOnly(True) with open(PATHS["doc"] + "/CHANGELOG", "r") as f: textEdit.setPlainText(f.read()) ourLayout.addWidget(textEdit) self.hide() def closeEvent(self, event): self.hide() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/descriptiontextwidget.py0000644000175000017500000001130100000000000020135 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ) This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library #Engine import engine.api as api #Third Party from PyQt5 import QtCore, QtGui, QtWidgets class DescriptionController(object): """Not a subclass. Controls a TextEditWidget to hold the session-description from nsm-data. Can be used on multiple text widgets. One controller for one textwidget.""" def __init__(self, mainWindow, parentGroupBox:QtWidgets.QGroupBox, plainTextWidget:QtWidgets.QPlainTextEdit): self.mainWindow = mainWindow self.description = plainTextWidget self.parentGroupBox = parentGroupBox self.placeHolderDescription = QtCore.QCoreApplication.translate("LoadedSessionDescription", "Double click to add the client nsm-data to write here.\nUse it for notes, TODO, references etc…") #We do NOT use the Qt placeholder text because that shows even when editing is possible. Our visual feedback is changing from placeholder to empty. self.description.setEnabled(False) self._reactCallback_dataClientDescriptionChanged(None) #set up description self.parentGroupBox.mouseDoubleClickEvent = self._doubleClickToResumeOrAddNsmData self.description.textChanged.connect(self.sendDescriptionToApi) #TODO: this is every keystroke, but it is impossible to solve a multi-GUI network system where a save signal comes from outside.. we need update every char. Maybe a genius in the future will solve this. #self.description.focusOutEvent = self._descriptionFocusOut api.callbacks.dataClientDescriptionChanged.append(self._reactCallback_dataClientDescriptionChanged) #Session description def sendDescriptionToApi(self): """We cannot send every keystroke over the OSC-network. Therefore we wait: This is called by several events that "feel" like editing is done now: Focus out, Ctlr+S, Alt+S.""" api.setDescription(self.description.toPlainText()) #this is not save yet. Just forward to data client. #def _descriptionFocusOut(self, event): # self.sendDescriptionToApi() # QtWidgets.QPlainTextEdit.focusOutEvent(self.description, event) def _reactCallback_dataClientDescriptionChanged(self, desc:str): """Put the session description into our text field. We send each change, so we receive this signal each detail change. The cursor changes in between so we force the position. """ self.description.blockSignals(True) if not desc is None: #may be None for closing session oldPos = self.description.textCursor().position() self.description.setPlainText(desc) #plain textedit self.description.setEnabled(True) c = self.description.textCursor() c.setPosition(oldPos) self.description.setTextCursor(c) else: self.description.setEnabled(False) self.description.setPlainText(self.placeHolderDescription) self.description.blockSignals(False) def _doubleClickToResumeOrAddNsmData(self, event): """Intended for doubleClickEvent, so we get an event. Do nothing when nsm-data is present. Add it when it was never there. Resume it if stopped in the session. When QPlainTextEdit is disabled it will forward doubleClick to the parent widget, which is this function. If enabled the groupBox description and frame will be clickable, we do nothing in this case. """ if self.description.isEnabled(): pass else: d = api.executableInSession("nsm-data") if d: api.clientResume(d["clientId"]) else: api.clientAdd("nsm-data") QtWidgets.QGroupBox.mouseDoubleClickEvent(self.parentGroupBox, event) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/designer/mainwindow.py0000644000175000017500000006413300000000000017470 0ustar00nilsnils# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'mainwindow.ui' # # Created by: PyQt5 UI code generator 5.15.6 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(953, 737) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setSpacing(0) self.verticalLayout.setObjectName("verticalLayout") self.mainPageSwitcher = QtWidgets.QStackedWidget(self.centralwidget) self.mainPageSwitcher.setLineWidth(0) self.mainPageSwitcher.setObjectName("mainPageSwitcher") self.tabPage = QtWidgets.QWidget() self.tabPage.setObjectName("tabPage") self.verticalLayout_12 = QtWidgets.QVBoxLayout(self.tabPage) self.verticalLayout_12.setObjectName("verticalLayout_12") self.jackTransportControls = QtWidgets.QGroupBox(self.tabPage) self.jackTransportControls.setObjectName("jackTransportControls") self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.jackTransportControls) self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.jackTransportPlayPause = QtWidgets.QPushButton(self.jackTransportControls) self.jackTransportPlayPause.setObjectName("jackTransportPlayPause") self.horizontalLayout_2.addWidget(self.jackTransportPlayPause) self.jackTransportRewind = QtWidgets.QPushButton(self.jackTransportControls) self.jackTransportRewind.setObjectName("jackTransportRewind") self.horizontalLayout_2.addWidget(self.jackTransportRewind) self.jackTransportTimeline = QtWidgets.QProgressBar(self.jackTransportControls) self.jackTransportTimeline.setProperty("value", 24) self.jackTransportTimeline.setObjectName("jackTransportTimeline") self.horizontalLayout_2.addWidget(self.jackTransportTimeline) self.jackTransportMaxTime = QtWidgets.QSpinBox(self.jackTransportControls) self.jackTransportMaxTime.setMinimum(1) self.jackTransportMaxTime.setMaximum(999) self.jackTransportMaxTime.setProperty("value", 5) self.jackTransportMaxTime.setObjectName("jackTransportMaxTime") self.horizontalLayout_2.addWidget(self.jackTransportMaxTime) self.verticalLayout_12.addWidget(self.jackTransportControls) self.tabbyCat = QtWidgets.QTabWidget(self.tabPage) self.tabbyCat.setObjectName("tabbyCat") self.tab_detailed = QtWidgets.QWidget() self.tab_detailed.setObjectName("tab_detailed") self.horizontalLayout = QtWidgets.QHBoxLayout(self.tab_detailed) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") self.detailedStackedWidget = QtWidgets.QStackedWidget(self.tab_detailed) self.detailedStackedWidget.setObjectName("detailedStackedWidget") self.stack_no_session = QtWidgets.QWidget() self.stack_no_session.setObjectName("stack_no_session") self.l_2 = QtWidgets.QHBoxLayout(self.stack_no_session) self.l_2.setContentsMargins(9, 9, 9, 9) self.l_2.setSpacing(6) self.l_2.setObjectName("l_2") self.widget = QtWidgets.QWidget(self.stack_no_session) self.widget.setObjectName("widget") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.widget) self.verticalLayout_3.setObjectName("verticalLayout_3") self.button_new_session = QtWidgets.QPushButton(self.widget) self.button_new_session.setObjectName("button_new_session") self.verticalLayout_3.addWidget(self.button_new_session) self.button_new_quick_session = QtWidgets.QPushButton(self.widget) self.button_new_quick_session.setObjectName("button_new_quick_session") self.verticalLayout_3.addWidget(self.button_new_quick_session) self.button_load_selected_session = QtWidgets.QPushButton(self.widget) self.button_load_selected_session.setEnabled(False) self.button_load_selected_session.setObjectName("button_load_selected_session") self.verticalLayout_3.addWidget(self.button_load_selected_session) self.button_rename_selected_session = QtWidgets.QPushButton(self.widget) self.button_rename_selected_session.setEnabled(False) self.button_rename_selected_session.setObjectName("button_rename_selected_session") self.verticalLayout_3.addWidget(self.button_rename_selected_session) self.button_copy_selected_session = QtWidgets.QPushButton(self.widget) self.button_copy_selected_session.setEnabled(False) self.button_copy_selected_session.setObjectName("button_copy_selected_session") self.verticalLayout_3.addWidget(self.button_copy_selected_session) self.button_delete_selected_session = QtWidgets.QPushButton(self.widget) self.button_delete_selected_session.setEnabled(False) self.button_delete_selected_session.setObjectName("button_delete_selected_session") self.verticalLayout_3.addWidget(self.button_delete_selected_session) self.button_remove_lock_selected_session = QtWidgets.QPushButton(self.widget) self.button_remove_lock_selected_session.setEnabled(False) self.button_remove_lock_selected_session.setObjectName("button_remove_lock_selected_session") self.verticalLayout_3.addWidget(self.button_remove_lock_selected_session) self.checkBoxNested = QtWidgets.QCheckBox(self.widget) self.checkBoxNested.setChecked(True) self.checkBoxNested.setObjectName("checkBoxNested") self.verticalLayout_3.addWidget(self.checkBoxNested) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_3.addItem(spacerItem) self.l_2.addWidget(self.widget) self.session_tree = QtWidgets.QTreeWidget(self.stack_no_session) self.session_tree.setObjectName("session_tree") self.session_tree.headerItem().setText(0, "1") self.l_2.addWidget(self.session_tree) self.detailedStackedWidget.addWidget(self.stack_no_session) self.stack_loaded_session = QtWidgets.QWidget() self.stack_loaded_session.setObjectName("stack_loaded_session") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.stack_loaded_session) self.verticalLayout_4.setContentsMargins(9, 9, 9, 9) self.verticalLayout_4.setSpacing(0) self.verticalLayout_4.setObjectName("verticalLayout_4") self.vSplitterProgramsLog = QtWidgets.QSplitter(self.stack_loaded_session) self.vSplitterProgramsLog.setOrientation(QtCore.Qt.Vertical) self.vSplitterProgramsLog.setObjectName("vSplitterProgramsLog") self.hSplitterLauncherClients = QtWidgets.QSplitter(self.vSplitterProgramsLog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(7) sizePolicy.setHeightForWidth(self.hSplitterLauncherClients.sizePolicy().hasHeightForWidth()) self.hSplitterLauncherClients.setSizePolicy(sizePolicy) self.hSplitterLauncherClients.setOrientation(QtCore.Qt.Horizontal) self.hSplitterLauncherClients.setChildrenCollapsible(False) self.hSplitterLauncherClients.setObjectName("hSplitterLauncherClients") self.session_programs = QtWidgets.QGroupBox(self.hSplitterLauncherClients) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(7) sizePolicy.setHeightForWidth(self.session_programs.sizePolicy().hasHeightForWidth()) self.session_programs.setSizePolicy(sizePolicy) self.session_programs.setObjectName("session_programs") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.session_programs) self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) self.verticalLayout_5.setSpacing(0) self.verticalLayout_5.setObjectName("verticalLayout_5") self.loadedSessionsLauncher = QtWidgets.QTreeWidget(self.session_programs) self.loadedSessionsLauncher.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.loadedSessionsLauncher.setIconSize(QtCore.QSize(64, 64)) self.loadedSessionsLauncher.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.loadedSessionsLauncher.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.loadedSessionsLauncher.setObjectName("loadedSessionsLauncher") self.loadedSessionsLauncher.headerItem().setText(0, "1") self.verticalLayout_5.addWidget(self.loadedSessionsLauncher) self.session_loaded = QtWidgets.QGroupBox(self.hSplitterLauncherClients) self.session_loaded.setObjectName("session_loaded") self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.session_loaded) self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) self.verticalLayout_6.setSpacing(0) self.verticalLayout_6.setObjectName("verticalLayout_6") self.loadedSessionClients = QtWidgets.QTreeWidget(self.session_loaded) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(3) sizePolicy.setHeightForWidth(self.loadedSessionClients.sizePolicy().hasHeightForWidth()) self.loadedSessionClients.setSizePolicy(sizePolicy) self.loadedSessionClients.setAlternatingRowColors(True) self.loadedSessionClients.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.loadedSessionClients.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.loadedSessionClients.setObjectName("loadedSessionClients") self.loadedSessionClients.headerItem().setText(0, "1") self.verticalLayout_6.addWidget(self.loadedSessionClients) self.loadedSessionDescriptionGroupBox = QtWidgets.QGroupBox(self.vSplitterProgramsLog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(2) sizePolicy.setHeightForWidth(self.loadedSessionDescriptionGroupBox.sizePolicy().hasHeightForWidth()) self.loadedSessionDescriptionGroupBox.setSizePolicy(sizePolicy) self.loadedSessionDescriptionGroupBox.setObjectName("loadedSessionDescriptionGroupBox") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.loadedSessionDescriptionGroupBox) self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) self.verticalLayout_2.setSpacing(0) self.verticalLayout_2.setObjectName("verticalLayout_2") self.loadedSessionDescription = QtWidgets.QPlainTextEdit(self.loadedSessionDescriptionGroupBox) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.loadedSessionDescription.sizePolicy().hasHeightForWidth()) self.loadedSessionDescription.setSizePolicy(sizePolicy) self.loadedSessionDescription.setObjectName("loadedSessionDescription") self.verticalLayout_2.addWidget(self.loadedSessionDescription) self.verticalLayout_4.addWidget(self.vSplitterProgramsLog) self.detailedStackedWidget.addWidget(self.stack_loaded_session) self.horizontalLayout.addWidget(self.detailedStackedWidget) self.tabbyCat.addTab(self.tab_detailed, "") self.tab_information = QtWidgets.QWidget() self.tab_information.setObjectName("tab_information") self.verticalLayout_11 = QtWidgets.QVBoxLayout(self.tab_information) self.verticalLayout_11.setObjectName("verticalLayout_11") self.informationTreeWidget = QtWidgets.QTreeWidget(self.tab_information) self.informationTreeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.informationTreeWidget.setAlternatingRowColors(True) self.informationTreeWidget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.informationTreeWidget.setItemsExpandable(False) self.informationTreeWidget.setObjectName("informationTreeWidget") self.informationTreeWidget.headerItem().setTextAlignment(0, QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) item_0 = QtWidgets.QTreeWidgetItem(self.informationTreeWidget) item_0 = QtWidgets.QTreeWidgetItem(self.informationTreeWidget) item_0 = QtWidgets.QTreeWidgetItem(self.informationTreeWidget) item_0 = QtWidgets.QTreeWidgetItem(self.informationTreeWidget) item_0 = QtWidgets.QTreeWidgetItem(self.informationTreeWidget) self.informationTreeWidget.header().setVisible(False) self.informationTreeWidget.header().setDefaultSectionSize(150) self.informationTreeWidget.header().setMinimumSectionSize(50) self.verticalLayout_11.addWidget(self.informationTreeWidget) self.tabbyCat.addTab(self.tab_information, "") self.verticalLayout_12.addWidget(self.tabbyCat) self.mainPageSwitcher.addWidget(self.tabPage) self.messagePage = QtWidgets.QWidget() self.messagePage.setObjectName("messagePage") self.verticalLayout_13 = QtWidgets.QVBoxLayout(self.messagePage) self.verticalLayout_13.setObjectName("verticalLayout_13") self.messageLabel = QtWidgets.QLabel(self.messagePage) self.messageLabel.setAlignment(QtCore.Qt.AlignCenter) self.messageLabel.setObjectName("messageLabel") self.verticalLayout_13.addWidget(self.messageLabel) self.waitDialogErrorButton = QtWidgets.QPushButton(self.messagePage) self.waitDialogErrorButton.setObjectName("waitDialogErrorButton") self.verticalLayout_13.addWidget(self.waitDialogErrorButton) self.mainPageSwitcher.addWidget(self.messagePage) self.verticalLayout.addWidget(self.mainPageSwitcher) MainWindow.setCentralWidget(self.centralwidget) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 953, 20)) self.menubar.setObjectName("menubar") self.menuControl = QtWidgets.QMenu(self.menubar) self.menuControl.setObjectName("menuControl") self.menuSession = QtWidgets.QMenu(self.menubar) self.menuSession.setObjectName("menuSession") self.menuClientNameId = QtWidgets.QMenu(self.menubar) self.menuClientNameId.setObjectName("menuClientNameId") MainWindow.setMenuBar(self.menubar) self.actionMenuQuit = QtWidgets.QAction(MainWindow) self.actionMenuQuit.setObjectName("actionMenuQuit") self.actionAbout = QtWidgets.QAction(MainWindow) self.actionAbout.setObjectName("actionAbout") self.actionHide_in_System_Tray = QtWidgets.QAction(MainWindow) self.actionHide_in_System_Tray.setObjectName("actionHide_in_System_Tray") self.actionSessionAddClient = QtWidgets.QAction(MainWindow) self.actionSessionAddClient.setObjectName("actionSessionAddClient") self.actionSessionSave = QtWidgets.QAction(MainWindow) self.actionSessionSave.setObjectName("actionSessionSave") self.actionSessionSaveAs = QtWidgets.QAction(MainWindow) self.actionSessionSaveAs.setObjectName("actionSessionSaveAs") self.actionSessionSaveAndClose = QtWidgets.QAction(MainWindow) self.actionSessionSaveAndClose.setObjectName("actionSessionSaveAndClose") self.actionSessionAbort = QtWidgets.QAction(MainWindow) self.actionSessionAbort.setObjectName("actionSessionAbort") self.actionClientStop = QtWidgets.QAction(MainWindow) self.actionClientStop.setObjectName("actionClientStop") self.actionClientResume = QtWidgets.QAction(MainWindow) self.actionClientResume.setObjectName("actionClientResume") self.actionClientSave_separately = QtWidgets.QAction(MainWindow) self.actionClientSave_separately.setObjectName("actionClientSave_separately") self.actionClientRemove = QtWidgets.QAction(MainWindow) self.actionClientRemove.setObjectName("actionClientRemove") self.actionClientToggleVisible = QtWidgets.QAction(MainWindow) self.actionClientToggleVisible.setObjectName("actionClientToggleVisible") self.actionShow_All_Clients = QtWidgets.QAction(MainWindow) self.actionShow_All_Clients.setObjectName("actionShow_All_Clients") self.actionHide_All_Clients = QtWidgets.QAction(MainWindow) self.actionHide_All_Clients.setObjectName("actionHide_All_Clients") self.actionRebuild_Program_Database = QtWidgets.QAction(MainWindow) self.actionRebuild_Program_Database.setObjectName("actionRebuild_Program_Database") self.actionClientRename = QtWidgets.QAction(MainWindow) self.actionClientRename.setObjectName("actionClientRename") self.actionSettings = QtWidgets.QAction(MainWindow) self.actionSettings.setObjectName("actionSettings") self.actionManual = QtWidgets.QAction(MainWindow) self.actionManual.setObjectName("actionManual") self.actionChangelog = QtWidgets.QAction(MainWindow) self.actionChangelog.setObjectName("actionChangelog") self.actionSplit_Session_View_The_Other_Way = QtWidgets.QAction(MainWindow) self.actionSplit_Session_View_The_Other_Way.setCheckable(True) self.actionSplit_Session_View_The_Other_Way.setObjectName("actionSplit_Session_View_The_Other_Way") self.menuControl.addAction(self.actionManual) self.menuControl.addAction(self.actionChangelog) self.menuControl.addAction(self.actionRebuild_Program_Database) self.menuControl.addAction(self.actionSplit_Session_View_The_Other_Way) self.menuControl.addAction(self.actionHide_in_System_Tray) self.menuControl.addAction(self.actionSettings) self.menuControl.addAction(self.actionMenuQuit) self.menuSession.addAction(self.actionSessionAddClient) self.menuSession.addAction(self.actionSessionSave) self.menuSession.addAction(self.actionSessionSaveAs) self.menuSession.addAction(self.actionSessionSaveAndClose) self.menuSession.addAction(self.actionSessionAbort) self.menuSession.addSeparator() self.menuSession.addAction(self.actionShow_All_Clients) self.menuSession.addAction(self.actionHide_All_Clients) self.menuClientNameId.addAction(self.actionClientRename) self.menuClientNameId.addAction(self.actionClientToggleVisible) self.menuClientNameId.addAction(self.actionClientSave_separately) self.menuClientNameId.addAction(self.actionClientStop) self.menuClientNameId.addAction(self.actionClientResume) self.menuClientNameId.addSeparator() self.menuClientNameId.addAction(self.actionClientRemove) self.menubar.addAction(self.menuControl.menuAction()) self.menubar.addAction(self.menuSession.menuAction()) self.menubar.addAction(self.menuClientNameId.menuAction()) self.retranslateUi(MainWindow) self.mainPageSwitcher.setCurrentIndex(0) self.tabbyCat.setCurrentIndex(0) self.detailedStackedWidget.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "Agordejo")) self.jackTransportControls.setTitle(_translate("MainWindow", "Global Playback Controls")) self.jackTransportPlayPause.setText(_translate("MainWindow", "PlayPause")) self.jackTransportRewind.setText(_translate("MainWindow", "Rewind")) self.jackTransportTimeline.setFormat(_translate("MainWindow", " time-placeholder")) self.jackTransportMaxTime.setSuffix(_translate("MainWindow", " min")) self.button_new_session.setText(_translate("MainWindow", "New")) self.button_new_quick_session.setText(_translate("MainWindow", "Quick New")) self.button_load_selected_session.setText(_translate("MainWindow", "Load Selected")) self.button_rename_selected_session.setText(_translate("MainWindow", "Rename Selected")) self.button_copy_selected_session.setText(_translate("MainWindow", "Copy Selected")) self.button_delete_selected_session.setText(_translate("MainWindow", "Delete Selected")) self.button_remove_lock_selected_session.setText(_translate("MainWindow", "Remove Lock")) self.checkBoxNested.setText(_translate("MainWindow", "Tree View")) self.session_programs.setTitle(_translate("MainWindow", "Double-click to load program")) self.session_loaded.setTitle(_translate("MainWindow", "In current session")) self.loadedSessionDescriptionGroupBox.setTitle(_translate("MainWindow", "Session Notes")) self.tabbyCat.setTabText(self.tabbyCat.indexOf(self.tab_detailed), _translate("MainWindow", "Full View")) __sortingEnabled = self.informationTreeWidget.isSortingEnabled() self.informationTreeWidget.setSortingEnabled(False) self.informationTreeWidget.topLevelItem(0).setText(0, _translate("MainWindow", "JACK")) self.informationTreeWidget.topLevelItem(0).setText(1, _translate("MainWindow", "version and running")) self.informationTreeWidget.topLevelItem(1).setText(0, _translate("MainWindow", "NSM Server Mode")) self.informationTreeWidget.topLevelItem(1).setText(1, _translate("MainWindow", "Self-started, connected to, environment var")) self.informationTreeWidget.topLevelItem(2).setText(0, _translate("MainWindow", "NSM Url")) self.informationTreeWidget.topLevelItem(2).setText(1, _translate("MainWindow", "osc.upd ip port")) self.informationTreeWidget.topLevelItem(3).setText(0, _translate("MainWindow", "Session Root")) self.informationTreeWidget.topLevelItem(3).setText(1, _translate("MainWindow", "/home/usr/NSM Sessions")) self.informationTreeWidget.topLevelItem(4).setText(0, _translate("MainWindow", "Program Database")) self.informationTreeWidget.topLevelItem(4).setText(1, _translate("MainWindow", "Last Updated")) self.informationTreeWidget.setSortingEnabled(__sortingEnabled) self.tabbyCat.setTabText(self.tabbyCat.indexOf(self.tab_information), _translate("MainWindow", "Information")) self.messageLabel.setText(_translate("MainWindow", "Processing")) self.waitDialogErrorButton.setText(_translate("MainWindow", "I understand that I will need to resolve this problem on my own!")) self.menuControl.setTitle(_translate("MainWindow", "Control")) self.menuSession.setTitle(_translate("MainWindow", "SessionName")) self.menuClientNameId.setTitle(_translate("MainWindow", "ClientNameId")) self.actionMenuQuit.setText(_translate("MainWindow", "Quit")) self.actionMenuQuit.setShortcut(_translate("MainWindow", "Ctrl+Shift+Q")) self.actionAbout.setText(_translate("MainWindow", "About")) self.actionHide_in_System_Tray.setText(_translate("MainWindow", "Hide in System Tray")) self.actionHide_in_System_Tray.setShortcut(_translate("MainWindow", "Ctrl+Q")) self.actionSessionAddClient.setText(_translate("MainWindow", "Add Client (Prompt)")) self.actionSessionAddClient.setShortcut(_translate("MainWindow", "A")) self.actionSessionSave.setText(_translate("MainWindow", "Save")) self.actionSessionSave.setShortcut(_translate("MainWindow", "Ctrl+S")) self.actionSessionSaveAs.setText(_translate("MainWindow", "Save and Clone under different name")) self.actionSessionSaveAs.setShortcut(_translate("MainWindow", "Ctrl+Shift+S")) self.actionSessionSaveAndClose.setText(_translate("MainWindow", "Save and Close")) self.actionSessionSaveAndClose.setShortcut(_translate("MainWindow", "Ctrl+W")) self.actionSessionAbort.setText(_translate("MainWindow", "Close without Save (\"Abort\")")) self.actionSessionAbort.setShortcut(_translate("MainWindow", "Ctrl+Shift+W")) self.actionClientStop.setText(_translate("MainWindow", "Stop")) self.actionClientStop.setShortcut(_translate("MainWindow", "Alt+O")) self.actionClientResume.setText(_translate("MainWindow", "Resume")) self.actionClientResume.setShortcut(_translate("MainWindow", "Alt+R")) self.actionClientSave_separately.setText(_translate("MainWindow", "Save separately")) self.actionClientSave_separately.setShortcut(_translate("MainWindow", "Alt+S")) self.actionClientRemove.setText(_translate("MainWindow", "Remove")) self.actionClientRemove.setShortcut(_translate("MainWindow", "Alt+X")) self.actionClientToggleVisible.setText(_translate("MainWindow", "Toggle Visible")) self.actionClientToggleVisible.setShortcut(_translate("MainWindow", "Alt+T")) self.actionShow_All_Clients.setText(_translate("MainWindow", "Show All Clients")) self.actionHide_All_Clients.setText(_translate("MainWindow", "Hide All Clients")) self.actionRebuild_Program_Database.setText(_translate("MainWindow", "Rebuild Program Database")) self.actionClientRename.setText(_translate("MainWindow", "Rename")) self.actionClientRename.setShortcut(_translate("MainWindow", "F2")) self.actionSettings.setText(_translate("MainWindow", "Settings")) self.actionManual.setText(_translate("MainWindow", "Manual")) self.actionChangelog.setText(_translate("MainWindow", "News and Changelog")) self.actionSplit_Session_View_The_Other_Way.setText(_translate("MainWindow", "Split Session View the other way")) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/designer/mainwindow.ui0000644000175000017500000006372200000000000017460 0ustar00nilsnils MainWindow 0 0 953 737 Agordejo 0 0 0 0 0 0 0 Global Playback Controls PlayPause Rewind 24 time-placeholder min 1 999 5 0 Full View 0 0 0 0 0 0 6 9 9 9 9 New Quick New false Load Selected false Rename Selected false Copy Selected false Delete Selected false Remove Lock Tree View true Qt::Vertical 20 40 1 0 9 9 9 9 Qt::Vertical 0 7 Qt::Horizontal false 0 7 Double-click to load program 0 0 0 0 0 QAbstractItemView::NoSelection 64 64 QAbstractItemView::ScrollPerPixel QAbstractItemView::ScrollPerPixel 1 In current session 0 0 0 0 0 0 3 true QAbstractItemView::ScrollPerPixel QAbstractItemView::ScrollPerPixel 1 0 2 Session Notes 0 0 0 0 0 0 0 Information QAbstractItemView::NoEditTriggers true QAbstractItemView::ScrollPerPixel false false 50 150 AlignTrailing|AlignVCenter JACK AlignTrailing|AlignVCenter version and running NSM Server Mode AlignTrailing|AlignVCenter Self-started, connected to, environment var NSM Url AlignTrailing|AlignVCenter osc.upd ip port Session Root AlignTrailing|AlignVCenter /home/usr/NSM Sessions Program Database AlignTrailing|AlignVCenter Last Updated Processing Qt::AlignCenter I understand that I will need to resolve this problem on my own! 0 0 953 20 Control SessionName ClientNameId Quit Ctrl+Shift+Q About Hide in System Tray Ctrl+Q Add Client (Prompt) A Save Ctrl+S Save and Clone under different name Ctrl+Shift+S Save and Close Ctrl+W Close without Save ("Abort") Ctrl+Shift+W Stop Alt+O Resume Alt+R Save separately Alt+S Remove Alt+X Toggle Visible Alt+T Show All Clients Hide All Clients Rebuild Program Database Rename F2 Settings Manual News and Changelog true Split Session View the other way ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/designer/newsession.py0000644000175000017500000000470500000000000017510 0ustar00nilsnils# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'newsession.ui' # # Created by: PyQt5 UI code generator 5.15.4 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets class Ui_NewSession(object): def setupUi(self, NewSession): NewSession.setObjectName("NewSession") NewSession.resize(448, 222) self.verticalLayout = QtWidgets.QVBoxLayout(NewSession) self.verticalLayout.setObjectName("verticalLayout") self.nameGroupBox = QtWidgets.QGroupBox(NewSession) self.nameGroupBox.setObjectName("nameGroupBox") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.nameGroupBox) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) self.verticalLayout_3.setSpacing(0) self.verticalLayout_3.setObjectName("verticalLayout_3") self.verticalLayout.addWidget(self.nameGroupBox) self.checkBoxJack = QtWidgets.QCheckBox(NewSession) self.checkBoxJack.setChecked(True) self.checkBoxJack.setObjectName("checkBoxJack") self.verticalLayout.addWidget(self.checkBoxJack) self.checkBoxData = QtWidgets.QCheckBox(NewSession) self.checkBoxData.setChecked(True) self.checkBoxData.setObjectName("checkBoxData") self.verticalLayout.addWidget(self.checkBoxData) self.buttonBox = QtWidgets.QDialogButtonBox(NewSession) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.retranslateUi(NewSession) self.buttonBox.accepted.connect(NewSession.accept) self.buttonBox.rejected.connect(NewSession.reject) QtCore.QMetaObject.connectSlotsByName(NewSession) def retranslateUi(self, NewSession): _translate = QtCore.QCoreApplication.translate NewSession.setWindowTitle(_translate("NewSession", "Dialog")) self.nameGroupBox.setTitle(_translate("NewSession", "New Session Name")) self.checkBoxJack.setText(_translate("NewSession", "Save JACK Connections\n" "(adds clients \'jackpatch\')")) self.checkBoxData.setText(_translate("NewSession", "Client Renaming and Session Notes\n" "(adds client \'nsm-data\')")) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/qtgui/designer/newsession.ui0000644000175000017500000000507100000000000017472 0ustar00nilsnils NewSession 0 0 448 222 Dialog New Session Name 0 0 0 0 0 Save JACK Connections (adds clients 'jackpatch') true Client Renaming and Session Notes (adds client 'nsm-data') true Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() NewSession accept() 248 254 157 274 buttonBox rejected() NewSession reject() 316 260 286 274 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/qtgui/designer/projectname.py0000644000175000017500000000404400000000000017616 0ustar00nilsnils# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'projectname.ui' # # Created by: PyQt5 UI code generator 5.14.1 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets class Ui_ProjectName(object): def setupUi(self, ProjectName): ProjectName.setObjectName("ProjectName") ProjectName.resize(537, 84) self.gridLayout = QtWidgets.QGridLayout(ProjectName) self.gridLayout.setObjectName("gridLayout") self.buttonBox = QtWidgets.QDialogButtonBox(ProjectName) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.gridLayout.addWidget(self.buttonBox, 1, 1, 1, 1) self.labelError = QtWidgets.QLabel(ProjectName) self.labelError.setObjectName("labelError") self.gridLayout.addWidget(self.labelError, 2, 0, 1, 1) self.labelDescription = QtWidgets.QLabel(ProjectName) self.labelDescription.setObjectName("labelDescription") self.gridLayout.addWidget(self.labelDescription, 0, 0, 1, 1) self.name = QtWidgets.QLineEdit(ProjectName) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(3) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) self.name.setSizePolicy(sizePolicy) self.name.setObjectName("name") self.gridLayout.addWidget(self.name, 1, 0, 1, 1) self.retranslateUi(ProjectName) QtCore.QMetaObject.connectSlotsByName(ProjectName) def retranslateUi(self, ProjectName): _translate = QtCore.QCoreApplication.translate ProjectName.setWindowTitle(_translate("ProjectName", "Form")) self.labelError.setText(_translate("ProjectName", "Error Message")) self.labelDescription.setText(_translate("ProjectName", "Choose a project name. Use / for subdirectories")) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1642272567.974499 agordejo-0.3.1/qtgui/designer/projectname.ui0000644000175000017500000000253600000000000017607 0ustar00nilsnils ProjectName 0 0 537 84 Form QDialogButtonBox::Cancel|QDialogButtonBox::Ok Error Message Choose a project name. Use / for subdirectories 3 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/designer/settings.py0000644000175000017500000001174100000000000017151 0ustar00nilsnils# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'settings.ui' # # Created by: PyQt5 UI code generator 5.15.0 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(626, 387) self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) self.verticalLayout.setObjectName("verticalLayout") self.SettingsTab = QtWidgets.QTabWidget(Dialog) self.SettingsTab.setTabPosition(QtWidgets.QTabWidget.North) self.SettingsTab.setTabShape(QtWidgets.QTabWidget.Rounded) self.SettingsTab.setObjectName("SettingsTab") self.tab_2 = QtWidgets.QWidget() self.tab_2.setObjectName("tab_2") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.tab_2) self.verticalLayout_3.setObjectName("verticalLayout_3") self.label_help_launcher_whitelist = QtWidgets.QLabel(self.tab_2) self.label_help_launcher_whitelist.setWordWrap(True) self.label_help_launcher_whitelist.setObjectName("label_help_launcher_whitelist") self.verticalLayout_3.addWidget(self.label_help_launcher_whitelist) self.launcherWhitelistPlainTextEdit = QtWidgets.QPlainTextEdit(self.tab_2) self.launcherWhitelistPlainTextEdit.setObjectName("launcherWhitelistPlainTextEdit") self.verticalLayout_3.addWidget(self.launcherWhitelistPlainTextEdit) self.label_help_launcher_blacklist = QtWidgets.QLabel(self.tab_2) self.label_help_launcher_blacklist.setWordWrap(True) self.label_help_launcher_blacklist.setObjectName("label_help_launcher_blacklist") self.verticalLayout_3.addWidget(self.label_help_launcher_blacklist) self.launcherBlacklistPlainTextEdit = QtWidgets.QPlainTextEdit(self.tab_2) self.launcherBlacklistPlainTextEdit.setObjectName("launcherBlacklistPlainTextEdit") self.verticalLayout_3.addWidget(self.launcherBlacklistPlainTextEdit) self.SettingsTab.addTab(self.tab_2, "") self.tab = QtWidgets.QWidget() self.tab.setObjectName("tab") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab) self.verticalLayout_2.setObjectName("verticalLayout_2") self.label_help_programstart = QtWidgets.QLabel(self.tab) self.label_help_programstart.setWordWrap(True) self.label_help_programstart.setObjectName("label_help_programstart") self.verticalLayout_2.addWidget(self.label_help_programstart) self.label_help_path_rules = QtWidgets.QLabel(self.tab) self.label_help_path_rules.setWordWrap(True) self.label_help_path_rules.setObjectName("label_help_path_rules") self.verticalLayout_2.addWidget(self.label_help_path_rules) self.programPathsPlainTextEdit = QtWidgets.QPlainTextEdit(self.tab) self.programPathsPlainTextEdit.setObjectName("programPathsPlainTextEdit") self.verticalLayout_2.addWidget(self.programPathsPlainTextEdit) self.SettingsTab.addTab(self.tab, "") self.verticalLayout.addWidget(self.SettingsTab) self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.buttonBox.raise_() self.SettingsTab.raise_() self.retranslateUi(Dialog) self.SettingsTab.setCurrentIndex(0) self.buttonBox.accepted.connect(Dialog.accept) self.buttonBox.rejected.connect(Dialog.reject) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Settings")) self.label_help_launcher_whitelist.setText(_translate("Dialog", "Whitelist - Add executable names (not paths) to the program launcher. One executable per line.")) self.label_help_launcher_blacklist.setText(_translate("Dialog", "Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line.")) self.SettingsTab.setTabText(self.SettingsTab.indexOf(self.tab_2), _translate("Dialog", "Launcher")) self.label_help_programstart.setText(_translate("Dialog", "For advanced users only! Add executable paths to the environment, just for Agordejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab.")) self.label_help_path_rules.setText(_translate("Dialog", "Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don\'t matter.")) self.SettingsTab.setTabText(self.SettingsTab.indexOf(self.tab), _translate("Dialog", "$PATH")) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/designer/settings.ui0000644000175000017500000001013600000000000017133 0ustar00nilsnils Dialog 0 0 626 387 Settings QTabWidget::North QTabWidget::Rounded 0 Launcher Whitelist - Add executable names (not paths) to the program launcher. One executable per line. true Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line. true $PATH For advanced users only! Add executable paths to the environment, just for Agordejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab. true Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don't matter. true Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox SettingsTab buttonBox accepted() Dialog accept() 248 254 157 274 buttonBox rejected() Dialog reject() 316 260 286 274 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/designer/usermanual.py0000644000175000017500000000507500000000000017470 0ustar00nilsnils# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'usermanual.ui' # # Created by: PyQt5 UI code generator 5.11.3 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets class Ui_TemplateUserManual(object): def setupUi(self, TemplateUserManual): TemplateUserManual.setObjectName("TemplateUserManual") TemplateUserManual.resize(776, 733) self.verticalLayout = QtWidgets.QVBoxLayout(TemplateUserManual) self.verticalLayout.setObjectName("verticalLayout") self.widget = QtWidgets.QWidget(TemplateUserManual) self.widget.setObjectName("widget") self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget) self.horizontalLayout.setObjectName("horizontalLayout") self.home = QtWidgets.QPushButton(self.widget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.home.sizePolicy().hasHeightForWidth()) self.home.setSizePolicy(sizePolicy) self.home.setFlat(False) self.home.setObjectName("home") self.horizontalLayout.addWidget(self.home) self.back = QtWidgets.QPushButton(self.widget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.back.sizePolicy().hasHeightForWidth()) self.back.setSizePolicy(sizePolicy) self.back.setShortcut("Backspace") self.back.setObjectName("back") self.horizontalLayout.addWidget(self.back) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.verticalLayout.addWidget(self.widget) self.textBrowser = QtWidgets.QTextBrowser(TemplateUserManual) self.textBrowser.setObjectName("textBrowser") self.verticalLayout.addWidget(self.textBrowser) self.retranslateUi(TemplateUserManual) QtCore.QMetaObject.connectSlotsByName(TemplateUserManual) def retranslateUi(self, TemplateUserManual): _translate = QtCore.QCoreApplication.translate TemplateUserManual.setWindowTitle(_translate("TemplateUserManual", "Form")) self.home.setText(_translate("TemplateUserManual", "Home")) self.back.setText(_translate("TemplateUserManual", "Back")) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/designer/usermanual.ui0000644000175000017500000000371400000000000017453 0ustar00nilsnils TemplateUserManual 0 0 776 733 Form 0 0 Home false 0 0 Back Backspace Qt::Horizontal 40 20 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/eventloop.py0000644000175000017500000000553000000000000015523 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") from PyQt5 import QtCore class EventLoop(object): def __init__(self): """The loop for all things GUI and controlling the GUI (e.g. by a control midi in port) By default use fastConnect. 0 ms means "if there is time". 10ms-20ms is smooth. 100ms is still ok. Influences everything. Control Midi In Latency, playback cursor scrolling smoothnes etc. But not realtime. This is not the realtime loop. Converting midi into instrument sounds or playing back sequenced midi data is not handled by this loop at all. Creating a non-qt class for the loop is an abstraction layer that enables the engine to work without modification for non-gui situations. In this case it will use its own loop, like python async etc. A qt event loop needs the qt-app started. Otherwise it will not run. We init the event loop outside of main but call start from the mainWindow. """ self.fastLoop = QtCore.QTimer() self.slowLoop = QtCore.QTimer() def fastConnect(self, function): self.fastLoop.timeout.connect(function) def slowConnect(self, function): self.slowLoop.timeout.connect(function) def fastDisconnect(self, function): """The function must be the exact instance that was registered""" self.fastLoop.timeout.disconnect(function) def slowDisconnect(self, function): """The function must be the exact instance that was registered""" self.slowLoop.timeout.disconnect(function) def start(self): """The event loop MUST be started after the Qt Application instance creation""" logger.info("Starting fast qt event loop") self.fastLoop.start(20) logger.info("Starting slow qt event loop") self.slowLoop.start(200) def stop(self): logger.info("Stopping fast qt event loop") self.fastLoop.stop() logger.info("Stopping slow qt event loop") self.slowLoop.stop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/helper.py0000644000175000017500000002212100000000000014762 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of Laborejo ( https://www.laborejo.org ) This application 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 . """ from PyQt5 import QtCore, QtGui, QtWidgets from hashlib import md5 def iconFromString(st, size=128): px = QtGui.QPixmap(size,size) color = stringToColor(st) px.fill(color) return QtGui.QIcon(px) def sizeof_fmt(num, suffix='B'): """https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size""" for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 return "%.1f%s%s" % (num, 'Yi', suffix) def stringToColor(st): """Convert a string to QColor. Same string, same color Is used for group coloring""" if st: c = md5(st.encode()).hexdigest() return QtGui.QColor(int(c[0:9],16) % 255, int(c[10:19],16) % 255, int(c[20:29],16)% 255, 255) else: return QtGui.QColor(255,255,255,255) #Return White def invertColor(qcolor): r = 255 - qcolor.red() g = 255 - qcolor.green() b = 255 - qcolor.blue() return QtGui.QColor(r, g, b, qcolor.alpha()) def removeInstancesFromScene(qtGraphicsClass): """"Remove all instances of a qt class that implements .instances=[] from the QGraphicsScene. Don't use for items or anything in the notation view. This is used by the likes of the conductor only since they exist only once and gets redrawn completely each time.""" for instance in qtGraphicsClass.instances: instance.setParentItem(None) instance.scene().removeWhenIdle(instance) qtGraphicsClass.instances = [] def callContextMenu(listOfLabelsAndFunctions): menu = QtWidgets.QMenu() for text, function in listOfLabelsAndFunctions: if text == "separator": menu.addSeparator() else: a = QtWidgets.QAction(text, menu) menu.addAction(a) a.triggered.connect(function) pos = QtGui.QCursor.pos() pos.setY(pos.y() + 5) menu.exec_(pos) def stretchLine(qGraphicsLineItem, factor): line = qGraphicsLineItem.line() line.setLength(line.length() * factor) qGraphicsLineItem.setLine(line) def stretchRect(qGraphicsRectItem, factor): r = qGraphicsRectItem.rect() r.setRight(r.right() * factor) qGraphicsRectItem.setRect(r) class QHLine(QtWidgets.QFrame): def __init__(self): super().__init__() self.setFrameShape(QtWidgets.QFrame.HLine) self.setFrameShadow(QtWidgets.QFrame.Sunken) def setPaletteAndFont(qtApp): """Set our programs color This is in its own function because it is a annoying to scroll by that in init. http://doc.qt.io/qt-5/qpalette.html""" fPalBlue = QtGui.QPalette() fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Window, QtGui.QColor(32, 35, 39)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Window, QtGui.QColor(37, 40, 45)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Window, QtGui.QColor(37, 40, 45)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtGui.QColor(89, 95, 104)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.WindowText, QtGui.QColor(223, 237, 255)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, QtGui.QColor(223, 237, 255)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Base, QtGui.QColor(48, 53, 60)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, QtGui.QColor(55, 61, 69)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Base, QtGui.QColor(55, 61, 69)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, QtGui.QColor(60, 64, 67)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, QtGui.QColor(69, 73, 77)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, QtGui.QColor(69, 73, 77)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, QtGui.QColor(182, 193, 208)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, QtGui.QColor(182, 193, 208)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, QtGui.QColor(182, 193, 208)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, QtGui.QColor(42, 44, 48)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, QtGui.QColor(42, 44, 48)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, QtGui.QColor(42, 44, 48)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(96, 103, 113)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, QtGui.QColor(210, 222, 240)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Text, QtGui.QColor(210, 222, 240)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Button, QtGui.QColor(51, 55, 62)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Button, QtGui.QColor(59, 63, 71)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Button, QtGui.QColor(59, 63, 71)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtGui.QColor(98, 104, 114)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, QtGui.QColor(210, 222, 240)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, QtGui.QColor(210, 222, 240)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, QtGui.QColor(255, 255, 255)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.BrightText, QtGui.QColor(255, 255, 255)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, QtGui.QColor(255, 255, 255)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Light, QtGui.QColor(59, 64, 72)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Light, QtGui.QColor(63, 68, 76)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Light, QtGui.QColor(63, 68, 76)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, QtGui.QColor(48, 52, 59)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Midlight, QtGui.QColor(51, 56, 63)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, QtGui.QColor(51, 56, 63)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, QtGui.QColor(18, 19, 22)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Dark, QtGui.QColor(20, 22, 25)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, QtGui.QColor(20, 22, 25)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, QtGui.QColor(28, 30, 34)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Mid, QtGui.QColor(32, 35, 39)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, QtGui.QColor(32, 35, 39)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, QtGui.QColor(13, 14, 16)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Shadow, QtGui.QColor(15, 16, 18)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, QtGui.QColor(15, 16, 18)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Highlight, QtGui.QColor(32, 35, 39)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Highlight, QtGui.QColor(14, 14, 17)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight, QtGui.QColor(27, 28, 33)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, QtGui.QColor(89, 95, 104)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText, QtGui.QColor(217, 234, 253)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText, QtGui.QColor(223, 237, 255)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Link, QtGui.QColor(79, 100, 118)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Link, QtGui.QColor(156, 212, 255)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Link, QtGui.QColor(156, 212, 255)) fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.LinkVisited, QtGui.QColor(51, 74, 118)) fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.LinkVisited, QtGui.QColor(64, 128, 255)) fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.LinkVisited, QtGui.QColor(64, 128, 255)) qtApp.setPalette(fPalBlue) font = QtGui.QFont("DejaVu Sans", 10) font.setPixelSize(12) qtApp.setFont(font) return fPalBlue ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/jacktransport.py0000644000175000017500000001315600000000000016400 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library from datetime import timedelta #Third Party from PyQt5 import QtCore, QtGui, QtWidgets #Engine import engine.api as api class JackTransportControls(object): """Singleton object for code-structure reasons""" def __init__(self, mainWindow): self.mainWindow = mainWindow self.ui = self.mainWindow.ui self.visible = None self.currentMaximumInSeconds = self.ui.jackTransportMaxTime.value() * 60 #widget is in minutes self.resolutionFactor = 10 #Around 10 seems to be the maximum. The progressbar will still "Jump" above that. self.ui.jackTransportPlayPause.setCheckable(True) self.ui.jackTransportPlayPause.setText("") self.ui.jackTransportPlayPause.setIcon(QtGui.QIcon(':playpause.png')) self.ui.jackTransportRewind.setText("") self.ui.jackTransportRewind.setIcon(QtGui.QIcon(':tostart.png')) api.callbacks.sessionOpenReady.append(self.show) api.callbacks.sessionClosed.append(self.hide) api.callbacks.setPlaybackSeconds.append(self._react_playbackSeconds) api.callbacks.dataClientTimelineMaximumDurationChanged.append(self._callback_MaximumChanged) self.ui.jackTransportMaxTime.valueChanged.connect(self._gui_MaximumChanged) self.ui.jackTransportPlayPause.clicked.connect(api.jackClient.playPause) self.ui.jackTransportRewind.clicked.connect(api.jackClient.rewind) #Maximum Resolution of 100 is not enough. It is too "steppy" self.ui.jackTransportTimeline.setMaximum(100 * self.resolutionFactor) self.ui.jackTransportTimeline.reset() self.ui.jackTransportTimeline.mousePressEvent = self._timelineMousePressEvent self.ui.jackTransportTimeline.mouseMoveEvent = self._timelineMouseMoveEvent self.hide() def hide(self, *args): self.ui.jackTransportControls.hide() self.visible = False def show(self, *args): self.ui.jackTransportControls.show() self.visible = True def _gui_MaximumChanged(self, minutes:int): """Send to the api. Our own widget will get changed via callback.""" api.setTimelineMaximumDuration(minutes) def _callback_MaximumChanged(self, minutes:int): """Get a new value from the api. This is either from loading or a roundtrip from our own widget change. Can be None if nsm-data left the session. In this case we just ignore and continue.""" if minutes: #0 is not possible. May be None. self.currentMaximumInSeconds = minutes * 60 self.ui.jackTransportMaxTime.blockSignals(True) self.ui.jackTransportMaxTime.setValue(minutes) self.ui.jackTransportMaxTime.blockSignals(False) def _react_playbackSeconds(self, seconds, isTransportRunning): if self.visible: #optimisation realPercent = seconds / self.currentMaximumInSeconds progressPercent = realPercent * 100 * self.resolutionFactor #100 because that is 0.xy -> xy% ) progressPercent = int(progressPercent) prettyTime = str(timedelta(seconds=int(seconds))) #timedelta without int will print microseconds if self.currentMaximumInSeconds < 3600: #less than an hour prettyTime = prettyTime[2:] if progressPercent <= 100 * self.resolutionFactor: self.ui.jackTransportTimeline.setValue(progressPercent) else: self.ui.jackTransportTimeline.setValue(100 * self.resolutionFactor) if isTransportRunning: self.ui.jackTransportPlayPause.setChecked(True) self.ui.jackTransportTimeline.setFormat("▶ " + prettyTime) #we don't use the Qt format substitutions. We just set a fixed value. else: self.ui.jackTransportPlayPause.setChecked(False) self.ui.jackTransportTimeline.setFormat("■ " + prettyTime) def _timelineMousePressEvent(self, event): """The positions are in pixels. If the window is different we get different numbers. Normalize first.""" percentage = event.x() / self.ui.jackTransportTimeline.width() #x is relative to the widget. same as localPos().x() seconds = percentage * self.currentMaximumInSeconds api.jackClient.seek(seconds) def _timelineMouseMoveEvent(self, event): """We did not set any qt-flags on this widget so mouseMoveEvent only works when the left mouse button is down, and not just simply by hovering. Which is exactly what we want: dragging""" percentage = event.x() / self.ui.jackTransportTimeline.width() #x is relative to the widget. same as localPos().x() seconds = percentage * self.currentMaximumInSeconds api.jackClient.seek(seconds) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/mainwindow.py0000644000175000017500000006370400000000000015673 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library from sys import argv as sysargv from sys import exit as sysexit #Third Party from PyQt5 import QtCore, QtGui, QtWidgets logger.info(f"PyQt Version: {QtCore.PYQT_VERSION_STR}") #Engine from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. from engine.start import PATHS, qtApp import engine.api as api #This loads the engine and starts a session. #Qt from .systemtray import SystemTray from .eventloop import EventLoop from .designer.mainwindow import Ui_MainWindow from .usermanual import UserManual from .changelog import Changelog from .helper import setPaletteAndFont from .helper import iconFromString from .sessiontreecontroller import SessionTreeController from .opensessioncontroller import OpenSessionController from .projectname import ProjectNameWidget from .addclientprompt import askForExecutable, updateWordlist from .waitdialog import WaitDialog from .resources import * from .settings import SettingsDialog from .jacktransport import JackTransportControls api.eventLoop = EventLoop() #Setup the translator before classes are set up. Otherwise we can't use non-template translation. #to test use LANGUAGE=de_DE.UTF-8 ./agordejo language = QtCore.QLocale().languageToString(QtCore.QLocale().language()) logger.info("{}: Language set to {}".format(METADATA["name"], language)) if language in METADATA["supportedLanguages"]: translator = QtCore.QTranslator() translator.load(METADATA["supportedLanguages"][language], ":/translations/") #colon to make it a resource URL qtApp.installTranslator(translator) else: """silently fall back to English by doing nothing""" def nothing(*args): pass class RecentlyOpenedSessions(object): """Class to make it easier handle recently opened session with qt settings, type conversions limiting the size of the list and uniqueness""" def __init__(self): self.data = [] def load(self, dataFromQtSettings): """Handle qt settings load. triggered by restoreWindowSettings in mainWindow init""" if dataFromQtSettings: for name in dataFromQtSettings: self.add(name) def add(self, nsmSessionName:str): if nsmSessionName in self.data: #Just sort self.data.remove(nsmSessionName) self.data.append(nsmSessionName) return self.data.append(nsmSessionName) if len(self.data) > 3: self.data.pop(0) assert len(self.data) <= 3, len(self.data) def get(self)->list: """List of nsmSessionName strings""" sessionList = api.sessionList() self.data = [n for n in self.data if n in sessionList] return self.data def last(self)->str: """Return the last active session. Useful for continue-mode command line arg. """ if self.data: return self.get()[-1] else: return None class MainWindow(QtWidgets.QMainWindow): def __init__(self): super().__init__() self.qtApp = qtApp self.qtApp.setWindowIcon(QtGui.QIcon(":icon.png")) #non-template part of the program self.qtApp.setApplicationName(f"{METADATA['name']}") self.qtApp.setApplicationDisplayName(f"{METADATA['name']}") self.qtApp.setOrganizationName("Laborejo Software Suite") self.qtApp.setOrganizationDomain("laborejo.org") self.qtApp.setApplicationVersion(METADATA["version"]) QtGui.QIcon.setThemeName("hicolor") #audio applications can be found here. We have no need for other icons. logger.info("Init MainWindow") #QtGui.QIcon.setFallbackThemeName("hicolor") #only one, not a list. This is the fallback if the theme can't be found. Not if icons can't be found in a theme. #iconPaths = QtGui.QIcon.themeSearchPaths() #iconPaths += ["/usr/share/icons/hicolor", "/usr/share/pixmaps"] #QtGui.QIcon.setThemeSearchPaths(iconPaths) logger.info(f"Program icons path: {QtGui.QIcon.themeSearchPaths()}, {QtGui.QIcon.themeName()}") QtCore.QT_TRANSLATE_NOOP("NOOPEngineStrings", "ERROR! Copied session data is different from source session. Please check you data!") QtCore.QT_TRANSLATE_NOOP("NOOPEngineStrings", "Veryfying file-integrity. This may take a while...") #Set up the user interface from Designer and other widgets self.ui = Ui_MainWindow() self.ui.setupUi(self) self.fPalBlue = setPaletteAndFont(self.qtApp) assert self.ui.tabbyCat.currentIndex() == 0, self.ui.tabbyCat.currentIndex() # this is critical. If you left the Qt Designer with the wrong tab open this is the error that happens. It will trigger the tab changed later that will go wrong because setup is not complete yet and you'll get AttributeError self.userManual = UserManual(mainWindow=self) self.changelog = Changelog(mainWindow=self) self.ui.mainPageSwitcher.setCurrentIndex(0) #1 is messageLabel 0 is the tab widget SettingsDialog.loadFromSettingsAndSendToEngine() #set blacklist, whitelist for programdatabase and addtional executable paths for environment #TODO: Hide information tab until the feature is ready self.ui.tabbyCat.removeTab(1) self.programIcons = {} #executableName:QIcon. Filled by self._updateGUIWithCachedPrograms which calls _updateIcons self.sessionController = SessionController(mainWindow=self) self.systemTray = SystemTray(mainWindow=self) self.connectMenu() self.recentlyOpenedSessions = RecentlyOpenedSessions() #Setup JackTransportControls Widget. It configures itself on init: self.jackTransportControls = JackTransportControls(mainWindow = self) #not a widget, just an object #self.ui.stack_loaded_session is only visible when there is a loaded session and the full view tab is active #we link the session context menu to the session menu menu. self.ui.stack_loaded_session.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.ui.stack_loaded_session.customContextMenuRequested.connect(self.customContextMenu) #Api Callbacks api.callbacks.sessionClosed.append(self.reactCallback_sessionClosed) api.callbacks.sessionOpenReady.append(self.reactCallback_sessionOpen) api.callbacks.singleInstanceActivateWindow.append(self.activateAndRaise) #Handle the application data cache. #This must happen before engineStart. If a session is already running a set of initial #client-callbacks will arrive immediately, even before the eventLoop starts. #If not present instruct the engine to build one. #This is also needed by the prompt in sessionController and the icons logger.info("Trying to restore cached program database") settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) if settings.contains("engineCache"): engineCache = settings.value("engineCache", type=dict) api.setCache(engineCache) logger.info("Restored program database from qt cache to engine") self._updateGUIWithCachedPrograms() else: #First or fresh start #A single shot timer with 0 durations is executed only after the app starts, thus the main window is ready. logger.info("First run. Instructing engine to build program database") QtCore.QTimer.singleShot(0, self.updateProgramDatabase) #includes self._updateGUIWithCachedPrograms() #Starting the engine sends initial GUI data. Every window and widget must be ready to receive callbacks here api.eventLoop.start() api.startEngine() self.restoreWindowSettings() #populates recentlyOpenedSessions if PATHS["startHidden"] and self.systemTray.available: logger.info("Starting hidden") self.toggleVisible(force=False) else: logger.info("Starting visible") self.toggleVisible(force=True) if PATHS["continueLastSession"]: #will be None if --load-session=NAME was given as command line parameter and --continue on top. continueSession = self.recentlyOpenedSessions.last() if continueSession: logger.info(f"Got continue session as command line parameter. Opening: {continueSession}") api.sessionOpen(continueSession) else: logger.info(f"Got continue session as command line parameter but there is no session available.") if not self.isVisible(): text = QtCore.QCoreApplication.translate("mainWindow", "Agordejo ready") self.systemTray.showMessage("Agordejo", text, QtWidgets.QSystemTrayIcon.Information, 2000) #title, message, icon, timeout. #has messageClicked() signal. logger.info("Ready for user input. Exec_ Qt.") qtApp.exec_() #No code after exec_ except atexit def tabtest(self): import subprocess from time import sleep #xdotool search --name xeyes #xdotool search --pid 12345 subprocess.Popen(["patchage"], shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) #parameters are for not waiting sleep(1) result = subprocess.run(["xdotool", "search", "--name", "patchage"], stdout=subprocess.PIPE).stdout.decode('utf-8') if "\n" in result: windowID = int(result.split("\n")[0]) else: windowID = int(result) window = QtGui.QWindow.fromWinId(int(windowID)) window.setFlags(QtCore.Qt.FramelessWindowHint) widget = QtWidgets.QWidget.createWindowContainer(window) self.ui.tabbyCat.addTab(widget, "Patchage") def hideEvent(self, event): if self.systemTray.available: super().hideEvent(event) else: event.ignore() def activateAndRaise(self): self.toggleVisible(force=True) getattr(self, "raise")() #raise is python syntax. Can't use that directly self.activateWindow() text = QtCore.QCoreApplication.translate("mainWindow", "Another GUI tried to launch.") self.systemTray.showMessage("Agordejo", text, QtWidgets.QSystemTrayIcon.Information, 2000) #title, message, icon, timeout. #has messageClicked() signal. def _updateGUIWithCachedPrograms(self): logger.info("Updating entire program with cached program lists") updateWordlist() #addclientprompt.py self._updateIcons() self.sessionController.openSessionController.launcherTable.buildPrograms() def _updateIcons(self): logger.info("Creating icon database") engineCache = api.getCache() assert engineCache programs = engineCache["programs"] self.programIcons.clear() for entry in programs: exe = entry["agordejoExec"] icon = None if "agordejoIconPath" in entry and entry["agordejoIconPath"]: #not null icon = QtGui.QIcon(entry["agordejoIconPath"]) if not icon or icon.isNull(): #the DB cache could be wrong. Deinstalled a program and not updated the DB. if "icon" in entry: icon = QtGui.QIcon.fromTheme(entry["icon"]) else: icon = QtGui.QIcon.fromTheme(exe) if icon.isNull(): icon = QtGui.QIcon.fromTheme(exe) if icon.isNull(): icon = iconFromString(exe) assert not icon.isNull() self.programIcons[exe] = icon def updateProgramDatabase(self): """Display a progress-dialog that waits for the database to be build. Automatically called on first start or when instructed by the user""" text = QtCore.QCoreApplication.translate("mainWindow", "Updating Program Database.\nThank you for your patience.\nIf progress freezes please kill and restart the whole program.") settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) settings.remove("engineCache") logger.info("Asking api to generate program and icon database while waiting") diag = WaitDialog(self, text, api.buildSystemPrograms) #save in local var to keep alive assert api.getCache() settings.setValue("engineCache", api.getCache()) # dict self._updateGUIWithCachedPrograms() def reactCallback_sessionClosed(self): self.setWindowTitle("") def reactCallback_sessionOpen(self, nsmSessionExportDict): self.setWindowTitle(nsmSessionExportDict["nsmSessionName"]) self.recentlyOpenedSessions.add(nsmSessionExportDict["nsmSessionName"]) def toggleVisible(self, force:bool=None): if force is None: newState = not self.isVisible() else: newState = force if newState: logger.info("Show") self.restoreWindowSettings() self.show() self.setVisible(True) else: logger.info("Hide") self.storeWindowSettings() self.hide() self.setVisible(False) #self.systemTray.buildContextMenu() #Don't. This crashes Qt through some delayed execution of who knows what. Workaround: tray context menu now say "Show/Hide" and not only the actual state. def toggleSplitSessionView(self, state:bool): self.ui.actionSplit_Session_View_The_Other_Way.blockSignals(True) if state: orientation = QtCore.Qt.Vertical #this is weird. Is the Qt enum wrongly labeled? This splits in an upper and lower part. else: orientation = QtCore.Qt.Horizontal self.ui.actionSplit_Session_View_The_Other_Way.setChecked(state) #This funtion can be called by functions, e.g. restoreWindowSettings on startup. self.ui.hSplitterLauncherClients.setOrientation(orientation) self.ui.actionSplit_Session_View_The_Other_Way.blockSignals(False) def _askBeforeQuit(self, nsmSessionName): """If you quit while in a session ask what to do. The TrayIcon context menu uses different functions and directly acts, without a question""" text = QtCore.QCoreApplication.translate("AskBeforeQuit", "About to quit but session {} still open").format(nsmSessionName) informativeText = QtCore.QCoreApplication.translate("AskBeforeQuit", "Do you want to save?") title = QtCore.QCoreApplication.translate("AskBeforeQuit", "About to quit") box = QtWidgets.QMessageBox() box.setWindowFlag(QtCore.Qt.Popup, True) box.setIcon(box.Warning) box.setText(text) box.setWindowTitle(title) box.setInformativeText(informativeText) stay = box.addButton(QtCore.QCoreApplication.translate("AskBeforeQuit", "Don't Quit"), box.RejectRole) #0 box.addButton(QtCore.QCoreApplication.translate("AskBeforeQuit", "Save"), box.YesRole) #1 box.addButton(QtCore.QCoreApplication.translate("AskBeforeQuit", "Discard Changes"), box.DestructiveRole) #2 box.setDefaultButton(stay) ret = box.exec() #Return values are NOT the button roles. if ret == 2: logger.info("Quit: Don't save.") api.sessionAbort(blocking=True) return True elif ret == 1: logger.info("Quit: Close and Save. Waiting for clients to close.") api.sessionClose(blocking=True) return True else: #Escape, window close through WM etc. logger.info("Quit: Changed your mind, stay in session.") return False def abortAndQuit(self): """For the context menu. A bit A bit redundant, but that is ok :)""" api.sessionAbort(blocking=True) self._callSysExit() def closeAndQuit(self): api.sessionClose(blocking=True) self._callSysExit() def _callSysExit(self): """The process of quitting After sysexit the atexit handler gets called. That closes nsmd, if we started ourselves. """ self.storeWindowSettings() sysexit(0) #directly afterwards @atexit is handled, but this function does not return. logging.error("Code executed after sysexit. This message should not have been visible.") def menuRealQuit(self): """Called by the menu. The TrayIcon provides another method of quitting that does not call this function, but it will call _actualQuit. """ if api.ourOwnServer() and api.currentSession(): result = self._askBeforeQuit(api.currentSession()) else: result = True if result: self.storeWindowSettings() self._callSysExit() def closeEvent(self, event): """Window manager close. Ignore. We use it to send the GUI into hiding.""" event.ignore() self.toggleVisible(force=False) def connectMenu(self): #Control self.ui.actionManual.triggered.connect(self.userManual.show) self.ui.actionChangelog.triggered.connect(self.changelog.show) self.ui.actionRebuild_Program_Database.triggered.connect(self.updateProgramDatabase) self.ui.actionSettings.triggered.connect(self._reactMenu_settings) self.ui.actionSplit_Session_View_The_Other_Way.toggled.connect(self.toggleSplitSessionView) self.ui.actionHide_in_System_Tray.triggered.connect(lambda: self.toggleVisible(force=False)) self.ui.actionMenuQuit.triggered.connect(self.menuRealQuit) def _reactMenu_settings(self): widget = SettingsDialog(self) #blocks until closed if widget.success: self.updateProgramDatabase() def customContextMenu(self, qpoint): pos = QtGui.QCursor.pos() pos.setY(pos.y() + 5) self.ui.menuSession.exec_(pos) def storeWindowSettings(self): """Window state is not saved in the real save file. That would lead to portability problems between computers, like different screens and resolutions. For convenience that means we just use the damned qt settings and save wherever qt wants. bottom line: get a tiling window manager. """ settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) settings.setValue("geometry", self.saveGeometry()) settings.setValue("windowState", self.saveState()) #settings.setValue("visible", self.isVisible()) Deprecated. see restoreWindowSettings settings.setValue("recentlyOpenedSessions", self.recentlyOpenedSessions.get()) settings.setValue("tab", self.ui.tabbyCat.currentIndex()) settings.setValue("sessionSplitOrientationNonDefault", self.ui.actionSplit_Session_View_The_Other_Way.isChecked()) def restoreWindowSettings(self): """opposite of storeWindowSettings. Read there.""" logger.info("Restoring window settings, geometry and recently opened session") settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) actions = { "geometry":self.restoreGeometry, "windowState":self.restoreState, "recentlyOpenedSessions":self.recentlyOpenedSessions.load, "tab": lambda i: self.ui.tabbyCat.setCurrentIndex(int(i)), "sessionSplitOrientationNonDefault": lambda b: self.toggleSplitSessionView(bool(b)), } types = { "recentlyOpenedSessions": list, "tab": int, "sessionSplitOrientationNonDefault": bool, } for key in settings.allKeys(): if key in actions: #if not it doesn't matter. this is all uncritical. if key in types: actions[key](settings.value(key, type=types[key])) else: actions[key](settings.value(key)) #Deprecated. Always open the GUI when started normally, saving minimzed has little value. #Instead we introduced a command line options and .desktop option to auto-load the last session and start Agordejo GUI hidden. """ if self.systemTray.available and settings.contains("visible") and settings.value("visible") == "false": self.setVisible(False) else: self.setVisible(True) #This is also the default state if there is no config """ class SessionController(object): """Controls the StackWidget that contains the Session Tree and Opened Session/Client. Can be controlled from up and down the hierarchy. """ def __init__(self, mainWindow): super().__init__() self.mainWindow = mainWindow self.ui = self.mainWindow.ui self.sessionTreeController = SessionTreeController(mainWindow=mainWindow) self.openSessionController = OpenSessionController(mainWindow=mainWindow) self._connectMenu() #Callbacks #api.callbacks.sessionOpenReady.append(lambda nsmSessionExportDict: self._switch("open")) #When loading ist done. This takes a while when non-nsm clients are in the session api.callbacks.sessionClosed.append(lambda: self._setMenuEnabled(None)) api.callbacks.sessionClosed.append(lambda: self._switch("choose")) #The rest is handled by the widget itself. It keeps itself updated, no matter if visible or not. api.callbacks.sessionOpenReady.append(lambda nsmSessionExportDict: self._setMenuEnabled(nsmSessionExportDict)) api.callbacks.sessionOpenLoading.append(lambda nsmSessionExportDict: self._switch("open")) #Convenience Signals to directly disable the client messages on gui instruction. #This is purely for speed and preventing the user from sending a signal while the session is shutting down self.mainWindow.ui.actionSessionAbort.triggered.connect(lambda: self._setMenuEnabled(None)) self.mainWindow.ui.actionSessionSaveAndClose.triggered.connect(lambda: self._setMenuEnabled(None)) #GUI signals self.mainWindow.ui.tabbyCat.currentChanged.connect(self._activeTabChanged) #self._activeTabChanged(self.mainWindow.ui.tabbyCat.currentIndex()) def _activeTabChanged(self, index:int): """index 0 is open session, 1 is info etc""" if index == 0: #detailed state = bool(api.currentSession()) else: #quick and information and future tabs state = False self.ui.menuClientNameId.menuAction().setVisible(state) #already deactivated self.ui.menuSession.menuAction().setVisible(state) #already deactivated #It is not enough to disable the menu itself. Shortcuts will still work. We need the children! for action in self.ui.menuSession.actions(): action.setEnabled(state) if state and not self.openSessionController.clientTabe.clientsTreeWidget.currentItem(): state = False #we wanted to activate, but there is no client selected. for action in self.ui.menuClientNameId.actions(): action.setEnabled(state) def _connectMenu(self): #Session #Only Active when a session is currently available self.ui.actionSessionSave.triggered.connect(api.sessionSave) self.ui.actionSessionAbort.triggered.connect(api.sessionAbort) self.ui.actionSessionSaveAs.triggered.connect(self._reactMenu_SaveAs) #NSM "Duplicate" self.ui.actionSessionSaveAndClose.triggered.connect(api.sessionClose) self.ui.actionShow_All_Clients.triggered.connect(api.clientShowAll) self.ui.actionHide_All_Clients.triggered.connect(api.clientHideAll) self.ui.actionSessionAddClient.triggered.connect(lambda: askForExecutable(self.mainWindow)) #Prompt version def _reactMenu_SaveAs(self): """Only when a session is open. We could either check the session controller or the simple one for the name.""" currentName = api.currentSession() assert currentName widget = ProjectNameWidget(parent=self.mainWindow, startwith=currentName+"-new") if widget.result: api.sessionSaveAs(widget.result) def _setMenuEnabled(self, nsmSessionExportDictOrNone): """We receive the sessionDict or None""" state = bool(nsmSessionExportDictOrNone) if state: self.ui.menuSession.setTitle(nsmSessionExportDictOrNone["nsmSessionName"]) self.ui.menuSession.menuAction().setVisible(True) self.ui.menuClientNameId.menuAction().setVisible(True) #session controller might disable that else: self.ui.menuSession.setTitle("Session") self.ui.menuSession.menuAction().setVisible(False) self.ui.menuClientNameId.menuAction().setVisible(False) #already deactivated #self.ui.menuSession.setEnabled(state) #It is not enough to disable the menu itself. Shortcuts will still work. for action in self.ui.menuSession.actions(): action.setEnabled(state) #Maybe the tab state overrules everything self._activeTabChanged(self.mainWindow.ui.tabbyCat.currentIndex()) def _switch(self, page:str): """Only called by the sub-controllers. For example when an existing session gets opened""" if page == "choose": pageIndex = 0 elif page == "open": pageIndex = 1 else: raise ValueError(f"_switch accepts choose or open, not {page}") self.mainWindow.ui.detailedStackedWidget.setCurrentIndex(pageIndex) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/opensessioncontroller.py0000644000175000017500000006062500000000000020167 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library #Third Party from PyQt5 import QtCore, QtGui, QtWidgets #Engine import engine.api as api import engine.findicons as findicons #Qt from .descriptiontextwidget import DescriptionController from .helper import iconFromString iconSize = QtCore.QSize(16,16) class ClientItem(QtWidgets.QTreeWidgetItem): """ Item on the right side. Clients of the session, in various states. clientDict = { "clientId":clientId, #for convenience, included internally as well "dumbClient":True, #Bool. Real nsm or just any old program? status "Ready" switches this. "reportedName":None, #str "label":None, #str "lastStatus":None, #str "statusHistory":[], #list "hasOptionalGUI": False, #bool "visible": None, # bool "dirty": None, # bool } """ allItems = {} # clientId : ClientItem def __init__(self, parentController, clientDict:dict): ClientItem.allItems[clientDict["clientId"]] = self self.parentController = parentController self.clientDict = clientDict parameterList = [] #later in update super().__init__(parameterList, type=1000) #type 0 is default qt type. 1000 is subclassed user type) self.defaultFlags = self.flags() self.setFlags(self.defaultFlags | QtCore.Qt.ItemIsEditable) #We have editTrigger to none so we can explicitly allow to only edit the name column on menuAction #self.treeWidget() not ready at this point self.updateData(clientDict) self.updateIcon(clientDict) def dataClientNameOverride(self, name:str): """Either string or None. If None we reset to nsmd name""" logger.info(f"Custom name for id {self.clientDict['clientId']} {self.clientDict['reportedName']}: {name}") if name: text = name else: text = self.clientDict["reportedName"] index = self.parentController.clientsTreeWidgetColumns.index("reportedName") self.setText(index, text) def updateData(self, clientDict:dict): """Arrives via parenTreeWidget api callback statusChanged, which is nsm status changed""" self.clientDict = clientDict for index, key in enumerate(self.parentController.clientsTreeWidgetColumns): if clientDict[key] is None: t = "" else: value = clientDict[key] if key == "visible": if value == True: t = "✔" else: t = "✖" elif key == "dirty": if value == True: t = QtCore.QCoreApplication.translate("OpenSession", "not saved") else: t = QtCore.QCoreApplication.translate("OpenSession", "clean") elif key == "reportedName" and self.parentController.clientOverrideNamesCache: if clientDict["clientId"] in self.parentController.clientOverrideNamesCache: t = self.parentController.clientOverrideNamesCache[clientDict["clientId"]] logger.info(f"Update Data: custom name for id {self.clientDict['clientId']} {self.clientDict['reportedName']}: {t}") else: t = str(value) else: t = str(value) self.setText(index, t) nameColumn = self.parentController.clientsTreeWidgetColumns.index("reportedName") if clientDict["reportedName"] is None: self.setText(nameColumn, clientDict["executable"]) def updateIcon(self, clientDict:dict): """Just called during init""" programIcons = self.parentController.mainWindow.programIcons assert programIcons assert "executable" in clientDict, clientDict iconColumn = self.parentController.clientsTreeWidgetColumns.index("reportedName") if clientDict["executable"] in programIcons: icon = programIcons[clientDict["executable"]] assert icon, icon self.setIcon(iconColumn, icon) #reported name is correct here. this is just the column. else: #Not NSM client added by the prompt widget result = findicons.findIconPath(clientDict["executable"]) if result: icon = QtGui.QIcon.fromTheme(str(result[0])) else: icon = QtGui.QIcon.fromTheme(clientDict["executable"]) if not icon.isNull(): self.setIcon(iconColumn, icon) else: self.setIcon(iconColumn, iconFromString(clientDict["executable"])) class ClientTable(object): """Controls the QTreeWidget that holds loaded clients""" def __init__(self, mainWindow, parent): self.mainWindow = mainWindow self.parent = parent self.clientOverrideNamesCache = None #None or dict. Dict is never truly empty, it has at least empty categories. self.sortByColumnValue = 0 #by name self.sortDescendingValue = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending self.clientsTreeWidget = self.mainWindow.ui.loadedSessionClients self.clientsTreeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.clientsTreeWidget.customContextMenuRequested.connect(self.clientsContextMenu) self.clientsTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.clientsTreeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.clientsTreeWidget.setIconSize(iconSize) self.clientsTreeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) #We only allow explicit editing. self.clientsTreeWidgetColumns = ("reportedName", "label", "lastStatus", "visible", "dirty", "clientId") #basically an enum self.clientHeaderLabels = [ QtCore.QCoreApplication.translate("OpenSession", "Name"), QtCore.QCoreApplication.translate("OpenSession", "Label"), QtCore.QCoreApplication.translate("OpenSession", "Status"), QtCore.QCoreApplication.translate("OpenSession", "Visible"), QtCore.QCoreApplication.translate("OpenSession", "Changes"), QtCore.QCoreApplication.translate("OpenSession", "ID"), ] self.clientsTreeWidget.setHeaderLabels(self.clientHeaderLabels) self.clientsTreeWidget.setSortingEnabled(True) self.clientsTreeWidget.setAlternatingRowColors(True) #Signals self.clientsTreeWidget.currentItemChanged.connect(self._reactSignal_currentClientChanged) self.clientsTreeWidget.itemDoubleClicked.connect(self._reactSignal_itemDoubleClicked) #This is hide/show or restart and NOT edit self.clientsTreeWidget.itemDelegate().closeEditor.connect(self._reactSignal_itemEditingFinished) self.clientsTreeWidget.model().layoutAboutToBeChanged.connect(self._reactSignal_rememberSorting) #self.clientsTreeWidget.model().layoutChanged.connect(self._reactSignal_restoreSorting) #Convenience Signals to directly disable the client messages on gui instruction. #This is purely for speed and preventing the user from sending a signal while the session is shutting down self.mainWindow.ui.actionSessionAbort.triggered.connect(lambda: self._updateClientMenu(deactivate=True)) self.mainWindow.ui.actionSessionSaveAndClose.triggered.connect(lambda: self._updateClientMenu(deactivate=True)) #API Callbacks api.callbacks.sessionOpenLoading.append(self._cleanClients) api.callbacks.sessionOpenReady.append(self._updateClientMenu) api.callbacks.sessionClosed.append(lambda: self._updateClientMenu(deactivate=True)) api.callbacks.clientStatusChanged.append(self._reactCallback_clientStatusChanged) api.callbacks.dataClientNamesChanged.append(self._reactCallback_dataClientNamesChanged) def _adjustColumnSize(self): self.clientsTreeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue) for index in range(self.clientsTreeWidget.columnCount()): self.clientsTreeWidget.resizeColumnToContents(index) def _cleanClients(self, nsmSessionExportDict:dict): """Reset everything to the initial, empty state. We do not reset in in openReady because that signifies that the session is ready. And not in session closed because we want to setup data structures.""" ClientItem.allItems.clear() self.clientsTreeWidget.clear() def allItems(self): return ClientItem.allItems def clientsContextMenu(self, qpoint): """Reuses the menubar menus""" pos = QtGui.QCursor.pos() pos.setY(pos.y() + 5) item = self.clientsTreeWidget.itemAt(qpoint) if not type(item) is ClientItem: self.mainWindow.ui.menuSession.exec_(pos) return if not item is self.clientsTreeWidget.currentItem(): #Some mouse combinations can lead to getting a different context menu than the clicked item. self.clientsTreeWidget.setCurrentItem(item) menu = self.mainWindow.ui.menuClientNameId menu.exec_(pos) def _startEditingName(self, *args): currentItem = self.clientsTreeWidget.currentItem() self.editableItem = currentItem column = self.clientsTreeWidgetColumns.index("reportedName") self.clientsTreeWidget.editItem(currentItem, column) def _reactSignal_itemEditingFinished(self, qLineEdit, returnCode): """This is a hacky signal. It arrives every change, programatically or manually. We therefore only connect this signal right after a double click and disconnect it afterwards. And we still need to block signals while this is running. returnCode: no clue? Integers all over the place... """ treeWidgetItem = self.editableItem self.editableItem = None self.clientsTreeWidget.blockSignals(True) if treeWidgetItem: #We send the signal directly. Updating is done via callback. newName = treeWidgetItem.text(0) if not newName == treeWidgetItem.clientDict["reportedName"]: api.clientNameOverride(treeWidgetItem.clientDict["clientId"], newName) self.clientsTreeWidget.blockSignals(False) def _reactSignal_currentClientChanged(self, treeWidgetItem, previousItem): """Cache the current id for the client menu and shortcuts""" if treeWidgetItem: self.currentClientId = treeWidgetItem.clientDict["clientId"] else: self.currentClientId = None self._updateClientMenu() def _reactSignal_itemDoubleClicked(self, item:QtWidgets.QTreeWidgetItem, column:int): if item.clientDict["lastStatus"] == "stopped": api.clientResume(item.clientDict["clientId"]) elif item.clientDict["hasOptionalGUI"]: api.clientToggleVisible(item.clientDict["clientId"]) def _reactCallback_clientStatusChanged(self, clientDict:dict): """The major client callback. Maps to nsmd status changes. We will create and delete client tableWidgetItems based on this """ assert clientDict clientId = clientDict["clientId"] if clientId in ClientItem.allItems: if clientDict["lastStatus"] == "removed": index = self.clientsTreeWidget.indexOfTopLevelItem(ClientItem.allItems[clientId]) self.clientsTreeWidget.takeTopLevelItem(index) del ClientItem.allItems[clientId] else: ClientItem.allItems[clientId].updateData(clientDict) self._updateClientMenu() #Update here is fine because shutdown sets to status removed. else: #Create new. Item will be parented by Qt, so Python GC will not delete item = ClientItem(parentController=self, clientDict=clientDict) self.clientsTreeWidget.addTopLevelItem(item) self._adjustColumnSize() #Do not put a general menuUpdate here. It will re-open the client menu during shutdown, enabling the user to send false commands to the client. def _reactCallback_dataClientNamesChanged(self, clientOverrideNames:dict): """We either expect a dict or None. If None we return after clearing the data. We clear every callback and re-build. The dict can be content-empty of course.""" logger.info(f"Received dataStorage names update: {clientOverrideNames}") #Clear current GUI data. for clientInstance in ClientItem.allItems.values(): clientInstance.dataClientNameOverride(None) if clientOverrideNames is None: #This only happens if there was a client present and that exits. self.clientOverrideNamesCache = None else: #Real data #assert "origin" in data, data . Not in a fresh session, after adding! #assert data["origin"] == "https://www.laborejo.org/agordejo/nsm-data", data["origin"] self.clientOverrideNamesCache = clientOverrideNames #Can be empty dict as well clients = ClientItem.allItems for clientId, name in clientOverrideNames.items(): #It is possible on session start, that a client has not yet loaded but we already receive a name override. nsm-data is instructed to only announce after session has loaded, but that can go wrong when nsmd has a bad day. #Long story short: better to not rename right now, have some name mismatch and wait for a general update later, which will happen after every client load anyway. if clientId in clients: clients[clientId].dataClientNameOverride(name) self._updateClientMenu() #Update because we need to en/disable the rename action self._adjustColumnSize() def _updateClientMenu(self, deactivate=False): """The client menu changes with every currentItem edit to reflect the name and capabilities""" ui = self.mainWindow.ui menu = ui.menuClientNameId if deactivate: currentItem = None else: currentItem = self.clientsTreeWidget.currentItem() if currentItem: clientId = currentItem.clientDict["clientId"] state = True #if currentItem.clientDict["label"]: # name = currentItem.clientDict["label"] #else: # name = currentItem.clientDict["reportedName"] name = currentItem.text(self.clientsTreeWidgetColumns.index("reportedName")) else: state = False name = "Client" menu.setTitle(name) #menu.setEnabled(state) #It is not enough to disable the menu itself. Shortcuts will still work. for action in menu.actions(): action.setEnabled(state) if state: ui.actionClientRename.triggered.disconnect() ui.actionClientRename.triggered.connect(self._startEditingName) #ui.actionClientRename.triggered.connect(lambda: self.clientsTreeWidget.editItem(currentItem, self.clientsTreeWidgetColumns.index("reportedName"))) ui.actionClientSave_separately.triggered.disconnect() ui.actionClientSave_separately.triggered.connect(lambda: api.clientSave(clientId)) ui.actionClientStop.triggered.disconnect() ui.actionClientStop.triggered.connect(lambda: api.clientStop(clientId)) #ui.actionClientStop.triggered.connect(self._updateClientMenu) #too early. We need to wait for the status callback ui.actionClientResume.triggered.disconnect() ui.actionClientResume.triggered.connect(lambda: api.clientResume(clientId)) #ui.actionClientResume.triggered.connect(self._updateClientMenu) #too early. We need to wait for the status callback ui.actionClientRemove.triggered.disconnect() ui.actionClientRemove.triggered.connect(lambda: api.clientRemove(clientId)) #Deactivate depending on the state of the program if currentItem.clientDict["lastStatus"] == "stopped": ui.actionClientSave_separately.setEnabled(False) ui.actionClientStop.setEnabled(False) ui.actionClientToggleVisible.setEnabled(False) ui.actionClientResume.setEnabled(True) else: ui.actionClientResume.setEnabled(False) #Hide and show shall only be enabled and connected if supported by the client try: ui.actionClientToggleVisible.triggered.disconnect() except TypeError: #TypeError: disconnect() failed between 'triggered' and all its connections pass if currentItem.clientDict["hasOptionalGUI"]: ui.actionClientToggleVisible.setEnabled(True) ui.actionClientToggleVisible.triggered.connect(lambda: api.clientToggleVisible(clientId)) else: ui.actionClientToggleVisible.setEnabled(False) #Only rename when dataclient is present #None or dict, even empty dict if self.clientOverrideNamesCache is None: ui.actionClientRename.setEnabled(False) else: ui.actionClientRename.setEnabled(True) def _reactSignal_rememberSorting(self, *args): self.sortByColumnValue = self.clientsTreeWidget.header().sortIndicatorSection() self.sortDescendingValue = self.clientsTreeWidget.header().sortIndicatorOrder() def _reactSignal_restoreSorting(self, *args): """Do not use as signal!!! Will lead to infinite recursion since Qt 5.12.2""" #self.clientsTreeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue) raise RuntimeError() class LauncherProgram(QtWidgets.QTreeWidgetItem): """ An item on the left side of the window. Used to start programs and show info, but nothing more. Example: { 'categories': 'AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack;', 'comment': 'Easy to use pattern sequencer for JACK and NSM', 'comment[de]': 'Einfach zu bedienender Pattern-Sequencer', 'exec': 'patroneo', 'genericname': 'Sequencer', 'icon': 'patroneo', 'name': 'Patroneo', 'startupnotify': 'false', 'terminal': 'false', 'type': 'Application', 'version': '1.0', #desktop spec version, not progra, 'x-nsm-capable': 'true'} Also: 'agordejoExec' : the actual nsm exe 'agordejoIconPath' : a priority path the engine found for us """ allItems = {} # clientId : ClientItem def __init__(self, parentController, launcherDict:dict): LauncherProgram.allItems[launcherDict["agordejoExec"]] = self self.parentController = parentController self.launcherDict = launcherDict self.executable = launcherDict["agordejoExec"] parameterList = [] #later in update super().__init__(parameterList, type=1000) #type 0 is default qt type. 1000 is subclassed user type) self.updateData(launcherDict) def updateData(self, launcherDict:dict): """Arrives via parenTreeWidget api callback""" self.launcherDict = launcherDict for index, key in enumerate(self.parentController.columns): if (not key in launcherDict) or launcherDict[key] is None: t = "" else: t = str(launcherDict[key]) self.setText(index, t) programIcons = self.parentController.mainWindow.programIcons assert programIcons if launcherDict["agordejoExec"] in programIcons: icon = programIcons[launcherDict["agordejoExec"]] self.setIcon(self.parentController.columns.index("name"), icon) #name is correct here. this is just the column. class LauncherTable(object): """Controls the QTreeWidget that holds programs in the PATH. """ def __init__(self, mainWindow, parent): self.mainWindow = mainWindow self.parent = parent self.sortByColumnValue = 0 # by name self.sortDescendingValue = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending self.launcherWidget = self.mainWindow.ui.loadedSessionsLauncher self.launcherWidget.setIconSize(iconSize) self.columns = ("name", "comment", "agordejoFullPath") #basically an enum self.headerLables = [ QtCore.QCoreApplication.translate("Launcher", "Name"), QtCore.QCoreApplication.translate("Launcher", "Description"), QtCore.QCoreApplication.translate("Launcher", "Path"), ] self.launcherWidget.setHeaderLabels(self.headerLables) self.launcherWidget.setSortingEnabled(True) self.launcherWidget.setAlternatingRowColors(True) self.launcherWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.launcherWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) ##The actual program entries are handled by the LauncherProgram item class #self.buildPrograms() #Don't call here. MainWindow calls it when everything is ready. #Signals self.launcherWidget.itemDoubleClicked.connect(self._reactSignal_launcherItemDoubleClicked) def _adjustColumnSize(self): self.launcherWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue) for index in range(self.launcherWidget.columnCount()): self.launcherWidget.resizeColumnToContents(index) def _reactSignal_launcherItemDoubleClicked(self, item): api.clientAdd(item.executable) def buildPrograms(self): """Called by mainWindow.updateProgramDatabase Receive entries from the engine. Entry is a dict modelled after a .desktop file. But not all entries have all data. Some are barebones executable name and path. Only guaranteed keys are agordejoExec and agordejoFullPath, which in turn are files guaranteed to exist in the path. """ self.launcherWidget.clear() engineCache = api.getCache() programs = engineCache["programs"] for entry in programs: item = LauncherProgram(parentController=self, launcherDict=entry) self.launcherWidget.addTopLevelItem(item) self._adjustColumnSize() class OpenSessionController(object): """Not a subclass. Controls the visible tab, when a session is open. There is only one open instance at a time that controls the GUI and cleans itself.""" def __init__(self, mainWindow): self.mainWindow = mainWindow self.clientTabe = ClientTable(mainWindow=mainWindow, parent=self) self.launcherTable = LauncherTable(mainWindow=mainWindow, parent=self) self.descriptionController = DescriptionController(mainWindow, self.mainWindow.ui.loadedSessionDescriptionGroupBox, self.mainWindow.ui.loadedSessionDescription) self.sessionLoadedPanel = mainWindow.ui.session_loaded #groupbox self.sessionProgramsPanel = mainWindow.ui.session_programs #groupbox #API Callbacks api.callbacks.sessionOpenReady.append(self._reactCallback_sessionOpen) logger.info("Full View Open Session Controller ready") def allSessionItems(self): """Can be used by external parts, like the tray icon""" return self.clientTabe.allItems().values() #dict clientId:SessionItem def _reactCallback_sessionOpen(self, nsmSessionExportDict:dict): """Open does not mean we come from the session chooser. Switching does not close a session""" #self.description.clear() #Deletes the placesholder and text! self.sessionLoadedPanel.setTitle(nsmSessionExportDict["nsmSessionName"]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/projectname.py0000644000175000017500000001463300000000000016023 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library import pathlib import os #Third Party from PyQt5 import QtCore, QtWidgets #QtGui from .designer.projectname import Ui_ProjectName from .designer.newsession import Ui_NewSession #Engine from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. import engine.api as api class ProjectNameWidget(QtWidgets.QDialog): """Ask the user for a project name. Either for renaming, new or copy. Will have a live-detection """ def __init__(self, parent, startwith:str="", start=True, alsoDisable=None): super().__init__(parent) self.ui = Ui_ProjectName() self.ui.setupUi(self) self.alsoDisable = alsoDisable #in case we are embedded #self.ui.labelDescription self.ui.labelError.setText("") self.ui.name.setText(startwith) self.check(startwith) self.ui.name.textEdited.connect(self.check) #not called when text is changed programatically self.result = None self.ui.buttonBox.accepted.connect(self.process) self.ui.buttonBox.rejected.connect(self.reject) if start: self.setWindowFlag(QtCore.Qt.Popup, True) self.setModal(True) self.setFocus(True) self.ui.name.setFocus(True) self.exec_() def check(self, currentText): """Called every keypress. We do preliminary error and collision checking here, so the engine does not have to throw an error """ if currentText.endswith("/"): currentText = currentText[:-1] fullpath = pathlib.Path(api.sessionRoot(), currentText) path = pathlib.Path(currentText) errorMessage = "" if currentText == "": errorMessage = QtCore.QCoreApplication.translate("ProjectNameWidget", "Name must not be empty.") elif pathlib.PurePosixPath(path).match("/*") or str(pathlib.PurePosixPath(path)).startswith("/"): errorMessage = QtCore.QCoreApplication.translate("ProjectNameWidget", "Name must be a relative path.") elif pathlib.PurePosixPath(path).match("../*") or pathlib.PurePosixPath(path).match("*..*"): errorMessage = QtCore.QCoreApplication.translate("ProjectNameWidget", "Moving to parent directory not allowed.") elif "/" in currentText and fullpath.parent.exists() and not os.access(fullpath.parent, os.W_OK): errorMessage = QtCore.QCoreApplication.translate("ProjectNameWidget", "Writing in this directory is not permitted.") elif fullpath.exists(): errorMessage = QtCore.QCoreApplication.translate("ProjectNameWidget", "Name is already in use.") ok = self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Ok) if errorMessage: if self.alsoDisable: self.alsoDisable.setEnabled(False) ok.setEnabled(False) self.ui.labelError.setText(""+errorMessage+"") else: #print (path) if self.alsoDisable: self.alsoDisable.setEnabled(True) self.ui.labelError.setText("") ok.setEnabled(True) def process(self): """Careful! Calling this eats python errors without notice. Make sure your objects exists and your syntax is correct""" self.result = self.ui.name.text() self.done(True) class NewSessionDialog(QtWidgets.QDialog): def __init__(self, parent, startwith:str=""): super().__init__(parent) self.ui = Ui_NewSession() self.ui.setupUi(self) #send our childWidget our own ok button so it can disable it for the name check self.ok = self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Ok) #Embed a project name widget self.projectName = ProjectNameWidget(self, startwith, start=False, alsoDisable=self.ok) self.projectName.ui.buttonBox.hide() self.ui.nameGroupBox.layout().addWidget(self.projectName) self.projectName.ui.name.returnPressed.connect(self.ok.click) #We want to accept everything when return is pressed. self.projectName.reject = self.reject #forward escape to our reject self.result = None nsmExecutables = api.getNsmExecutables() #type set, cached, very fast. data = METADATA["preferredClients"]["data"] con = METADATA["preferredClients"]["connections"] self.ui.checkBoxJack.setEnabled(con in nsmExecutables) self.ui.checkBoxJack.setVisible(con in nsmExecutables) self.ui.checkBoxData.setEnabled(data in nsmExecutables) self.ui.checkBoxData.setVisible(data in nsmExecutables) self.ui.buttonBox.accepted.connect(self.process) self.ui.buttonBox.rejected.connect(self.reject) self.setModal(True) self.setWindowFlag(QtCore.Qt.Popup, True) self.projectName.ui.name.setFocus(True) self.exec_() def process(self): """Careful! Calling this eats python errors without notice. Make sure your objects exists and your syntax is correct""" assert self.ok.isEnabled() data = METADATA["preferredClients"]["data"] con = METADATA["preferredClients"]["connections"] startclients = [] if self.ui.checkBoxJack.isChecked(): startclients.append(con) if self.ui.checkBoxData.isChecked(): startclients.append(data) self.result = { "name" : self.projectName.ui.name.text(), "startclients" : startclients, } self.done(True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/resources.py0000644000175000017500000026676700000000000015547 0ustar00nilsnils# -*- coding: utf-8 -*- # Resource object code # # Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore qt_resource_data = b"\ \x00\x00\x00\xb4\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ \x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ \x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ \x30\x20\x30\x20\x38\x20\x38\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\ \x3d\x22\x4d\x33\x2e\x31\x31\x20\x37\x2e\x31\x37\x4c\x2e\x30\x34\ \x38\x20\x34\x2e\x31\x30\x36\x6c\x31\x2e\x31\x34\x34\x2d\x31\x2e\ \x31\x34\x34\x20\x33\x2e\x30\x36\x32\x20\x33\x2e\x30\x36\x32\x7a\ \x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x38\x2e\x30\ \x36\x33\x20\x32\x2e\x32\x30\x38\x4c\x33\x2e\x37\x33\x35\x20\x36\ \x2e\x35\x33\x36\x6c\x2d\x31\x2e\x32\x32\x2d\x31\x2e\x32\x32\x4c\ \x36\x2e\x38\x34\x33\x2e\x39\x38\x38\x7a\x22\x2f\x3e\x3c\x2f\x73\ \x76\x67\x3e\ \x00\x00\x03\x3c\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x26\x00\x00\x00\x1e\x08\x06\x00\x00\x00\x40\x14\x6c\x6e\ \x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ \x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x03\xb5\x00\x00\x03\xb5\ \x01\x0a\x7e\x6a\x5b\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ \x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ \x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x0b\x74\x45\ \x58\x74\x54\x69\x74\x6c\x65\x00\x53\x68\x61\x70\x65\x26\xef\x99\ \x91\x00\x00\x02\xa2\x49\x44\x41\x54\x58\x85\xbd\x97\xcd\x4b\x54\ \x51\x18\x87\x9f\xf7\x5e\x23\xec\xeb\x8e\x4a\xa1\x60\xda\x28\xd6\ \xc2\x98\x89\xb2\x02\x8d\x42\x8a\x72\x61\x9b\x48\x6a\x59\x1b\x97\ \xd5\x4e\xff\x80\x36\x2e\x5a\xb4\x89\xa0\x8d\xb4\x49\x32\x22\xab\ \x85\x8b\x4a\x6a\xe1\xca\x42\x87\x8c\x4a\xfc\xca\x16\x42\x99\x73\ \x8b\x9a\x4a\xef\x7d\x5b\xe8\x84\x44\xcc\x39\x3a\x73\xfd\x2d\x67\ \x9e\xc3\x7d\xce\xef\x3d\x9c\x3b\x23\xa9\x89\xb4\x92\x23\xae\x14\ \x55\xd4\xc7\xb7\xcc\xe6\x62\x4c\x19\x1e\xff\x72\x44\x1c\xb7\x32\ \x19\xf7\x7a\x4c\xec\xc0\x80\x16\x95\x56\xa7\xaf\x39\x26\x50\x44\ \x24\x1f\xa9\xd4\x64\xba\xdd\x11\xe7\x99\x03\x3b\x4c\xec\xe8\x8c\ \x5f\x5a\x56\xed\xf7\x0b\x72\xa9\x28\x9f\x87\xe6\xca\xc0\x80\x16\ \x95\x55\xf9\x57\x51\x3a\x6c\xf8\xe1\xf1\xaf\xbb\x83\x85\xf0\x21\ \xb0\x07\x20\x12\xb1\xd1\x19\xbf\x34\x58\xf0\xef\x02\xc7\x6d\xf8\ \x91\x49\xbf\x45\x34\xec\x01\xbc\xec\x67\xc6\x51\xae\x36\x4b\x3b\ \xd7\x41\x7b\xa9\xf9\xcb\xa2\xfa\x78\xa5\x14\x14\xb8\xb1\xe5\x9d\ \xdf\x01\x62\x26\x76\x6c\x4c\x37\x66\x9c\xaf\x37\x51\xbd\xf0\xbf\ \xef\x0b\xd6\xd8\x8a\x9d\x1b\xa5\xde\x4c\x7d\xaf\xc8\xb8\xfe\x73\ \xe4\xff\x52\x50\x80\xc6\x4c\x3b\xff\x37\xa3\x93\xf3\xfb\x16\xc3\ \x85\x3e\xa0\x2a\x17\x97\x97\xd8\xab\xb1\x6f\xdb\x33\xae\x7f\x0f\ \x38\x6a\xc3\xa7\x26\xd2\x6d\x81\xd2\x0d\x6c\x32\xb1\x6b\x16\x7b\ \x3d\x31\x9f\x0c\x08\xfa\x80\x6a\xab\x05\xca\x09\xa0\x15\xb0\xba\ \x17\xd7\x74\xc6\x52\x13\xe9\xb6\x10\x19\x14\x5b\x29\x40\xd1\xd3\ \xb6\x52\xab\x16\x53\x55\x49\x4d\xa6\x3b\x80\x1e\x2c\xc6\x91\x4f\ \xac\x47\x39\x32\x3b\xbb\x39\x35\xe5\xdf\x16\xe5\x4c\x94\x42\xd9\ \x58\x89\x8d\x8c\xcd\x55\x4a\xc6\x7d\x80\x72\x20\x6a\xa1\x6c\x8c\ \xa3\x0c\x43\x1a\xc4\x75\x87\xd6\x53\x0a\x22\x78\x25\x15\x2a\x46\ \x31\xc7\x61\x48\x83\xa0\x01\xe1\xe5\x7a\x08\xfd\x7d\xae\x0d\x94\ \xac\x2b\xfb\xa8\xc5\x3f\x8f\xa9\x70\x3f\x6a\xa1\x6c\xac\x47\x99\ \x2c\x2f\xff\x9e\xd8\xe5\x9d\x45\xe8\x04\xc2\x08\x9d\x80\x55\x9e\ \x31\x11\xd1\x44\x3c\xd6\x05\x9c\x07\x7e\x44\xa3\xb4\x94\x35\x1d\ \xfe\x44\x4d\xac\xd7\x41\x1b\x15\xa6\x6d\xd7\x08\xf2\x08\xc8\xf9\ \xff\x22\x6f\x31\x80\xbd\x35\x25\x23\x41\xe0\x1e\x04\x5e\x58\x2d\ \x10\x9e\x00\xe7\xb0\x6c\x3a\xaf\xeb\x62\x7f\xdd\xd6\x4f\xc5\x81\ \x77\x12\x95\x6e\x1b\x3e\x51\x13\xeb\x75\x45\x9b\x80\x0f\x91\x8a\ \x01\xd4\xd5\xc9\xaf\x44\xad\x77\x51\x45\xaf\x00\x81\x89\xaf\x8f\ \x97\x0c\x2f\x06\x6e\x03\x86\xa6\x0b\x76\xc1\x26\xe3\x25\xd7\x55\ \xa4\x15\x48\x9b\x58\x9b\xa6\x0b\x7a\xf3\x27\xe3\x5e\x7f\xa8\xce\ \x61\xe0\xad\x89\x35\x35\x5d\xf0\x57\xd2\xbe\xda\x6d\xef\xdd\x0d\ \xd2\x04\x3c\xb5\xe1\x57\x34\xed\x47\x2a\x06\x50\xbf\xd3\xfb\x32\ \x37\xed\xb5\xa0\x74\xd9\xc9\x79\xfd\xa1\x3a\x87\x80\x77\x91\x8a\ \x01\x34\x37\xcb\x62\xa2\x36\xd6\x09\xb4\x03\xbf\x4d\xfc\x72\xd3\ \x8d\x2c\x37\x1d\xf9\xaf\x8b\x44\x4d\xec\x96\x88\x9e\xd2\x90\xcf\ \x26\x36\xdb\xb4\xc0\x8d\x3f\xdd\x96\xf1\x25\x4a\xd3\x97\xc3\x00\ \x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x04\xfc\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x26\x00\x00\x00\x1e\x08\x06\x00\x00\x00\x40\x14\x6c\x6e\ \x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ \x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x03\xb4\x00\x00\x03\xb4\ \x01\xd8\x39\x88\xbf\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ \x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ \x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x0b\x74\x45\ \x58\x74\x54\x69\x74\x6c\x65\x00\x53\x68\x61\x70\x65\x26\xef\x99\ \x91\x00\x00\x04\x62\x49\x44\x41\x54\x58\x85\xb5\x97\x5d\x4c\x5b\ \x75\x18\xc6\x9f\xf7\x9c\xf2\x3d\xd6\x76\xa0\x63\xe8\xc2\x5a\xac\ \x71\x94\x42\x34\xc4\xcc\x69\x08\xc4\x68\x62\x62\x30\x8e\x48\x30\ \x3a\xe3\xe2\x85\xde\xb8\x68\x8c\xba\xb9\x0b\x13\xbd\xf1\x13\x33\ \x2e\x4c\x8c\x66\x1a\x3f\x92\x05\x75\xd9\x84\xed\x82\xcc\x91\x90\ \xb9\xc4\x8c\x7d\x40\x01\x49\x18\x2d\x6c\x8e\x39\x07\x96\x02\x2b\ \x50\x7a\xce\xe3\x85\x80\xdd\xe9\x29\x3d\x94\xee\xb9\xeb\xfb\x7f\ \xde\xf7\xfd\x35\xff\xaf\xf3\x17\x64\x40\xfe\xc0\xec\x66\x4d\x5b\ \x28\x86\x6a\x2b\x50\x35\x4e\xce\xe4\xce\x8c\xef\xdc\xba\x75\x6e\ \x3d\x35\x25\x9d\xa4\xae\x2e\xda\x8a\xcb\xc2\xbb\x40\x3c\x4d\x41\ \x1d\x80\x12\x13\xdb\x65\x01\x3a\x00\x1c\xfd\xc3\x65\x3f\xd5\x24\ \xa2\xdd\x36\x30\x92\x4a\x5f\x20\xfc\xb2\x08\xde\x01\x70\xf7\x1a\ \x52\xfb\x85\xb2\xcf\x57\x6e\x3f\x9e\x71\xb0\xde\xd1\x29\x17\x74\ \x7e\x27\x90\x87\xd7\x00\x64\x6c\xf7\x4b\x9e\x16\xdb\xed\xf1\x14\ \x4d\x67\x04\xcc\x3f\x32\x55\x43\x41\x3b\xcc\xa7\x6c\x6d\x22\x07\ \x45\x97\x06\x9f\xc7\x31\xb2\x9a\x4d\x49\x80\x08\x86\xea\x7a\x7a\ \x98\xb5\xfc\x7b\x60\x6c\xa6\x82\x82\x5f\x33\x02\x05\x00\x22\x15\ \x54\x71\xf2\xfc\xf0\xcc\x1d\xab\xda\x8c\x81\xbe\xe0\xd4\x21\x10\ \x3e\x2a\x68\x12\xc1\x14\x74\x9c\x05\x51\x9e\x24\xff\x6f\x02\xdf\ \xaa\x0a\x4f\x50\xcf\x1a\xcc\xd5\x16\xe6\x22\x59\x6a\x91\xa2\x49\ \x05\x85\x0d\x00\x9e\x03\x50\x90\x24\xb7\x5b\x9d\xb3\x3f\xe6\xf5\ \x4a\xd4\x6c\xd0\x96\x24\xa9\x46\x74\x5c\x00\xf1\x3b\xc4\x14\x4a\ \x17\xe0\x53\x65\x2e\xfa\x5e\x2c\x27\x2f\x1f\x88\x6d\xf1\xb9\x37\ \x5c\x5f\x1a\x9b\x06\x10\x04\x70\x7c\x20\x38\xfb\xae\xc6\xc5\x8f\ \x01\x79\xde\xa4\x46\xad\x96\x1f\x7e\x0d\xc0\x47\x66\x00\x09\x53\ \x19\x27\x3b\x04\x8f\x9b\xc4\xa3\x24\x9a\x7c\x6e\xc7\x5b\x5e\xef\ \x9d\xb3\x36\xe8\x1b\x75\xc8\x6f\xfe\x91\xd0\x2e\xa3\xd1\xeb\xda\ \xf0\x57\x95\xdb\xb9\x5b\x80\x37\x4d\x3b\x10\xfb\x07\xae\x84\x37\ \xad\x15\xcc\x5c\x82\xbd\xd5\xe5\x8e\x9f\x0d\xd1\x02\x8a\xfc\xd4\ \x37\x32\xf5\x01\xc9\x84\x9a\x3e\xb7\xe3\x13\x40\x5a\x4c\xaa\x39\ \xb4\x45\xfd\xf5\xf5\x81\x11\x31\x08\x4e\x55\xb9\x1c\x5f\x24\x45\ \x16\xbc\xed\x0f\x86\x0f\xf7\x8c\x8f\xe7\x1b\x07\xd5\xb9\x8d\xfb\ \x01\x5c\x32\x49\x6b\x5c\x1f\x98\xc0\x06\xa2\xbe\x37\x10\x3a\x18\ \xbf\x6b\x4d\xf4\x4c\xf6\x7c\xfe\x99\xc1\x40\xa8\x2c\x3e\xe8\xf5\ \x4a\x94\xe0\xfb\x26\xfe\xed\x17\x47\xa6\xef\x4d\x1f\x6c\x09\x4f\ \x20\x7b\x73\x36\x85\x4f\x5f\x08\x86\xb6\xad\xe2\xab\xd6\x20\x3d\ \x7d\x81\x50\x6d\x7c\x30\x5f\xd3\x8f\x02\x48\xd8\x85\xa2\x68\x0f\ \x59\x01\x63\x2a\x3a\x02\x0f\xda\x28\x67\xfb\x47\x43\xf5\xab\x78\ \x8a\x01\xe9\xec\x0b\x86\x5e\x5c\x8e\x2d\x9d\xf8\x7d\x09\x10\xba\ \x94\x5a\x01\x4b\x75\x1b\x04\x08\xb6\x92\x78\x76\x7e\xc2\x71\x3a\ \x85\x37\x07\x94\xaf\x7b\x03\xa1\x83\x6d\xa4\xba\x54\x7e\xdc\x68\ \xa2\x30\x01\x2c\xd9\x39\x16\xaf\x08\x80\x33\x14\x76\x28\x9a\x7e\ \xc4\x77\x4f\xd1\x95\xf8\xc1\x81\x4b\x61\x0b\x25\xe2\x24\xb4\x25\ \xcc\x89\x28\x8b\x56\xc1\x02\x00\x4e\x12\x7a\x47\xbe\xe6\xec\xf4\ \x78\x64\x61\x6d\xdd\x57\xb4\x00\xe1\x2b\xd5\x2e\xe7\x37\x2b\x11\ \xe2\x2e\xa3\x89\xe4\xb5\x94\x60\xcc\x9b\x7f\xb5\xba\xa4\xe4\x66\ \x9a\x20\x2b\x12\x60\x82\x60\x63\x95\xcb\xd9\xbd\x1c\xfb\xef\x7e\ \xd4\x2a\x13\xbc\x94\xab\x29\xc1\x32\x01\x05\xa0\x57\x05\x9f\xaa\ \x70\x3b\xc7\xe2\x83\x59\xaa\xde\x4c\x40\x35\x78\x49\x3d\xd6\x6d\ \x88\xa5\x71\xf2\xa7\xd6\x8f\xd1\xdc\xc8\x4e\x23\xd4\xd0\xd0\x8d\ \x42\x82\x07\x12\xdc\x82\xf3\xd5\x9e\xa2\x3f\x6f\x27\x18\x41\x7c\ \xe8\x73\xd9\x9b\x6b\x4a\x4b\x23\xb7\x0c\x90\x12\xcd\xce\xfa\x0a\ \xc0\xe6\x04\x2e\xe2\xb0\x59\x31\x2b\xbb\xf2\x96\x06\xfe\xc0\x74\ \x0b\x04\xff\x54\xb9\xed\xf1\xa7\xf8\x4d\x21\x5f\xf0\x95\x3b\x8f\ \x18\x73\xba\xba\x68\xeb\x1f\x0d\xb7\x02\x68\x32\xf9\x2b\x57\x17\ \xf2\x22\x9f\x9b\xf5\xb2\xfc\x69\xdd\x46\xaa\xf7\x8d\x86\xbf\x04\ \xb1\x67\x29\xf5\x98\x68\x7c\x43\x17\x75\x46\x55\x62\x5b\x2a\xdd\ \xce\x5e\x63\xce\xc5\xd1\xd0\xfd\xaa\xae\xb4\x12\x7c\xc4\xb4\x39\ \xf9\x92\xaf\xdc\x79\x28\x6d\xb0\xe1\x61\xe6\xcc\xab\xe1\x1f\x08\ \x18\x2f\xdc\x28\xc1\x4e\x11\x69\x57\x84\xc3\x42\x84\x62\xba\x14\ \x29\x8a\x78\x49\x36\x00\xa8\x47\xf2\xe5\xd2\xe6\x73\xd9\x9b\x45\ \xc4\xf4\xa6\xb1\x04\xb6\xf4\x55\xbb\xc7\x8a\xd7\x92\x04\xe7\xa2\ \x39\x91\x5a\xe3\x5a\x8c\x97\xa5\xc5\x6f\x93\xac\x03\x10\x9c\xcb\ \x10\x56\x77\x2c\xa6\x3e\xb1\x1a\x14\x60\x11\xac\x62\x5b\xc1\x35\ \x35\x12\xad\x03\xf8\x3d\x2c\x5c\xf2\x49\xa4\x01\xd2\x32\x39\x66\ \x7f\xf4\x01\x4f\xe1\x8d\x54\xe6\x35\xbf\xc4\x7b\x83\xe1\x1d\x42\ \x7e\x06\x60\x87\xd5\x1c\x81\xb4\x2b\xaa\xb2\xcf\x5b\x56\x38\x68\ \x3d\x27\x4d\xf9\x47\x67\xb6\x53\xd7\x1b\x01\x3e\x09\xa0\x12\xff\ \xbf\x86\x74\x80\xd7\x49\x19\x12\xb0\x43\x74\x39\x96\xea\x0d\x99\ \x51\xb0\x78\x91\x14\xff\xe5\xb0\x43\x34\x5b\xf6\xc4\x58\xc1\x64\ \x7d\xbd\xc4\xd6\x5b\xf3\x5f\x2e\x0a\xbd\x45\xc2\xd5\x25\x47\x00\ \x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x01\x25\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ \x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ \x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ \x30\x20\x30\x20\x34\x34\x32\x20\x33\x38\x33\x22\x3e\x3c\x74\x69\ \x74\x6c\x65\x3e\x53\x68\x61\x70\x65\x20\x2b\x20\x53\x68\x61\x70\ \x65\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x3c\x67\x20\x66\x69\x6c\x6c\ \x2d\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x3e\ \x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x34\x34\x31\x2e\x34\x33\ \x20\x33\x38\x32\x2e\x33\x38\x48\x30\x4c\x32\x32\x30\x2e\x37\x33\ \x20\x30\x6c\x32\x32\x30\x2e\x37\x20\x33\x38\x32\x2e\x33\x38\x7a\ \x4d\x38\x37\x2e\x37\x20\x33\x33\x31\x2e\x37\x33\x68\x32\x36\x36\ \x2e\x30\x31\x4c\x32\x32\x30\x2e\x37\x33\x20\x31\x30\x31\x2e\x33\ \x31\x20\x38\x37\x2e\x37\x20\x33\x33\x31\x2e\x37\x33\x68\x2e\x30\ \x30\x33\x7a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\ \x32\x30\x33\x20\x31\x37\x37\x68\x33\x35\x76\x39\x34\x2e\x37\x38\ \x68\x2d\x33\x35\x56\x31\x37\x37\x7a\x6d\x30\x20\x31\x31\x30\x2e\ \x37\x32\x68\x33\x35\x76\x32\x37\x2e\x38\x39\x68\x2d\x33\x35\x76\ \x2d\x32\x37\x2e\x38\x39\x7a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\ \x73\x76\x67\x3e\ \x00\x00\x01\x4f\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ \x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ \x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ \x30\x20\x30\x20\x31\x32\x30\x20\x31\x32\x30\x22\x3e\x3c\x70\x61\ \x74\x68\x20\x64\x3d\x22\x4d\x36\x30\x20\x31\x39\x2e\x30\x39\x43\ \x32\x32\x2e\x33\x38\x32\x20\x31\x39\x2e\x30\x39\x2e\x30\x35\x33\ \x20\x36\x30\x20\x2e\x30\x35\x33\x20\x36\x30\x53\x32\x32\x2e\x33\ \x38\x33\x20\x31\x30\x30\x2e\x39\x31\x20\x36\x30\x20\x31\x30\x30\ \x2e\x39\x31\x20\x31\x31\x39\x2e\x39\x35\x20\x36\x30\x20\x31\x31\ \x39\x2e\x39\x35\x20\x36\x30\x20\x39\x37\x2e\x36\x31\x38\x20\x31\ \x39\x2e\x30\x39\x20\x36\x30\x20\x31\x39\x2e\x30\x39\x7a\x6d\x30\ \x20\x36\x35\x2e\x33\x32\x63\x2d\x31\x33\x2e\x34\x36\x20\x30\x2d\ \x32\x34\x2e\x34\x31\x2d\x31\x30\x2e\x39\x35\x2d\x32\x34\x2e\x34\ \x31\x2d\x32\x34\x2e\x34\x31\x53\x34\x36\x2e\x35\x34\x20\x33\x35\ \x2e\x35\x39\x20\x36\x30\x20\x33\x35\x2e\x35\x39\x20\x38\x34\x2e\ \x34\x30\x37\x20\x34\x36\x2e\x35\x34\x20\x38\x34\x2e\x34\x30\x37\ \x20\x36\x30\x73\x2d\x31\x30\x2e\x39\x35\x20\x32\x34\x2e\x34\x31\ \x2d\x32\x34\x2e\x34\x31\x20\x32\x34\x2e\x34\x31\x7a\x22\x2f\x3e\ \x3c\x63\x69\x72\x63\x6c\x65\x20\x63\x79\x3d\x22\x36\x30\x2e\x35\ \x38\x33\x22\x20\x63\x78\x3d\x22\x36\x30\x22\x20\x72\x3d\x22\x31\ \x34\x2e\x34\x30\x39\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x00\x60\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ \x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ \x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ \x30\x20\x30\x20\x31\x32\x30\x20\x31\x32\x30\x22\x3e\x3c\x70\x61\ \x74\x68\x20\x64\x3d\x22\x4d\x31\x36\x20\x31\x36\x68\x38\x38\x76\ \x38\x38\x48\x31\x36\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x02\x70\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ \x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ \x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ \x30\x20\x30\x20\x31\x32\x30\x20\x31\x32\x30\x22\x3e\x3c\x70\x61\ \x74\x68\x20\x64\x3d\x22\x4d\x33\x2e\x32\x37\x32\x20\x35\x38\x2e\ \x37\x37\x32\x63\x30\x20\x33\x31\x2e\x32\x30\x34\x20\x32\x35\x2e\ \x32\x39\x36\x20\x35\x36\x2e\x35\x20\x35\x36\x2e\x35\x20\x35\x36\ \x2e\x35\x20\x33\x31\x2e\x32\x30\x33\x20\x30\x20\x35\x36\x2e\x35\ \x2d\x32\x35\x2e\x32\x39\x36\x20\x35\x36\x2e\x35\x2d\x35\x36\x2e\ \x35\x20\x30\x2d\x31\x36\x2e\x32\x36\x34\x2d\x36\x2e\x38\x38\x32\ \x2d\x33\x30\x2e\x39\x31\x2d\x31\x37\x2e\x38\x38\x2d\x34\x31\x2e\ \x32\x32\x4c\x38\x34\x2e\x30\x33\x20\x33\x32\x2e\x38\x38\x35\x63\ \x36\x2e\x39\x31\x32\x20\x36\x2e\x34\x38\x32\x20\x31\x31\x2e\x32\ \x34\x34\x20\x31\x35\x2e\x36\x38\x38\x20\x31\x31\x2e\x32\x34\x34\ \x20\x32\x35\x2e\x38\x39\x20\x30\x20\x31\x39\x2e\x35\x37\x34\x2d\ \x31\x35\x2e\x39\x32\x36\x20\x33\x35\x2e\x35\x2d\x33\x35\x2e\x35\ \x20\x33\x35\x2e\x35\x2d\x31\x39\x2e\x35\x37\x35\x20\x30\x2d\x33\ \x35\x2e\x35\x2d\x31\x35\x2e\x39\x32\x36\x2d\x33\x35\x2e\x35\x2d\ \x33\x35\x2e\x35\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x30\x31\x30\ \x31\x30\x31\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\ \x39\x35\x2e\x32\x37\x32\x20\x35\x38\x2e\x37\x37\x32\x63\x30\x20\ \x31\x39\x2e\x35\x37\x35\x2d\x31\x35\x2e\x39\x32\x35\x20\x33\x35\ \x2e\x35\x2d\x33\x35\x2e\x35\x20\x33\x35\x2e\x35\x2d\x31\x39\x2e\ \x35\x37\x34\x20\x30\x2d\x33\x35\x2e\x35\x2d\x31\x35\x2e\x39\x32\ \x36\x2d\x33\x35\x2e\x35\x2d\x33\x35\x2e\x35\x20\x30\x2d\x31\x30\ \x2e\x32\x20\x34\x2e\x33\x33\x32\x2d\x31\x39\x2e\x34\x30\x36\x20\ \x31\x31\x2e\x32\x34\x35\x2d\x32\x35\x2e\x38\x39\x6c\x2d\x31\x34\ \x2e\x33\x36\x35\x2d\x31\x35\x2e\x33\x33\x43\x31\x30\x2e\x31\x35\ \x34\x20\x32\x37\x2e\x38\x36\x20\x33\x2e\x32\x37\x32\x20\x34\x32\ \x2e\x35\x30\x37\x20\x33\x2e\x32\x37\x32\x20\x35\x38\x2e\x37\x37\ \x63\x30\x20\x33\x31\x2e\x32\x30\x34\x20\x32\x35\x2e\x32\x39\x37\ \x20\x35\x36\x2e\x35\x20\x35\x36\x2e\x35\x20\x35\x36\x2e\x35\x20\ \x33\x31\x2e\x32\x30\x34\x20\x30\x20\x35\x36\x2e\x35\x2d\x32\x35\ \x2e\x32\x39\x37\x20\x35\x36\x2e\x35\x2d\x35\x36\x2e\x35\x22\x20\ \x66\x69\x6c\x6c\x3d\x22\x23\x30\x31\x30\x31\x30\x31\x22\x2f\x3e\ \x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x34\x39\x2e\x37\x38\x35\ \x20\x30\x48\x36\x39\x2e\x37\x36\x76\x36\x31\x2e\x35\x37\x36\x48\ \x34\x39\x2e\x37\x38\x35\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\ \x30\x31\x30\x31\x30\x31\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x06\xec\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ \x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ \x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x02\x4d\x00\x00\x02\x4d\ \x01\x88\x6f\xfb\x19\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ \x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ \x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x06\x69\x49\x44\ \x41\x54\x78\x9c\xed\x9b\xdd\x4b\x14\x5d\x1c\xc7\xbf\xdb\xae\xfb\ \x96\xde\x74\x11\x68\xc8\x9a\xa5\x8f\xe4\xe3\x5f\x90\x18\xa2\x48\ \xe2\xd2\xcb\x4d\xe0\x45\x37\x81\x61\x50\xac\x84\x94\x44\x98\x04\ \x4a\x42\xc8\x9a\x75\xd7\xd5\x42\xf0\x10\xd4\x85\x2b\x46\xf4\x76\ \xa3\x77\xb9\x2f\xca\x4c\x25\xba\xbb\xae\x65\xd6\x75\x39\x2f\xbb\ \x3b\xf3\x5c\xc4\x2e\xfb\xac\xae\x7b\xde\xb4\x60\x9f\x2f\x88\x3b\ \x33\xbf\xf3\x3d\x67\x3f\x3b\xe7\xfc\xce\x9c\x99\x01\x80\x27\x00\ \x0c\x00\x66\x99\xfd\x19\x00\x9e\x58\x00\x64\x00\x1c\x40\x79\xca\ \xb0\xe0\x17\x0d\x9c\x3c\x79\x12\xad\xad\xad\xbf\xb9\x3d\xfb\xa3\ \xb9\xb9\x39\xcc\xcf\xcf\xe7\xb6\x4d\x00\xe6\xf0\xf0\xb0\x59\x2e\ \x1a\x1e\x1e\xce\x75\x85\x72\x3d\xf5\x73\x2a\x7b\x00\x36\x51\x46\ \xdf\xbf\x7f\xc7\x8b\x17\x2f\x44\xd9\xed\xaa\xee\xee\x6e\x1c\x3e\ \x7c\x58\x88\x97\x10\x00\xa6\x69\x62\x7a\x7a\x1a\x7d\x7d\x7d\x22\ \xec\x4a\xea\xf1\xe3\xc7\xb8\x74\xe9\x12\x2c\x16\x0b\xb7\x97\x90\ \x2e\xa0\x28\x0a\xa2\xd1\xa8\x08\x2b\x22\x45\xa3\x51\x28\x8a\x22\ \xc4\x4b\x08\x80\x4c\x26\x03\x49\x92\x44\x58\x11\x49\x92\x24\x64\ \x32\x19\x21\x5e\x42\x00\xb8\x5c\x2e\xc8\xb2\x2c\xc2\x8a\x48\xb2\ \x2c\xc3\xe5\x72\x09\xf1\x12\x02\x40\x55\x55\x7c\xfb\xf6\x4d\x84\ \x15\x91\x36\x37\x37\xa1\x69\x9a\x10\x2f\x21\x00\x56\x57\x57\x45\ \xd8\xfc\x96\x3a\x85\x00\x08\x85\x42\x22\x6c\xa8\xb4\xb0\xb0\x20\ \xc4\x87\x1b\xc0\xd6\xd6\x16\xc2\xe1\xb0\x88\xb6\x50\x29\x12\x89\ \x08\xc9\x04\xdc\x00\xd2\xe9\xf4\xbe\x0e\x80\x59\xc9\xb2\x8c\x54\ \x2a\xc5\xed\xc3\x0d\x60\xbf\x33\x40\x56\xb2\x2c\xc3\xed\x76\x73\ \xfb\x70\x03\x48\xa5\x52\xf8\xfa\xf5\x2b\x53\xd9\xce\xce\x4e\x74\ \x76\x76\x32\x95\xdd\xd8\xd8\x10\x92\x09\xb8\xa7\xc2\x2b\x2b\x2b\ \x4c\xe5\xec\x76\x3b\xa6\xa6\xa6\x60\xb5\x5a\xd1\xd2\xd2\xc2\xf4\ \x65\x62\xb1\x18\x5a\x5a\x5a\x98\xea\xcf\x8a\xfb\x0c\x60\x1d\x00\ \x07\x07\x07\xd1\xd4\xd4\x84\x86\x86\x06\x5c\xbf\x7e\x7d\x5f\xeb\ \xce\x17\x17\x00\x45\x51\x98\x1a\x51\x5b\x5b\x8b\x5b\xb7\x6e\xe5\ \xb6\x6f\xdf\xbe\x8d\xba\xba\x3a\x6a\x9f\x70\x38\xcc\x9d\x09\xb8\ \x00\xa4\x52\x29\xa6\x01\xf0\xc1\x83\x07\x38\x78\xf0\x60\x6e\xdb\ \xed\x76\x63\x62\x62\x82\xda\x47\x44\x26\xe0\x02\xc0\x92\x01\xba\ \xba\xba\x70\xee\xdc\xb9\x6d\xfb\xcf\x9f\x3f\x8f\x9e\x9e\x1e\x2a\ \x2f\x11\xd7\x04\x5c\x00\xd2\xe9\x34\x36\x36\x36\x88\xe3\x1d\x0e\ \x07\xa6\xa6\xa6\x8a\x1e\x9f\x9c\x9c\x84\xd3\xe9\x24\xf6\xfb\xfc\ \xf9\x33\xd2\xe9\x34\x71\xfc\x4e\xe2\x02\xb0\xba\xba\x0a\xd3\x34\ \x89\xe3\x87\x86\x86\xd0\xd8\xd8\x58\xf4\xf8\xb1\x63\xc7\x70\xe3\ \xc6\x0d\xaa\x36\xc4\x62\x31\xaa\xf8\x42\x71\x01\x88\x44\x22\xc4\ \xb1\x1e\x8f\x87\xe8\xcb\x0d\x0d\x0d\xa1\xbe\xbe\x9e\xd8\x97\x37\ \x13\x30\x03\x50\x14\x85\xea\x22\xe8\xe1\xc3\x87\x44\x33\x37\x97\ \xcb\x85\x47\x8f\x1e\x11\xfb\x86\xc3\x61\xa8\xaa\x4a\x1c\x5f\x28\ \x66\x00\x34\x19\xe0\xec\xd9\xb3\xf0\x7a\xbd\xc4\xde\xa7\x4f\x9f\ \xc6\x99\x33\x67\x88\x62\x65\x59\x86\xae\xeb\xc4\xde\x85\x62\x06\ \xe0\x74\x3a\x89\x96\xc1\x1c\x0e\x07\xfc\x7e\x3f\xb5\xff\xe4\xe4\ \x24\x1c\x0e\x47\xc9\x38\x49\x92\xb8\x32\x01\x33\x00\xc3\x30\xf0\ \xe5\xcb\x97\x92\x71\x9a\xa6\x31\x0d\x54\xc9\x64\x92\xe8\x97\xe5\ \xcd\x04\xcc\x00\x62\xb1\x18\x71\x06\xb8\x72\xe5\x0a\xd5\x5c\x5f\ \xd7\x75\xf4\xf7\xf7\x13\xf9\x9b\xa6\x89\x44\x22\x41\xec\x5d\x28\ \x66\x00\x34\x19\x60\x79\x79\x99\xaa\x1b\x4c\x4c\x4c\xe0\xc3\x87\ \x0f\x7b\xd2\x96\x42\x31\x01\x50\x55\x95\x7a\x19\xec\xee\xdd\xbb\ \x44\xbf\xd4\xfa\xfa\x3a\x46\x47\x47\xa9\xbc\x43\xa1\x10\x73\x26\ \x60\x02\xa0\xeb\x3a\xf5\x14\x78\x6b\x6b\x0b\x83\x83\x83\x25\xe3\ \x06\x06\x06\xf0\xe3\xc7\x0f\x2a\x6f\x9e\x4c\xc0\x04\x80\x34\x03\ \x14\xea\xd9\xb3\x67\x98\x9d\x9d\x2d\x7a\xfc\xd5\xab\x57\x78\xfe\ \xfc\x39\xb5\xaf\x24\x49\x54\x53\xe8\x7c\x31\x01\x30\x4d\x13\xeb\ \xeb\xeb\x4c\x15\xfa\x7c\xbe\x1d\x4f\x57\x5d\xd7\x71\xed\xda\x35\ \x26\xcf\x64\x32\x49\x35\x25\xcf\x17\x13\x80\x78\x3c\xce\x5c\xe1\ \xca\xca\x0a\xee\xdf\xbf\xbf\x6d\xff\xf8\xf8\x38\x3e\x7d\xfa\xc4\ \xe4\xc9\x93\x09\x98\x00\xf0\xce\xbf\xc7\xc6\xc6\x10\x8f\xc7\x73\ \xdb\xc9\x64\x12\xe3\xe3\xe3\x5c\x9e\xac\x99\x80\x1a\x80\xa6\x69\ \xdc\x00\x14\x45\xc1\xc0\xc0\x40\x6e\xfb\xea\xd5\xab\xf8\xf9\xf3\ \x27\x97\x27\x6b\x26\xa0\x5e\x14\xd5\x34\x4d\xc8\x9d\xe0\xe9\xe9\ \x69\xcc\xcc\xcc\xc0\x66\xb3\x21\x18\x0c\x72\xfb\x49\x92\x04\x5d\ \xd7\xa9\x07\x43\x6a\x00\x0e\x87\x43\xd8\x7d\x00\x9f\xcf\x27\xc4\ \x07\xf8\x95\x0a\x59\x32\x01\x35\x00\x8b\xc5\x82\x64\x32\x49\x5d\ \xd1\x4e\xe2\x5d\xcc\xc8\xd7\xda\xda\x1a\x53\x39\xea\x31\x20\x91\ \x48\xc0\x30\x0c\xa6\xca\xf6\x52\x86\x61\x30\x41\xa0\x06\xc0\x33\ \xef\xde\x6b\xb1\xb4\x8d\x0a\x80\xa6\x69\xbf\xe5\x56\x38\xa9\x42\ \xa1\x10\xf5\x1d\x26\x6a\x00\xfb\xf9\x2c\x10\xad\x24\x49\xda\x5b\ \x00\x22\x33\xc0\x5e\x48\x96\x65\xa2\x55\xa4\x7c\x51\x01\xb0\x58\ \x2c\x5c\x8b\x0f\xf9\xaa\xaf\xaf\xc7\xcc\xcc\x0c\x5e\xbf\x7e\x8d\ \xa6\xa6\x26\x21\x9e\xf1\x78\x9c\xfa\xd9\x41\x2a\x00\x6b\x6b\x6b\ \xdc\x19\xc0\x6e\xb7\xe3\xe6\xcd\x9b\x90\x24\x09\x3d\x3d\x3d\xe8\ \xe8\xe8\x40\x34\x1a\xc5\xbd\x7b\xf7\x98\xaf\xe8\xb2\x32\x0c\x83\ \x3a\x45\x53\x01\xe0\x7d\x18\xb2\xd8\x97\x2d\x84\xc2\xa3\xc5\xc5\ \x45\xaa\x78\x62\x00\xba\xae\x33\x3f\x98\x74\xe4\xc8\x11\x04\x02\ \x81\x92\xa7\x7b\xb6\x5b\x04\x83\x41\x1c\x3d\x7a\x94\xa9\xae\xf7\ \xef\xdf\x53\x0d\x84\xc4\x00\x54\x55\xa5\x1e\x00\x2b\x2a\x2a\xe0\ \xf3\xf9\xf0\xf1\xe3\x47\x5c\xbc\x78\x91\xb8\x9c\xd7\xeb\x85\x24\ \x49\x18\x19\x19\xa1\xee\x16\xb2\x2c\xef\x0d\x00\xbb\xdd\x4e\x05\ \xa0\xbd\xbd\x1d\x91\x48\x04\x7e\xbf\x1f\x95\x95\x95\xc4\xe5\xb2\ \x72\xb9\x5c\xb8\x73\xe7\x0e\x96\x96\x96\xd0\xdd\xdd\x4d\x5c\x8e\ \x36\x13\x10\x03\xb0\xd9\x6c\xff\xb9\x86\x2f\xa6\x9a\x9a\x1a\x04\ \x02\x01\xbc\x7d\xfb\x16\x27\x4e\x9c\x20\x6e\x48\x31\x1d\x3f\x7e\ \x1c\xb3\xb3\xb3\x08\x06\x83\xf0\x78\x3c\x25\xe3\x63\xb1\x18\x0e\ \x1c\x20\x1f\xda\x88\x23\x13\x89\xc4\xae\x0f\x28\xb3\x9e\xee\xa4\ \xf2\x7a\xbd\x90\x65\x19\x23\x23\x23\xbb\xfe\xc2\x99\x4c\x86\x2a\ \x13\x10\x03\x58\x5a\x5a\x2a\x7a\xec\xd4\xa9\x53\x08\x87\xc3\xf0\ \xfb\xfd\xa8\xaa\xaa\x22\xae\x9c\x56\x6e\xb7\x3b\xd7\x2d\xba\xba\ \xba\x8a\xc6\xed\xd6\xd6\x42\x11\x01\x28\x96\x01\xaa\xab\xab\x11\ \x08\x04\xf0\xee\xdd\x3b\x34\x37\x37\x13\x57\xca\xab\x86\x86\x06\ \xbc\x7c\xf9\x12\xc1\x60\x10\xb5\xb5\xb5\xdb\x8e\x2f\x2c\x2c\x10\ \x2f\x93\x13\xad\x07\xa8\xaa\x8a\xba\xba\x3a\x5c\xbe\x7c\x39\xb7\ \xaf\xb1\xb1\x11\x17\x2e\x5c\x40\x55\x55\x15\x14\x45\x81\xd3\xe9\ \xa4\xea\x7b\xbc\x52\x55\x15\x6d\x6d\x6d\x98\x9f\x9f\xc7\xd3\xa7\ \x4f\xb1\xbc\xbc\x9c\x3b\xe6\xf1\x78\xa0\xaa\x2a\xec\x76\x7b\x49\ \x1f\x22\x00\x6e\xb7\x1b\xbd\xbd\xbd\xe8\xed\xed\xdd\xf1\xb8\x69\ \x9a\xb9\xa7\xb5\xb2\x53\xd1\x62\xff\x8b\xed\xcb\xfa\xe4\xaf\x36\ \xe7\x6f\x17\xee\x07\x00\xab\xd5\x8a\x43\x87\x0e\xa1\xbf\xbf\x7f\ \x5b\x9b\x48\x33\x01\x11\x00\x9b\xcd\x06\x9b\x4d\xd8\xfb\x55\x7f\ \x94\xfe\x7f\x6d\x2e\xfb\xe1\xcd\x9b\x37\xc2\xde\xc2\xf8\xd3\x35\ \x37\x37\x97\xfb\x5c\xf6\x2f\x4f\x5b\x01\xfc\x05\xe0\x6f\xfc\x82\ \x51\x4e\x32\x01\xfc\xf3\x2f\xe8\x31\x0e\x04\xd7\xcb\x8f\x12\x00\ \x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x02\xbb\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x26\x00\x00\x00\x1e\x08\x06\x00\x00\x00\x40\x14\x6c\x6e\ \x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ \x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x03\xb4\x00\x00\x03\xb4\ \x01\xd8\x39\x88\xbf\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ \x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ \x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x0b\x74\x45\ \x58\x74\x54\x69\x74\x6c\x65\x00\x53\x68\x61\x70\x65\x26\xef\x99\ \x91\x00\x00\x02\x21\x49\x44\x41\x54\x58\x85\xcd\xd7\x3d\x68\x13\ \x71\x18\xc7\xf1\xdf\xf3\xbf\x88\x16\xdb\x26\xb1\x20\x22\x08\xf1\ \x42\x17\x4d\xd2\x2a\x75\x10\xba\x58\x70\x71\x89\x08\x82\x83\xae\ \x5d\xec\xe0\x26\xb8\x58\x9c\xc4\xd9\x0a\x8e\x22\x14\x37\x11\x71\ \xe9\xa0\xe0\x6b\xa0\x8b\xb9\x4b\xa7\x34\x97\x48\x41\x07\xb9\xe6\ \xce\x06\x94\x36\x77\x8f\x43\xcf\xe6\x4f\x68\x2f\x2f\x77\x97\xf8\ \x5d\x92\x70\x2f\xf9\xdc\x93\x67\x09\x69\x86\xf5\x74\x7b\x33\xbe\ \x30\x33\x43\x3b\x08\x98\x6e\x58\x97\x19\x58\xf1\x39\xa5\x94\x53\ \x13\xd9\xb5\x6a\xe3\x84\xc3\xcd\x1f\x7e\xf7\x12\x00\xe6\x0f\x1f\ \xfb\xf5\xb6\xb8\xbe\x75\x3c\x28\x0c\x00\xf9\x1f\x25\xf2\x5e\xfc\ \xcf\xf3\x60\x60\xf0\x2c\x09\xe7\xcb\xd7\xb2\x79\x36\x04\x5c\x28\ \x09\xe9\xbd\x2a\x14\xa5\xa0\x55\xeb\xf9\xa1\x69\xa4\x44\xdb\xe7\ \x51\x30\xbd\x2c\x56\xea\x8b\xcc\xdc\x71\xdc\x51\xd6\x0e\x03\x76\ \x57\xe0\x7e\xc9\xb0\x97\x3f\x6f\x6c\x8c\x0c\x5c\xe4\xb5\x1f\x0c\ \x00\xc0\x84\x1b\xa3\x3b\x63\x9f\xf4\x75\xf3\xd4\x20\x41\xff\x3a\ \x10\xe6\x75\x8e\x45\xac\x50\xaa\xd8\x17\x06\xa2\x91\xea\x04\x03\ \xc0\x27\x5d\xe2\xf7\xba\x61\xdf\x8a\x9e\xd3\xaa\x0b\x18\x00\xe0\ \x08\x83\x9f\x69\x15\xeb\x21\x33\x77\x7b\x4d\xa0\x7a\xf9\x12\x02\ \xe1\xae\x5e\xb3\x5f\x97\xcb\xe6\x78\x64\x22\xaf\xde\x9f\x9e\x71\ \xe5\xb7\xa2\x7c\x2c\xd6\xac\xd3\x11\x78\xf6\xea\xf7\x67\xc9\x92\ \x8b\xd5\x52\xad\x7e\x29\x54\x8d\x54\x90\x7d\x99\x70\x5d\x5a\xd1\ \x0c\xfb\x76\x68\x1a\xa9\x58\xf0\xeb\xf9\xb1\x66\x58\xb9\xed\xcd\ \xf8\x02\x60\x87\x82\x02\x82\x4d\x2c\xd2\x82\x4e\xac\x09\xd0\x9d\ \x9c\x1a\x5f\x02\x00\xdd\xb0\x42\x20\xed\x16\x04\x66\x0a\xc1\xd7\ \x33\xa9\xc4\xbb\xd0\x34\x52\xfd\xc2\x74\x16\xc8\x67\x52\xc9\x6a\ \xa8\x1a\xa9\x3e\x76\x8c\xdf\x8c\x38\xce\xec\x54\x2a\x11\x19\x0a\ \xe8\x6d\x62\x0c\xc6\xa3\xac\x9a\xb8\x47\x44\x6e\x64\x22\xaf\x6e\ \x61\x7f\x08\x34\x9f\x4d\xc7\x9f\x47\xaa\x91\xea\x02\x46\xdf\x05\ \xe3\x6a\x26\x1d\x5f\x8d\x9e\xd3\xaa\x13\xac\x10\x13\xb1\x6b\x67\ \x52\x47\x7d\xff\x6a\x45\xd1\x81\xcb\x4f\x8c\x17\x8d\x43\x5b\x73\ \xc3\x40\x01\xfb\x4f\x8c\x99\xf9\x41\x2e\x9d\x5c\x1c\x34\x46\xae\ \x1d\xd6\x00\xf1\xcd\x29\x35\xf9\x6a\x28\x1a\xa9\x16\x8c\x50\x71\ \x9b\x4e\x7e\x7a\x72\x62\x6d\x88\x9e\xbd\xbc\x1d\xa3\x0f\xcd\xa6\ \x72\xf1\x7f\x41\x01\x80\x20\xe0\x89\xf9\x6d\x7c\xee\xfc\xe4\xd8\ \xcf\x61\x63\xe4\xfe\x02\x9c\xf5\xa3\xbb\x96\xa3\x3a\x87\x00\x00\ \x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x00\x21\ \x3c\ \xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\xa7\ \x00\x00\x00\x05\x69\x74\x5f\x49\x54\x88\x00\x00\x00\x02\x01\x01\ \ \x00\x00\x3c\xb2\ \x3c\ \xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\xa7\ \x00\x00\x00\x05\x64\x65\x5f\x44\x45\x42\x00\x00\x04\x48\x00\x00\ \x00\x41\x00\x00\x0d\xb8\x00\x00\x04\x92\x00\x00\x12\x30\x00\x00\ \x04\xd4\x00\x00\x22\x2f\x00\x00\x54\xc7\x00\x00\x16\x52\x00\x02\ \x73\xfe\x00\x00\x0d\x06\x00\x04\x87\x9b\x00\x00\x31\x18\x00\x04\ \xd6\x8d\x00\x00\x24\xdb\x00\x04\xd6\x8d\x00\x00\x31\x4f\x00\x04\ \xe5\x7b\x00\x00\x15\x27\x00\x04\xf6\x35\x00\x00\x31\x7a\x00\x05\ \x48\x35\x00\x00\x0b\x36\x00\x05\x48\x35\x00\x00\x22\x84\x00\x05\ \x48\x35\x00\x00\x30\x0c\x00\x05\x68\xa8\x00\x00\x0b\x5f\x00\x05\ \x68\xa8\x00\x00\x30\x38\x00\x05\x8c\x04\x00\x00\x17\x64\x00\x05\ \x98\xc5\x00\x00\x02\x29\x00\x05\x98\xc5\x00\x00\x19\x68\x00\x05\ \xa1\x05\x00\x00\x30\xb2\x00\x05\xab\x60\x00\x00\x1c\x5b\x00\x1c\ \x8e\x95\x00\x00\x1b\x9d\x00\x29\x46\x88\x00\x00\x02\x61\x00\x3f\ \x4b\x4f\x00\x00\x32\x73\x00\x43\x02\x93\x00\x00\x30\xe0\x00\x47\ \x96\xc4\x00\x00\x0d\xd8\x00\x48\x36\xff\x00\x00\x0e\xa5\x00\x48\ \x37\x02\x00\x00\x0e\xc9\x00\x48\x37\x03\x00\x00\x0e\xed\x00\x48\ \x37\x04\x00\x00\x0f\x11\x00\x48\x37\x08\x00\x00\x0f\x35\x00\x52\ \x78\xbc\x00\x00\x22\x55\x00\x58\xc0\x60\x00\x00\x33\x72\x00\x6a\ \x2b\x7e\x00\x00\x23\x21\x00\x80\x80\x83\x00\x00\x01\x28\x00\xa8\ \x87\xd4\x00\x00\x18\xb8\x00\xe5\x78\xa9\x00\x00\x21\x3d\x01\x43\ \xa7\x7e\x00\x00\x04\x20\x01\x6c\x14\x93\x00\x00\x1c\x08\x01\x71\ \x98\x3f\x00\x00\x33\x13\x01\x99\x40\x64\x00\x00\x1b\x72\x01\x9e\ \xa3\x45\x00\x00\x20\xe4\x01\xce\x99\x65\x00\x00\x17\x95\x01\xd7\ \x71\xee\x00\x00\x02\x8b\x01\xe1\x97\x5e\x00\x00\x25\xbc\x02\x0a\ \xed\x1e\x00\x00\x00\x67\x02\x30\x48\xfe\x00\x00\x26\x3d\x02\x4c\ \x1f\x64\x00\x00\x10\x49\x02\x83\x1f\x6e\x00\x00\x27\x35\x02\xda\ \x36\xee\x00\x00\x26\xc4\x02\xe5\x82\x07\x00\x00\x12\x51\x03\x1e\ \xbf\x15\x00\x00\x15\xfe\x03\x7f\x5c\xdc\x00\x00\x16\x2c\x03\x98\ \xbd\x62\x00\x00\x0d\x31\x04\x26\x57\x6e\x00\x00\x34\x75\x04\x67\ \xb4\xfe\x00\x00\x05\xb1\x04\xab\x8f\x01\x00\x00\x10\x85\x04\xab\ \x8f\x03\x00\x00\x10\xaa\x04\xab\x8f\x07\x00\x00\x11\x50\x04\xaf\ \x83\x57\x00\x00\x20\xbf\x04\xff\x6f\xd9\x00\x00\x1a\x8e\x05\x2c\ \xaa\x07\x00\x00\x1d\x4d\x05\x38\x5b\x7c\x00\x00\x15\xc9\x05\x3f\ \x2f\x2e\x00\x00\x24\xff\x05\x46\xa4\xee\x00\x00\x14\x88\x05\x54\ \xc4\xe9\x00\x00\x13\x61\x05\x6c\xc7\x3c\x00\x00\x31\xb9\x05\x82\ \xae\x8e\x00\x00\x30\x64\x05\x8c\x46\xc5\x00\x00\x18\x04\x05\x8c\ \x48\x35\x00\x00\x18\x7f\x05\x8c\xac\x35\x00\x00\x18\xfe\x05\x8c\ \xe0\x44\x00\x00\x19\x37\x05\xa6\x7a\xf1\x00\x00\x1d\x7f\x05\xaa\ \x8b\xc3\x00\x00\x22\xb0\x05\xe6\x3d\xae\x00\x00\x2e\x84\x06\x25\ \x05\x65\x00\x00\x16\xed\x06\x39\x75\x8d\x00\x00\x11\xb1\x06\x79\ \xf9\xd4\x00\x00\x23\x50\x06\x7b\x87\x94\x00\x00\x1d\x1f\x06\x8e\ \x99\xee\x00\x00\x2c\xb5\x07\x5f\xff\x0e\x00\x00\x09\x79\x07\x6e\ \x5a\x79\x00\x00\x0f\x84\x07\x75\xd1\x0e\x00\x00\x1f\x08\x07\x7b\ \x80\xb3\x00\x00\x23\x93\x08\x18\x71\x89\x00\x00\x34\x12\x08\x4b\ \x9e\x39\x00\x00\x1f\xe2\x08\x4c\x83\x51\x00\x00\x13\xb9\x08\x94\ \x32\x13\x00\x00\x12\x93\x08\xc4\x9b\xe2\x00\x00\x08\xfb\x08\xe6\ \xdb\xb5\x00\x00\x16\x7a\x09\x42\x2c\xe4\x00\x00\x15\x52\x09\x4d\ \x67\xfe\x00\x00\x0a\xf6\x09\x68\xe3\x3e\x00\x00\x14\xe7\x09\x7c\ \xcf\x74\x00\x00\x15\x7d\x09\x9f\xca\xd3\x00\x00\x1b\x28\x09\xcf\ \xe7\x17\x00\x00\x16\xb4\x09\xe8\x4d\x83\x00\x00\x21\xf4\x0a\x2f\ \xc5\xf3\x00\x00\x2d\xaf\x0a\x65\xb9\x1c\x00\x00\x10\x11\x0a\x65\ \xca\x45\x00\x00\x2f\xbf\x0a\xb4\xa1\xee\x00\x00\x35\x95\x0a\xba\ \x1f\xaf\x00\x00\x33\xa4\x0a\xc1\x78\x5c\x00\x00\x2f\x07\x0a\xc2\ \xca\x31\x00\x00\x2e\xd0\x0a\xd7\xdb\xc5\x00\x00\x1c\x86\x0a\xe1\ \x95\xe4\x00\x00\x11\x75\x0a\xf7\x7b\x42\x00\x00\x1a\xde\x0b\x75\ \x94\x47\x00\x00\x1c\xe1\x0b\x80\x57\x65\x00\x00\x19\x9d\x0b\x97\ \x7b\xad\x00\x00\x2c\x3d\x0b\xfc\xa6\x8e\x00\x00\x2a\x45\x0c\x2d\ \xf1\xfb\x00\x00\x18\x3b\x0c\x7c\x88\xff\x00\x00\x01\x81\x0c\xa4\ \x22\xae\x00\x00\x36\x2c\x0c\xbb\x01\x73\x00\x00\x09\x3c\x0c\xbb\ \x01\x73\x00\x00\x1b\xc7\x0c\xd1\x5e\x0e\x00\x00\x2f\x73\x0c\xfc\ \x45\xce\x00\x00\x27\xf0\x0d\x09\xf9\x75\x00\x00\x22\xe2\x0d\x1b\ \xa6\xf3\x00\x00\x0d\x83\x0d\x44\x5a\x01\x00\x00\x10\xcf\x0d\x44\ \x5a\x03\x00\x00\x10\xfa\x0d\x44\x5a\x07\x00\x00\x11\x25\x0d\x6c\ \xe5\x0e\x00\x00\x2d\xe4\x0d\xe6\xc5\xa4\x00\x00\x0f\x59\x0e\x61\ \x3e\x04\x00\x00\x01\xe3\x0e\x68\xb0\x7f\x00\x00\x0e\x6e\x0e\x81\ \x4e\x34\x00\x00\x00\x00\x0e\x8f\x55\x45\x00\x00\x1a\x33\x0e\xb5\ \x5d\x1e\x00\x00\x2b\x62\x0e\xc3\x60\x16\x00\x00\x0b\x88\x0f\x10\ \x07\x39\x00\x00\x35\x42\x0f\x53\xf3\xc5\x00\x00\x24\x94\x0f\x6b\ \x84\x53\x00\x00\x13\x06\x0f\x7b\x26\x8d\x00\x00\x2e\x30\x0f\x9d\ \x2c\x77\x00\x00\x17\x1c\x0f\xb7\x6f\xd9\x00\x00\x0e\x04\x0f\xb7\ \x6f\xd9\x00\x00\x32\x0b\x69\x00\x00\x38\x3f\x03\x00\x00\x00\x38\ \x00\x50\x00\x72\x00\x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\ \x00\x20\x00\x73\x00\x6f\x00\x6c\x00\x6c\x00\x20\x00\x62\x00\x65\ \x00\x65\x00\x6e\x00\x64\x00\x65\x00\x74\x00\x20\x00\x77\x00\x65\ \x00\x72\x00\x64\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\ \x00\x0d\x41\x62\x6f\x75\x74\x20\x74\x6f\x20\x71\x75\x69\x74\x07\ \x00\x00\x00\x0d\x41\x73\x6b\x42\x65\x66\x6f\x72\x65\x51\x75\x69\ \x74\x01\x03\x00\x00\x00\x78\x00\x50\x00\x72\x00\x6f\x00\x67\x00\ \x72\x00\x61\x00\x6d\x00\x6d\x00\x20\x00\x73\x00\x6f\x00\x6c\x00\ \x6c\x00\x20\x00\x62\x00\x65\x00\x65\x00\x6e\x00\x64\x00\x65\x00\ \x74\x00\x20\x00\x77\x00\x65\x00\x72\x00\x64\x00\x65\x00\x6e\x00\ \x2c\x00\x20\x00\x61\x00\x62\x00\x65\x00\x72\x00\x20\x00\x53\x00\ \x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x7b\x00\ \x7d\x00\x20\x00\x69\x00\x73\x00\x74\x00\x20\x00\x6e\x00\x6f\x00\ \x63\x00\x68\x00\x20\x00\x6f\x00\x66\x00\x66\x00\x65\x00\x6e\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x27\x41\x62\x6f\x75\x74\x20\x74\ \x6f\x20\x71\x75\x69\x74\x20\x62\x75\x74\x20\x73\x65\x73\x73\x69\ \x6f\x6e\x20\x7b\x7d\x20\x73\x74\x69\x6c\x6c\x20\x6f\x70\x65\x6e\ \x07\x00\x00\x00\x0d\x41\x73\x6b\x42\x65\x66\x6f\x72\x65\x51\x75\ \x69\x74\x01\x03\x00\x00\x00\x28\x00\xc4\x00\x6e\x00\x64\x00\x65\ \x00\x72\x00\x75\x00\x6e\x00\x67\x00\x65\x00\x6e\x00\x20\x00\x56\ \x00\x65\x00\x72\x00\x77\x00\x65\x00\x72\x00\x66\x00\x65\x00\x6e\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x0f\x44\x69\x73\x63\x61\x72\ \x64\x20\x43\x68\x61\x6e\x67\x65\x73\x07\x00\x00\x00\x0d\x41\x73\ \x6b\x42\x65\x66\x6f\x72\x65\x51\x75\x69\x74\x01\x03\x00\x00\x00\ \x2c\x00\x4d\x00\xf6\x00\x63\x00\x68\x00\x74\x00\x65\x00\x6e\x00\ \x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x73\x00\x70\x00\x65\x00\ \x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\x00\x3f\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x14\x44\x6f\x20\x79\x6f\x75\x20\x77\x61\ \x6e\x74\x20\x74\x6f\x20\x73\x61\x76\x65\x3f\x07\x00\x00\x00\x0d\ \x41\x73\x6b\x42\x65\x66\x6f\x72\x65\x51\x75\x69\x74\x01\x03\x00\ \x00\x00\x1a\x00\x4e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\ \x42\x00\x65\x00\x65\x00\x6e\x00\x64\x00\x65\x00\x6e\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x0a\x44\x6f\x6e\x27\x74\x20\x51\x75\x69\ \x74\x07\x00\x00\x00\x0d\x41\x73\x6b\x42\x65\x66\x6f\x72\x65\x51\ \x75\x69\x74\x01\x03\x00\x00\x00\x12\x00\x53\x00\x70\x00\x65\x00\ \x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\x08\x00\x00\x00\x00\ \x06\x00\x00\x00\x04\x53\x61\x76\x65\x07\x00\x00\x00\x0d\x41\x73\ \x6b\x42\x65\x66\x6f\x72\x65\x51\x75\x69\x74\x01\x03\x00\x00\x00\ \x0a\x00\x24\x00\x50\x00\x41\x00\x54\x00\x48\x08\x00\x00\x00\x00\ \x06\x00\x00\x00\x05\x24\x50\x41\x54\x48\x07\x00\x00\x00\x06\x44\ \x69\x61\x6c\x6f\x67\x01\x03\x00\x00\x01\x02\x00\x45\x00\x69\x00\ \x6e\x00\x6e\x00\x20\x00\x61\x00\x62\x00\x73\x00\x6f\x00\x6c\x00\ \x75\x00\x74\x00\x65\x00\x72\x00\x20\x00\x50\x00\x66\x00\x61\x00\ \x64\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x20\x00\x5a\x00\x65\x00\ \x69\x00\x6c\x00\x65\x00\x20\x00\x28\x00\x7a\x00\x2e\x00\x42\x00\ \x2e\x00\x20\x00\x2f\x00\x68\x00\x6f\x00\x6d\x00\x65\x00\x2f\x00\ \x75\x00\x73\x00\x65\x00\x72\x00\x2f\x00\x61\x00\x75\x00\x64\x00\ \x69\x00\x6f\x00\x2d\x00\x62\x00\x69\x00\x6e\x00\x29\x00\x2e\x00\ \x20\x00\x53\x00\x63\x00\x68\x00\x72\x00\xe4\x00\x67\x00\x73\x00\ \x74\x00\x72\x00\x69\x00\x63\x00\x68\x00\x20\x00\x61\x00\x6d\x00\ \x20\x00\x45\x00\x6e\x00\x64\x00\x65\x00\x20\x00\x73\x00\x70\x00\ \x69\x00\x65\x00\x6c\x00\x74\x00\x20\x00\x6b\x00\x65\x00\x69\x00\ \x6e\x00\x65\x00\x20\x00\x52\x00\x6f\x00\x6c\x00\x6c\x00\x65\x00\ \x2e\x00\x20\x00\x4b\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x20\x00\ \x57\x00\x69\x00\x6c\x00\x64\x00\x63\x00\x61\x00\x72\x00\x64\x00\ \x73\x00\x20\x00\x77\x00\x69\x00\x65\x00\x20\x00\x2a\x00\x20\x00\ \x6f\x00\x64\x00\x65\x00\x72\x00\x20\x00\x2e\x00\x2e\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x78\x41\x64\x64\x20\x6f\x6e\x65\x20\x61\ \x62\x73\x6f\x6c\x75\x74\x65\x20\x70\x61\x74\x68\x20\x74\x6f\x20\ \x61\x20\x64\x69\x72\x65\x63\x74\x6f\x72\x79\x20\x28\x65\x2e\x67\ \x2e\x20\x2f\x68\x6f\x6d\x65\x2f\x75\x73\x65\x72\x2f\x61\x75\x64\ \x69\x6f\x2d\x62\x69\x6e\x29\x20\x70\x65\x72\x20\x6c\x69\x6e\x65\ \x2e\x20\x4e\x6f\x20\x77\x69\x6c\x64\x63\x61\x72\x64\x73\x2e\x20\ \x54\x72\x61\x69\x6c\x69\x6e\x67\x20\x73\x6c\x61\x73\x68\x65\x73\ \x2f\x20\x64\x6f\x6e\x27\x74\x20\x6d\x61\x74\x74\x65\x72\x2e\x07\ \x00\x00\x00\x06\x44\x69\x61\x6c\x6f\x67\x01\x03\x00\x00\x01\x12\ \x00\x42\x00\x6c\x00\x61\x00\x63\x00\x6b\x00\x6c\x00\x69\x00\x73\ \x00\x74\x00\x20\x00\x2d\x00\x20\x00\x41\x00\x75\x00\x73\x00\x66\ \x00\xfc\x00\x68\x00\x72\x00\x62\x00\x61\x00\x72\x00\x65\x00\x20\ \x00\x44\x00\x61\x00\x74\x00\x65\x00\x69\x00\x6e\x00\x61\x00\x6d\ \x00\x65\x00\x6e\x00\x20\x00\x28\x00\x6b\x00\x65\x00\x69\x00\x6e\ \x00\x65\x00\x20\x00\x67\x00\x61\x00\x6e\x00\x7a\x00\x65\x00\x6e\ \x00\x20\x00\x50\x00\x66\x00\x61\x00\x64\x00\x65\x00\x29\x00\x20\ \x00\x68\x00\x69\x00\x6e\x00\x7a\x00\x75\x00\x66\x00\xfc\x00\x67\ \x00\x65\x00\x6e\x00\x20\x00\x75\x00\x6d\x00\x20\x00\x64\x00\x69\ \x00\x65\x00\x73\x00\x65\x00\x20\x00\x61\x00\x75\x00\x73\x00\x20\ \x00\x64\x00\x65\x00\x6d\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x67\ \x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x73\x00\x74\x00\x61\x00\x72\ \x00\x74\x00\x65\x00\x72\x00\x20\x00\x7a\x00\x75\x00\x20\x00\x65\ \x00\x6e\x00\x74\x00\x66\x00\x65\x00\x72\x00\x6e\x00\x65\x00\x6e\ \x00\x2e\x00\x20\x00\x45\x00\x69\x00\x6e\x00\x20\x00\x50\x00\x72\ \x00\x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x20\x00\x70\ \x00\x72\x00\x6f\x00\x20\x00\x5a\x00\x65\x00\x69\x00\x6c\x00\x65\ \x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x64\x42\x6c\x61\x63\ \x6b\x6c\x69\x73\x74\x20\x2d\x20\x45\x78\x63\x6c\x75\x64\x65\x20\ \x65\x78\x65\x63\x75\x74\x61\x62\x6c\x65\x20\x6e\x61\x6d\x65\x73\ \x20\x28\x6e\x6f\x74\x20\x70\x61\x74\x68\x73\x29\x20\x66\x72\x6f\ \x6d\x20\x74\x68\x65\x20\x70\x72\x6f\x67\x72\x61\x6d\x20\x6c\x61\ \x75\x6e\x63\x68\x65\x72\x2e\x20\x4f\x6e\x65\x20\x65\x78\x65\x63\ \x75\x74\x61\x62\x6c\x65\x20\x70\x65\x72\x20\x6c\x69\x6e\x65\x2e\ \x07\x00\x00\x00\x06\x44\x69\x61\x6c\x6f\x67\x01\x03\x00\x00\x02\ \x5c\x00\x4e\x00\x75\x00\x72\x00\x20\x00\x66\x00\xfc\x00\x72\x00\ \x20\x00\x66\x00\x6f\x00\x72\x00\x74\x00\x67\x00\x65\x00\x73\x00\ \x63\x00\x68\x00\x72\x00\x69\x00\x74\x00\x74\x00\x65\x00\x6e\x00\ \x65\x00\x20\x00\x42\x00\x65\x00\x6e\x00\x75\x00\x74\x00\x7a\x00\ \x65\x00\x72\x00\x21\x00\x20\x00\x48\x00\x69\x00\x65\x00\x72\x00\ \x20\x00\x61\x00\x62\x00\x73\x00\x6f\x00\x6c\x00\x75\x00\x74\x00\ \x65\x00\x20\x00\x50\x00\x66\x00\x61\x00\x64\x00\x65\x00\x20\x00\ \x68\x00\x69\x00\x6e\x00\x7a\x00\x75\x00\x66\x00\xfc\x00\x67\x00\ \x65\x00\x6e\x00\x2c\x00\x20\x00\x69\x00\x6e\x00\x20\x00\x64\x00\ \x65\x00\x6e\x00\x65\x00\x6e\x00\x20\x00\x73\x00\x69\x00\x63\x00\ \x68\x00\x20\x00\x61\x00\x75\x00\x73\x00\x66\x00\xfc\x00\x68\x00\ \x72\x00\x62\x00\x61\x00\x72\x00\x65\x00\x20\x00\x44\x00\x61\x00\ \x74\x00\x65\x00\x69\x00\x65\x00\x6e\x00\x20\x00\x62\x00\x65\x00\ \x66\x00\x69\x00\x6e\x00\x64\x00\x65\x00\x6e\x00\x2e\x00\x20\x00\ \x44\x00\x69\x00\x65\x00\x73\x00\x65\x00\x20\x00\x45\x00\x69\x00\ \x6e\x00\x73\x00\x74\x00\x65\x00\x6c\x00\x6c\x00\x75\x00\x6e\x00\ \x67\x00\x65\x00\x6e\x00\x20\x00\x67\x00\x65\x00\x6c\x00\x74\x00\ \x65\x00\x6e\x00\x20\x00\x6e\x00\x75\x00\x72\x00\x20\x00\x66\x00\ \xfc\x00\x72\x00\x20\x00\x41\x00\x67\x00\x6f\x00\x72\x00\x64\x00\ \x65\x00\x6a\x00\x6f\x00\x2f\x00\x4e\x00\x53\x00\x4d\x00\x2e\x00\ \x20\x00\xc4\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x75\x00\x6e\x00\ \x67\x00\x65\x00\x6e\x00\x20\x00\x62\x00\x65\x00\x6e\x00\xf6\x00\ \x74\x00\x69\x00\x67\x00\x65\x00\x6e\x00\x20\x00\x65\x00\x69\x00\ \x6e\x00\x65\x00\x6e\x00\x20\x00\x6b\x00\x6f\x00\x6d\x00\x70\x00\ \x6c\x00\x65\x00\x74\x00\x74\x00\x65\x00\x6e\x00\x20\x00\x50\x00\ \x72\x00\x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x6e\x00\ \x65\x00\x75\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x2e\x00\ \x20\x00\x55\x00\x6d\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x67\x00\ \x72\x00\x61\x00\x6d\x00\x6d\x00\x65\x00\x20\x00\x7a\x00\x75\x00\ \x6d\x00\x20\x00\x53\x00\x63\x00\x68\x00\x6e\x00\x65\x00\x6c\x00\ \x6c\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x65\x00\x72\x00\ \x20\x00\x68\x00\x69\x00\x6e\x00\x7a\x00\x75\x00\x7a\x00\x75\x00\ \x66\x00\xfc\x00\x67\x00\x65\x00\x6e\x00\x20\x00\x62\x00\x69\x00\ \x74\x00\x74\x00\x65\x00\x20\x00\x64\x00\x65\x00\x6e\x00\x20\x00\ \x54\x00\x61\x00\x62\x00\x20\x00\x22\x00\x50\x00\x72\x00\x6f\x00\ \x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x73\x00\x74\x00\x61\x00\ \x72\x00\x74\x00\x65\x00\x72\x00\x22\x00\x20\x00\x62\x00\x65\x00\ \x6e\x00\x75\x00\x74\x00\x7a\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\xd3\x46\x6f\x72\x20\x61\x64\x76\x61\x6e\ \x63\x65\x64\x20\x75\x73\x65\x72\x73\x20\x6f\x6e\x6c\x79\x21\x20\ \x41\x64\x64\x20\x65\x78\x65\x63\x75\x74\x61\x62\x6c\x65\x20\x70\ \x61\x74\x68\x73\x20\x74\x6f\x20\x74\x68\x65\x20\x65\x6e\x76\x69\ \x72\x6f\x6e\x6d\x65\x6e\x74\x2c\x20\x6a\x75\x73\x74\x20\x66\x6f\ \x72\x20\x41\x67\x6f\x72\x64\x65\x6a\x6f\x20\x61\x6e\x64\x20\x4e\ \x53\x4d\x2e\x20\x43\x68\x61\x6e\x67\x65\x73\x20\x6e\x65\x65\x64\ \x20\x61\x20\x70\x72\x6f\x67\x72\x61\x6d\x20\x72\x65\x73\x74\x61\ \x72\x74\x20\x61\x66\x74\x65\x72\x77\x61\x72\x64\x73\x2e\x20\x49\ \x66\x20\x79\x6f\x75\x20\x77\x61\x6e\x74\x20\x79\x6f\x75\x72\x20\ \x70\x72\x6f\x67\x72\x61\x6d\x73\x20\x69\x6e\x20\x74\x68\x65\x20\ \x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x20\x6c\x61\x75\x6e\ \x63\x68\x65\x72\x20\x75\x73\x65\x20\x74\x68\x65\x20\x6c\x61\x75\ \x6e\x63\x68\x65\x72\x20\x74\x61\x62\x2e\x07\x00\x00\x00\x06\x44\ \x69\x61\x6c\x6f\x67\x01\x03\x00\x00\x00\x1e\x00\x50\x00\x72\x00\ \x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x73\x00\x74\x00\ \x61\x00\x72\x00\x74\x00\x65\x00\x72\x08\x00\x00\x00\x00\x06\x00\ \x00\x00\x08\x4c\x61\x75\x6e\x63\x68\x65\x72\x07\x00\x00\x00\x06\ \x44\x69\x61\x6c\x6f\x67\x01\x03\x00\x00\x00\x1a\x00\x45\x00\x69\ \x00\x6e\x00\x73\x00\x74\x00\x65\x00\x6c\x00\x6c\x00\x75\x00\x6e\ \x00\x67\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x08\ \x53\x65\x74\x74\x69\x6e\x67\x73\x07\x00\x00\x00\x06\x44\x69\x61\ \x6c\x6f\x67\x01\x03\x00\x00\x01\x04\x00\x57\x00\x68\x00\x69\x00\ \x74\x00\x65\x00\x6c\x00\x69\x00\x73\x00\x74\x00\x20\x00\x2d\x00\ \x20\x00\x41\x00\x75\x00\x73\x00\x66\x00\xfc\x00\x68\x00\x72\x00\ \x62\x00\x61\x00\x72\x00\x65\x00\x20\x00\x44\x00\x61\x00\x74\x00\ \x65\x00\x69\x00\x6e\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\x20\x00\ \x28\x00\x6b\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x20\x00\x67\x00\ \x61\x00\x6e\x00\x7a\x00\x65\x00\x6e\x00\x20\x00\x50\x00\x66\x00\ \x61\x00\x64\x00\x65\x00\x29\x00\x20\x00\x68\x00\x69\x00\x6e\x00\ \x7a\x00\x75\x00\x66\x00\xfc\x00\x67\x00\x65\x00\x6e\x00\x20\x00\ \x75\x00\x6d\x00\x20\x00\x64\x00\x69\x00\x65\x00\x73\x00\x65\x00\ \x20\x00\x69\x00\x6d\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x67\x00\ \x72\x00\x61\x00\x6d\x00\x6d\x00\x73\x00\x74\x00\x61\x00\x72\x00\ \x74\x00\x65\x00\x72\x00\x20\x00\x61\x00\x6e\x00\x7a\x00\x75\x00\ \x7a\x00\x65\x00\x69\x00\x67\x00\x65\x00\x6e\x00\x2e\x00\x20\x00\ \x45\x00\x69\x00\x6e\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x67\x00\ \x72\x00\x61\x00\x6d\x00\x6d\x00\x20\x00\x70\x00\x72\x00\x6f\x00\ \x20\x00\x5a\x00\x65\x00\x69\x00\x6c\x00\x65\x00\x2e\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x5e\x57\x68\x69\x74\x65\x6c\x69\x73\x74\ \x20\x2d\x20\x41\x64\x64\x20\x65\x78\x65\x63\x75\x74\x61\x62\x6c\ \x65\x20\x6e\x61\x6d\x65\x73\x20\x28\x6e\x6f\x74\x20\x70\x61\x74\ \x68\x73\x29\x20\x74\x6f\x20\x74\x68\x65\x20\x70\x72\x6f\x67\x72\ \x61\x6d\x20\x6c\x61\x75\x6e\x63\x68\x65\x72\x2e\x20\x4f\x6e\x65\ \x20\x65\x78\x65\x63\x75\x74\x61\x62\x6c\x65\x20\x70\x65\x72\x20\ \x6c\x69\x6e\x65\x2e\x07\x00\x00\x00\x06\x44\x69\x61\x6c\x6f\x67\ \x01\x03\x00\x00\x00\x18\x00\x42\x00\x65\x00\x73\x00\x63\x00\x68\ \x00\x72\x00\x65\x00\x69\x00\x62\x00\x75\x00\x6e\x00\x67\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x0b\x44\x65\x73\x63\x72\x69\x70\x74\ \x69\x6f\x6e\x07\x00\x00\x00\x08\x4c\x61\x75\x6e\x63\x68\x65\x72\ \x01\x03\x00\x00\x00\x08\x00\x4e\x00\x61\x00\x6d\x00\x65\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x04\x4e\x61\x6d\x65\x07\x00\x00\x00\ \x08\x4c\x61\x75\x6e\x63\x68\x65\x72\x01\x03\x00\x00\x00\x08\x00\ \x50\x00\x66\x00\x61\x00\x64\x08\x00\x00\x00\x00\x06\x00\x00\x00\ \x04\x50\x61\x74\x68\x07\x00\x00\x00\x08\x4c\x61\x75\x6e\x63\x68\ \x65\x72\x01\x03\x00\x00\x00\xee\x00\x55\x00\x6d\x00\x20\x00\x68\ \x00\x69\x00\x65\x00\x72\x00\x20\x00\x7a\x00\x75\x00\x20\x00\x73\ \x00\x63\x00\x68\x00\x72\x00\x65\x00\x69\x00\x62\x00\x65\x00\x6e\ \x00\x20\x00\x64\x00\x6f\x00\x70\x00\x70\x00\x65\x00\x6c\x00\x6b\ \x00\x6c\x00\x69\x00\x63\x00\x6b\x00\x65\x00\x6e\x00\x20\x00\x53\ \x00\x69\x00\x65\x00\x20\x00\x61\x00\x75\x00\x66\x00\x20\x00\x64\ \x00\x69\x00\x65\x00\x73\x00\x65\x00\x73\x00\x20\x00\x46\x00\x65\ \x00\x6c\x00\x64\x00\x20\x00\x28\x00\x73\x00\x74\x00\x61\x00\x72\ \x00\x74\x00\x65\x00\x74\x00\x20\x00\x6e\x00\x73\x00\x6d\x00\x2d\ \x00\x64\x00\x61\x00\x74\x00\x61\x00\x29\x00\x0a\x00\x46\x00\xfc\ \x00\x72\x00\x20\x00\x4e\x00\x6f\x00\x74\x00\x69\x00\x7a\x00\x65\ \x00\x6e\x00\x2c\x00\x20\x00\x54\x00\x4f\x00\x44\x00\x4f\x00\x2c\ \x00\x20\x00\x52\x00\x65\x00\x66\x00\x65\x00\x72\x00\x65\x00\x6e\ \x00\x7a\x00\x65\x00\x6e\x00\x2c\x00\x20\x00\x51\x00\x75\x00\x65\ \x00\x6c\x00\x6c\x00\x65\x00\x6e\x00\x20\x00\x65\x00\x74\x00\x63\ \x00\xe2\x00\x80\x00\xa6\x08\x00\x00\x00\x00\x06\x00\x00\x00\x63\ \x44\x6f\x75\x62\x6c\x65\x20\x63\x6c\x69\x63\x6b\x20\x74\x6f\x20\ \x61\x64\x64\x20\x74\x68\x65\x20\x63\x6c\x69\x65\x6e\x74\x20\x6e\ \x73\x6d\x2d\x64\x61\x74\x61\x20\x74\x6f\x20\x77\x72\x69\x74\x65\ \x20\x68\x65\x72\x65\x2e\x0a\x55\x73\x65\x20\x69\x74\x20\x66\x6f\ \x72\x20\x6e\x6f\x74\x65\x73\x2c\x20\x54\x4f\x44\x4f\x2c\x20\x72\ \x65\x66\x65\x72\x65\x6e\x63\x65\x73\x20\x65\x74\x63\xc3\xa2\xc2\ \x80\xc2\xa6\x07\x00\x00\x00\x18\x4c\x6f\x61\x64\x65\x64\x53\x65\ \x73\x73\x69\x6f\x6e\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\ \x01\x03\x00\x00\x00\x08\x00\x20\x00\x6d\x00\x69\x00\x6e\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x04\x20\x6d\x69\x6e\x07\x00\x00\x00\ \x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\ \x22\x00\x20\x00\x74\x00\x69\x00\x6d\x00\x65\x00\x2d\x00\x70\x00\ \x6c\x00\x61\x00\x63\x00\x65\x00\x68\x00\x6f\x00\x6c\x00\x64\x00\ \x65\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\x11\x20\x74\x69\ \x6d\x65\x2d\x70\x6c\x61\x63\x65\x68\x6f\x6c\x64\x65\x72\x07\x00\ \x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\ \xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x16\x2f\x68\x6f\ \x6d\x65\x2f\x75\x73\x72\x2f\x4e\x53\x4d\x20\x53\x65\x73\x73\x69\ \x6f\x6e\x73\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\ \x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\ \x00\x01\x41\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\ \x6f\x77\x01\x03\x00\x00\x00\x08\x00\xdc\x00\x62\x00\x65\x00\x72\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x05\x41\x62\x6f\x75\x74\x07\ \x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\ \x00\x00\x00\x38\x00\x50\x00\x72\x00\x6f\x00\x67\x00\x72\x00\x61\ \x00\x6d\x00\x6d\x00\x20\x00\x68\x00\x69\x00\x6e\x00\x7a\x00\x75\ \x00\x66\x00\xfc\x00\x67\x00\x65\x00\x6e\x00\x20\x00\x28\x00\x50\ \x00\x72\x00\x6f\x00\x6d\x00\x70\x00\x74\x00\x29\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x13\x41\x64\x64\x20\x43\x6c\x69\x65\x6e\x74\ \x20\x28\x50\x72\x6f\x6d\x70\x74\x29\x07\x00\x00\x00\x0a\x4d\x61\ \x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x10\x00\x41\ \x00\x67\x00\x6f\x00\x72\x00\x64\x00\x65\x00\x6a\x00\x6f\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x08\x41\x67\x6f\x72\x64\x65\x6a\x6f\ \x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\ \x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x05\x41\ \x6c\x74\x2b\x4f\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\ \x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\ \x00\x00\x05\x41\x6c\x74\x2b\x52\x07\x00\x00\x00\x0a\x4d\x61\x69\ \x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x05\x41\x6c\x74\x2b\x53\x07\x00\x00\x00\ \x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\ \xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x05\x41\x6c\x74\x2b\x54\ \x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\ \x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x05\x41\ \x6c\x74\x2b\x58\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\ \x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\ \x00\x00\x0c\x43\x6c\x69\x65\x6e\x74\x4e\x61\x6d\x65\x49\x64\x07\ \x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\ \x00\x00\x00\x52\x00\x53\x00\x63\x00\x68\x00\x6c\x00\x69\x00\x65\ \x00\xdf\x00\x65\x00\x6e\x00\x20\x00\x6f\x00\x68\x00\x6e\x00\x65\ \x00\x20\x00\x7a\x00\x75\x00\x20\x00\x53\x00\x70\x00\x65\x00\x69\ \x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\x00\x20\x00\x28\x00\x22\ \x00\x41\x00\x62\x00\x62\x00\x72\x00\x65\x00\x63\x00\x68\x00\x65\ \x00\x6e\x00\x22\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1c\ \x43\x6c\x6f\x73\x65\x20\x77\x69\x74\x68\x6f\x75\x74\x20\x53\x61\ \x76\x65\x20\x28\x22\x41\x62\x6f\x72\x74\x22\x29\x07\x00\x00\x00\ \x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\ \x12\x00\x53\x00\x74\x00\x65\x00\x75\x00\x65\x00\x72\x00\x75\x00\ \x6e\x00\x67\x08\x00\x00\x00\x00\x06\x00\x00\x00\x07\x43\x6f\x6e\ \x74\x72\x6f\x6c\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\ \x64\x6f\x77\x01\x03\x00\x00\x00\x10\x00\x4b\x00\x6f\x00\x70\x00\ \x69\x00\x65\x00\x72\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\ \x00\x00\x0d\x43\x6f\x70\x79\x20\x53\x65\x6c\x65\x63\x74\x65\x64\ \x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\ \x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\x43\ \x74\x72\x6c\x2b\x51\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\ \x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\ \x00\x00\x00\x06\x43\x74\x72\x6c\x2b\x53\x07\x00\x00\x00\x0a\x4d\ \x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x0c\x43\x74\x72\x6c\x2b\x53\x68\ \x69\x66\x74\x2b\x51\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\ \x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\ \x00\x00\x00\x0c\x43\x74\x72\x6c\x2b\x53\x68\x69\x66\x74\x2b\x53\ \x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\ \x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0c\x43\ \x74\x72\x6c\x2b\x53\x68\x69\x66\x74\x2b\x57\x07\x00\x00\x00\x0a\ \x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\x43\x74\x72\x6c\x2b\x57\ \x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\ \x03\x00\x00\x00\x0e\x00\x4c\x00\xf6\x00\x73\x00\x63\x00\x68\x00\ \x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0f\x44\x65\x6c\ \x65\x74\x65\x20\x53\x65\x6c\x65\x63\x74\x65\x64\x07\x00\x00\x00\ \x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\ \x44\x00\x44\x00\x6f\x00\x70\x00\x70\x00\x65\x00\x6c\x00\x6b\x00\ \x6c\x00\x69\x00\x63\x00\x6b\x00\x20\x00\x75\x00\x6d\x00\x20\x00\ \x50\x00\x72\x00\x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\ \x20\x00\x7a\x00\x75\x00\x20\x00\x73\x00\x74\x00\x61\x00\x72\x00\ \x74\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1c\x44\ \x6f\x75\x62\x6c\x65\x2d\x63\x6c\x69\x63\x6b\x20\x74\x6f\x20\x6c\ \x6f\x61\x64\x20\x70\x72\x6f\x67\x72\x61\x6d\x07\x00\x00\x00\x0a\ \x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x02\x46\x32\x07\x00\x00\x00\ \x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\ \x1a\x00\x56\x00\x6f\x00\x6c\x00\x6c\x00\x65\x00\x20\x00\x41\x00\ \x6e\x00\x73\x00\x69\x00\x63\x00\x68\x00\x74\x08\x00\x00\x00\x00\ \x06\x00\x00\x00\x09\x46\x75\x6c\x6c\x20\x56\x69\x65\x77\x07\x00\ \x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\ \x00\x00\x3c\x00\x47\x00\x6c\x00\x6f\x00\x62\x00\x61\x00\x6c\x00\ \x65\x00\x72\x00\x20\x00\x5a\x00\x75\x00\x67\x00\x72\x00\x69\x00\ \x66\x00\x66\x00\x20\x00\x61\x00\x75\x00\x66\x00\x73\x00\x20\x00\ \x50\x00\x6c\x00\x61\x00\x79\x00\x62\x00\x61\x00\x63\x00\x6b\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x18\x47\x6c\x6f\x62\x61\x6c\x20\ \x50\x6c\x61\x79\x62\x61\x63\x6b\x20\x43\x6f\x6e\x74\x72\x6f\x6c\ \x73\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\ \x01\x03\x00\x00\x00\x2c\x00\x56\x00\x65\x00\x72\x00\x73\x00\x74\ \x00\x65\x00\x63\x00\x6b\x00\x65\x00\x20\x00\x61\x00\x6c\x00\x6c\ \x00\x65\x00\x20\x00\x43\x00\x6c\x00\x69\x00\x65\x00\x6e\x00\x74\ \x00\x73\x08\x00\x00\x00\x00\x06\x00\x00\x00\x10\x48\x69\x64\x65\ \x20\x41\x6c\x6c\x20\x43\x6c\x69\x65\x6e\x74\x73\x07\x00\x00\x00\ \x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\ \x26\x00\x4d\x00\x69\x00\x6e\x00\x69\x00\x6d\x00\x69\x00\x65\x00\ \x72\x00\x65\x00\x6e\x00\x20\x00\x7a\x00\x75\x00\x6d\x00\x20\x00\ \x54\x00\x72\x00\x61\x00\x79\x08\x00\x00\x00\x00\x06\x00\x00\x00\ \x13\x48\x69\x64\x65\x20\x69\x6e\x20\x53\x79\x73\x74\x65\x6d\x20\ \x54\x72\x61\x79\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\ \x64\x6f\x77\x01\x03\x00\x00\x00\x70\x00\x49\x00\x63\x00\x68\x00\ \x20\x00\x76\x00\x65\x00\x72\x00\x73\x00\x74\x00\x65\x00\x68\x00\ \x65\x00\x2c\x00\x20\x00\x64\x00\x61\x00\x73\x00\x73\x00\x20\x00\ \x69\x00\x63\x00\x68\x00\x20\x00\x64\x00\x69\x00\x65\x00\x73\x00\ \x65\x00\x73\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x62\x00\x6c\x00\ \x65\x00\x6d\x00\x20\x00\x73\x00\x65\x00\x6c\x00\x62\x00\x73\x00\ \x74\x00\x20\x00\x6c\x00\xf6\x00\x73\x00\x65\x00\x6e\x00\x20\x00\ \x6d\x00\x75\x00\x73\x00\x73\x00\x21\x08\x00\x00\x00\x00\x06\x00\ \x00\x00\x40\x49\x20\x75\x6e\x64\x65\x72\x73\x74\x61\x6e\x64\x20\ \x74\x68\x61\x74\x20\x49\x20\x77\x69\x6c\x6c\x20\x6e\x65\x65\x64\ \x20\x74\x6f\x20\x72\x65\x73\x6f\x6c\x76\x65\x20\x74\x68\x69\x73\ \x20\x70\x72\x6f\x62\x6c\x65\x6d\x20\x6f\x6e\x20\x6d\x79\x20\x6f\ \x77\x6e\x21\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\ \x6f\x77\x01\x03\x00\x00\x00\x2e\x00\x4d\x00\x6f\x00\x6d\x00\x65\ \x00\x6e\x00\x74\x00\x61\x00\x6e\x00\x20\x00\x69\x00\x6e\x00\x20\ \x00\x64\x00\x65\x00\x72\x00\x20\x00\x53\x00\x65\x00\x73\x00\x73\ \x00\x69\x00\x6f\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x12\ \x49\x6e\x20\x63\x75\x72\x72\x65\x6e\x74\x20\x73\x65\x73\x73\x69\ \x6f\x6e\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\ \x77\x01\x03\x00\x00\x00\x16\x00\x49\x00\x6e\x00\x66\x00\x6f\x00\ \x72\x00\x6d\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x0b\x49\x6e\x66\x6f\x72\x6d\x61\x74\x69\ \x6f\x6e\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\ \x77\x01\x03\x00\x00\x00\x08\x00\x4a\x00\x41\x00\x43\x00\x4b\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x04\x4a\x41\x43\x4b\x07\x00\x00\ \x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\ \xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0c\x4c\x61\x73\x74\ \x20\x55\x70\x64\x61\x74\x65\x64\x07\x00\x00\x00\x0a\x4d\x61\x69\ \x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x20\x00\x4c\x00\ \x61\x00\x64\x00\x65\x00\x20\x00\x41\x00\x75\x00\x73\x00\x67\x00\ \x65\x00\x77\x00\xe4\x00\x68\x00\x6c\x00\x74\x00\x65\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x0d\x4c\x6f\x61\x64\x20\x53\x65\x6c\x65\ \x63\x74\x65\x64\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\ \x64\x6f\x77\x01\x03\x00\x00\x00\x10\x00\x48\x00\x61\x00\x6e\x00\ \x64\x00\x62\x00\x75\x00\x63\x00\x68\x08\x00\x00\x00\x00\x06\x00\ \x00\x00\x06\x4d\x61\x6e\x75\x61\x6c\x07\x00\x00\x00\x0a\x4d\x61\ \x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x0f\x4e\x53\x4d\x20\x53\x65\x72\x76\ \x65\x72\x20\x4d\x6f\x64\x65\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\ \x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x07\x4e\x53\x4d\x20\x55\x72\x6c\x07\x00\x00\ \x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\ \x00\x06\x00\x4e\x00\x65\x00\x75\x08\x00\x00\x00\x00\x06\x00\x00\ \x00\x03\x4e\x65\x77\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\ \x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x12\x00\x50\x00\x6c\x00\x61\ \x00\x79\x00\x50\x00\x61\x00\x75\x00\x73\x00\x65\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x09\x50\x6c\x61\x79\x50\x61\x75\x73\x65\x07\ \x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\ \x00\x00\x00\x10\x00\x41\x00\x72\x00\x62\x00\x65\x00\x69\x00\x74\ \x00\x65\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0a\x50\x72\ \x6f\x63\x65\x73\x73\x69\x6e\x67\x07\x00\x00\x00\x0a\x4d\x61\x69\ \x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x10\x50\x72\x6f\x67\x72\x61\x6d\x20\x44\ \x61\x74\x61\x62\x61\x73\x65\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\ \x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x20\x00\x53\x00\x63\ \x00\x68\x00\x6e\x00\x65\x00\x6c\x00\x6c\x00\x73\x00\x74\x00\x61\ \x00\x72\x00\x74\x00\x20\x00\x4e\x00\x65\x00\x75\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x09\x51\x75\x69\x63\x6b\x20\x4e\x65\x77\x07\ \x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\ \x00\x00\x00\x0e\x00\x42\x00\x65\x00\x65\x00\x6e\x00\x64\x00\x65\ \x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x51\x75\x69\x74\ \x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\ \x03\x00\x00\x00\x38\x00\x50\x00\x72\x00\x6f\x00\x67\x00\x72\x00\ \x61\x00\x6d\x00\x6d\x00\x64\x00\x61\x00\x74\x00\x65\x00\x6e\x00\ \x62\x00\x61\x00\x6e\x00\x6b\x00\x20\x00\x67\x00\x65\x00\x6e\x00\ \x65\x00\x72\x00\x69\x00\x65\x00\x72\x00\x65\x00\x6e\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x18\x52\x65\x62\x75\x69\x6c\x64\x20\x50\ \x72\x6f\x67\x72\x61\x6d\x20\x44\x61\x74\x61\x62\x61\x73\x65\x07\ \x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\ \x00\x00\x00\x12\x00\x45\x00\x6e\x00\x74\x00\x66\x00\x65\x00\x72\ \x00\x6e\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\ \x52\x65\x6d\x6f\x76\x65\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\ \x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x1a\x00\x4c\x00\x6f\x00\ \x63\x00\x6b\x00\x20\x00\x61\x00\x75\x00\x66\x00\x68\x00\x65\x00\ \x62\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0b\x52\ \x65\x6d\x6f\x76\x65\x20\x4c\x6f\x63\x6b\x07\x00\x00\x00\x0a\x4d\ \x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x14\x00\ \x55\x00\x6d\x00\x62\x00\x65\x00\x6e\x00\x65\x00\x6e\x00\x6e\x00\ \x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\x52\x65\x6e\ \x61\x6d\x65\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\ \x6f\x77\x01\x03\x00\x00\x00\x18\x00\x4e\x00\x61\x00\x6d\x00\x65\ \x00\x6e\x00\x20\x00\xe4\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x6e\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x0f\x52\x65\x6e\x61\x6d\x65\ \x20\x53\x65\x6c\x65\x63\x74\x65\x64\x07\x00\x00\x00\x0a\x4d\x61\ \x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x14\x00\x46\ \x00\x6f\x00\x72\x00\x74\x00\x66\x00\xfc\x00\x68\x00\x72\x00\x65\ \x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\x52\x65\x73\x75\ \x6d\x65\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\ \x77\x01\x03\x00\x00\x00\x0c\x00\x52\x00\x65\x00\x77\x00\x69\x00\ \x6e\x00\x64\x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\x52\x65\x77\ \x69\x6e\x64\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\ \x6f\x77\x01\x03\x00\x00\x00\x12\x00\x53\x00\x70\x00\x65\x00\x69\ \x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\x08\x00\x00\x00\x00\x06\ \x00\x00\x00\x04\x53\x61\x76\x65\x07\x00\x00\x00\x0a\x4d\x61\x69\ \x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x54\x00\x53\x00\ \x70\x00\x65\x00\x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\x00\ \x20\x00\x75\x00\x6e\x00\x64\x00\x20\x00\x6d\x00\x69\x00\x74\x00\ \x20\x00\x61\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x65\x00\x6d\x00\ \x20\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\x20\x00\x6e\x00\ \x65\x00\x75\x00\x20\x00\xf6\x00\x66\x00\x66\x00\x6e\x00\x65\x00\ \x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x23\x53\x61\x76\x65\x20\ \x61\x6e\x64\x20\x43\x6c\x6f\x6e\x65\x20\x75\x6e\x64\x65\x72\x20\ \x64\x69\x66\x66\x65\x72\x65\x6e\x74\x20\x6e\x61\x6d\x65\x07\x00\ \x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\ \x00\x00\x2e\x00\x53\x00\x70\x00\x65\x00\x69\x00\x63\x00\x68\x00\ \x65\x00\x72\x00\x6e\x00\x20\x00\x75\x00\x6e\x00\x64\x00\x20\x00\ \x53\x00\x63\x00\x68\x00\x6c\x00\x69\x00\x65\x00\xdf\x00\x65\x00\ \x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\x53\x61\x76\x65\x20\ \x61\x6e\x64\x20\x43\x6c\x6f\x73\x65\x07\x00\x00\x00\x0a\x4d\x61\ \x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x22\x00\x53\ \x00\x65\x00\x70\x00\x65\x00\x72\x00\x61\x00\x74\x00\x20\x00\x73\ \x00\x70\x00\x65\x00\x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x0f\x53\x61\x76\x65\x20\x73\ \x65\x70\x61\x72\x61\x74\x65\x6c\x79\x07\x00\x00\x00\x0a\x4d\x61\ \x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x2b\x53\x65\x6c\x66\x2d\x73\x74\x61\ \x72\x74\x65\x64\x2c\x20\x63\x6f\x6e\x6e\x65\x63\x74\x65\x64\x20\ \x74\x6f\x2c\x20\x65\x6e\x76\x69\x72\x6f\x6e\x6d\x65\x6e\x74\x20\ \x76\x61\x72\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\ \x6f\x77\x01\x03\x00\x00\x00\x1e\x00\x53\x00\x65\x00\x73\x00\x73\ \x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x4e\x00\x6f\x00\x74\x00\x69\ \x00\x7a\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0d\ \x53\x65\x73\x73\x69\x6f\x6e\x20\x4e\x6f\x74\x65\x73\x07\x00\x00\ \x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\ \xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0c\x53\x65\x73\x73\ \x69\x6f\x6e\x20\x52\x6f\x6f\x74\x07\x00\x00\x00\x0a\x4d\x61\x69\ \x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x4e\x61\ \x6d\x65\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\ \x77\x01\x03\x00\x00\x00\x1a\x00\x45\x00\x69\x00\x6e\x00\x73\x00\ \x74\x00\x65\x00\x6c\x00\x6c\x00\x75\x00\x6e\x00\x67\x00\x65\x00\ \x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x08\x53\x65\x74\x74\x69\ \x6e\x67\x73\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\ \x6f\x77\x01\x03\x00\x00\x00\x24\x00\x5a\x00\x65\x00\x69\x00\x67\ \x00\x65\x00\x20\x00\x61\x00\x6c\x00\x6c\x00\x65\x00\x20\x00\x43\ \x00\x6c\x00\x69\x00\x65\x00\x6e\x00\x74\x00\x73\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x10\x53\x68\x6f\x77\x20\x41\x6c\x6c\x20\x43\ \x6c\x69\x65\x6e\x74\x73\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\ \x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x08\x00\x53\x00\x74\x00\ \x6f\x00\x70\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x53\x74\x6f\ \x70\x07\x00\x00\x00\x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\ \x01\x03\x00\x00\x00\x2e\x00\x53\x00\x69\x00\x63\x00\x68\x00\x74\ \x00\x62\x00\x61\x00\x72\x00\x6b\x00\x65\x00\x69\x00\x74\x00\x20\ \x00\x75\x00\x6d\x00\x73\x00\x63\x00\x68\x00\x61\x00\x6c\x00\x74\ \x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\x54\x6f\ \x67\x67\x6c\x65\x20\x56\x69\x73\x69\x62\x6c\x65\x07\x00\x00\x00\ \x0a\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\ \x16\x00\x42\x00\x61\x00\x75\x00\x6d\x00\x61\x00\x6e\x00\x73\x00\ \x69\x00\x63\x00\x68\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\ \x09\x54\x72\x65\x65\x20\x56\x69\x65\x77\x07\x00\x00\x00\x0a\x4d\ \x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x0f\x6f\x73\x63\x2e\x75\x70\x64\ \x20\x69\x70\x20\x70\x6f\x72\x74\x07\x00\x00\x00\x0a\x4d\x61\x69\ \x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\xff\xff\xff\xff\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x13\x76\x65\x72\x73\x69\x6f\x6e\x20\x61\ \x6e\x64\x20\x72\x75\x6e\x6e\x69\x6e\x67\x07\x00\x00\x00\x0a\x4d\ \x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x01\x10\x00\ \x45\x00\x52\x00\x52\x00\x4f\x00\x52\x00\x21\x00\x20\x00\x4b\x00\ \x6f\x00\x70\x00\x69\x00\x65\x00\x72\x00\x74\x00\x65\x00\x20\x00\ \x44\x00\x61\x00\x74\x00\x65\x00\x6e\x00\x20\x00\x64\x00\x65\x00\ \x72\x00\x20\x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\ \x6e\x00\x20\x00\x75\x00\x6e\x00\x74\x00\x65\x00\x72\x00\x73\x00\ \x63\x00\x68\x00\x65\x00\x69\x00\x64\x00\x65\x00\x6e\x00\x20\x00\ \x73\x00\x69\x00\x63\x00\x68\x00\x20\x00\x76\x00\x6f\x00\x6e\x00\ \x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\x55\x00\x72\x00\x73\x00\ \x70\x00\x72\x00\xfc\x00\x6e\x00\x6c\x00\x69\x00\x6e\x00\x67\x00\ \x6c\x00\x69\x00\x63\x00\x68\x00\x65\x00\x6e\x00\x21\x00\x20\x00\ \x42\x00\x69\x00\x74\x00\x74\x00\x65\x00\x20\x00\xfc\x00\x62\x00\ \x65\x00\x72\x00\x70\x00\x72\x00\xfc\x00\x66\x00\x65\x00\x6e\x00\ \x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x64\x00\x69\x00\x65\x00\ \x20\x00\x49\x00\x6e\x00\x74\x00\x65\x00\x67\x00\x72\x00\x69\x00\ \x74\x00\xe4\x00\x74\x00\x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\ \x6b\x00\x6f\x00\x70\x00\x69\x00\x65\x00\x72\x00\x74\x00\x65\x00\ \x6e\x00\x20\x00\x44\x00\x61\x00\x74\x00\x65\x00\x6e\x00\x21\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x53\x45\x52\x52\x4f\x52\x21\x20\ \x43\x6f\x70\x69\x65\x64\x20\x73\x65\x73\x73\x69\x6f\x6e\x20\x64\ \x61\x74\x61\x20\x69\x73\x20\x64\x69\x66\x66\x65\x72\x65\x6e\x74\ \x20\x66\x72\x6f\x6d\x20\x73\x6f\x75\x72\x63\x65\x20\x73\x65\x73\ \x73\x69\x6f\x6e\x2e\x20\x50\x6c\x65\x61\x73\x65\x20\x63\x68\x65\ \x63\x6b\x20\x79\x6f\x75\x20\x64\x61\x74\x61\x21\x07\x00\x00\x00\ \x11\x4e\x4f\x4f\x50\x45\x6e\x67\x69\x6e\x65\x53\x74\x72\x69\x6e\ \x67\x73\x01\x03\x00\x00\x00\x82\x00\xdc\x00\x62\x00\x65\x00\x72\ \x00\x70\x00\x72\x00\xfc\x00\x66\x00\x65\x00\x20\x00\x49\x00\x6e\ \x00\x74\x00\x65\x00\x67\x00\x72\x00\x69\x00\x74\x00\xe4\x00\x74\ \x00\x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\x44\x00\x61\x00\x74\ \x00\x65\x00\x69\x00\x65\x00\x6e\x00\x2e\x00\x20\x00\x42\x00\x69\ \x00\x74\x00\x74\x00\x65\x00\x20\x00\x68\x00\x61\x00\x62\x00\x65\ \x00\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x65\x00\x74\ \x00\x77\x00\x61\x00\x73\x00\x20\x00\x47\x00\x65\x00\x64\x00\x75\ \x00\x6c\x00\x64\x00\x2e\x00\x2e\x00\x2e\x08\x00\x00\x00\x00\x06\ \x00\x00\x00\x32\x56\x65\x72\x79\x66\x79\x69\x6e\x67\x20\x66\x69\ \x6c\x65\x2d\x69\x6e\x74\x65\x67\x72\x69\x74\x79\x2e\x20\x54\x68\ \x69\x73\x20\x6d\x61\x79\x20\x74\x61\x6b\x65\x20\x61\x20\x77\x68\ \x69\x6c\x65\x2e\x2e\x2e\x07\x00\x00\x00\x11\x4e\x4f\x4f\x50\x45\ \x6e\x67\x69\x6e\x65\x53\x74\x72\x69\x6e\x67\x73\x01\x03\x00\x00\ \x00\x84\x00\x55\x00\x6d\x00\x62\x00\x65\x00\x6e\x00\x65\x00\x6e\ \x00\x6e\x00\x65\x00\x6e\x00\x20\x00\x75\x00\x6e\x00\x64\x00\x20\ \x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x2d\ \x00\x4e\x00\x6f\x00\x74\x00\x69\x00\x74\x00\x7a\x00\x65\x00\x6e\ \x00\x20\x00\x61\x00\x6b\x00\x74\x00\x69\x00\x76\x00\x69\x00\x65\ \x00\x72\x00\x65\x00\x6e\x00\x20\x00\x28\x00\x6d\x00\x69\x00\x74\ \x00\x20\x00\x43\x00\x6c\x00\x69\x00\x65\x00\x6e\x00\x74\x00\x20\ \x00\x27\x00\x6e\x00\x73\x00\x6d\x00\x2d\x00\x64\x00\x61\x00\x74\ \x00\x61\x00\x27\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\x00\x3a\ \x43\x6c\x69\x65\x6e\x74\x20\x52\x65\x6e\x61\x6d\x69\x6e\x67\x20\ \x61\x6e\x64\x20\x53\x65\x73\x73\x69\x6f\x6e\x20\x4e\x6f\x74\x65\ \x73\x0a\x28\x61\x64\x64\x73\x20\x63\x6c\x69\x65\x6e\x74\x20\x27\ \x6e\x73\x6d\x2d\x64\x61\x74\x61\x27\x29\x07\x00\x00\x00\x0a\x4e\ \x65\x77\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\xff\xff\xff\xff\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x06\x44\x69\x61\x6c\x6f\x67\x07\ \x00\x00\x00\x0a\x4e\x65\x77\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\ \x00\x00\x00\x2a\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x20\x00\x66\ \x00\xfc\x00\x72\x00\x20\x00\x6e\x00\x65\x00\x75\x00\x65\x00\x20\ \x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x10\x4e\x65\x77\x20\x53\x65\x73\x73\ \x69\x6f\x6e\x20\x4e\x61\x6d\x65\x07\x00\x00\x00\x0a\x4e\x65\x77\ \x53\x65\x73\x73\x69\x6f\x6e\x01\x03\x00\x00\x00\x68\x00\x53\x00\ \x70\x00\x65\x00\x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x65\x00\ \x20\x00\x4a\x00\x41\x00\x43\x00\x4b\x00\x2d\x00\x56\x00\x65\x00\ \x72\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\x00\x6e\x00\x67\x00\ \x65\x00\x6e\x00\x20\x00\x28\x00\x6d\x00\x69\x00\x74\x00\x20\x00\ \x43\x00\x6c\x00\x69\x00\x65\x00\x6e\x00\x74\x00\x20\x00\x27\x00\ \x6a\x00\x61\x00\x63\x00\x6b\x00\x70\x00\x61\x00\x74\x00\x63\x00\ \x68\x00\x27\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\x00\x30\x53\ \x61\x76\x65\x20\x4a\x41\x43\x4b\x20\x43\x6f\x6e\x6e\x65\x63\x74\ \x69\x6f\x6e\x73\x0a\x28\x61\x64\x64\x73\x20\x63\x6c\x69\x65\x6e\ \x74\x73\x20\x27\x6a\x61\x63\x6b\x70\x61\x74\x63\x68\x27\x29\x07\ \x00\x00\x00\x0a\x4e\x65\x77\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\ \x00\x00\x00\x14\x00\xc4\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x75\ \x00\x6e\x00\x67\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\ \x00\x07\x43\x68\x61\x6e\x67\x65\x73\x07\x00\x00\x00\x0b\x4f\x70\ \x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\x00\x00\x00\x04\x00\ \x49\x00\x44\x08\x00\x00\x00\x00\x06\x00\x00\x00\x02\x49\x44\x07\ \x00\x00\x00\x0b\x4f\x70\x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\ \x03\x00\x00\x00\x0a\x00\x4c\x00\x61\x00\x62\x00\x65\x00\x6c\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x05\x4c\x61\x62\x65\x6c\x07\x00\ \x00\x00\x0b\x4f\x70\x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\ \x00\x00\x00\x08\x00\x4e\x00\x61\x00\x6d\x00\x65\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x04\x4e\x61\x6d\x65\x07\x00\x00\x00\x0b\x4f\ \x70\x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\x00\x00\x00\x0c\ \x00\x53\x00\x74\x00\x61\x00\x74\x00\x75\x00\x73\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x06\x53\x74\x61\x74\x75\x73\x07\x00\x00\x00\ \x0b\x4f\x70\x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\x00\x00\ \x00\x18\x00\x53\x00\x69\x00\x63\x00\x68\x00\x74\x00\x62\x00\x61\ \x00\x72\x00\x6b\x00\x65\x00\x69\x00\x74\x08\x00\x00\x00\x00\x06\ \x00\x00\x00\x07\x56\x69\x73\x69\x62\x6c\x65\x07\x00\x00\x00\x0b\ \x4f\x70\x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\x00\x00\x00\ \x0a\x00\x63\x00\x6c\x00\x65\x00\x61\x00\x6e\x08\x00\x00\x00\x00\ \x06\x00\x00\x00\x05\x63\x6c\x65\x61\x6e\x07\x00\x00\x00\x0b\x4f\ \x70\x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\x00\x00\x00\x1a\ \x00\x75\x00\x6e\x00\x67\x00\x65\x00\x73\x00\x70\x00\x65\x00\x69\ \x00\x63\x00\x68\x00\x65\x00\x72\x00\x74\x08\x00\x00\x00\x00\x06\ \x00\x00\x00\x09\x6e\x6f\x74\x20\x73\x61\x76\x65\x64\x07\x00\x00\ \x00\x0b\x4f\x70\x65\x6e\x53\x65\x73\x73\x69\x6f\x6e\x01\x03\x00\ \x00\x00\xb2\x00\x57\x00\xe4\x00\x68\x00\x6c\x00\x65\x00\x6e\x00\ \x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x65\x00\x69\x00\x6e\x00\ \x65\x00\x6e\x00\x20\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\ \x20\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x64\x00\x69\x00\x65\x00\ \x20\x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\ \x2e\x00\x20\x00\x42\x00\x65\x00\x6e\x00\x75\x00\x74\x00\x7a\x00\ \x65\x00\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x2f\x00\ \x20\x00\x75\x00\x6d\x00\x20\x00\x55\x00\x6e\x00\x74\x00\x65\x00\ \x72\x00\x76\x00\x65\x00\x72\x00\x7a\x00\x65\x00\x69\x00\x63\x00\ \x68\x00\x6e\x00\x69\x00\x73\x00\x73\x00\x65\x00\x20\x00\x65\x00\ \x69\x00\x6e\x00\x7a\x00\x75\x00\x72\x00\x69\x00\x63\x00\x68\x00\ \x74\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x2f\x43\ \x68\x6f\x6f\x73\x65\x20\x61\x20\x70\x72\x6f\x6a\x65\x63\x74\x20\ \x6e\x61\x6d\x65\x2e\x20\x55\x73\x65\x20\x2f\x20\x66\x6f\x72\x20\ \x73\x75\x62\x64\x69\x72\x65\x63\x74\x6f\x72\x69\x65\x73\x07\x00\ \x00\x00\x0b\x50\x72\x6f\x6a\x65\x63\x74\x4e\x61\x6d\x65\x01\x03\ \x00\x00\x00\x1a\x00\x46\x00\x65\x00\x68\x00\x6c\x00\x65\x00\x72\ \x00\x6d\x00\x65\x00\x6c\x00\x64\x00\x75\x00\x6e\x00\x67\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x0d\x45\x72\x72\x6f\x72\x20\x4d\x65\ \x73\x73\x61\x67\x65\x07\x00\x00\x00\x0b\x50\x72\x6f\x6a\x65\x63\ \x74\x4e\x61\x6d\x65\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\ \x06\x00\x00\x00\x04\x46\x6f\x72\x6d\x07\x00\x00\x00\x0b\x50\x72\ \x6f\x6a\x65\x63\x74\x4e\x61\x6d\x65\x01\x03\x00\x00\x00\x70\x00\ \x44\x00\x65\x00\x72\x00\x20\x00\x5a\x00\x75\x00\x67\x00\x72\x00\ \x69\x00\x66\x00\x66\x00\x20\x00\x61\x00\x75\x00\x66\x00\x20\x00\ \x64\x00\x61\x00\x73\x00\x20\x00\x45\x00\x6c\x00\x74\x00\x65\x00\ \x72\x00\x6e\x00\x76\x00\x65\x00\x72\x00\x7a\x00\x65\x00\x69\x00\ \x63\x00\x68\x00\x6e\x00\x69\x00\x73\x00\x20\x00\x69\x00\x73\x00\ \x74\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\ \x65\x00\x72\x00\x6c\x00\x61\x00\x75\x00\x62\x00\x74\x00\x2e\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x27\x4d\x6f\x76\x69\x6e\x67\x20\ \x74\x6f\x20\x70\x61\x72\x65\x6e\x74\x20\x64\x69\x72\x65\x63\x74\ \x6f\x72\x79\x20\x6e\x6f\x74\x20\x61\x6c\x6c\x6f\x77\x65\x64\x2e\ \x07\x00\x00\x00\x11\x50\x72\x6f\x6a\x65\x63\x74\x4e\x61\x6d\x65\ \x57\x69\x64\x67\x65\x74\x01\x03\x00\x00\x00\x44\x00\x44\x00\x65\ \x00\x72\x00\x20\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x20\x00\x69\ \x00\x73\x00\x74\x00\x20\x00\x62\x00\x65\x00\x72\x00\x65\x00\x69\ \x00\x74\x00\x73\x00\x20\x00\x69\x00\x6e\x00\x20\x00\x42\x00\x65\ \x00\x6e\x00\x75\x00\x74\x00\x7a\x00\x75\x00\x6e\x00\x67\x00\x2e\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x4e\x61\x6d\x65\x20\x69\ \x73\x20\x61\x6c\x72\x65\x61\x64\x79\x20\x69\x6e\x20\x75\x73\x65\ \x2e\x07\x00\x00\x00\x11\x50\x72\x6f\x6a\x65\x63\x74\x4e\x61\x6d\ \x65\x57\x69\x64\x67\x65\x74\x01\x03\x00\x00\x00\x44\x00\x4e\x00\ \x61\x00\x6d\x00\x65\x00\x20\x00\x6d\x00\x75\x00\x73\x00\x73\x00\ \x20\x00\x65\x00\x69\x00\x6e\x00\x20\x00\x72\x00\x65\x00\x6c\x00\ \x61\x00\x74\x00\x69\x00\x76\x00\x65\x00\x72\x00\x20\x00\x50\x00\ \x66\x00\x61\x00\x64\x00\x20\x00\x73\x00\x65\x00\x69\x00\x6e\x00\ \x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1d\x4e\x61\x6d\x65\x20\ \x6d\x75\x73\x74\x20\x62\x65\x20\x61\x20\x72\x65\x6c\x61\x74\x69\ \x76\x65\x20\x70\x61\x74\x68\x2e\x07\x00\x00\x00\x11\x50\x72\x6f\ \x6a\x65\x63\x74\x4e\x61\x6d\x65\x57\x69\x64\x67\x65\x74\x01\x03\ \x00\x00\x00\x34\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x20\x00\x64\ \x00\x61\x00\x72\x00\x66\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\ \x00\x74\x00\x20\x00\x6c\x00\x65\x00\x65\x00\x72\x00\x20\x00\x73\ \x00\x65\x00\x69\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\ \x00\x17\x4e\x61\x6d\x65\x20\x6d\x75\x73\x74\x20\x6e\x6f\x74\x20\ \x62\x65\x20\x65\x6d\x70\x74\x79\x2e\x07\x00\x00\x00\x11\x50\x72\ \x6f\x6a\x65\x63\x74\x4e\x61\x6d\x65\x57\x69\x64\x67\x65\x74\x01\ \x03\x00\x00\x00\x6a\x00\x53\x00\x69\x00\x65\x00\x20\x00\x68\x00\ \x61\x00\x62\x00\x65\x00\x6e\x00\x20\x00\x6b\x00\x65\x00\x69\x00\ \x6e\x00\x65\x00\x20\x00\x53\x00\x63\x00\x68\x00\x72\x00\x65\x00\ \x69\x00\x62\x00\x72\x00\x65\x00\x63\x00\x68\x00\x74\x00\x65\x00\ \x20\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x64\x00\x69\x00\x65\x00\ \x73\x00\x65\x00\x73\x00\x20\x00\x56\x00\x65\x00\x72\x00\x7a\x00\ \x65\x00\x69\x00\x63\x00\x68\x00\x6e\x00\x69\x00\x73\x00\x2e\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x2b\x57\x72\x69\x74\x69\x6e\x67\ \x20\x69\x6e\x20\x74\x68\x69\x73\x20\x64\x69\x72\x65\x63\x74\x6f\ \x72\x79\x20\x69\x73\x20\x6e\x6f\x74\x20\x70\x65\x72\x6d\x69\x74\ \x74\x65\x64\x2e\x07\x00\x00\x00\x11\x50\x72\x6f\x6a\x65\x63\x74\ \x4e\x61\x6d\x65\x57\x69\x64\x67\x65\x74\x01\x03\x00\x00\x01\xa0\ \x00\x42\x00\x65\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x20\x00\x6e\ \x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\x67\x00\x65\x00\x66\ \x00\x75\x00\x6e\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x6f\x00\x64\ \x00\x65\x00\x72\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\ \x00\x20\x00\x61\x00\x6b\x00\x7a\x00\x65\x00\x70\x00\x74\x00\x69\ \x00\x65\x00\x72\x00\x74\x00\x21\x00\x3c\x00\x62\x00\x72\x00\x3e\ \x00\x50\x00\x61\x00\x72\x00\x61\x00\x6d\x00\x65\x00\x74\x00\x65\ \x00\x72\x00\x2c\x00\x20\x00\x2d\x00\x2d\x00\x73\x00\x63\x00\x68\ \x00\x61\x00\x6c\x00\x74\x00\x65\x00\x72\x00\x20\x00\x75\x00\x6e\ \x00\x64\x00\x20\x00\x72\x00\x65\x00\x6c\x00\x61\x00\x74\x00\x69\ \x00\x76\x00\x65\x00\x20\x00\x50\x00\x66\x00\x61\x00\x64\x00\x65\ \x00\x20\x00\x73\x00\x69\x00\x6e\x00\x64\x00\x20\x00\x6e\x00\x69\ \x00\x63\x00\x68\x00\x74\x00\x20\x00\x65\x00\x72\x00\x6c\x00\x61\ \x00\x75\x00\x62\x00\x74\x00\x2e\x00\x3c\x00\x62\x00\x72\x00\x3e\ \x00\x42\x00\x65\x00\x6e\x00\x75\x00\x74\x00\x7a\x00\x65\x00\x6e\ \x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x73\x00\x74\x00\x61\ \x00\x74\x00\x74\x00\x64\x00\x65\x00\x73\x00\x73\x00\x65\x00\x6e\ \x00\x20\x00\x65\x00\x69\x00\x6e\x00\x20\x00\x50\x00\x72\x00\x6f\ \x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x20\x00\x77\x00\x69\ \x00\x65\x00\x20\x00\x6e\x00\x73\x00\x6d\x00\x2d\x00\x70\x00\x72\ \x00\x6f\x00\x78\x00\x79\x00\x20\x00\x6f\x00\x64\x00\x65\x00\x72\ \x00\x20\x00\x73\x00\x63\x00\x68\x00\x72\x00\x65\x00\x69\x00\x62\ \x00\x65\x00\x6e\x00\x20\x00\x73\x00\x69\x00\x65\x00\x20\x00\x73\ \x00\x65\x00\x6c\x00\x62\x00\x73\x00\x74\x00\x20\x00\x65\x00\x69\ \x00\x6e\x00\x20\x00\x53\x00\x74\x00\x61\x00\x72\x00\x74\x00\x65\ \x00\x72\x00\x73\x00\x63\x00\x72\x00\x69\x00\x70\x00\x74\x00\x2e\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x94\x43\x6f\x6d\x6d\x61\x6e\ \x64\x20\x6e\x6f\x74\x20\x66\x6f\x75\x6e\x64\x20\x6f\x72\x20\x6e\ \x6f\x74\x20\x61\x63\x63\x65\x70\x74\x65\x64\x21\x3c\x62\x72\x3e\ \x50\x61\x72\x61\x6d\x65\x74\x65\x72\x73\x2c\x20\x2d\x2d\x73\x77\ \x69\x74\x63\x68\x65\x73\x20\x61\x6e\x64\x20\x72\x65\x6c\x61\x74\ \x69\x76\x65\x20\x70\x61\x74\x68\x73\x20\x61\x72\x65\x20\x6e\x6f\ \x74\x20\x61\x6c\x6c\x6f\x77\x65\x64\x2e\x3c\x62\x72\x3e\x55\x73\ \x65\x20\x6e\x73\x6d\x2d\x70\x72\x6f\x78\x79\x20\x6f\x72\x20\x77\ \x72\x69\x74\x65\x20\x61\x20\x73\x74\x61\x72\x74\x65\x72\x2d\x73\ \x63\x72\x69\x70\x74\x20\x69\x6e\x73\x74\x65\x61\x64\x2e\x07\x00\ \x00\x00\x0c\x50\x72\x6f\x6d\x70\x74\x57\x69\x64\x67\x65\x74\x01\ \x03\x00\x00\x00\xbe\x00\x4b\x00\x65\x00\x69\x00\x6e\x00\x65\x00\ \x20\x00\x50\x00\x72\x00\x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\ \x6d\x00\x2d\x00\x44\x00\x61\x00\x74\x00\x65\x00\x6e\x00\x62\x00\ \x61\x00\x6e\x00\x6b\x00\x20\x00\x67\x00\x65\x00\x66\x00\x75\x00\ \x6e\x00\x64\x00\x65\x00\x6e\x00\x2e\x00\x20\x00\x42\x00\x69\x00\ \x74\x00\x74\x00\x65\x00\x20\x00\x62\x00\x65\x00\x6e\x00\x75\x00\ \x74\x00\x7a\x00\x65\x00\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\ \x20\x00\x64\x00\x61\x00\x73\x00\x20\x00\x53\x00\x74\x00\x65\x00\ \x75\x00\x65\x00\x72\x00\x75\x00\x6e\x00\x67\x00\x73\x00\x6d\x00\ \x65\x00\x6e\x00\xfc\x00\x20\x00\x75\x00\x6d\x00\x20\x00\x64\x00\ \x69\x00\x65\x00\x73\x00\x65\x00\x20\x00\x7a\x00\x75\x00\x20\x00\ \x65\x00\x72\x00\x73\x00\x74\x00\x65\x00\x6c\x00\x6c\x00\x65\x00\ \x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x3e\x4e\x6f\x20\ \x70\x72\x6f\x67\x72\x61\x6d\x20\x64\x61\x74\x61\x62\x61\x73\x65\ \x20\x66\x6f\x75\x6e\x64\x2e\x20\x50\x6c\x65\x61\x73\x65\x20\x75\ \x70\x64\x61\x74\x65\x20\x74\x68\x72\x6f\x75\x67\x68\x20\x43\x6f\ \x6e\x74\x72\x6f\x6c\x20\x6d\x65\x6e\x75\x2e\x07\x00\x00\x00\x0c\ \x50\x72\x6f\x6d\x70\x74\x57\x69\x64\x67\x65\x74\x01\x03\x00\x00\ \x00\x84\x00\x53\x00\x63\x00\x68\x00\x72\x00\x65\x00\x69\x00\x62\ \x00\x65\x00\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x64\ \x00\x65\x00\x6e\x00\x20\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x6e\ \x00\x20\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x72\x00\x20\x00\x61\ \x00\x75\x00\x73\x00\x66\x00\xfc\x00\x68\x00\x72\x00\x62\x00\x61\ \x00\x72\x00\x65\x00\x6e\x00\x20\x00\x44\x00\x61\x00\x74\x00\x65\ \x00\x69\x00\x20\x00\x76\x00\x6f\x00\x6e\x00\x20\x00\x69\x00\x68\ \x00\x72\x00\x65\x00\x6d\x00\x20\x00\x53\x00\x79\x00\x73\x00\x74\ \x00\x65\x00\x6d\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x36\ \x54\x79\x70\x65\x20\x69\x6e\x20\x74\x68\x65\x20\x6e\x61\x6d\x65\ \x20\x6f\x66\x20\x61\x6e\x20\x65\x78\x65\x63\x75\x74\x61\x62\x6c\ \x65\x20\x66\x69\x6c\x65\x20\x6f\x6e\x20\x79\x6f\x75\x72\x20\x73\ \x79\x73\x74\x65\x6d\x2e\x07\x00\x00\x00\x0c\x50\x72\x6f\x6d\x70\ \x74\x57\x69\x64\x67\x65\x74\x01\x03\x00\x00\x00\x3e\x00\x53\x00\ \x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x7b\x00\ \x7d\x00\x20\x00\x73\x00\x6f\x00\x6c\x00\x6c\x00\x20\x00\x67\x00\ \x65\x00\x6c\x00\xf6\x00\x73\x00\x63\x00\x68\x00\x74\x00\x20\x00\ \x77\x00\x65\x00\x72\x00\x64\x00\x65\x00\x6e\x08\x00\x00\x00\x00\ \x06\x00\x00\x00\x1a\x41\x62\x6f\x75\x74\x20\x74\x6f\x20\x64\x65\ \x6c\x65\x74\x65\x20\x53\x65\x73\x73\x69\x6f\x6e\x20\x7b\x7d\x07\ \x00\x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\ \x03\x00\x00\x00\x9a\x00\x41\x00\x6c\x00\x6c\x00\x65\x00\x20\x00\ \x44\x00\x61\x00\x74\x00\x65\x00\x69\x00\x65\x00\x6e\x00\x20\x00\ \x61\x00\x75\x00\x73\x00\x20\x00\x64\x00\x69\x00\x65\x00\x73\x00\ \x65\x00\x6d\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x6a\x00\x65\x00\ \x6b\x00\x74\x00\x76\x00\x65\x00\x72\x00\x7a\x00\x65\x00\x69\x00\ \x63\x00\x68\x00\x6e\x00\x69\x00\x73\x00\x20\x00\x77\x00\x65\x00\ \x72\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x75\x00\x6e\x00\x77\x00\ \x69\x00\x65\x00\x64\x00\x65\x00\x72\x00\x62\x00\x72\x00\x69\x00\ \x6e\x00\x67\x00\x6c\x00\x69\x00\x63\x00\x68\x00\x20\x00\x67\x00\ \x65\x00\x6c\x00\xf6\x00\x73\x00\x63\x00\x68\x00\x74\x00\x2e\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x40\x41\x6c\x6c\x20\x66\x69\x6c\ \x65\x73\x20\x69\x6e\x20\x74\x68\x65\x20\x70\x72\x6f\x6a\x65\x63\ \x74\x20\x64\x69\x72\x65\x63\x74\x6f\x72\x79\x20\x77\x69\x6c\x6c\ \x20\x62\x65\x20\x69\x72\x72\x65\x76\x65\x72\x73\x69\x62\x6c\x79\ \x20\x64\x65\x6c\x65\x74\x65\x64\x2e\x07\x00\x00\x00\x0b\x53\x65\ \x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\x00\x00\x00\x0e\x00\ \x43\x00\x6c\x00\x69\x00\x65\x00\x6e\x00\x74\x00\x73\x08\x00\x00\ \x00\x00\x06\x00\x00\x00\x07\x43\x6c\x69\x65\x6e\x74\x73\x07\x00\ \x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\ \x00\x00\x00\x20\x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\ \x00\x6e\x00\x20\x00\x6b\x00\x6f\x00\x70\x00\x69\x00\x65\x00\x72\ \x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0c\x43\x6f\ \x70\x79\x20\x53\x65\x73\x73\x69\x6f\x6e\x07\x00\x00\x00\x0b\x53\ \x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\x00\x00\x00\x24\ \x00\x4b\x00\x6f\x00\x70\x00\x69\x00\x65\x00\x72\x00\x65\x00\x20\ \x00\x7b\x00\x7d\x00\x20\x00\x6e\x00\x61\x00\x63\x00\x68\x00\x20\ \x00\x7b\x00\x7d\x08\x00\x00\x00\x00\x06\x00\x00\x00\x10\x43\x6f\ \x70\x79\x69\x6e\x67\x20\x7b\x7d\x20\x74\x6f\x20\x7b\x7d\x07\x00\ \x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\ \x00\x00\x00\x1e\x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\ \x00\x6e\x00\x20\x00\x6c\x00\xf6\x00\x73\x00\x63\x00\x68\x00\x65\ \x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\x44\x65\x6c\x65\ \x74\x65\x20\x53\x65\x73\x73\x69\x6f\x6e\x07\x00\x00\x00\x0b\x53\ \x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\x00\x00\x00\x10\ \x00\x4c\x00\xf6\x00\x73\x00\x63\x00\x68\x00\x65\x00\x6e\x00\x21\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x07\x44\x65\x6c\x65\x74\x65\ \x21\x07\x00\x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\x65\ \x65\x01\x03\x00\x00\x00\x3a\x00\x4c\x00\x6f\x00\x63\x00\x6b\x00\ \x64\x00\x61\x00\x74\x00\x65\x00\x69\x00\x20\x00\x41\x00\x75\x00\ \x66\x00\x68\x00\x65\x00\x62\x00\x75\x00\x6e\x00\x67\x00\x20\x00\ \x65\x00\x72\x00\x7a\x00\x77\x00\x69\x00\x6e\x00\x67\x00\x65\x00\ \x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x12\x46\x6f\x72\x63\x65\ \x20\x4c\x6f\x63\x6b\x20\x52\x65\x6d\x6f\x76\x61\x6c\x07\x00\x00\ \x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\x00\ \x00\x00\x20\x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\ \x6e\x00\x20\x00\x62\x00\x65\x00\x68\x00\x61\x00\x6c\x00\x74\x00\ \x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0c\x4b\x65\x65\ \x70\x20\x53\x65\x73\x73\x69\x6f\x6e\x07\x00\x00\x00\x0b\x53\x65\ \x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\x00\x00\x00\x24\x00\ \x4c\x00\x65\x00\x74\x00\x7a\x00\x74\x00\x65\x00\x20\x00\x53\x00\ \x70\x00\x65\x00\x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x75\x00\ \x6e\x00\x67\x08\x00\x00\x00\x00\x06\x00\x00\x00\x09\x4c\x61\x73\ \x74\x20\x53\x61\x76\x65\x07\x00\x00\x00\x0b\x53\x65\x73\x73\x69\ \x6f\x6e\x54\x72\x65\x65\x01\x03\x00\x00\x00\x08\x00\x4e\x00\x61\ \x00\x6d\x00\x65\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x4e\x61\ \x6d\x65\x07\x00\x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\ \x65\x65\x01\x03\x00\x00\x00\x08\x00\x50\x00\x66\x00\x61\x00\x64\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x50\x61\x74\x68\x07\x00\ \x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\ \x00\x00\x00\x20\x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\ \x00\x6e\x00\x20\x00\x75\x00\x6d\x00\x62\x00\x65\x00\x6e\x00\x6e\ \x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\x52\x65\ \x6e\x61\x6d\x65\x20\x53\x65\x73\x73\x69\x6f\x6e\x07\x00\x00\x00\ \x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\x00\x00\ \x00\x0a\x00\x47\x00\x72\x00\xf6\x00\xdf\x00\x65\x08\x00\x00\x00\ \x00\x06\x00\x00\x00\x04\x53\x69\x7a\x65\x07\x00\x00\x00\x0b\x53\ \x65\x73\x73\x69\x6f\x6e\x54\x72\x65\x65\x01\x03\x00\x00\x00\x10\ \x00\x53\x00\x79\x00\x6d\x00\x6c\x00\x69\x00\x6e\x00\x6b\x00\x73\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x08\x53\x79\x6d\x6c\x69\x6e\ \x6b\x73\x07\x00\x00\x00\x0b\x53\x65\x73\x73\x69\x6f\x6e\x54\x72\ \x65\x65\x01\x03\x00\x00\x00\x0c\x00\x5a\x00\x75\x00\x72\x00\xfc\ \x00\x63\x00\x6b\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x42\x61\ \x63\x6b\x07\x00\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x55\ \x73\x65\x72\x4d\x61\x6e\x75\x61\x6c\x01\x03\xff\xff\xff\xff\x08\ \x00\x00\x00\x00\x06\x00\x00\x00\x04\x46\x6f\x72\x6d\x07\x00\x00\ \x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x55\x73\x65\x72\x4d\x61\ \x6e\x75\x61\x6c\x01\x03\x00\x00\x00\x14\x00\x53\x00\x74\x00\x61\ \x00\x72\x00\x74\x00\x73\x00\x65\x00\x69\x00\x74\x00\x65\x08\x00\ \x00\x00\x00\x06\x00\x00\x00\x04\x48\x6f\x6d\x65\x07\x00\x00\x00\ \x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x55\x73\x65\x72\x4d\x61\x6e\ \x75\x61\x6c\x01\x03\x00\x00\x00\x20\x00\x42\x00\x65\x00\x6e\x00\ \x75\x00\x74\x00\x7a\x00\x65\x00\x72\x00\x68\x00\x61\x00\x6e\x00\ \x64\x00\x62\x00\x75\x00\x63\x00\x68\x08\x00\x00\x00\x00\x06\x00\ \x00\x00\x0b\x55\x73\x65\x72\x20\x4d\x61\x6e\x75\x61\x6c\x07\x00\ \x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x55\x73\x65\x72\x4d\ \x61\x6e\x75\x61\x6c\x01\x03\x00\x00\x00\x38\x00\x50\x00\x72\x00\ \x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x20\x00\x68\x00\ \x69\x00\x6e\x00\x7a\x00\x75\x00\x66\x00\xfc\x00\x67\x00\x65\x00\ \x6e\x00\x20\x00\x28\x00\x50\x00\x72\x00\x6f\x00\x6d\x00\x70\x00\ \x74\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\x00\x13\x41\x64\x64\ \x20\x43\x6c\x69\x65\x6e\x74\x20\x28\x50\x72\x6f\x6d\x70\x74\x29\ \x07\x00\x00\x00\x08\x54\x72\x61\x79\x49\x63\x6f\x6e\x01\x03\x00\ \x00\x00\x5e\x00\x53\x00\x63\x00\x68\x00\x6c\x00\x69\x00\x65\x00\ \xdf\x00\x65\x00\x6e\x00\x20\x00\x6f\x00\x68\x00\x6e\x00\x65\x00\ \x20\x00\x7a\x00\x75\x00\x20\x00\x53\x00\x70\x00\x65\x00\x69\x00\ \x63\x00\x68\x00\x65\x00\x72\x00\x6e\x00\x20\x00\x26\x00\x26\x00\ \x20\x00\x41\x00\x67\x00\x6f\x00\x72\x00\x64\x00\x65\x00\x6a\x00\ \x6f\x00\x20\x00\x62\x00\x65\x00\x65\x00\x6e\x00\x64\x00\x65\x00\ \x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x25\x43\x6c\x6f\x73\x65\ \x20\x77\x69\x74\x68\x6f\x75\x74\x20\x53\x61\x76\x69\x6e\x67\x20\ \x26\x26\x20\x51\x75\x69\x74\x20\x41\x67\x6f\x72\x64\x65\x6a\x6f\ \x07\x00\x00\x00\x08\x54\x72\x61\x79\x49\x63\x6f\x6e\x01\x03\x00\ \x00\x00\x30\x00\x56\x00\x65\x00\x72\x00\x73\x00\x74\x00\x65\x00\ \x63\x00\x6b\x00\x65\x00\x2f\x00\x5a\x00\x65\x00\x69\x00\x67\x00\ \x65\x00\x20\x00\x41\x00\x67\x00\x6f\x00\x72\x00\x64\x00\x65\x00\ \x6a\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x12\x48\x69\x64\ \x65\x2f\x53\x68\x6f\x77\x20\x41\x67\x6f\x72\x64\x65\x6a\x6f\x07\ \x00\x00\x00\x08\x54\x72\x61\x79\x49\x63\x6f\x6e\x01\x03\x00\x00\ \x00\x10\x00\x42\x00\x65\x00\x65\x00\x6e\x00\x64\x00\x65\x00\x6e\ \x00\x20\x08\x00\x00\x00\x00\x06\x00\x00\x00\x05\x51\x75\x69\x74\ \x20\x07\x00\x00\x00\x08\x54\x72\x61\x79\x49\x63\x6f\x6e\x01\x03\ \x00\x00\x00\x3c\x00\x41\x00\x62\x00\x62\x00\x72\x00\x65\x00\x63\ \x00\x68\x00\x65\x00\x6e\x00\x20\x00\x75\x00\x6e\x00\x64\x00\x20\ \x00\x41\x00\x67\x00\x6f\x00\x72\x00\x64\x00\x65\x00\x6a\x00\x6f\ \x00\x20\x00\x42\x00\x65\x00\x65\x00\x6e\x00\x64\x00\x65\x00\x6e\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x53\x61\x76\x65\x20\x26\ \x26\x20\x51\x75\x69\x74\x20\x41\x67\x6f\x72\x64\x65\x6a\x6f\x07\ \x00\x00\x00\x08\x54\x72\x61\x79\x49\x63\x6f\x6e\x01\x03\x00\x00\ \x00\x2e\x00\x53\x00\x69\x00\x63\x00\x68\x00\x74\x00\x62\x00\x61\ \x00\x72\x00\x6b\x00\x65\x00\x69\x00\x74\x00\x20\x00\x75\x00\x6d\ \x00\x73\x00\x63\x00\x68\x00\x61\x00\x6c\x00\x74\x00\x65\x00\x6e\ \x08\x00\x00\x00\x00\x06\x00\x00\x00\x18\x54\x6f\x67\x67\x6c\x65\ \x20\x43\x6c\x69\x65\x6e\x74\x20\x56\x69\x73\x69\x62\x69\x6c\x69\ \x74\x79\x07\x00\x00\x00\x08\x54\x72\x61\x79\x49\x63\x6f\x6e\x01\ \x03\x00\x00\x00\x76\x00\x42\x00\x69\x00\x74\x00\x74\x00\x65\x00\ \x20\x00\x62\x00\x65\x00\x73\x00\x74\x00\xe4\x00\x74\x00\x69\x00\ \x67\x00\x65\x00\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\ \x64\x00\x75\x00\x72\x00\x63\x00\x68\x00\x20\x00\x65\x00\x69\x00\ \x6e\x00\x65\x00\x6e\x00\x20\x00\x4b\x00\x6c\x00\x69\x00\x63\x00\ \x6b\x00\x20\x00\x61\x00\x75\x00\x66\x00\x20\x00\x64\x00\x65\x00\ \x6e\x00\x20\x00\x4b\x00\x6e\x00\x6f\x00\x70\x00\x66\x00\x20\x00\ \x75\x00\x6e\x00\x74\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\ \x06\x00\x00\x00\x38\x50\x6c\x65\x61\x73\x65\x20\x63\x6f\x6e\x66\ \x69\x72\x6d\x20\x77\x69\x74\x68\x20\x61\x20\x63\x6c\x69\x63\x6b\ \x20\x6f\x6e\x20\x74\x68\x65\x20\x62\x75\x74\x74\x6f\x6e\x20\x61\ \x74\x20\x74\x68\x65\x20\x62\x6f\x74\x74\x6f\x6d\x2e\x07\x00\x00\ \x00\x0a\x57\x61\x69\x74\x44\x69\x61\x6c\x6f\x67\x01\x03\x00\x00\ \x00\x26\x00\x41\x00\x67\x00\x6f\x00\x72\x00\x64\x00\x65\x00\x6a\ \x00\x6f\x00\x20\x00\x69\x00\x73\x00\x74\x00\x20\x00\x62\x00\x65\ \x00\x72\x00\x65\x00\x69\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\ \x00\x0e\x41\x67\x6f\x72\x64\x65\x6a\x6f\x20\x72\x65\x61\x64\x79\ \x07\x00\x00\x00\x0a\x6d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\ \x03\x00\x00\x00\x5c\x00\x45\x00\x73\x00\x20\x00\x77\x00\x75\x00\ \x72\x00\x64\x00\x65\x00\x20\x00\x76\x00\x65\x00\x72\x00\x73\x00\ \x75\x00\x63\x00\x68\x00\x74\x00\x20\x00\x65\x00\x69\x00\x6e\x00\ \x65\x00\x20\x00\x77\x00\x65\x00\x69\x00\x74\x00\x65\x00\x72\x00\ \x65\x00\x20\x00\x47\x00\x55\x00\x49\x00\x20\x00\x7a\x00\x75\x00\ \x20\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x65\x00\x6e\x00\ \x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1c\x41\x6e\x6f\x74\x68\ \x65\x72\x20\x47\x55\x49\x20\x74\x72\x69\x65\x64\x20\x74\x6f\x20\ \x6c\x61\x75\x6e\x63\x68\x2e\x07\x00\x00\x00\x0a\x6d\x61\x69\x6e\ \x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x01\x7e\x00\x50\x00\x72\ \x00\x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x64\x00\x61\ \x00\x74\x00\x65\x00\x6e\x00\x62\x00\x61\x00\x6e\x00\x6b\x00\x20\ \x00\x77\x00\x69\x00\x72\x00\x64\x00\x20\x00\x61\x00\x6b\x00\x74\ \x00\x75\x00\x61\x00\x6c\x00\x69\x00\x73\x00\x69\x00\x65\x00\x72\ \x00\x74\x00\x2e\x00\x0a\x00\x56\x00\x69\x00\x65\x00\x6c\x00\x65\ \x00\x6e\x00\x20\x00\x44\x00\x61\x00\x6e\x00\x6b\x00\x20\x00\x66\ \x00\xfc\x00\x72\x00\x20\x00\x69\x00\x68\x00\x72\x00\x65\x00\x20\ \x00\x47\x00\x65\x00\x64\x00\x75\x00\x6c\x00\x64\x00\x2e\x00\x0a\ \x00\x57\x00\x65\x00\x6e\x00\x6e\x00\x20\x00\x64\x00\x65\x00\x72\ \x00\x20\x00\x50\x00\x72\x00\x6f\x00\x7a\x00\x65\x00\x73\x00\x73\ \x00\x20\x00\x65\x00\x69\x00\x6e\x00\x67\x00\x65\x00\x66\x00\x6f\ \x00\x72\x00\x65\x00\x6e\x00\x20\x00\x69\x00\x73\x00\x74\x00\x20\ \x00\x65\x00\x72\x00\x7a\x00\x77\x00\x69\x00\x6e\x00\x67\x00\x65\ \x00\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x62\x00\x69\ \x00\x74\x00\x74\x00\x65\x00\x20\x00\x64\x00\x69\x00\x65\x00\x20\ \x00\x42\x00\x65\x00\x65\x00\x6e\x00\x64\x00\x75\x00\x6e\x00\x67\ \x00\x20\x00\x64\x00\x65\x00\x73\x00\x20\x00\x50\x00\x72\x00\x6f\ \x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\x73\x00\x20\x00\x75\ \x00\x6e\x00\x64\x00\x20\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\ \x00\x65\x00\x6e\x00\x20\x00\x65\x00\x73\x00\x20\x00\x65\x00\x72\ \x00\x6e\x00\x65\x00\x75\x00\x74\x00\x2e\x00\x20\x00\x49\x00\x68\ \x00\x72\x00\x65\x00\x20\x00\x44\x00\x61\x00\x74\x00\x65\x00\x6e\ \x00\x20\x00\x73\x00\x69\x00\x6e\x00\x64\x00\x20\x00\x73\x00\x69\ \x00\x63\x00\x68\x00\x65\x00\x72\x00\x2e\x08\x00\x00\x00\x00\x06\ \x00\x00\x00\x76\x55\x70\x64\x61\x74\x69\x6e\x67\x20\x50\x72\x6f\ \x67\x72\x61\x6d\x20\x44\x61\x74\x61\x62\x61\x73\x65\x2e\x0a\x54\ \x68\x61\x6e\x6b\x20\x79\x6f\x75\x20\x66\x6f\x72\x20\x79\x6f\x75\ \x72\x20\x70\x61\x74\x69\x65\x6e\x63\x65\x2e\x0a\x49\x66\x20\x70\ \x72\x6f\x67\x72\x65\x73\x73\x20\x66\x72\x65\x65\x7a\x65\x73\x20\ \x70\x6c\x65\x61\x73\x65\x20\x6b\x69\x6c\x6c\x20\x61\x6e\x64\x20\ \x72\x65\x73\x74\x61\x72\x74\x20\x74\x68\x65\x20\x77\x68\x6f\x6c\ \x65\x20\x70\x72\x6f\x67\x72\x61\x6d\x2e\x07\x00\x00\x00\x0a\x6d\ \x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x88\x00\x00\x00\x02\x01\ \x01\ " qt_resource_name = b"\ \x00\x0b\ \x00\xbd\xd0\x67\ \x00\x72\ \x00\x75\x00\x6e\x00\x6e\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0b\ \x08\x61\x82\x07\ \x00\x74\ \x00\x6f\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x0c\ \x0d\xfc\x11\x13\ \x00\x74\ \x00\x72\x00\x61\x00\x6e\x00\x73\x00\x6c\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\ \x00\x08\ \x06\x63\x59\x27\ \x00\x6c\ \x00\x6f\x00\x6f\x00\x70\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x0b\ \x0c\x46\xd2\x07\ \x00\x72\ \x00\x65\x00\x6d\x00\x6f\x00\x76\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0a\ \x0a\xcc\x85\x87\ \x00\x68\ \x00\x69\x00\x64\x00\x64\x00\x65\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0b\ \x06\x42\x36\x27\ \x00\x73\ \x00\x74\x00\x6f\x00\x70\x00\x70\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x09\ \x0d\xc5\xb4\x07\ \x00\x70\ \x00\x6f\x00\x77\x00\x65\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x08\ \x0a\x61\x5a\xa7\ \x00\x69\ \x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x0d\ \x02\xc6\x5b\x87\ \x00\x70\ \x00\x6c\x00\x61\x00\x79\x00\x70\x00\x61\x00\x75\x00\x73\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x05\ \x00\x70\x75\x7d\ \x00\x69\ \x00\x74\x00\x2e\x00\x71\x00\x6d\ \x00\x05\ \x00\x6a\x85\x7d\ \x00\x64\ \x00\x65\x00\x2e\x00\x71\x00\x6d\ " qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x00\xec\x00\x00\x00\x00\x00\x01\x00\x00\x15\x3c\ \x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x74\ \x00\x00\x00\x56\x00\x00\x00\x00\x00\x01\x00\x00\x03\xf8\ \x00\x00\x00\x1c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb8\ \x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x0e\x4c\ \x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x21\ \x00\x00\x00\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf8\ \x00\x00\x00\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xd8\ \x00\x00\x00\x38\x00\x02\x00\x00\x00\x02\x00\x00\x00\x0b\ \x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00\x18\x20\ \x00\x00\x01\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x17\xfb\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8b\ \x00\x00\x00\xec\x00\x00\x00\x00\x00\x01\x00\x00\x15\x3c\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8e\ \x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x74\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8e\ \x00\x00\x00\x56\x00\x00\x00\x00\x00\x01\x00\x00\x03\xf8\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8e\ \x00\x00\x00\x1c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb8\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8e\ \x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x0e\x4c\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8b\ \x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x21\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8b\ \x00\x00\x00\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf8\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8b\ \x00\x00\x00\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xd8\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8e\ \x00\x00\x00\x38\x00\x02\x00\x00\x00\x02\x00\x00\x00\x0b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00\x18\x20\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8b\ \x00\x00\x01\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x17\xfb\ \x00\x00\x01\x7e\x5f\x0a\xf3\x8b\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] if qt_version < [5, 8, 0]: rcc_version = 1 qt_resource_struct = qt_resource_struct_v1 else: rcc_version = 2 qt_resource_struct = qt_resource_struct_v2 def qInitResources(): QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) def qCleanupResources(): QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) qInitResources() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/buildresources.sh0000644000175000017500000000042300000000000020532 0ustar00nilsnils#!/bin/bash #https://doc.qt.io/qt-5/resources.html #Resources are kept up-to-date upstream. They are not part of the make and build process. pyrcc5 -no-compress resources.qrc -o ../resources.py #put them into the gui directly. Engine does not need any translation or images. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/hidden.svg0000644000175000017500000000051700000000000017124 0ustar00nilsnils././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/icon.png0000644000175000017500000000335400000000000016610 0ustar00nilsnilsPNG  IHDR@@iqsBIT|d pHYsMMotEXtSoftwarewww.inkscape.org<iIDATxK]ǿۮthȚ_HME7aPDJBȚuBԅ+Fvw/L%eu9/;\.{޴`/;3=g?;Μ' fXd@yʰ |Xibzz}}}"JǸt, .( Ѩ+"EQ("KL&IDXI$d2!^B\.Ȳ,ŠH,r @UU|M677i/!VWWE:B"l ćᰈP)YɲT* `3@V,vspHR+SNtvv2 +++Lv;`ZebZZZϊ `Ԅ\~}_EQQ[[[no߾:jp8̝ R)8x`nvcbbGD&pܹmϟ?*/\4666$34qN 4㇆Xcp 6b1BqD"ı ؗ70P"ÇD37˅GaJ_(f4ٳzާOƙ3gbeYޅbt:~?$G8I230˗q1 Tdbqr \_uD"A](f4`yyLLLÇ{ҖB1PUzݻD:FGGCs&`:xkk %*oL4ٳg-zիWx9$ITS|10ML|OW]q5&d2I5%x<\ ߿m8>}ɓ οsd\iE@nի''k&^4Mȝif! rI]שCjC}' Y25łd2I]N]S91 H0 Ra0A3k iV8B&j,$I["3^HeU|QX,\ ^~&!xA*kkkn͛$ ===@4Ž{2 :ES}ؗ-£Exb3?t{[A=zS TU+**G\x$I, NH~r\sM\6l/}'N nH1?~x<%c ڈ#Į(ze###™L*XZZ*zԩS"Vn;-B(;477Wʫ|`ێ/,,/:\|9.\@UUE{RUmmmӧO;x*v{I"niSb6oՊC[H3MUm.͛7577\/O[oQN2/1ˏIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/loop.png0000644000175000017500000000237400000000000016632 0ustar00nilsnilsPNG  IHDR&@lnsBIT|d pHYs9tEXtSoftwarewww.inkscape.org< tEXtTitleShape&bIDATX]L[uƟ=vcZqB4ihbb0H0:޸h 3.Lf?uل̑Č}@I-l9+Pzㅀ)=5d@fM[(j+P5NΌܺun=5%.ڊ»@*htQ$oߪ OP"YjI P$[?JlЖ$Ft\;JSe.^,'/m7\_p| 8ŏyޤF~ Gf S';ģ$|n[^6uo.W۹[7M;7\原 72Ʉ>@ZL9E1NU_$Eչ\2Ik\7:kML|@,>J&GMl O {s6O_ =}Pm|0_ӏH؅hYc*:(gGCx ^\-} ZKuxv~q:7{mT~h0,93v(~wOѕKa %$%̉(VNzGxdamW+.7+.䵔`̛f +``cٽ~*)2Wp;YL@5xI=mq֏N#ЍB܂՞?o'A|sٛkJK#  .Y1+t TM!_;sh h2+W"iFg)h|CuFUb[*^c|Ĵ9y(ma̫/(NiWBb)xI6GsٛEUNJג9ZZoVw,>`b[55=,\I29fOT5{B~`+[V8h='MGgS> tICt9 QxC4[Xd}[_. E%GIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/loop.svg0000644000175000017500000000556000000000000016645 0ustar00nilsnils image/svg+xml Shape Shape ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/playpause.png0000644000175000017500000000127300000000000017661 0ustar00nilsnilsPNG  IHDR&@lnsBIT|d pHYs9tEXtSoftwarewww.inkscape.org< tEXtTitleShape&!IDATX=hq& "BM*uXpq]&X "7qkK4HA6wCOh//w]p/ܓg it{303C;nXX9Sٵj~u<( %^``, ײy6\( *UiDQ0,VqQvW~ɰ?ol \ ;cu A:uEP']aߊӪ i!3w{Mz^_xd"ޟq巢|,֬xgɒR~)TT}p]Z vhXfX`M,҂N Н_ݰB f 3Ļ4RtgRj>vߌ8T* mb ƣGDnd"na4MǟGFj&_ӪkgRG}jEсOC[s@OA.\4F)5j(PqN~zrbmͦrA m|ac:IENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/playpause.svg0000644000175000017500000000525100000000000017674 0ustar00nilsnils image/svg+xml Shape Shape ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/power.svg0000644000175000017500000000116000000000000017020 0ustar00nilsnils././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/removed.svg0000644000175000017500000000044500000000000017332 0ustar00nilsnilsShape + Shape././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/resources.qrc0000644000175000017500000000061100000000000017664 0ustar00nilsnils icon.png hidden.svg power.svg running.svg stopped.svg removed.svg translations/de.qm translations/it.qm loop.png playpause.png tostart.png ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/running.svg0000644000175000017500000000026400000000000017350 0ustar00nilsnils././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/stopped.svg0000644000175000017500000000014000000000000017337 0ustar00nilsnils././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/tostart.png0000644000175000017500000000147400000000000017361 0ustar00nilsnilsPNG  IHDR&@lnsBIT|d pHYs ~j[tEXtSoftwarewww.inkscape.org< tEXtTitleShape&IDATXKTQ^#J`(˜BraHjYN6.ZI2"JjBJBsJ}[D9:s-g}=;##UǷbLrD2zLV9&PD$d;L_ZV r(UWQ:l񯻃! 4XmIE4gQ6K;A{ˢxb&vlL7f7Q ؊L}ȸsRPL;7Å>*ثo38jç&m l2k{=1 j tR[)@ӶRSUIM;,ƑOG92;95LBXUJ}r jl CuS "x%*F1aHz} +?p?jlG,/EU1D<~D5DMAm"o15%#A^X-l:bOŁwn>QuEɯDwQE /n v&%UHX z'^a୉55]WҾm  image/svg+xml Shape Shape ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/translations/config.pro0000644000175000017500000000064400000000000021661 0ustar00nilsnilsSOURCES = ../../addclientprompt.py ../../changelog.py ../../descriptiontextwidget.py ../../mainwindow.py ../../projectname.py ../../sessiontreecontroller.py ../../opensessioncontroller.py ../../systemtray.py ../../waitdialog.py ../../usermanual.py ../../designer/mainwindow.py ../../designer/newsession.py ../../designer/projectname.py ../../designer/settings.py ../../designer/usermanual.py TRANSLATIONS = de.ts it.ts ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/translations/de.qm0000644000175000017500000003626200000000000020626 0ustar00nilsnils |t} (  M! /- e eE/ 5 3 x\/ 1.  u {B uG We {,= *E -; | "6, s < s ^/s E' u"  DZ DZ DZ% l- ŤYa>hnN4UE3]+b` 95BS$kS{&.0,woo2 i8?8Programm soll beendet werden About to quit AskBeforeQuitxProgramm soll beendet werden, aber Session {} ist noch offen'About to quit but session {} still open AskBeforeQuit(nderungen VerwerfenDiscard Changes AskBeforeQuit,Mchten Sie speichern?Do you want to save? AskBeforeQuitNicht Beenden Don't Quit AskBeforeQuitSpeichernSave AskBeforeQuit $PATH$PATHDialogEinn absoluter Pfad pro Zeile (z.B. /home/user/audio-bin). Schrgstrich am Ende spielt keine Rolle. Keine Wildcards wie * oder ..xAdd one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don't matter.DialogBlacklist - Ausfhrbare Dateinamen (keine ganzen Pfade) hinzufgen um diese aus dem Programmstarter zu entfernen. Ein Programm pro Zeile.dBlacklist - Exclude executable names (not paths) from the program launcher. One executable per line.Dialog\Nur fr fortgeschrittene Benutzer! Hier absolute Pfade hinzufgen, in denen sich ausfhrbare Dateien befinden. Diese Einstellungen gelten nur fr Agordejo/NSM. nderungen bentigen einen kompletten Programmneustart. Um Programme zum Schnellstarter hinzuzufgen bitte den Tab "Programmstarter" benutzen.For advanced users only! Add executable paths to the environment, just for Agordejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab.DialogProgrammstarterLauncherDialogEinstellungenSettingsDialogWhitelist - Ausfhrbare Dateinamen (keine ganzen Pfade) hinzufgen um diese im Programmstarter anzuzeigen. Ein Programm pro Zeile.^Whitelist - Add executable names (not paths) to the program launcher. One executable per line.DialogBeschreibung DescriptionLauncherNameNameLauncherPfadPathLauncherUm hier zu schreiben doppelklicken Sie auf dieses Feld (startet nsm-data) Fr Notizen, TODO, Referenzen, Quellen etccDouble click to add the client nsm-data to write here. Use it for notes, TODO, references etc…LoadedSessionDescription min min MainWindow" time-placeholder time-placeholder MainWindow/home/usr/NSM Sessions MainWindowA MainWindowberAbout MainWindow8Programm hinzufgen (Prompt)Add Client (Prompt) MainWindowAgordejoAgordejo MainWindowAlt+O MainWindowAlt+R MainWindowAlt+S MainWindowAlt+T MainWindowAlt+X MainWindow ClientNameId MainWindowRSchlieen ohne zu Speichern ("Abbrechen")Close without Save ("Abort") MainWindowSteuerungControl MainWindowKopieren Copy Selected MainWindowCtrl+Q MainWindowCtrl+S MainWindow Ctrl+Shift+Q MainWindow Ctrl+Shift+S MainWindow Ctrl+Shift+W MainWindowCtrl+W MainWindowLschenDelete Selected MainWindowDDoppelklick um Programm zu startenDouble-click to load program MainWindowF2 MainWindowVolle Ansicht Full View MainWindow<Globaler Zugriff aufs PlaybackGlobal Playback Controls MainWindow,Verstecke alle ClientsHide All Clients MainWindow&Minimieren zum TrayHide in System Tray MainWindowpIch verstehe, dass ich dieses Problem selbst lsen muss!@I understand that I will need to resolve this problem on my own! MainWindow.Momentan in der SessionIn current session MainWindowInformation Information MainWindowJACKJACK MainWindow Last Updated MainWindow Lade Ausgewhlte Load Selected MainWindowHandbuchManual MainWindowNSM Server Mode MainWindowNSM Url MainWindowNeuNew MainWindowPlayPause PlayPause MainWindowArbeitet Processing MainWindowProgram Database MainWindow Schnellstart Neu Quick New MainWindowBeendenQuit MainWindow8Programmdatenbank generierenRebuild Program Database MainWindowEntfernenRemove MainWindowLock aufheben Remove Lock MainWindowUmbenennenRename MainWindowNamen ndernRename Selected MainWindowFortfhrenResume MainWindow RewindRewind MainWindowSpeichernSave MainWindowTSpeichern und mit anderem Namen neu ffnen#Save and Clone under different name MainWindow.Speichern und SchlieenSave and Close MainWindow"Seperat speichernSave separately MainWindow+Self-started, connected to, environment var MainWindowSession Notizen Session Notes MainWindow Session Root MainWindow SessionName MainWindowEinstellungenSettings MainWindow$Zeige alle ClientsShow All Clients MainWindowStopStop MainWindow.Sichtbarkeit umschaltenToggle Visible MainWindowBaumansicht Tree View MainWindowosc.upd ip port MainWindowversion and running MainWindowERROR! Kopierte Daten der Session unterscheiden sich von der Ursprnlinglichen! Bitte berprfen Sie die Integritt der kopierten Daten!SERROR! Copied session data is different from source session. Please check you data!NOOPEngineStringsberprfe Integritt der Dateien. Bitte haben Sie etwas Geduld...2Veryfying file-integrity. This may take a while...NOOPEngineStringsUmbenennen und Session-Notitzen aktivieren (mit Client 'nsm-data'):Client Renaming and Session Notes (adds client 'nsm-data') NewSessionDialog NewSession*Name fr neue SessionNew Session Name NewSessionhSpeichere JACK-Verbindungen (mit Client 'jackpatch')0Save JACK Connections (adds clients 'jackpatch') NewSessionnderungenChanges OpenSessionIDID OpenSession LabelLabel OpenSessionNameName OpenSession StatusStatus OpenSessionSichtbarkeitVisible OpenSession cleanclean OpenSessionungespeichert not saved OpenSessionWhlen Sie einen Namen fr die Session. Benutzen Sie / um Unterverzeichnisse einzurichten/Choose a project name. Use / for subdirectories ProjectNameFehlermeldung Error Message ProjectNameForm ProjectNamepDer Zugriff auf das Elternverzeichnis ist nicht erlaubt.'Moving to parent directory not allowed.ProjectNameWidgetDDer Name ist bereits in Benutzung.Name is already in use.ProjectNameWidgetDName muss ein relativer Pfad sein.Name must be a relative path.ProjectNameWidget4Name darf nicht leer sein.Name must not be empty.ProjectNameWidgetjSie haben keine Schreibrechte fr dieses Verzeichnis.+Writing in this directory is not permitted.ProjectNameWidgetBefehl nicht gefunden oder nicht akzeptiert!<br>Parameter, --schalter und relative Pfade sind nicht erlaubt.<br>Benutzen Sie stattdessen ein Programm wie nsm-proxy oder schreiben sie selbst ein Starterscript.Command not found or not accepted!
Parameters, --switches and relative paths are not allowed.
Use nsm-proxy or write a starter-script instead. PromptWidgetKeine Programm-Datenbank gefunden. Bitte benutzen Sie das Steuerungsmen um diese zu erstellen.>No program database found. Please update through Control menu. PromptWidgetSchreiben Sie den Namen einer ausfhrbaren Datei von ihrem System.6Type in the name of an executable file on your system. PromptWidget>Session {} soll gelscht werdenAbout to delete Session {} SessionTreeAlle Dateien aus diesem Projektverzeichnis werden unwiederbringlich gelscht.@All files in the project directory will be irreversibly deleted. SessionTreeClientsClients SessionTree Session kopieren Copy Session SessionTree$Kopiere {} nach {}Copying {} to {} SessionTreeSession lschenDelete Session SessionTreeLschen!Delete! SessionTree:Lockdatei Aufhebung erzwingenForce Lock Removal SessionTree Session behalten Keep Session SessionTree$Letzte Speicherung Last Save SessionTreeNameName SessionTreePfadPath SessionTree Session umbennenRename Session SessionTree GreSize SessionTreeSymlinksSymlinks SessionTree ZurckBackTemplateUserManualFormTemplateUserManualStartseiteHomeTemplateUserManual Benutzerhandbuch User ManualTemplateUserManual8Programm hinzufgen (Prompt)Add Client (Prompt)TrayIcon^Schlieen ohne zu Speichern && Agordejo beenden%Close without Saving && Quit AgordejoTrayIcon0Verstecke/Zeige AgordejoHide/Show AgordejoTrayIconBeenden Quit TrayIcon<Abbrechen und Agordejo BeendenSave && Quit AgordejoTrayIcon.Sichtbarkeit umschaltenToggle Client VisibilityTrayIconvBitte besttigen Sie durch einen Klick auf den Knopf unten.8Please confirm with a click on the button at the bottom. WaitDialog&Agordejo ist bereitAgordejo ready mainWindow\Es wurde versucht eine weitere GUI zu starten.Another GUI tried to launch. mainWindow~Programmdatenbank wird aktualisiert. Vielen Dank fr ihre Geduld. Wenn der Prozess eingeforen ist erzwingen Sie bitte die Beendung des Programms und starten es erneut. Ihre Daten sind sicher.vUpdating Program Database. Thank you for your patience. If progress freezes please kill and restart the whole program. mainWindow././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/translations/de.ts0000644000175000017500000007557500000000000020651 0ustar00nilsnils AskBeforeQuit About to quit but session {} still open Programm soll beendet werden, aber Session {} ist noch offen Do you want to save? Möchten Sie speichern? About to quit Programm soll beendet werden Don't Quit Nicht Beenden Save Speichern Discard Changes Änderungen Verwerfen Dialog Settings Einstellungen Whitelist - Add executable names (not paths) to the program launcher. One executable per line. Whitelist - Ausführbare Dateinamen (keine ganzen Pfade) hinzufügen um diese im Programmstarter anzuzeigen. Ein Programm pro Zeile. Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line. Blacklist - Ausführbare Dateinamen (keine ganzen Pfade) hinzufügen um diese aus dem Programmstarter zu entfernen. Ein Programm pro Zeile. Launcher Programmstarter For advanced users only! Add executable paths to the environment, just for Agordejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab. Nur für fortgeschrittene Benutzer! Hier absolute Pfade hinzufügen, in denen sich ausführbare Dateien befinden. Diese Einstellungen gelten nur für Agordejo/NSM. Änderungen benötigen einen kompletten Programmneustart. Um Programme zum Schnellstarter hinzuzufügen bitte den Tab "Programmstarter" benutzen. Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don't matter. Einn absoluter Pfad pro Zeile (z.B. /home/user/audio-bin). Schrägstrich am Ende spielt keine Rolle. Keine Wildcards wie * oder .. $PATH $PATH Launcher Name Name Description Beschreibung Path Pfad LoadedSessionDescription Double click to add the client nsm-data to write here. Use it for notes, TODO, references etc… Um hier zu schreiben doppelklicken Sie auf dieses Feld (startet nsm-data) Für Notizen, TODO, Referenzen, Quellen etc… MainWindow Agordejo Agordejo Start New Session Neue Session starten Save Speichern Save and Close Speichern und Schließen Session Notes Session Notizen Quick View Schnelle Ansicht New Neu Load Selected Lade Ausgewählte Tree View Baumansicht Double-click to load program Doppelklick um Programm zu starten In current session Momentan in der Session Full View Volle Ansicht JACK JACK version and running NSM Server Mode Self-started, connected to, environment var NSM Url osc.upd ip port Session Root /home/usr/NSM Sessions Program Database Last Updated Information Information Processing Arbeitet Control Steuerung SessionName ClientNameId Quit Beenden Ctrl+Shift+Q About Über Manual Handbuch Hide in System Tray Minimieren zum Tray Ctrl+Q Add Client (Prompt) Programm hinzufügen (Prompt) A Ctrl+S Save As Speichern unter Ctrl+Shift+S Ctrl+W Abort Abbrechen Ctrl+Shift+W Stop Stop Alt+O Resume Fortführen Alt+R Save separately Seperat speichern Alt+S Remove Entfernen Alt+X Toggle Visible Sichtbarkeit umschalten Alt+T Show All Clients Zeige alle Clients Hide All Clients Verstecke alle Clients Rebuild Program Database Programmdatenbank generieren Rename Umbenennen F2 Settings Einstellungen Save and Clone under different name Speichern und mit anderem Namen neu öffnen Close without Save ("Abort") Schließen ohne zu Speichern ("Abbrechen") Global Playback Controls Globaler Zugriff aufs Playback PlayPause PlayPause Rewind Rewind time-placeholder time-placeholder min min Quick New Schnellstart Neu Rename Selected Namen ändern Copy Selected Kopieren Delete Selected Löschen Remove Lock Lock aufheben I understand that I will need to resolve this problem on my own! Ich verstehe, dass ich dieses Problem selbst lösen muss! NOOPEngineStrings ERROR! Copied session data is different from source session. Please check you data! ERROR! Kopierte Daten der Session unterscheiden sich von der Ursprünlinglichen! Bitte überprüfen Sie die Integrität der kopierten Daten! Veryfying file-integrity. This may take a while... Überprüfe Integrität der Dateien. Bitte haben Sie etwas Geduld... NewSession Dialog New Session Name Name für neue Session Save JACK Connections (adds clients 'jackpatch') Speichere JACK-Verbindungen (mit Client 'jackpatch') Client Renaming and Session Notes (adds client 'nsm-data') Umbenennen und Session-Notitzen aktivieren (mit Client 'nsm-data') OpenSession not saved ungespeichert clean clean (command not found) (Befehl nicht gefunden) Name Name Label Label Status Status Visible Sichtbarkeit Changes Änderungen ID ID ? ProjectName Form Error Message Fehlermeldung Choose a project name. Use / for subdirectories Wählen Sie einen Namen für die Session. Benutzen Sie / um Unterverzeichnisse einzurichten ProjectNameWidget Name must not be empty. Name darf nicht leer sein. Name must be a relative path. Name muss ein relativer Pfad sein. Moving to parent directory not allowed. Der Zugriff auf das Elternverzeichnis ist nicht erlaubt. Writing in this directory is not permitted. Sie haben keine Schreibrechte für dieses Verzeichnis. Name is already in use. Der Name ist bereits in Benutzung. PromptWidget Type in the name of an executable file on your system. Schreiben Sie den Namen einer ausführbaren Datei von ihrem System. No program database found. Please update through Control menu. Keine Programm-Datenbank gefunden. Bitte benutzen Sie das Steuerungsmenü um diese zu erstellen. Command not accepted!<br>Parameters, --switches and relative paths are not allowed.<br>Use nsm-proxy or write a starter-script instead. Befehl nicht akzeptiert!<br>Parameter, --schalter und relative Pfade sind nicht erlaubt.<br>Benutzen Sie stattdessen ein Programm wie nsm-proxy oder schreiben sie selbst ein Starterscript. Command not found or not accepted!<br>Parameters, --switches and relative paths are not allowed.<br>Use nsm-proxy or write a starter-script instead. Befehl nicht gefunden oder nicht akzeptiert!<br>Parameter, --schalter und relative Pfade sind nicht erlaubt.<br>Benutzen Sie stattdessen ein Programm wie nsm-proxy oder schreiben sie selbst ein Starterscript. SessionTree Name Name Last Save Letzte Speicherung Clients Clients Size Größe Symlinks Symlinks Path Pfad About to delete Session {} Session {} soll gelöscht werden All files in the project directory will be irreversibly deleted. Alle Dateien aus diesem Projektverzeichnis werden unwiederbringlich gelöscht. Keep Session Session behalten Delete! Löschen! Copy Session Session kopieren Force Lock Removal Lockdatei Aufhebung erzwingen Rename Session Session umbennen Delete Session Session löschen Copying {} to {} Kopiere {} nach {} TemplateUserManual User Manual Benutzerhandbuch Form Home Startseite Back Zurück TrayIcon Hide/Show Agordejo Verstecke/Zeige Agordejo Save && Quit Agordejo Abbrechen und Agordejo Beenden Quit Beenden Add Client (Prompt) Programm hinzufügen (Prompt) Close without Saving && Quit Agordejo Schließen ohne zu Speichern && Agordejo beenden Toggle Client Visibility Sichtbarkeit umschalten WaitDialog Please confirm with a click on the button at the bottom. Bitte bestätigen Sie durch einen Klick auf den Knopf unten. mainWindow Agordejo ready Agordejo ist bereit Another GUI tried to launch. Es wurde versucht eine weitere GUI zu starten. Updating Program Database. Thank you for your patience. Programmdatenbank wird aktualisiert. Vielen Dank für ihre Geduld. Updating Program Database. Thank you for your patience. If progress freezes please kill and restart the whole program. Programmdatenbank wird aktualisiert. Vielen Dank für ihre Geduld. Wenn der Prozess eingeforen ist erzwingen Sie bitte die Beendung des Programms und starten es erneut. Ihre Daten sind sicher. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/translations/it.qm0000644000175000017500000000004100000000000020634 0ustar00nilsnils AskBeforeQuit About to quit but session {} still open Do you want to save? About to quit Don't Quit Save Discard Changes Dialog Settings Whitelist - Add executable names (not paths) to the program launcher. One executable per line. Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line. Launcher For advanced users only! Add executable paths to the environment, just for Agordejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab. Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don't matter. $PATH Launcher Name Description Path LoadedSessionDescription Double click to add the client nsm-data to write here. Use it for notes, TODO, references etc… MainWindow Agordejo Save Save and Close Session Notes New Load Selected Tree View Double-click to load program In current session Full View JACK version and running NSM Server Mode Self-started, connected to, environment var NSM Url osc.upd ip port Session Root /home/usr/NSM Sessions Program Database Last Updated Information Processing Control SessionName ClientNameId Quit Ctrl+Shift+Q About Hide in System Tray Ctrl+Q Add Client (Prompt) A Ctrl+S Save and Clone under different name Ctrl+Shift+S Ctrl+W Close without Save ("Abort") Ctrl+Shift+W Stop Alt+O Resume Alt+R Save separately Alt+S Remove Alt+X Toggle Visible Alt+T Show All Clients Hide All Clients Rebuild Program Database Rename F2 Settings Manual Global Playback Controls PlayPause Rewind time-placeholder min Quick New Rename Selected Copy Selected Delete Selected Remove Lock I understand that I will need to resolve this problem on my own! NOOPEngineStrings ERROR! Copied session data is different from source session. Please check you data! Veryfying file-integrity. This may take a while... NewSession Dialog New Session Name Save JACK Connections (adds clients 'jackpatch') Client Renaming and Session Notes (adds client 'nsm-data') OpenSession not saved clean Name Label Status Visible Changes ID ProjectName Form Error Message Choose a project name. Use / for subdirectories ProjectNameWidget Name must not be empty. Name must be a relative path. Moving to parent directory not allowed. Writing in this directory is not permitted. Name is already in use. PromptWidget Type in the name of an executable file on your system. No program database found. Please update through Control menu. Command not found or not accepted!<br>Parameters, --switches and relative paths are not allowed.<br>Use nsm-proxy or write a starter-script instead. SessionTree Name Last Save Clients Size Symlinks Path About to delete Session {} All files in the project directory will be irreversibly deleted. Keep Session Delete! Copy Session Force Lock Removal Rename Session Delete Session Copying {} to {} TemplateUserManual User Manual Form Home Back TrayIcon Hide/Show Agordejo Add Client (Prompt) Save && Quit Agordejo Close without Saving && Quit Agordejo Quit Toggle Client Visibility WaitDialog Please confirm with a click on the button at the bottom. mainWindow Agordejo ready Another GUI tried to launch. Updating Program Database. Thank you for your patience. If progress freezes please kill and restart the whole program. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/translations/it2.ts0000644000175000017500000006315200000000000020743 0ustar00nilsnils AskBeforeQuit About to quit but session {} still open Sto per chiudere ma la sessione {} è ancora aperta Do you want to save? Vuoi salvare la sessione? About to quit Chiusura Don't Quit Non Uscire Save Salva Discard Changes Scarta Modifiche Dialog Settings Impostazioni Whitelist - Add executable names (not paths) to the program launcher. One executable per line. Lista Bianca - Aggiungi eseguibili (non percorsi) all'avviatore. Un nome per ogni linea. Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line. Lista Nera - Escludi eseguibili (non percorsi) dall'avviatore. Un nome per ogni linea. Launcher Avviatore For advanced users only! Add executable paths to the environment, just for Agordejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab. Solamente per utenti avanzati! Aggiungi percorsi degli eseguibili all'ambiente, solo per Agordejo e NSM. Per rendere effettive le modifiche sarà necessario il riavvio dell'applicazione. Se vuoi aggiungere un programma all'avviatore, usa la relativa scheda. Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don't matter. Aggiungi un percorso assoluto di una directory (es.: /home/user/audio-bin) per linea. No caratteri jolly. Le barre finali non sono richieste. $PATH Launcher Name Nome Description Descrizione Path Percorso LoadedSessionDescription Double click to add the client nsm-data to write here. Use it for notes, TODO, references etc… Doppio click per aggiugere il client nsm-data da scrivere qui. Usalo per note, lista TODO, riferimenti ecc… MainWindow Agordejo Start New Session Avvia Nuova Sessione Session Name Goes Here Inserire Qui Nome Sessione Save Salva Save and Close Salva e Chiudi Session Notes Note Sessione Quick View Vista Rapida New Nuova Load Selected Carica Selezionata Tree View Vista Albero Double-click to load program Doppio click per caricare un programma In current session Nella sessione corrente Full View Vista Completa JACK version and running versione ed esecuzione NSM Server Mode Modalità Server NSM Self-started, connected to, environment var Auto-avvio, connesso a, var ambiente NSM Url osc.upd ip port Session Root Root Sessione /home/usr/NSM Sessions Program Database Database Programma Last Updated Ultimo Aggiornamento Information Informazioni Processing Elaborazione Control Controllo SessionName ClientNameId Quit Esci Ctrl+Shift+Q Ctrl+Maiusc+Q About Informazioni Hide in System Tray Nascondi nell'Area di Notifica Ctrl+Q Add Client (Prompt) Aggiungi Client (Prompt) A Ctrl+S Save and Clone under different name Salva e Clona con un nome diverso Ctrl+Shift+S Ctrl+Maiusc+S Ctrl+W Close without Save ("Abort") Chiudi senza Salvare ("Abort") Ctrl+Shift+W Ctrl+Maiusc+W Stop Ferma Alt+O Resume Riprendi Alt+R Save separately Salva separatamente Alt+S Remove Rimuovi Alt+X Toggle Visible Alterna Visibilità Alt+T Show All Clients Mostra Tutti Client Hide All Clients Nascondi Tutti Client Rebuild Program Database Ricostruisci Database Programma Rename Rinomina F2 Settings Impostazioni Manual Manuale NewSession Dialog Finestra di dialogo New Session Name Nome Nuova Sessione Save JACK Connections (adds clients 'jackpatch') Salva Connessioni JACK (aggiunge client 'jackpatch') Client Renaming and Session Notes (adds client 'nsm-data') Rinomina Client e Note Sessione (aggiunge client 'nsm-data') OpenSession not saved non salvato clean pulisci Name Nome Label Etichetta Status Stato Visible VIsibile Changes Modifiche ID ProjectName Form Error Message Messaggio Errore Choose a project name. Use / for subdirectories Scegli un nome per il progetto. Usa / per le sottodirectory ProjectNameWidget Name must not be empty. Il nome non può rimanere vuoto. Name must be a relative path. Il nome dev'essere un percorso relativo. Moving to parent directory not allowed. Lo spostamento alla directory superiore non è consentito. Writing in this directory is not permitted. Non è concesso scrivere in questa directory. Name is already in use. Nome già in uso. PromptWidget Type in the name of an executable file on your system. Digita il nome di un file eseguibile nel tuo sistema. No program database found. Please update through Control menu. Database del programma non trovato. Si prega di aggiornarlo tramite menu Controllo. Command not found or not accepted!<br>Parameters, --switches and relative paths are not allowed.<br>Use nsm-proxy or write a starter-script instead. Comando non trovato o non accettato!<br>Parametri, --argomenti e percorsi relativi non sono ammessi. <br>Usa nsm-proxy oppure crea uno script di avvio. SessionTree Name Nome Last Save Ultimo Salvataggio Clients Client Size Dimensione Symlinks Collegamenti Simbolici Path Percorso About to delete Session {} Eliminazione Sessione {} All files in the project directory will be irreversibly deleted. Tutti i file nella directory del progetto verranno eliminati irreversibilmente. Keep Session Mantieni Sessione Delete! Elimina! Copy Session Copia Sessione Force Lock Removal Rimozione Forzata Blocco Rename Session Rinomina Sessione Delete Session Elimina Sessione TemplateUserManual User Manual Manuale Utente Form Home Principale Back Indietro TrayIcon Hide/Show Agordejo Nascondi/Mostra Agordejo Add Client (Prompt) Aggiungi Client (Prompt) Save && Quit Agordejo Salva e Chiudi Agordejo Close without Saving && Quit Agordejo Chiudi senza Salvare e Chiudi Agordejo Quit Esci mainWindow Agordejo ready Agordejo pronto Another GUI tried to launch. Un'altra GUI ha tentato di avviarsi. Updating Program Database. Thank you for your patience. Aggiornamento Database in corso. Si prega di attendere. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9778323 agordejo-0.3.1/qtgui/resources/translations/update.sh0000644000175000017500000000034200000000000021503 0ustar00nilsnils#!/bin/sh set -e pylupdate5 config.pro echo "linguist-qt5 de.ts" echo "linguist-qt5 it.ts" echo "Release from inside qt-linguist and then cd .. && sh buildresources.sh. Also read the manual, which has a translation chapter" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/sessiontreecontroller.py0000644000175000017500000004417700000000000020171 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library #Third Party from PyQt5 import QtCore, QtGui, QtWidgets #Engine import engine.api as api #Qt from .helper import sizeof_fmt from .projectname import ProjectNameWidget from .projectname import NewSessionDialog from .waitdialog import WaitDialog class DirectoryItem(QtWidgets.QTreeWidgetItem): """A plain directory with no session content""" def __lt__(self, other): """Treeview uses only less than. less equal, both greater and equal are not used. We always want to be on top, no matter what column or sort order """ if type(other) is SessionItem: if self.treeWidget().header().sortIndicatorOrder(): #descending? return False else: return True else: return QtWidgets.QTreeWidgetItem.__lt__(self, other) class SessionItem(QtWidgets.QTreeWidgetItem): """Subclass to enable sorting of size by actual value, not by human readable display. entry["nsmSessionName"] = projectName entry["name"] = os.path.basename(projectName) entry["lastSavedDate"] = "2016-05-21 16:36" entry["fullPath"] = actual path entry["sizeInBytes"] = 623623 entry["numberOfClients"] = 3 entry["hasSymlinks"] = True entry["parents"] = [] Also: entry["locked"] = True """ allItems = {} #nsmSessionName : SessionItem def __init__(self, sessionDict): SessionItem.allItems[sessionDict["nsmSessionName"]] = self self.sessionDict = sessionDict symlinks = "Yes" if sessionDict["hasSymlinks"] else "No" #TODO: Translate parameterList = [sessionDict["name"], sessionDict["lastSavedDate"], str(sessionDict["numberOfClients"]), sizeof_fmt(sessionDict["sizeInBytes"]), symlinks, sessionDict["fullPath"], ] super().__init__(parameterList, type=1000) #type 0 is default qt type. 1000 is subclassed user type) self.setTextAlignment(2, QtCore.Qt.AlignHCenter) #clients self.setTextAlignment(4, QtCore.Qt.AlignHCenter) #symlinks self.setLocked(sessionDict["locked"]) def updateData(self): """Actively queries the api for new data""" sessionDict = api.sessionQuery(self.sessionDict["nsmSessionName"]) self.sessionDict = sessionDict self.setText(0, sessionDict["name"]) self.setText(1, sessionDict["lastSavedDate"]) self.setText(2, str(sessionDict["numberOfClients"])) self.setText(3, sizeof_fmt(sessionDict["sizeInBytes"])) self.setText(4, "Yes" if sessionDict["hasSymlinks"] else "No") #TODO: Translate self.setText(5, sessionDict["fullPath"]) def setLocked(self, state:bool): """Number of clients, symlinks and size change frequently while a session is open/locked. We deactivate the display of these values while locked""" return if not state == self.isDisabled(): self.setDisabled(state) if state: self.setText(2, "") #number of clients self.setText(3, "") #Symlinks self.setText(4, "") #Size else: self.updateData() def updateTimestamp(self, timestamp:str): #Column 1 "Last Save" self.setText(1, timestamp) def __lt__(self, other): """Treeview uses only less than. less equal, both greater and equal are not used. There is no check between two directory-items here because these are standard WidgetItems """ if type(other) is DirectoryItem: #Just a dir return False #we are "greater"=later column = self.treeWidget().sortColumn() if column == 3: #bytes return self.sessionDict["sizeInBytes"] > other.sessionDict["sizeInBytes"] elif column == 2: #number of clients return self.sessionDict["numberOfClients"] > other.sessionDict["numberOfClients"] else: return QtWidgets.QTreeWidgetItem.__lt__(self, other) class SessionTreeController(object): """Controls a treeWidget, but does not subclass""" def __init__(self, mainWindow): self.mainWindow = mainWindow self.treeWidget = mainWindow.ui.session_tree self._cachedSessionDicts = None self.mainWindow.ui.checkBoxNested.stateChanged.connect(self._reactSignal_nestedFlatChanged) self._reactSignal_nestedFlatChanged(self.mainWindow.ui.checkBoxNested.isChecked()) #initial state #Configure the treewidget #columns: name, path (relative from session dir), number of programs, disk size (links resolved?) self.treeWidget.setColumnCount(4) self.headerLabels = [ QtCore.QCoreApplication.translate("SessionTree", "Name"), QtCore.QCoreApplication.translate("SessionTree", "Last Save"), QtCore.QCoreApplication.translate("SessionTree", "Clients"), QtCore.QCoreApplication.translate("SessionTree", "Size"), QtCore.QCoreApplication.translate("SessionTree", "Symlinks"), QtCore.QCoreApplication.translate("SessionTree", "Path"), ] self.treeWidget.setHeaderLabels(self.headerLabels) self.treeWidget.setSortingEnabled(True) self.treeWidget.setAlternatingRowColors(True) self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) #TODO: save sorting in user-wide qt application settings #We remember sorting via signals layoutAboutToBeChanged and restore via layoutChanged self.sortByColumnValue = 0 #by name self.sortDescendingValue = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending self.treeWidget.header().setSortIndicator(0,0) #Hack/Workaround. On startup it is not enough to set sorting. New items will be added in a random position. Maybe that is our async network adding. #self.treeWidget.sortByColumn(self.sortByColumnValue, self.sortDescendingValue) api.callbacks.sessionsChanged.append(self._reactCallback_sessionsChanged) api.callbacks.sessionLocked.append(self._reactCallback_sessionLocked) api.callbacks.sessionFileChanged.append(self._reactCallback_sessionFileChanged) self.treeWidget.currentItemChanged.connect(self._reactSelectionChanged) #click anywhere self.treeWidget.itemDoubleClicked.connect(self._reactSignal_itemDoubleClicked) self.treeWidget.customContextMenuRequested.connect(self.contextMenu) self.treeWidget.itemExpanded.connect(self._reactSignal_itemExpanded) self.treeWidget.itemCollapsed.connect(self._reactSignal_itemExpanded) self.treeWidget.model().layoutAboutToBeChanged.connect(self._reactSignal_rememberSorting) #self.treeWidget.model().layoutChanged.connect(self._reactSignal_restoreSorting) self.mainWindow.ui.button_new_session.clicked.connect(self._reactSignal_newSession) self.mainWindow.ui.button_new_quick_session.clicked.connect(api.sessionNewTimestamped) #The next ones are only available if a session item is currently selected. The connected lambda functions do not test for this, but we only enable the buttons when such an item is selected. self.mainWindow.ui.button_load_selected_session.clicked.connect(self._reactSignal_openSelected) self.mainWindow.ui.button_copy_selected_session.clicked.connect(lambda: self._askForCopyAndCopy(self.treeWidget.currentItem().sessionDict["nsmSessionName"])) self.mainWindow.ui.button_rename_selected_session.clicked.connect(lambda: self._askForNameAndRenameSession(self.treeWidget.currentItem().sessionDict["nsmSessionName"])) self.mainWindow.ui.button_delete_selected_session.clicked.connect(lambda: self.deleteSessionItem(self.treeWidget.currentItem())) self.mainWindow.ui.button_remove_lock_selected_session.clicked.connect(lambda: api.sessionForceLiftLock(self.treeWidget.currentItem().sessionDict["nsmSessionName"])) logger.info("Full View Session Chooser ready") def _reactCallback_sessionFileChanged(self, name:str, timestamp:str): """Timestamp of "last saved" changed""" SessionItem.allItems[name].updateTimestamp(timestamp) def _reactCallback_sessionLocked(self, name:str, state:bool): SessionItem.allItems[name].setLocked(state) def _reactCallback_sessionsChanged(self, sessionDicts:list): """Main callback for new, added, removed, moved sessions etc. We also get this for every client change so we can update our numbers""" self.treeWidget.clear() self._cachedSessionDicts = sessionDicts #in case we change the flat/nested mode. for sessionDict in sessionDicts: self.addSessionItem(sessionDict) for i in range(len(self.headerLabels)): self.treeWidget.resizeColumnToContents(i) #Make the name column a few pixels wider self.treeWidget.setColumnWidth(0, self.treeWidget.columnWidth(0) + 25) self.treeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue) def _addItemNested(self, sessionDict:dict): assert sessionDict, sessionDict item = SessionItem(sessionDict) if sessionDict["parents"]: #These are already a hirarchy, sorted from parents to children last = None #first is toplevel for parentDir in sessionDict["parents"]: alreadyExist = self.treeWidget.findItems(parentDir, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive, column=0) if alreadyExist: directoryItem = alreadyExist[0] else: directoryItem = DirectoryItem([parentDir]) #directoryItem = QtWidgets.QTreeWidgetItem([parentDir], 0) #type 0 is qt default if last: last.addChild(directoryItem) else: self.treeWidget.addTopLevelItem(directoryItem) last = directoryItem #After the loop: All subdirs built. Now add the item to the last one last.addChild(item) else: self.treeWidget.addTopLevelItem(item) def _addItemFlat(self, sessionDict:dict): assert sessionDict, sessionDict sessionDict["name"] = sessionDict["nsmSessionName"] item = SessionItem(sessionDict) self.treeWidget.addTopLevelItem(item) def addSessionItem(self, sessionDict:dict): if self.mode == "nested": self._addItemNested(sessionDict) elif self.mode == "flat": self._addItemFlat(sessionDict) else: raise ValueError("Unknown SessionTree display mode") def deleteSessionItem(self, item:SessionItem): """Instruct the engine to fully delete a complete session item. Will show a warning before.""" text = QtCore.QCoreApplication.translate("SessionTree", "About to delete Session {}").format(item.sessionDict["nsmSessionName"]) informativeText = QtCore.QCoreApplication.translate("SessionTree", "All files in the project directory will be irreversibly deleted.") title = QtCore.QCoreApplication.translate("SessionTree", "All files in the project directory will be irreversibly deleted.") title = QtCore.QCoreApplication.translate("SessionTree", "About to delete Session {}").format(item.sessionDict["nsmSessionName"]) box = QtWidgets.QMessageBox(self.treeWidget) box.setIcon(box.Warning) box.setText(text) box.setWindowTitle(title) box.setInformativeText(informativeText) keep = box.addButton(QtCore.QCoreApplication.translate("SessionTree", "Keep Session"), box.RejectRole) box.addButton(QtCore.QCoreApplication.translate("SessionTree", "Delete!"), box.AcceptRole) box.setDefaultButton(keep) ret = box.exec() #0 or 1. Return values are NOT the button roles. if ret: #Delete api.sessionDelete(item.sessionDict["nsmSessionName"]) def contextMenu(self, qpoint): item = self.treeWidget.itemAt(qpoint) if not type(item) is SessionItem: return menu = QtWidgets.QMenu() listOfLabelsAndFunctions = [ (QtCore.QCoreApplication.translate("SessionTree", "Copy Session"), lambda: self._askForCopyAndCopy(item.sessionDict["nsmSessionName"])) ] if item.isDisabled(): listOfLabelsAndFunctions.append((QtCore.QCoreApplication.translate("SessionTree", "Force Lock Removal"), lambda: api.sessionForceLiftLock(item.sessionDict["nsmSessionName"]))) else: listOfLabelsAndFunctions.append((QtCore.QCoreApplication.translate("SessionTree", "Rename Session"), lambda: self._askForNameAndRenameSession(item.sessionDict["nsmSessionName"]))) #Delete should be the bottom item. listOfLabelsAndFunctions.append((QtCore.QCoreApplication.translate("SessionTree", "Delete Session"), lambda: self.deleteSessionItem(item))) for text, function in listOfLabelsAndFunctions: if function is None: l = QtWidgets.QLabel(text) l.setAlignment(QtCore.Qt.AlignCenter) a = QtWidgets.QWidgetAction(menu) a.setDefaultWidget(l) menu.addAction(a) else: a = QtWidgets.QAction(text, menu) menu.addAction(a) a.triggered.connect(function) pos = QtGui.QCursor.pos() pos.setY(pos.y() + 5) menu.exec_(pos) #GUI Signals def _reactSelectionChanged(self, item, previous): """User clicks on an entry in the session chooser, or in the empty space. in any case, the selection changes and we can decide if we activate/deactivate certain buttons""" if not item or not type(item) is SessionItem or item.sessionDict["locked"] == True: sessionSelectedState = False else: sessionSelectedState = True self.mainWindow.ui.button_load_selected_session.setEnabled(sessionSelectedState) self.mainWindow.ui.button_copy_selected_session.setEnabled(sessionSelectedState) self.mainWindow.ui.button_rename_selected_session.setEnabled(sessionSelectedState) self.mainWindow.ui.button_delete_selected_session.setEnabled(sessionSelectedState) self.mainWindow.ui.button_remove_lock_selected_session.setEnabled(sessionSelectedState and item.sessionDict["locked"] == True) def _reactSignal_itemDoubleClicked(self, item:QtWidgets.QTreeWidgetItem, column:int): if not item.isDisabled() and type(item) is SessionItem: api.sessionOpen(item.sessionDict["nsmSessionName"]) def _reactSignal_itemExpanded(self, item:QtWidgets.QTreeWidgetItem): """Also for collapsed!""" for i in range(len(self.headerLabels)): self.treeWidget.resizeColumnToContents(i) def _reactSignal_openSelected(self): item = self.treeWidget.currentItem() if item: self._reactSignal_itemDoubleClicked(item, column=0) def _reactSignal_newSession(self): widget = NewSessionDialog(parent=self.treeWidget, startwith="") #widget = ProjectNameWidget(parent=self.treeWidget, startwith="") if widget.result: #result = {"name":str, "startclients":list} api.sessionNew(widget.result["name"], widget.result["startclients"]) def _askForCopyAndCopy(self, nsmSessionName:str): """Called by button and context menu""" copyString = QtCore.QCoreApplication.translate("SessionTree", "Copying {} to {}") widget = ProjectNameWidget(parent=self.treeWidget, startwith=nsmSessionName+"-copy") if widget.result: logger.info("Asking api to copy a session while waiting") copyString = copyString.format(nsmSessionName, widget.result) def longrunningfunction(progressHook): api.sessionCopy(nsmSessionName, widget.result, progressHook) diag = WaitDialog(self.mainWindow, copyString, longrunningfunction) #save in local var to keep alive #Somehow session list is wrong, symlinks are not calculated. Force update. api.requestSessionList() def _askForNameAndRenameSession(self, nsmSessionName:str): """Only for non-locked sessions. Context menu is only available if not locked.""" widget = ProjectNameWidget(parent=self.treeWidget, startwith=nsmSessionName) if widget.result and not widget.result == nsmSessionName: api.sessionRename(nsmSessionName, widget.result) def _reactSignal_rememberSorting(self, *args): self.sortByColumnValue = self.treeWidget.header().sortIndicatorSection() self.sortDescendingValue = self.treeWidget.header().sortIndicatorOrder() def _reactSignal_restoreSorting(self, *args): """Do not use as signal!!! Will lead to infinite recursion since Qt 5.12.2""" #self.treeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue) raise RuntimeError() def _reactSignal_nestedFlatChanged(self, checkStatus:bool): """#flat does not create directory items but changes the session name to dir/foo/bar""" if checkStatus: self.mode = "nested" else: self.mode = "flat" #And rebuild the items without fetching new data. if self._cachedSessionDicts: #not startup self._reactCallback_sessionsChanged(self._cachedSessionDicts) self.treeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/settings.py0000644000175000017500000001127300000000000015351 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library import pathlib import os #Third Party from PyQt5 import QtCore, QtWidgets #Engine import engine.api as api from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. #QtGui from .designer.settings import Ui_Dialog class SettingsDialog(QtWidgets.QDialog): def __init__(self, mainWindow): super().__init__() self.ui = Ui_Dialog() self.ui.setupUi(self) self.mainWindow = mainWindow self.success = False logger.info("Init settings dialog") settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) if settings.contains("launcherBlacklistPlainTextEdit"): self.ui.launcherBlacklistPlainTextEdit.setPlainText(settings.value("launcherBlacklistPlainTextEdit", type=str)) else: self.ui.launcherBlacklistPlainTextEdit.setPlainText("") if settings.contains("launcherWhitelistPlainTextEdit"): self.ui.launcherWhitelistPlainTextEdit.setPlainText(settings.value("launcherWhitelistPlainTextEdit", type=str)) else: self.ui.launcherWhitelistPlainTextEdit.setPlainText("") if settings.contains("programPathsPlainTextEdit"): self.ui.programPathsPlainTextEdit.setPlainText(settings.value("programPathsPlainTextEdit", type=str)) else: self.ui.programPathsPlainTextEdit.setPlainText("") #self.ui.name.textEdited.connect(self.check) #not called when text is changed programatically self.ui.buttonBox.accepted.connect(self.process) self.ui.buttonBox.rejected.connect(self.reject) self.setWindowFlag(QtCore.Qt.Popup, True) self.setModal(True) self.setFocus(True) logger.info("Show settings dialog") self.exec_() @staticmethod def loadFromSettingsAndSendToEngine(): """Called on program start and in self.process, which has a bit overhead because it is saving to file and then reloading from file (qsettings)""" settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) if settings.contains("launcherBlacklistPlainTextEdit"): bl = settings.value("launcherBlacklistPlainTextEdit", type=str) else: bl = None if settings.contains("launcherWhitelistPlainTextEdit"): wl = settings.value("launcherWhitelistPlainTextEdit", type=str) else: wl = None if settings.contains("programPathsPlainTextEdit"): pth = settings.value("programPathsPlainTextEdit", type=str) else: pth = None blacklist = bl.split("\n") if bl else [] whitelist = wl.split("\n") if wl else [] api.systemProgramsSetBlacklist(blacklist) api.systemProgramsSetWhitelist(whitelist) #Depends on SettingsDialog: More executable paths for the engine. We do this in mainwindow because it has access to the qsettings safe file and is started before engine, program-database or nsmd. additionalExecutablePaths = pth.split("\n") if pth else [] if additionalExecutablePaths: os.environ["PATH"] = os.pathsep.join(additionalExecutablePaths) + os.pathsep + os.environ["PATH"] logger.info(f"Binary search paths: {os.environ['PATH']}") def process(self): settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) settings.setValue("launcherBlacklistPlainTextEdit", self.ui.launcherBlacklistPlainTextEdit.toPlainText()) settings.setValue("launcherWhitelistPlainTextEdit", self.ui.launcherWhitelistPlainTextEdit.toPlainText()) settings.setValue("programPathsPlainTextEdit", self.ui.programPathsPlainTextEdit.toPlainText()) SettingsDialog.loadFromSettingsAndSendToEngine() self.success = True self.done(True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/systemtray.py0000644000175000017500000001410300000000000015730 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Third Party from PyQt5 import QtCore, QtGui, QtWidgets #Engine import engine.api as api #This loads the engine and starts a session. #QtGui from .addclientprompt import askForExecutable from .resources import * class SystemTray(QtWidgets.QSystemTrayIcon): def __init__(self, mainWindow): super().__init__(QtGui.QIcon(":icon.png")) self.mainWindow = mainWindow self.available = self.isSystemTrayAvailable() self.show() #self.showMessage("Title", "Helllo!", QtWidgets.QSystemTrayIcon.Information) #title, message, icon, timeout. #has messageClicked() signal. #Don't build the context menu here. The engine is not ready to provide us with session information. Let the callbacks do it. #self.messageClicked.connect(self._reactSignal_messageClicked) self.activated.connect(self._reactSignal_activated) #Connect to api callbacks to rebuild context menu when session changes api.callbacks.sessionClosed.append(self.buildContextMenu) api.callbacks.sessionOpenReady.append(self.buildContextMenu) api.callbacks.sessionsChanged.append(self.buildContextMenu) #api.callbacks.clientStatusChanged.append(self.buildContextMenu) #too much. We deal with the client list separately. self.toggleVisMenu = None def updateToggleVisMenu(self): if not self.toggleVisMenu or not api.currentSession(): return #program start or not in a session for a in self.toggleVisMenu.findChildren(QtWidgets.QAction): if not a.menu(): self.toggleVisMenu.removeAction(a) del a for clientItem in self.mainWindow.sessionController.openSessionController.allSessionItems(): if clientItem.clientDict["hasOptionalGUI"]: a = QtWidgets.QAction(clientItem.clientDict["reportedName"], self.toggleVisMenu) self.toggleVisMenu.addAction(a) def createToggleLambda(clientId): return lambda: api.clientToggleVisible(clientId) command = createToggleLambda(clientItem.clientDict["clientId"]) a.triggered.connect(command) def buildContextMenu(self, *args): """In a function for readability. It gets rebuild everytime a session is opened or closed or the session list changed """ menu = QtWidgets.QMenu() def _add(text, function): a = QtWidgets.QAction(text, menu) menu.addAction(a) a.triggered.connect(function) nsmSessionName = api.currentSession() _add(QtCore.QCoreApplication.translate("TrayIcon", "Hide/Show Agordejo"), lambda: self.mainWindow.toggleVisible(force=None)) #explicit force=None because the qt signal is sending a bool menu.addSeparator() #Add other pre-defined actions if nsmSessionName: #We are in a loaded session menu.addAction(self.mainWindow.ui.actionShow_All_Clients) _add(QtCore.QCoreApplication.translate("TrayIcon", "Add Client (Prompt)"), lambda: askForExecutable(self.mainWindow.qtApp.desktop())) menu.addSeparator() menu.addAction(self.mainWindow.ui.actionShow_All_Clients) menu.addAction(self.mainWindow.ui.actionHide_All_Clients) #Create a submenu to toggle visibility for all supported clients. self.toggleVisMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrayIcon", "Toggle Client Visibility")) #Updated on each trayIcon show event with updateToggleVisMenu menu.addSeparator() _add(QtCore.QCoreApplication.translate("TrayIcon", "Save {}".format(nsmSessionName)), api.sessionSave) _add(QtCore.QCoreApplication.translate("TrayIcon", "Save && Close {}".format(nsmSessionName)), api.sessionClose) _add(QtCore.QCoreApplication.translate("TrayIcon", "Close without Saving {}".format(nsmSessionName)), api.sessionAbort) menu.addSeparator() _add(QtCore.QCoreApplication.translate("TrayIcon", "Save && Quit Agordejo"), self.mainWindow.closeAndQuit) _add(QtCore.QCoreApplication.translate("TrayIcon", "Close without Saving && Quit Agordejo"), self.mainWindow.abortAndQuit) menu.addSeparator() else: for recentName in self.mainWindow.recentlyOpenedSessions.get(): _add(f"Session: {recentName}", lambda: api.sessionOpen(recentName)) _add(QtCore.QCoreApplication.translate("TrayIcon", "Quit "), self.mainWindow.menuRealQuit) self.setContextMenu(menu) def _reactSignal_activated(self, qActivationReason): """ QtWidgets.QSystemTrayIcon.Unknown QtWidgets.QSystemTrayIcon.Context QtWidgets.QSystemTrayIcon.DoubleClick QtWidgets.QSystemTrayIcon.Trigger QtWidgets.QSystemTrayIcon.MiddleClick """ logger.info(f"System tray activated with reason {qActivationReason}") self.updateToggleVisMenu() if qActivationReason == QtWidgets.QSystemTrayIcon.Trigger: self.mainWindow.toggleVisible() #def _reactSignal_messageClicked(self): # """this signal is emitted when the message displayed using # showMessage() was clicked by the user.""" # print ("clicky") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/usermanual.py0000644000175000017500000000442100000000000015662 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #System Wide Modules from PyQt5 import QtCore, QtWidgets, QtGui #Local Modules from engine.config import * #imports METADATA from .designer.usermanual import Ui_TemplateUserManual from engine.start import PATHS class UserManual(QtWidgets.QWidget): """A modal window that is hidden""" def __init__(self, mainWindow): super().__init__() self.mainWindow = mainWindow self.ui = Ui_TemplateUserManual() self.ui.setupUi(self) self.text = self.ui.textBrowser self.text.setSearchPaths([PATHS["doc"]]) #it's a list! self.text.setSource(QtCore.QUrl("index.html")) self.text.highlighted.connect(self.blockLink) self.ui.back.clicked.connect(self.text.backward) self.ui.home.clicked.connect(self.text.home) self.setWindowTitle(METADATA["name"] + " " + QtCore.QCoreApplication.translate("TemplateUserManual", "User Manual")) self.hide() def blockLink(self, qurl): """The browser displays binary "text" when following a link to an image. prevent that""" if qurl.url().endswith(".png") or qurl.url().startswith("http") or qurl.url().startswith("mailto"): #About to click an url. self.text.setOpenLinks(False) return False else: self.text.setOpenLinks(True) return True def closeEvent(self, event): self.hide() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/qtgui/waitdialog.py0000644000175000017500000001023500000000000015632 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ) This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library #Third Party from PyQt5 import QtCore, QtGui, QtWidgets class WaitThread(QtCore.QThread): def __init__(self, mainWindow, longRunningFunction): self.longRunningFunction = longRunningFunction self.mainWindow = mainWindow self.finished = False QtCore.QThread.__init__(self) def __del__(self): """This gets called rather early in the objects life but is in the main thread!""" self.wait() #This is after run self.finished = True logger.info(f"Thread done {self.longRunningFunction}") def run(self): """This is in the side-thread. We can't access qt widgets here""" logger.info(f"Thread running {self.longRunningFunction}") self.longRunningFunction() self.finished = True class WaitDialog(object): """An information text that closes itself once a task is done. Executes and shows on construction""" def __init__(self, mainWindow, text, longRunningFunction): super().__init__() self.text = text self._errorText = None logger.info(f"Starting blocking message for {longRunningFunction}") self.mainWindow = mainWindow self.mainWindow.ui.messageLabel.setText(text) #self.mainWindow.ui.messageLabel.setWordWrap(True) #qt segfault! maybe something with threads... don't care. We truncate now, see below. self.mainWindow.ui.mainPageSwitcher.setCurrentIndex(1) #1 is messageLabel 0 is the tab widget self.mainWindow.ui.menubar.setEnabled(False) #TODO: this will leave the options in the TrayIcon menu available.. but well, who cares... self.mainWindow.ui.waitDialogErrorButton.hide() self.mainWindow.ui.waitDialogErrorButton.setEnabled(False) self.mainWindow.ui.waitDialogErrorButton.clicked.connect(lambda: self.weAreDone()) #for some reason this does not trigger if we don't use lambda self.buttonErrorText = QtCore.QCoreApplication.translate("WaitDialog", "Please confirm with a click on the button at the bottom.") def wrap(): longRunningFunction(self.progressInfo) wt = WaitThread(mainWindow, wrap) #wt.finished.connect(self.threadDone) #does NOT trigger wt.start() while not wt.finished: self.mainWindow.qtApp.processEvents() if self._errorText: #progressInfo activated it self.mainWindow.ui.messageLabel.setText(self._errorText + "\n" + self.buttonErrorText) self.mainWindow.ui.waitDialogErrorButton.show() self.mainWindow.ui.waitDialogErrorButton.setEnabled(True) else: self.weAreDone() def weAreDone(self): self.mainWindow.ui.menubar.setEnabled(True) self.mainWindow.ui.waitDialogErrorButton.setEnabled(False) self.mainWindow.ui.waitDialogErrorButton.hide() self.mainWindow.ui.mainPageSwitcher.setCurrentIndex(0) #1 is messageLabel 0 is the tab widget def progressInfo(self, updateText:str): """ProcessEvents is already called above in init""" #updateText can be very long. They will destroy our complete layout if unchecked. we truncate to -80 self.mainWindow.ui.messageLabel.setText(self.text + "\n\n" + updateText[-80:]) if "ERROR!" in updateText: self._errorText = updateText ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/tools/nsm-data0000755000175000017500000003656700000000000014615 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), more specifically its template base application. The Template Base Application 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 . """ import logging; logger = logging.getLogger("nsm-data"); logger.info("import") URL="https://www.laborejo.org/agordejo/nsm-data" HARD_LIMIT = 512 # no single message longer than this VERSION= 1.1 #In case the user tries to run this standalone. import argparse parser = argparse.ArgumentParser(description="nsm-data is a module for Agordejo. It only communicates over OSC in an NSM-Session and has no standalone functionality.") parser.add_argument("-v", "--version", action='version', version=str(VERSION)) args = parser.parse_args() import json import pathlib from time import sleep from sys import exit as sysexit from nsmclient import NSMClient from nsmclient import NSMNotRunningError def chunkstring(string): return [string[0+i:HARD_LIMIT+i] for i in range(0, len(string), HARD_LIMIT)] class DataClient(object): """ Keys are strings, While nsmd OSC support int, str and float we use json exclusively. We expect a json string and will parse it here. All message consist of two arguments maximum: a key and, if a create-function, a json string. Rule: all client-keys are send as strings, even in replies. All client-values are send as json-string, even if originally just a string. Description is a multi-part message, a string. DataClient will register itself as Data-Storage. All other communication is done via osc. In theory every application can read and write us (like a book!) We listen to OSC paths and reply to the sender, which must give its address explicitly. /agordejo/datastorage/readall s:request-host i:request-port #Request all data /agordejo/datastorage/read s:key s:request-host i:request-port #Request one value The write functions have no reply. They will print out to stdout/err but not send an error message back. /agordejo/datastorage/create s:key any:value #Write/Create one value /agordejo/datastorage/update s:kecy any:value #Update a value, but only if it exists /agordejo/datastorage/delete s:key #Remove a key/value completely """ def __init__(self): self.data = None #Dict. created in openOrNewCallbackFunction, saved as json self.absoluteJsonFilePath = None #pathlib.Path set by openOrNewCallbackFunction self._descriptionStringArray = {"identifier":None} #int:str self._descriptionId = None self.nsmClient = NSMClient(prettyName = "Data-Storage", #will raise an error and exit if this example is not run from NSM. saveCallback = self.saveCallbackFunction, openOrNewCallback = self.openOrNewCallbackFunction, supportsSaveStatus = True, # Change this to True if your program announces it's save status to NSM exitProgramCallback = self.exitCallbackFunction, broadcastCallback = None, hideGUICallback = None, #replace with your hiding function. You need to answer in your function with nsmClient.announceGuiVisibility(False) showGUICallback = None, #replace with your showing function. You need to answer in your function with nsmClient.announceGuiVisibility(True) sessionIsLoadedCallback = self.sessionIsLoadedCallback, #no parametersd loggingLevel = "error", #"info" for development or debugging, "error" for production. default is error. ) #Add custom callbacks. They all receive _IncomingMessage(data) self.nsmClient.reactions["/agordejo/datastorage/setclientoverridename"] = self.setClientOverrideName self.nsmClient.reactions["/agordejo/datastorage/getclientoverridename"] = self.getClientOverrideName self.nsmClient.reactions["/agordejo/datastorage/getall"] = self.getAll self.nsmClient.reactions["/agordejo/datastorage/getdescription"] = self.getDescription self.nsmClient.reactions["/agordejo/datastorage/setdescription"] = self.setDescription self.nsmClient.reactions["/agordejo/datastorage/gettimelinemaximum"] = self.getTimelineMaximum self.nsmClient.reactions["/agordejo/datastorage/settimelinemaximum"] = self.setTimelineMaximum #self.nsmClient.reactions["/agordejo/datastorage/read"] = self.reactRead #generic key/value storage #self.nsmClient.reactions["/agordejo/datastorage/readall"] = self.reactReadAll #self.nsmClient.reactions["/agordejo/datastorage/create"] = self.reactCreate #self.nsmClient.reactions["/agordejo/datastorage/update"] = self.reactUpdate #self.nsmClient.reactions["/agordejo/datastorage/delete"] = self.reactDelete #NsmClients only returns from init when it has a connection, and on top (for us) when session is ready. It is safe to announce now. self.nsmClient.broadcast("/agordejo/datastorage/announce", [self.nsmClient.ourClientId, HARD_LIMIT, self.nsmClient.ourOscUrl]) while True: self.nsmClient.reactToMessage() sleep(0.05) #20fps update cycle def getAll(self, msg): """A complete data dump, intended to use once after startup. Will split into multiple reply messages, if needed. Our mirror datastructure in nsmservercontrol.py calls that on init. """ senderHost, senderPort = msg.params path = "/agordejo/datastorage/reply/getall" encoded = json.dumps(self.data) chunks = chunkstring(encoded) l = len(chunks) for index, chunk in enumerate(chunks): listOfParameters = [index+0, l-1, chunk] self.nsmClient.send(path, listOfParameters, host=senderHost, port=senderPort) def getDescription(self, msg)->str: """Returns a normal string, not json""" senderHost, senderPort = msg.params path = "/agordejo/datastorage/reply/getdescription" chunks = chunkstring(self.data["description"]) l = len(chunks) for index, chunk in enumerate(chunks): listOfParameters = [index+0, l-1, chunk] self.nsmClient.send(path, listOfParameters, host=senderHost, port=senderPort) def setDescription(self, msg): """ Answers with descriptionId and index when data was received and saved. The GUI needs to buffer this a bit. Don't send every char as single message. This is for multi-part messages Index is 0 based, chunk is part of a simple string, not json. The descriptionId:int indicates the message the chunks belong to. If we see a new one we reset our storage. """ descriptionId, index, chunk, senderHost, senderPort = msg.params #str, int, str, str, int if not self._descriptionId == descriptionId: self._descriptionId = descriptionId self._descriptionStringArray.clear() self._descriptionStringArray[index] = chunk buildString = "".join([v for k,v in sorted(self._descriptionStringArray.items())]) self.data["description"] = buildString self.nsmClient.announceSaveStatus(False) def getClientOverrideName(self, msg): """Answers with empty string if clientId does not exist or has not data. This is a signal for the GUI/host to use the original name!""" clientId, senderHost, senderPort = msg.params path = "/agordejo/datastorage/reply/getclient" if clientId in self.data["clientOverrideNames"]: name = self.data["clientOverrideNames"][clientId] else: logger.info(f"We were instructed to read client {clientId}, but it does not exist") name = "" listOfParameters = [clientId, json.dumps(name)] self.nsmClient.send(path, listOfParameters, host=senderHost, port=senderPort) def setClientOverrideName(self, msg): """We accept empty string as a name to remove the name override. """ clientId, jsonValue = msg.params name = json.loads(jsonValue)[:HARD_LIMIT] if name: self.data["clientOverrideNames"][clientId] = name else: #It is possible that a client not present in our storage will send an empty string. Protect. if clientId in self.data["clientOverrideNames"]: del self.data["clientOverrideNames"][clientId] self.nsmClient.announceSaveStatus(False) def getTimelineMaximum(self, msg): """ In minutes If the GUI supports global jack transport controls this can be used to remember the users setting for the maximum timeline duration. JACKs own data is without an upper bound.""" senderHost, senderPort = msg.params path = "/agordejo/datastorage/reply/gettimelinemaximum" if "timelineMaximumDuration" in self.data: numericValue = self.data["timelineMaximumDuration"] else: logger.info(f"We were instructed to read the timeline maximum duration, but it does not exist yet") numericValue = 5# minutes. listOfParameters = [json.dumps(numericValue)] self.nsmClient.send(path, listOfParameters, host=senderHost, port=senderPort) def setTimelineMaximum(self, msg): """In minutes""" jsonValue = msg.params[0] #list of 1 numericValue = json.loads(jsonValue) if numericValue <= 1: numericValue = 1 self.data["timelineMaximumDuration"] = numericValue self.nsmClient.announceSaveStatus(False) #Generic Functions. Not in use and not ready. #Callback Reactions to OSC. They all receive _IncomingMessage(data) def reactReadAll(self, msg): senderHost, senderPort = msg.params path = "/agordejo/datastorage/reply/readall" encoded = json.dumps("") chunks = chunkstring(encoded, 512) l = len(chunks) for index, chunk in enumerate(chunks): listOfParameters = [index+0, l-1, chunk] self.nsmClient.send(path, listOfParameters, host=senderHost, port=senderPort) def reactRead(self, msg): key, senderHost, senderPort = msg.params if key in self.data: path = "/agordejo/datastorage/reply/read" listOfParameters = [key, json.dumps(self.data[key])] self.nsmClient.send(path, listOfParameters, host=senderHost, port=senderPort) else: logger.warning(f"We were instructed to read key {key}, but it does not exist") def reactCreate(self, msg): key, jsonValue = msg.params value = json.loads(jsonValue) self.data[key] = value self.nsmClient.announceSaveStatus(False) def reactUpdate(self, msg): key, jsonValue = msg.params value = json.loads(jsonValue) if key in self.data: self.data[key] = value self.nsmClient.announceSaveStatus(False) else: logger.warning(f"We were instructed to update key {key} with value {value}, but it does not exist") def reactDelete(self, msg): key = msg.params[0] if key in self.data: del self.data[key] self.nsmClient.announceSaveStatus(False) else: logger.warning(f"We were instructed to delete key {key}, but it does not exist") #NSM Callbacks and File Handling def saveCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM): result = self.data result["origin"] = URL result["version"] = VERSION jsonData = json.dumps(result, indent=2) try: with open(self.absoluteJsonFilePath, "w", encoding="utf-8") as f: f.write(jsonData) except Exception as e: logging.error("Will not load or save because: " + e.__repr__()) return self.absoluteJsonFilePath #nsmclient.py will send save status clean def openOrNewCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM): self.absoluteJsonFilePath = pathlib.Path(ourPath) try: self.data = self.openFromJson(self.absoluteJsonFilePath) except FileNotFoundError: self.data = None #This makes debugging output nicer. If we init Data() here all errors will be presented as follow-up error "while handling exception FileNotFoundError". except (NotADirectoryError, PermissionError) as e: self.data = None logger.error("Will not load or save because: " + e.__repr__()) #Version 1.1 save file updates if self.data: if not "timelineMaximumDuration" in self.data: self.data["timelineMaximumDuration"] = 5 #5 minutes as sensible default else: self.data = {"clientOverrideNames":{}, "description":"", "timelineMaximumDuration":5} #5 minutes as sensible default logger.info("New/Open complete") #Data is not send here. Instead the gui calls the getAll message later. def openFromJson(self, absoluteJsonFilePath): with open(absoluteJsonFilePath, "r", encoding="utf-8") as f: try: text = f.read() result = json.loads(text) except Exception as error: result = None logger.error(error) if result and "version" in result and "origin" in result and result["origin"] == URL: if result["version"] <= VERSION: assert type(result) is dict, (result, type(result)) logger.info("Loading file from json complete") return result else: logger.error(f"""{absoluteJsonFilePath} was saved with {result["version"]} but we need {VERSION}""") #self.nsmClient.setLabel... We cannot use nsm client here because at this point we are still in the open/new callback. and self.nsmClient does not exist yet. sysexit() else: logger.error(f"""Error. {absoluteJsonFilePath} not loaded. Not a sane agordejo/nsm-data file in json format""") sysexit() def sessionIsLoadedCallback(self): """At one point I thought we could send our data when session is ready, so the GUI actually has clients to rename. However, that turned out impossible or impractical. Instead the GUI now just fails if nameOverrides that we send are not available yet and tries again later. Leave that in for documentation. """ pass #def broadcastCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM, messagePath, listOfArguments): # print (__file__, "broadcast") def exitCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM): sysexit(0) if __name__ == '__main__': """Creating an instance starts the client and does not return""" try: DataClient() except NSMNotRunningError: parser.print_help() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/tools/nsmclient.py0000644000175000017500000010261100000000000015511 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles : http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite , All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import logging; logger: logging.Logger #filled by init with client logger. import struct import socket from os import getenv, getpid, kill import os import os.path import shutil from uuid import uuid4 from sys import argv from signal import signal, SIGTERM, SIGINT, SIGKILL #react to exit signals to close the client gracefully. Or kill if the client fails to do so. from urllib.parse import urlparse class _IncomingMessage(object): """Representation of a parsed datagram representing an OSC message. An OSC message consists of an OSC Address Pattern followed by an OSC Type Tag String followed by zero or more OSC Arguments. """ def __init__(self, dgram): #NSM Broadcasts are bundles, but very simple ones. We only need to care about the single message it contains. #Therefore we can strip the bundle prefix and handle it as normal message. if b"#bundle" in dgram: bundlePrefix, singleMessage = dgram.split(b"/", maxsplit=1) dgram = b"/" + singleMessage # / eaten by split self.isBroadcast = True else: self.isBroadcast = False self.LENGTH = 4 #32 bit self._dgram = dgram self._parameters = [] self.parse_datagram() def get_int(self, dgram, start_index): """Get a 32-bit big-endian two's complement integer from the datagram. Args: dgram: A datagram packet. start_index: An index where the integer starts in the datagram. Returns: A tuple containing the integer and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: if len(dgram[start_index:]) < self.LENGTH: raise ValueError('Datagram is too short') return ( struct.unpack('>i', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def get_string(self, dgram, start_index): """Get a python string from the datagram, starting at pos start_index. We receive always the full string, but handle only the part from the start_index internally. In the end return the offset so it can be added to the index for the next parameter. Each subsequent call handles less of the same string, starting further to the right. According to the specifications, a string is: "A sequence of non-null ASCII characters followed by a null, followed by 0-3 additional null characters to make the total number of bits a multiple of 32". Args: dgram: A datagram packet. start_index: An index where the string starts in the datagram. Returns: A tuple containing the string and the new end index. Raises: ValueError if the datagram could not be parsed. """ #First test for empty string, which is nothing, followed by a terminating \x00 padded by three additional \x00. if dgram[start_index:].startswith(b"\x00\x00\x00\x00"): return "", start_index + 4 #Otherwise we have a non-empty string that must follow the rules of the docstring. offset = 0 try: while dgram[start_index + offset] != 0: offset += 1 if offset == 0: raise ValueError('OSC string cannot begin with a null byte: %s' % dgram[start_index:]) # Align to a byte word. if (offset) % self.LENGTH == 0: offset += self.LENGTH else: offset += (-offset % self.LENGTH) # Python slices do not raise an IndexError past the last index, # do it ourselves. if offset > len(dgram[start_index:]): raise ValueError('Datagram is too short') data_str = dgram[start_index:start_index + offset] return data_str.replace(b'\x00', b'').decode('utf-8'), start_index + offset except IndexError as ie: raise ValueError('Could not parse datagram %s' % ie) except TypeError as te: raise ValueError('Could not parse datagram %s' % te) def get_float(self, dgram, start_index): """Get a 32-bit big-endian IEEE 754 floating point number from the datagram. Args: dgram: A datagram packet. start_index: An index where the float starts in the datagram. Returns: A tuple containing the float and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: return (struct.unpack('>f', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def parse_datagram(self): try: self._address_regexp, index = self.get_string(self._dgram, 0) if not self._dgram[index:]: # No params is legit, just return now. return # Get the parameters types. type_tag, index = self.get_string(self._dgram, index) if type_tag.startswith(','): type_tag = type_tag[1:] # Parse each parameter given its type. for param in type_tag: if param == "i": # Integer. val, index = self.get_int(self._dgram, index) elif param == "f": # Float. val, index = self.get_float(self._dgram, index) elif param == "s": # String. val, index = self.get_string(self._dgram, index) else: logger.warning("Unhandled parameter type: {0}".format(param)) continue self._parameters.append(val) except ValueError as pe: #raise ValueError('Found incorrect datagram, ignoring it', pe) # Raising an error is not ignoring it! logger.warning("Found incorrect datagram, ignoring it. {}".format(pe)) @property def oscpath(self): """Returns the OSC address regular expression.""" return self._address_regexp @staticmethod def dgram_is_message(dgram): """Returns whether this datagram starts as an OSC message.""" return dgram.startswith(b'/') @property def size(self): """Returns the length of the datagram for this message.""" return len(self._dgram) @property def dgram(self): """Returns the datagram from which this message was built.""" return self._dgram @property def params(self): """Convenience method for list(self) to get the list of parameters.""" return list(self) def __iter__(self): """Returns an iterator over the parameters of this message.""" return iter(self._parameters) class _OutgoingMessage(object): def __init__(self, oscpath): self.LENGTH = 4 #32 bit self.oscpath = oscpath self._args = [] def write_string(self, val): dgram = val.encode('utf-8') diff = self.LENGTH - (len(dgram) % self.LENGTH) dgram += (b'\x00' * diff) return dgram def write_int(self, val): return struct.pack('>i', val) def write_float(self, val): return struct.pack('>f', val) def add_arg(self, argument): t = {str:"s", int:"i", float:"f"}[type(argument)] self._args.append((t, argument)) def build(self): dgram = b'' #OSC Path dgram += self.write_string(self.oscpath) if not self._args: dgram += self.write_string(',') return dgram # Write the parameters. arg_types = "".join([arg[0] for arg in self._args]) dgram += self.write_string(',' + arg_types) for arg_type, value in self._args: f = {"s":self.write_string, "i":self.write_int, "f":self.write_float}[arg_type] dgram += f(value) return dgram class NSMNotRunningError(Exception): """Error raised when environment variable $NSM_URL was not found.""" class NSMClient(object): """The representation of the host programs as NSM sees it. Technically consists of an udp server and a udp client. Does not run an event loop itself and depends on the host loop. E.g. a Qt timer or just a simple while True: sleep(0.1) in Python.""" def __init__(self, prettyName, supportsSaveStatus, saveCallback, openOrNewCallback, exitProgramCallback, hideGUICallback=None, showGUICallback=None, broadcastCallback=None, sessionIsLoadedCallback=None, loggingLevel = "info"): self.nsmOSCUrl = self.getNsmOSCUrl() #this fails and raises NSMNotRunningError if NSM is not available. Host programs can ignore it or exit their program. self.realClient = True self.cachedSaveStatus = None #save status checks for this. global logger logger = logging.getLogger(prettyName) logger.info("import") if loggingLevel == "info" or loggingLevel == 20: logging.basicConfig(level=logging.INFO) #development logger.info("Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!") #the NSM name is not ready yet so we just use the pretty name elif loggingLevel == "error" or loggingLevel == 40: logging.basicConfig(level=logging.ERROR) #production else: logging.warning("Unknown logging level: {}. Choose 'info' or 'error'".format(loggingLevel)) logging.basicConfig(level=logging.INFO) #development #given parameters, self.prettyName = prettyName #keep this consistent! Settle for one name. self.supportsSaveStatus = supportsSaveStatus self.saveCallback = saveCallback self.exitProgramCallback = exitProgramCallback self.openOrNewCallback = openOrNewCallback #The host needs to: Create a jack client with ourClientNameUnderNSM - Open the saved file and all its resources self.broadcastCallback = broadcastCallback self.hideGUICallback = hideGUICallback self.showGUICallback = showGUICallback self.sessionIsLoadedCallback = sessionIsLoadedCallback #Reactions get the raw _IncomingMessage OSC object #A client can add to reactions. self.reactions = { "/nsm/client/save" : self._saveCallback, "/nsm/client/show_optional_gui" : lambda msg: self.showGUICallback(), "/nsm/client/hide_optional_gui" : lambda msg: self.hideGUICallback(), "/nsm/client/session_is_loaded" : self._sessionIsLoadedCallback, #Hello source-code reader. You can add your own reactions here by nsmClient.reactions[oscpath]=func, where func gets the raw _IncomingMessage OSC object as argument. #broadcast is handled directly by the function because it has more parameters } #self.discardReactions = set(["/nsm/client/session_is_loaded"]) self.discardReactions = set() #Networking and Init self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp self.sock.bind(('', 0)) #pick a free port on localhost. ip, port = self.sock.getsockname() self.ourOscUrl = f"osc.udp://{ip}:{port}/" self.executableName = self.getExecutableName() #UNIX Signals. Used for quit. signal(SIGTERM, self.sigtermHandler) #NSM sends only SIGTERM. #TODO: really? pynsm version 1 handled sigkill as well. signal(SIGINT, self.sigtermHandler) #The following instance parameters are all set in announceOurselves self.serverFeatures = None self.sessionName = None self.ourPath = None self.ourClientNameUnderNSM = None self.ourClientId = None # the "file extension" of ourClientNameUnderNSM self.isVisible = None #set in announceGuiVisibility self.saveStatus = True # true is clean. false means we need saving. self.announceOurselves() assert self.serverFeatures, self.serverFeatures assert self.sessionName, self.sessionName assert self.ourPath, self.ourPath assert self.ourClientNameUnderNSM, self.ourClientNameUnderNSM self.sock.setblocking(False) #We have waited for tha handshake. Now switch blocking off because we expect sock.recvfrom to be empty in 99.99...% of the time so we shouldn't wait for the answer. #After this point the host must include self.reactToMessage in its event loop #We assume we are save at startup. self.announceSaveStatus(isClean = True) logger.info("NSMClient client init complete. Going into listening mode.") def reactToMessage(self): """This is the main loop message. It is added to the clients event loop.""" try: data, addr = self.sock.recvfrom(4096) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096. except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. return None msg = _IncomingMessage(data) if msg.oscpath in self.reactions: self.reactions[msg.oscpath](msg) elif msg.oscpath in self.discardReactions: pass elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/open", "Loaded."]: #NSM sends that all programs of the session were loaded. logger.info ("Got /reply Loaded from NSM Server") elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/save", "Saved."]: #NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually logger.info ("Got /reply Saved from NSM Server") elif msg.isBroadcast: if self.broadcastCallback: logger.info (f"Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}") self.broadcastCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM, msg.oscpath, msg.params) else: logger.info (f"No callback for broadcast! Got messagePath {msg.oscpath} and listOfArguments {msg.params}") elif msg.oscpath == "/error": logger.warning("Got /error from NSM Server. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) else: logger.warning("Reaction not implemented:. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) def send(self, path:str, listOfParameters:list, host=None, port=None): """Send any osc message. Defaults to nsmd URL. Will not wait for an answer but return None.""" if host and port: url = (host, port) else: url = self.nsmOSCUrl msg = _OutgoingMessage(path) for arg in listOfParameters: msg.add_arg(arg) #type is auto-determined by outgoing message self.sock.sendto(msg.build(), url) def getNsmOSCUrl(self): """Return and save the nsm osc url or raise an error""" nsmOSCUrl = getenv("NSM_URL") if not nsmOSCUrl: raise NSMNotRunningError("New-Session-Manager environment variable $NSM_URL not found.") else: #osc.udp://hostname:portnumber/ o = urlparse(nsmOSCUrl) #return o.hostname, o.port #this always make the hostname lowercase. usually it does not matter, but we got crash reports. Alternative: return o.netloc.split(":")[0], o.port def getExecutableName(self): """Finding the actual executable name can be a bit hard in Python. NSM wants the real starting point, even if it was a bash script. """ #TODO: I really don't know how to find out the name of the bash script fullPath = argv[0] assert os.path.dirname(fullPath) in os.environ["PATH"], (fullPath, os.path.dirname(fullPath), os.environ["PATH"]) #NSM requires the executable to be in the path. No excuses. This will never happen since the reference NSM server-GUI already checks for this. executableName = os.path.basename(fullPath) assert not "/" in executableName, executableName #see above. return executableName def announceOurselves(self): """Say hello to NSM and tell it we are ready to receive instructions /nsm/server/announce s:application_name s:capabilities s:executable_name i:api_version_major i:api_version_minor i:pid""" def buildClientFeaturesString(): #:dirty:switch:progress: result = [] if self.supportsSaveStatus: result.append("dirty") if self.hideGUICallback and self.showGUICallback: result.append("optional-gui") if result: return ":".join([""] + result + [""]) else: return "" logger.info("Sending our NSM-announce message") announce = _OutgoingMessage("/nsm/server/announce") announce.add_arg(self.prettyName) #s:application_name announce.add_arg(buildClientFeaturesString()) #s:capabilities announce.add_arg(self.executableName) #s:executable_name announce.add_arg(1) #i:api_version_major announce.add_arg(2) #i:api_version_minor announce.add_arg(int(getpid())) #i:pid hostname, port = self.nsmOSCUrl assert hostname, self.nsmOSCUrl assert port, self.nsmOSCUrl self.sock.sendto(announce.build(), self.nsmOSCUrl) #Wait for /reply (aka 'Howdy, what took you so long?) data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if msg.oscpath == "/error": originalMessage, errorCode, reason = msg.params logger.error("Code {}: {}".format(errorCode, reason)) quit() elif msg.oscpath == "/reply": nsmAnnouncePath, welcomeMessage, managerName, self.serverFeatures = msg.params assert nsmAnnouncePath == "/nsm/server/announce", nsmAnnouncePath logger.info("Got /reply " + welcomeMessage) #Wait for /nsm/client/open data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) assert msg.oscpath == "/nsm/client/open", msg.oscpath self.ourPath, self.sessionName, self.ourClientNameUnderNSM = msg.params self.ourClientId = os.path.splitext(self.ourClientNameUnderNSM)[1][1:] logger.info("Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath)) self.openOrNewCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #Host function to either load an existing session or create a new one. logger.info("Our client should be done loading or creating the file {}".format(self.ourPath)) replyToOpen = _OutgoingMessage("/reply") replyToOpen.add_arg("/nsm/client/open") replyToOpen.add_arg("{} is opened or created".format(self.prettyName)) self.sock.sendto(replyToOpen.build(), self.nsmOSCUrl) else: raise ValueError("Unexpected message path after announce: {}".format((msg.oscpath, msg.params))) def announceGuiVisibility(self, isVisible): message = "/nsm/client/gui_is_shown" if isVisible else "/nsm/client/gui_is_hidden" self.isVisible = isVisible guiVisibility = _OutgoingMessage(message) logger.info("Telling NSM that our clients switched GUI visibility to: {}".format(message)) self.sock.sendto(guiVisibility.build(), self.nsmOSCUrl) def announceSaveStatus(self, isClean): """Only send to the NSM Server if there was really a change""" if not self.supportsSaveStatus: return if not isClean == self.cachedSaveStatus: message = "/nsm/client/is_clean" if isClean else "/nsm/client/is_dirty" self.cachedSaveStatus = isClean saveStatus = _OutgoingMessage(message) logger.info("Telling NSM that our clients save state is now: {}".format(message)) self.sock.sendto(saveStatus.build(), self.nsmOSCUrl) def _saveCallback(self, msg): logger.info("Telling our client to save as {}".format(self.ourPath)) self.saveCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) replyToSave = _OutgoingMessage("/reply") replyToSave.add_arg("/nsm/client/save") replyToSave.add_arg("{} saved".format(self.prettyName)) self.sock.sendto(replyToSave.build(), self.nsmOSCUrl) #it is assumed that after saving the state is clear self.announceSaveStatus(isClean = True) def _sessionIsLoadedCallback(self, msg): if self.sessionIsLoadedCallback: logger.info("Received 'Session is Loaded'. Our client supports it. Forwarding message...") self.sessionIsLoadedCallback() else: logger.info("Received 'Session is Loaded'. Our client does not support it, which is the default. Discarding message...") def sigtermHandler(self, signal, frame): """Wait for the user to quit the program The user function does not need to exit itself. Just shutdown audio engines etc. It is possible, that the client does not implement quit properly. In that case NSM protocol demands that we quit anyway. No excuses. Achtung GDB! If you run your program with gdb --args python foo.py the Python signal handler will not work. This has nothing to do with this library. """ logger.info("Telling our client to quit.") self.exitProgramCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #There is a chance that exitProgramCallback will hang and the program won't quit. However, this is broken design and bad programming. We COULD place a timeout here and just kill after 10s or so, but that would make quitting our responsibility and fixing a broken thing. #If we reach this point we have reached the point of no return. Say goodbye. logger.warning("Client did not quit on its own. Sending SIGKILL.") kill(getpid(), SIGKILL) logger.error("SIGKILL did nothing. Do it manually.") def debugResetDataAndExit(self): """This is solely meant for debugging and testing. The user way of action should be to remove the client from the session and add a new instance, which will get a different NSM-ID. Afterwards we perform a clean exit.""" logger.warning("debugResetDataAndExit will now delete {} and then request an exit.".format(self.ourPath)) if os.path.exists(self.ourPath): if os.path.isfile(self.ourPath): try: os.remove(self.ourPath) except Exception as e: logger.info(e) elif os.path.isdir(self.ourPath): try: shutil.rmtree(self.ourPath) except Exception as e: logger.info(e) else: logger.info("{} does not exist.".format(self.ourPath)) self.serverSendExitToSelf() def serverSendExitToSelf(self): """If you want a very strict client you can block any non-NSM quit-attempts, like ignoring a qt closeEvent, and instead send the NSM Server a request to close this client. This method is a shortcut to do just that. """ logger.info("Sending SIGTERM to ourselves to trigger the exit callback.") #if "server-control" in self.serverFeatures: # message = _OutgoingMessage("/nsm/server/stop") # message.add_arg("{}".format(self.ourClientId)) # self.sock.sendto(message.build(), self.nsmOSCUrl) #else: kill(getpid(), SIGTERM) #this calls the exit callback def serverSendSaveToSelf(self): """Some clients want to offer a manual Save function, mostly for psychological reasons. We offer a clean solution in calling this function which will trigger a round trip over the NSM server so our client thinks it received a Save instruction. This leads to a clean state with a good saveStatus and no required extra functionality in the client.""" logger.info("instructing the NSM-Server to send Save to ourselves.") if "server-control" in self.serverFeatures: #message = _OutgoingMessage("/nsm/server/save") # "Save All" Command. message = _OutgoingMessage("/nsm/gui/client/save") message.add_arg("{}".format(self.ourClientId)) self.sock.sendto(message.build(), self.nsmOSCUrl) else: logger.warning("...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures)) def changeLabel(self, label:str): """This function is implemented because it is provided by NSM. However, it does not much. The message gets received but is not saved. The official NSM GUI uses it but then does not save it. We would have to send it every startup ourselves. This is fine for us as clients, but you need to provide a GUI field to enter that label.""" logger.info("Telling the NSM-Server that our label is now " + label) message = _OutgoingMessage("/nsm/client/label") message.add_arg(label) #s:label self.sock.sendto(message.build(), self.nsmOSCUrl) def broadcast(self, path:str, arguments:list): """/nsm/server/broadcast s:path [arguments...] We, as sender, will not receive the broadcast back. Broadcasts starting with /nsm are not allowed and will get discarded by the server """ if path.startswith("/nsm"): logger.warning("Attempted broadbast starting with /nsm. Not allwoed") else: logger.info("Sending broadcast " + path + repr(arguments)) message = _OutgoingMessage("/nsm/server/broadcast") message.add_arg(path) for arg in arguments: message.add_arg(arg) #type autodetect self.sock.sendto(message.build(), self.nsmOSCUrl) def importResource(self, filePath): """aka. import into session ATTENTION! You will still receive an absolute path from this function. You need to make sure yourself that this path will not be saved in your save file, but rather use a place- holder that gets replaced by the actual session path each time. A good point is after serialisation. search&replace for the session prefix ("ourPath") and replace it with a tag e.g. . The opposite during load. Only such a behaviour will make your session portable. Do not use the following pattern: An alternative that comes to mind is to only work with relative paths and force your programs workdir to the session directory. Better work with absolute paths internally . Symlinks given path into session dir and returns the linked path relative to the ourPath. It can handles single files as well as whole directories. if filePath is already a symlink we do not follow it. os.path.realpath or os.readlink will not be used. Multilayer links may indicate a users ordering system that depends on abstractions. e.g. with mounted drives under different names which get symlinked to a reliable path. Basically do not question the type of our input filePath. tar with the follow symlink option has os.path.realpath behaviour and therefore is able to follow multiple levels of links anyway. A hardlink does not count as a link and will be detected and treated as real file. Cleaning up a session directory is either responsibility of the user or of our client program. We do not provide any means to unlink or delete files from the session directory. """ #Even if the project was not saved yet now it is time to make our directory in the NSM dir. if not os.path.exists(self.ourPath): os.makedirs(self.ourPath) filePath = os.path.abspath(filePath) #includes normalisation if not os.path.exists(self.ourPath):raise FileNotFoundError(self.ourPath) if not os.path.isdir(self.ourPath): raise NotADirectoryError(self.ourPath) if not os.access(self.ourPath, os.W_OK): raise PermissionError("not writable", self.ourPath) if not os.path.exists(filePath):raise FileNotFoundError(filePath) if os.path.isdir(filePath): raise IsADirectoryError(filePath) if not os.access(filePath, os.R_OK): raise PermissionError("not readable", filePath) filePathInOurSession = os.path.commonprefix([filePath, self.ourPath]) == self.ourPath linkedPath = os.path.join(self.ourPath, os.path.basename(filePath)) linkedPathAlreadyExists = os.path.exists(linkedPath) if not os.access(os.path.dirname(linkedPath), os.W_OK): raise PermissionError("not writable", os.path.dirname(linkedPath)) if filePathInOurSession: #loadResource from our session dir. Portable session, manually copied beforehand or just loading a link again. linkedPath = filePath #we could return here, but we continue to get the tests below. logger.info(f"tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ") elif linkedPathAlreadyExists and os.readlink(linkedPath) == filePath: #the imported file already exists as link in our session dir. We do not link it again but simply report the existing link. #We only check for the first target of the existing link and do not follow it through to a real file. #This way all user abstractions and file structures will be honored. linkedPath = linkedPath logger.info(f"tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath} ") elif linkedPathAlreadyExists: #A new file shall be imported but it would create a linked name which already exists in our session dir. #Because we already checked for a new link to the same file above this means actually linking a different file so we need to differentiate with a unique name firstpart, extension = os.path.splitext(linkedPath) uniqueLinkedPath = firstpart + "." + uuid4().hex + extension assert not os.path.exists(uniqueLinkedPath) os.symlink(filePath, uniqueLinkedPath) logger.info(self.ourClientNameUnderNSM + f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.") linkedPath = uniqueLinkedPath else: #this is the "normal" case. External resources will be linked. assert not os.path.exists(linkedPath) os.symlink(filePath, linkedPath) logger.info(f"imported external resource {filePath} as link {linkedPath}") assert os.path.exists(linkedPath), linkedPath return linkedPath class NullClient(object): """Use this as a drop-in replacement if your program has a mode without NSM but you don't want to change the code itself. This was originally written for programs that have a core-engine and normal mode of operations is a GUI with NSM but they also support commandline-scripts and batch processing. For these you don't want NSM.""" def __init__(self, *args, **kwargs): self.realClient = False self.ourClientNameUnderNSM = "NSM Null Client" def announceSaveStatus(self, *args): pass def announceGuiVisibility(self, *args): pass def reactToMessage(self): pass def importResource(self): return "" def serverSendExitToSelf(self): quit() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/tools/nsmcmdline.py0000644000175000017500000002000100000000000015636 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) The Non-Session-Manager by Jonathan Moore Liles : http://non.tuxfamily.org/nsm/ With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) API documentation: http://non.tuxfamily.org/nsm/API.html This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), more specifically its template base application. The Template Base Application 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 . """ #Standard Library import logging import threading from time import sleep import cmd from pprint import pprint import sys from nsmservercontrol import NsmServerControl class NSMCmd(cmd.Cmd): intro = "Welcome to the NSM commandline tester. Type help or ? to list commands.\nThere is no prompt, just type.\n" prompt = "" file = None #? lastClientId = None def _clientId(self, arg): if arg: NSMCmd.lastClientId = arg return arg elif NSMCmd.lastClientId: return NSMCmd.lastClientId else: return arg def do_announce(self, arg): """Announce ourselves as GUI. This is sent automatically at program start, but nsmd only remembers the last GUI that announces. Calling announce takes back control""" nsmServerControl.gui_announce() def do_ping(self, arg): """Ping the server""" nsmServerControl.ping() def do_broadcast(self, arg): """Send a message too all clients in the current session, except ourselves""" args = arg.split() path = args[0] arguments = args[1:] nsmServerControl.broadcast(path, arguments) def do_listSessions(self, arg): """Request a list of projects, or sessions, from the server.""" nsmServerControl.list() print ("Now call 'status'") #This won't work because when we get back control from list the data is not here yet. #print(nsmServerControl.internalState["sessions"]) def do_saveSessions(self, arg): """Save currently open session""" nsmServerControl.save() def do_closeSession(self, arg): """Close currently open session""" nsmServerControl.close() def do_abortSession(self, arg): """Close without saving!""" nsmServerControl.abort() def do_liftLockedSession(self, arg): """Remove the .lock file from a session. Use with caution. Intended for crash recovery. Does nothing if not locked.""" nsmServerControl.forceLiftLock(arg) def do_quitServer(self, arg): """Gracefully shut down the server, which will save. Then exit to OS.""" #nsmServerControl.quit() #We don't need that. Called by @atexit quit() def do_addClient(self, arg): """Add one client to current session. Executable must be in $PATH""" nsmServerControl.clientAdd(arg) def do_openSession(self, arg): """Open an existing session with a name as shown by the list command""" nsmServerControl.open(arg) def do_newSession(self, arg): """Saves the current session and creates a new session.""" nsmServerControl.new(arg) def do_duplicateSession(self, arg): """Saves the current session, closes it and opens a copy of it with the given name.""" nsmServerControl.duplicate(arg) def do_copySession(self, arg): """Copy a session with our internal methods. Does not use nsm duplicate and can operate at any time at any session, locked or not.""" nsmSessionName, newName = arg.split() nsmServerControl.copySession(nsmSessionName, newName) def do_hideClient(self, arg): """Instruct a client to hide its GUI. Will do nothing if client does not support it""" arg = self._clientId(arg) nsmServerControl.clientHide(arg) def do_showClient(self, arg): """Instruct a client to show its GUI again. Will do nothing if client does not support it""" arg = self._clientId(arg) nsmServerControl.clientShow(arg) def do_removeClient(self, arg): """Remove a stopped client from a running session""" arg = self._clientId(arg) nsmServerControl.clientRemove(arg) def do_stopClient(self, arg): """Stop a client in a running session""" arg = self._clientId(arg) nsmServerControl.clientStop(arg) def do_resumeClient(self, arg): """Resume a previously stopped client""" arg = self._clientId(arg) nsmServerControl.clientResume(arg) def do_saveClient(self, arg): """instruct a specific client to save""" arg = self._clientId(arg) nsmServerControl.clientSave(arg) def do_allClientsShow(self, arg): """Call clientShow for all clients""" nsmServerControl.allClientsShow() def do_allClientsHide(self, arg): """Call clientHide for all clients""" nsmServerControl.allClientsHide() def do_deleteSession(self, arg): """Delete a session directory. This is a destructive operation without undo""" nsmServerControl.deleteSession(arg) def do_renameSession(self, arg): """Rename a non-open session. Arguments: nsmSessionName (from listSessions) newName""" nsmSessionName, newName = arg.split() nsmServerControl.renameSession(nsmSessionName, newName) #Internal, not requests for nsm def do_status(self, arg): """show internal status. Does not query nsm for any updates or live data. Therefore can be out of date, e.g. after program start there are no listed sessions. call listSessions first.""" pprint(nsmServerControl.internalState) def do_loggingInfo(self, arg): """Set logging level to very verbose""" logging.basicConfig(level=logging.INFO) def do_loggingWarning(self, arg): """Set logging level to warnings and errors""" logging.basicConfig(level=logging.WARNING) def do_loggingError(self, arg): """Set logging level to only errors""" logging.basicConfig(level=logging.ERROR) #def default(self, arg): # nsmServerControl.send(arg) def run_receivingServer(): """Run forever http://sebastiandahlgren.se/2014/06/27/running-a-method-as-a-background-thread-in-python/ """ while True: nsmServerControl.process() sleep(0.001) def nothing(*args): pass if __name__ == '__main__': logging.basicConfig(level=logging.INFO) #development try: URL = sys.argv[1] print ("Start with URL", sys.argv[1]) except: URL = None nsmServerControl = NsmServerControl(useCallbacks=True,sessionOpenReadyHook=nothing,sessionOpenLoadingHook=nothing,sessionClosedHook=nothing,clientStatusHook=nothing,singleInstanceActivateWindowHook=nothing, dataClientNamesHook=nothing, dataClientDescriptionHook=nothing, parameterNsmOSCUrl=URL) thread = threading.Thread(target=run_receivingServer, args=()) thread.daemon = True # Daemonize thread thread.start() # Start the execution NSMCmd().cmdloop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642272567.9811656 agordejo-0.3.1/tools/nsmservercontrol.py0000644000175000017500000023573200000000000017155 0ustar00nilsnils#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) The Non-Session-Manager by Jonathan Moore Liles : http://non.tuxfamily.org/nsm/ New Session Manager, by LinuxAudio.org: https://github.com/linuxaudio/new-session-manager With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). This application 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library import struct import socket from os import getenv #to get NSM env var from shutil import rmtree as shutilrmtree from shutil import copytree as shutilcopytree from urllib.parse import urlparse #to convert NSM env var import subprocess import atexit import pathlib import json from uuid import uuid4 from datetime import datetime from sys import exit as sysexit def nothing(*args, **kwargs): pass class _IncomingMessage(object): """Representation of a parsed datagram representing an OSC message. An OSC message consists of an OSC Address Pattern followed by an OSC Type Tag String followed by zero or more OSC Arguments. """ def __init__(self, dgram): #NSM Broadcasts are bundles, but very simple ones. We only need to care about the single message it contains. #Therefore we can strip the bundle prefix and handle it as normal message. if b"#bundle" in dgram: bundlePrefix, singleMessage = dgram.split(b"/", maxsplit=1) dgram = b"/" + singleMessage # / eaten by split self.LENGTH = 4 #32 bit self._dgram = dgram self._parameters = [] self.parse_datagram() def get_int(self, dgram, start_index): """Get a 32-bit big-endian two's complement integer from the datagram. Args: dgram: A datagram packet. start_index: An index where the integer starts in the datagram. Returns: A tuple containing the integer and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: if len(dgram[start_index:]) < self.LENGTH: raise ValueError('Datagram is too short') return ( struct.unpack('>i', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def get_string(self, dgram, start_index): """Get a python string from the datagram, starting at pos start_index. We receive always the full string, but handle only the part from the start_index internally. In the end return the offset so it can be added to the index for the next parameter. Each subsequent call handles less of the same string, starting further to the right. According to the specifications, a string is: "A sequence of non-null ASCII characters followed by a null, followed by 0-3 additional null characters to make the total number of bits a multiple of 32". Args: dgram: A datagram packet. start_index: An index where the string starts in the datagram. Returns: A tuple containing the string and the new end index. Raises: ValueError if the datagram could not be parsed. """ #First test for empty string, which is nothing, followed by a terminating \x00 padded by three additional \x00. if dgram[start_index:].startswith(b"\x00\x00\x00\x00"): return "", start_index + 4 #Otherwise we have a non-empty string that must follow the rules of the docstring. offset = 0 try: while dgram[start_index + offset] != 0: offset += 1 if offset == 0: raise ValueError('OSC string cannot begin with a null byte: %s' % dgram[start_index:]) # Align to a byte word. if (offset) % self.LENGTH == 0: offset += self.LENGTH else: offset += (-offset % self.LENGTH) # Python slices do not raise an IndexError past the last index, # do it ourselves. if offset > len(dgram[start_index:]): raise ValueError('Datagram is too short') data_str = dgram[start_index:start_index + offset] return data_str.replace(b'\x00', b'').decode('utf-8'), start_index + offset except IndexError as ie: raise ValueError('Could not parse datagram %s' % ie) except TypeError as te: raise ValueError('Could not parse datagram %s' % te) def get_float(self, dgram, start_index): """Get a 32-bit big-endian IEEE 754 floating point number from the datagram. Args: dgram: A datagram packet. start_index: An index where the float starts in the datagram. Returns: A tuple containing the float and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: return (struct.unpack('>f', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def parse_datagram(self): try: self._address_regexp, index = self.get_string(self._dgram, 0) if not self._dgram[index:]: # No params is legit, just return now. return # Get the parameters types. type_tag, index = self.get_string(self._dgram, index) if type_tag.startswith(','): type_tag = type_tag[1:] # Parse each parameter given its type. for param in type_tag: if param == "i": # Integer. val, index = self.get_int(self._dgram, index) elif param == "f": # Float. val, index = self.get_float(self._dgram, index) elif param == "s": # String. val, index = self.get_string(self._dgram, index) else: logger.warning("Unhandled parameter type: {0}".format(param)) continue self._parameters.append(val) except ValueError as pe: #raise ValueError('Found incorrect datagram, ignoring it', pe) # Raising an error is not ignoring it! logger.warning("Found incorrect datagram, ignoring it. {}".format(pe)) @property def oscpath(self): """Returns the OSC address regular expression.""" return self._address_regexp @staticmethod def dgram_is_message(dgram): """Returns whether this datagram starts as an OSC message.""" return dgram.startswith(b'/') @property def size(self): """Returns the length of the datagram for this message.""" return len(self._dgram) @property def dgram(self): """Returns the datagram from which this message was built.""" return self._dgram @property def params(self): """Convenience method for list(self) to get the list of parameters.""" return list(self) def __iter__(self): """Returns an iterator over the parameters of this message.""" return iter(self._parameters) class _OutgoingMessage(object): def __init__(self, oscpath): self.LENGTH = 4 #32 bit self.oscpath = oscpath self._args = [] def write_string(self, val): dgram = val.encode('utf-8') diff = self.LENGTH - (len(dgram) % self.LENGTH) dgram += (b'\x00' * diff) return dgram def write_int(self, val): return struct.pack('>i', val) def write_float(self, val): return struct.pack('>f', val) def add_arg(self, argument): t = {str:"s", int:"i", float:"f"}[type(argument)] self._args.append((t, argument)) def build(self): dgram = b'' #OSC Path dgram += self.write_string(self.oscpath) if not self._args: dgram += self.write_string(',') return dgram # Write the parameters. arg_types = "".join([arg[0] for arg in self._args]) dgram += self.write_string(',' + arg_types) for arg_type, value in self._args: f = {"s":self.write_string, "i":self.write_int, "f":self.write_float}[arg_type] dgram += f(value) return dgram class NsmServerControl(object): """ The ServerControl can be started in three modes, regarding nsmd. We expect that starting our own nsmd will be the majority of cases. SessionRoot parameter is only honored if we start nsmd ourselves. Ascending lookup priority: 1) Default is to start our own nsmd. A single-instance watcher will prevent multiple programs on the same system. 2) When $NSM_URL is found as environment we will connect to that nsmd. 3) When hostname and portnumber are given explicitely as instance variables we will first test if a server is running at that URL, if not we will start our own with these parameters. This is not only a pure implemenation of the protocol. It is extended by us reacting to and storing incoming data. This data can be interpreted and enhanced by looking at the session dir ourselves. However, we don't do anything that is not possible by the original nsmd + human interaction. 100% Compatibility is the highest priority. The big problems are the async nature of communication, message come out of order or interleaved, and nsm is not consistent in its usage of osc-paths. For example it starts listing sessions with /nsm/gui/server/message, but sends the content with /reply [/nsm/server/list, nsmSessionName] and then ends it with /nsm/server/list [0, Done] (no reply!). So three message types, three callbacks for one logically connected process. To update our internal session information we therefore need to split the functionality into severall seemingly unconnected callbacks and you need to know how the protocol works to actually know the order of operations. Switch logging to info to learn more. We have a mix between NSM callbacks and our own functions. Most important there is a watchdog that looks at the session directory and creates its own callbacks if something changes. A typical operation, say sessionDelete or sessionCopy looks like this: * Ask (blocking) nsmd for a current list of sessions, update our internal state * Perform a file operation, like copy or delete or lift a lock * Let our watchdog discover the changes in the file system and trigger another (non-blocking) request for a list of sessions to adjust our internal state to reality. Granted, we could just call our blocking query again at the end, but we would still need to let the watchdog run for operations that the user does with a filemanager, which would end up in redundant calls. Bottom line: _updateSessionListBlocking is called at the beginning of a function, but not at the end. Docs: http://non.tuxfamily.org/nsm/ http://non.tuxfamily.org/wiki/Non%20Session%20Manager http://non.tuxfamily.org/wiki/ApplicationsSupportingNsm http://non.tuxfamily.org/nsm/API.html """ def __init__(self, sessionOpenReadyHook, sessionOpenLoadingHook, sessionClosedHook, clientStatusHook, singleInstanceActivateWindowHook, dataClientNamesHook, dataClientDescriptionHook, parameterNsmOSCUrl=None, sessionRoot=None, startupSession=None, useCallbacks=True): """If useCallbacks is False you will see every message in the log. This is just a development mode to see all messages, unfiltered. Normally we have special hook functions that save and interpret data, so they don't show in the logs""" #Deactivate hooks for now. During init no hooks may be called, #but some functions want to do that already. We setup the true hooks at the end of init self.sessionOpenReadyHook= self.sessionOpenLoadingHook= self.sessionClosedHook= self.clientStatusHook= self.singleInstanceActivateWindowHook= self.dataClientNamesHook= self.dataClientDescriptionHook= nothing self._queue = list() #Incoming OSC messages are buffered here. #Status variables that are set by our callbacks self.internalState = { "sessions" : set(), #nsmSessionNames:str . We use set for unqiue, just in case. But we also clear that on /nsm/gui/server/message ['Listing sessions'] to handle deleted sessions "currentSession" : None, "port" : None, #Our GUI port "serverPort" : None, #nsmd port "nsmUrl" : None, #the environment variable "clients" : {}, #clientId:dict see self._initializeEmptyClient . Gets replaced with a new dict instance on session changes. "broadcasts" : [], #in the order they appeared "datastorage" : None, #URL, if present in the session } self.dataStorage = None #Becomes DataStorage() every time a datastorage client does a broadcast announce. self._addToNextSession = [] #A list of executables in PATH. Filled by new, waits for reply that session is created and then will send clientNew and clear the list. if useCallbacks: self.callbacks = { "/nsm/gui/session/name" : self._reactCallback_activeSessionChanged, #"/nsm/gui/session/root" #Session root is an active blocking call in init "/nsm/gui/client/label" : self._reactCallback_ClientLabelChanged, "/nsm/gui/client/new" : self._reactCallback_ClientNew, "/nsm/gui/session/session" : self._reactCallback_SessionSession, "/nsm/gui/client/status" : self._reactCallback_statusChanged, #handles multiple status keywords "/reply" : self._reactCallback_reply, #handles multiple replies "/error" : self._reactCallback_error, "/nsm/gui/client/has_optional_gui" : self._reactCallback_clientHasOptionalGui, "/nsm/gui/client/gui_visible" : self._reactCallback_clientGuiVisible, "/nsm/gui/client/pid" : self._reactCallback_clientPid, "/nsm/gui/client/dirty" : self._reactCallback_clientDirty, "/nsm/gui/server/message" : self._reactCallback_serverMessage, "/nsm/gui/gui_announce" : self._reactCallback_guiAnnounce, #we rarely receive that, especially not in init. "/nsm/server/list" : self._reactCallback_serverList, "/nsm/server/broadcast" : self._reactCallback_broadcast, } else: #This is just a development mode to see all messages, unfiltered self.callbacks = set() #empty set is easiest to check #Networking and Init for our control part, not for the server self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp self.sock.bind(('', 0)) #pick a free port on localhost. self.sock.setblocking(False) self.internalState["port"] = self.sock.getsockname()[1] #only happens once, ports don't change during runtime #self.sock.close() Do not close, this runs until the end of the program ###Testing of existing servers, starting and connecting #First handle the NSM URL, or generate on. #self.nsmOSCUrl must be a tuple compatible to the result of urlparse. (hostname, port) self.singleInstanceSocket = None if parameterNsmOSCUrl: o = urlparse(parameterNsmOSCUrl) self.nsmOSCUrl = (o.hostname, o.port) else: envResult = self._getNsmOSCUrlFromEnvironment() if envResult: self.nsmOSCUrl = envResult else: #This is the default case. User just starts the GUI. The other modes are concious decisions to either start with URL as parameter or in an NSM environment. #But now we need to test if the user accidentaly opened a second GUI, which would start a second server. self._setupAndTestForSingleInstance() #This might quit the whole program and we will never see the next line. self.nsmOSCUrl = self._generateFreeNsmOSCUrl() assert self.nsmOSCUrl self.internalState["serverPort"] = self.nsmOSCUrl[1] #only happens once, ports don't change during runtime self.internalState["nsmUrl"] = f"osc.udp://{self.nsmOSCUrl[0]}:{self.nsmOSCUrl[1]}/" #only happens once, ports don't change during runtime #Then check if a server is running there. If not start one. self.ourOwnServer = None #Might become a subprocess handle if self._isNsmdRunning(self.nsmOSCUrl): #serverport = self.nsmOSCUrl[1] #No further action required. GUI announce below this testing. pass else: self._startNsmdOurselves(sessionRoot, startupSession) #Session root can be a commandline parameter we forward to the server if we start it ourselves. startupSession is an autoloader. Both are usually None. assert type(self.ourOwnServer) is subprocess.Popen, (self.ourOwnServer, type(self.ourOwnServer)) #Wait for the server, or test if it is reacting. self._waitForPingResponseBlocking() logger.info("nsmd is ready @ {}".format(self.nsmOSCUrl)) #Tell nsmd that we are a GUI and want to receive general messages async, not only after we request something self.sessionRoot = self._initial_announce() #Triggers "hi" and session root self.internalState["sessionRoot"] = self.sessionRoot self._forceProcessOnceToEmptyQueue() #process any leftover messages. atexit.register(self.quit) #mostly does stuff when we started nsmd ourself #Activate hooks for api callbacks, now that we are finished here. #Otherwise the hooks will get called from our functions (e.g. new client) while we are still during init self.sessionOpenReadyHook = sessionOpenReadyHook #self.sessionAsDict(nsmSessionName) as parameter self.sessionOpenLoadingHook = sessionOpenLoadingHook #self.sessionAsDict(nsmSessionName) as parameter self.sessionClosedHook = sessionClosedHook #no parameter. This is also "choose a session" mode self.clientStatusHook = clientStatusHook #all client status is done via this single hook. GUIs need to check if they already know the client or not. self.dataClientNamesHook = dataClientNamesHook self.dataClientDescriptionHook = dataClientDescriptionHook self.singleInstanceActivateWindowHook = singleInstanceActivateWindowHook #added to self.processSingleInstance() to listen for a message from another wannabe-instance self._receiverActive = True logger.info("nsmservercontrol init is complete. Ready for event loop") #Now an external event loop can add self.process #Internal Methods def _setupAndTestForSingleInstance(self): """on program startup trigger this if there is already another instance of us running. This socket is only """ self.singleInstanceSocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) logger.info("Testing if another non-specific Agordejo is running.") try: ## Create an abstract socket, by prefixing it with null. # this relies on a feature only in linux, when current process quits, the # socket will be deleted. self.singleInstanceSocket.bind('\0' + "agordejo") self.singleInstanceSocket.listen(1) self.singleInstanceSocket.setblocking(False) logger.info("No other non-specific Agordejo found. Starting GUI") #Continue in self.processSingleInstance() return True except socket.error: logger.error("GUI for this nsmd server already running. Informing the existing application to show itself.") self.singleInstanceSocket.connect('\0' + "agordejo") self.singleInstanceSocket.send("agordejoactivate".encode()); self.singleInstanceSocket.close() sysexit(1) #triggers atexit #print ("not executed") return False def processSingleInstance(self): """Tests our unix socket for an incoming signal. if received forward to the engine->gui Can be added to a slower event loop, so it is not in self.process""" if self.singleInstanceSocket: try: connection, client_address = self.singleInstanceSocket.accept() #This blocks and waits for a message incoming = connection.recv(1024) if incoming and incoming == b"agordejoactivate": self.singleInstanceActivateWindowHook() except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. In fact: this happens when in non-blocking mode. pass except socket.timeout: pass def _setPause(self, state:bool): """Set both the socket and the thread into waiting mode or not. With this we can wait for answers until we resume async operation""" if state: self.sock.setblocking(True) #explicitly wait. self.sock.settimeout(0.5) self._receiverActive = False logger.info("Suspending receiving async mode.") else: self.sock.setblocking(False) self._receiverActive = True logger.info("Resuming receiving async mode.") def _forceProcessOnceToEmptyQueue(self): """Sometimes we want to make sure everything is processed until we continue. For example in our init. Initial usecase was connecting to a running nsmd with session. The api first callback to export sessions to the GUI was freezing because listSession was chocking on leftover messages from gui_announce to a running session, which sends the session name and a list of clients. The latter is not happening when starting the server ourselves, so we weren't expecting this. To be honest, this is really a patch to work around a design flaw and we hope this is a one-off corner case.""" logger.info("Force processing queue") #First gather all osc messages still in the pipe while True: try: data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if msg: self._queue.append(msg) except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. break except socket.timeout: break #Now process them all. This is different than normal self.process(). for msg in self._queue: if msg.oscpath in self.callbacks: self.callbacks[msg.oscpath](msg.params) else: logger.warning(f"Unhandled message with path {msg.oscpath} and parameters {msg.params}") self._queue.clear() logger.info("Ended force processing queue") def process(self): """Use this in an external event loop""" if self._receiverActive: while True: try: data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if msg: self._queue.append(msg) except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. break except socket.timeout: break for msg in self._queue: if msg.oscpath in self.callbacks: self.callbacks[msg.oscpath](msg.params) else: logger.warning(f"Unhandled message with path {msg.oscpath} and parameters {msg.params}") self._queue.clear() def _getNsmOSCUrlFromEnvironment(self): """Return the nsm osc url or None""" nsmOSCUrl = getenv("NSM_URL") if not nsmOSCUrl: return None else: #osc.udp://hostname:portnumber/ o = urlparse(nsmOSCUrl) return o.hostname, o.port def _generateFreeNsmOSCUrl(self): #Instead of reading out the NSM port we get a free port ourselves and set up nsmd with that tempServerSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp tempServerSock.bind(('', 0)) #pick a free port on localhost. address, tempServerSockPort = tempServerSock.getsockname() tempServerSock.close() #We need to close it because nsmd will open it right away. nsmOSCUrl = ("0.0.0.0", tempServerSockPort) #compatible to result of urlparse logger.info("Generated our own free NSM_URL to start a server @ {}".format(nsmOSCUrl)) return nsmOSCUrl def _isNsmdRunning(self, nsmOSCUrl): """Test if the port is open or not""" logger.info(f"Testing if a server is running @ {nsmOSCUrl}") hostname, port = nsmOSCUrl tempServerSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp try: tempServerSock.bind((hostname, port)) logger.info(f"No external nsmd found (we tested if port is closed) @ {nsmOSCUrl}") return False except: logger.info(f"External nsmd found (we tested if port is closed) @ {nsmOSCUrl}") return True finally: tempServerSock.close() def _startNsmdOurselves(self, sessionRoot:str, startupSession:str): assert self.nsmOSCUrl hostname, port = self.nsmOSCUrl arguments = ["nsmd","--osc-port", str(port)] if sessionRoot: arguments += ["--session-root", sessionRoot] if startupSession: logger.info(f"Got start-session as command line parameter. Fowarding to nsmd command line: {startupSession}") arguments += ["--load-session", startupSession] #nsmd allows all executables in $PATH. For technical reasons our GUI extends this PATH before we start the server. #This is a convenience service for fellow developers, that does not belong in the server control. #However, if you wonder why there are are more applications from unknown PATHs check qtgui/settings.py self.ourOwnServer = subprocess.Popen(arguments) def _blockingRequest(self, path:str, arguments:list, answerPath:str, answerArguments:list, repeat=False)->list: """During start-up we need to wait for replies. Also some operations only make sense if we got data back. This is an abstraction that deals with messages that may come out-of-order and keeps them for later, but at least prevents our side from sending messages out-of-order itself. Default is: send once, wait for answer. repeat=True sends multiple times until an answer arrives. Returns list of arguments, can be empty. """ assert not self._queue, [(m.oscpath, m.params) for m in self._queue] logger.info(f"[wait for answer]: Sending {path}: {arguments}") self._setPause(True) out_msg = _OutgoingMessage(path) for arg in arguments: out_msg.add_arg(arg) if not repeat: self.sock.sendto(out_msg.build(), self.nsmOSCUrl) #Wait for answer ready = False while not ready: if repeat: #we need to send multiple times. self.sock.sendto(out_msg.build(), self.nsmOSCUrl) try: data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if answerArguments and msg.oscpath == answerPath and msg.params == answerArguments: result = msg.params logger.info(f"[wait from {path}] Received {answerPath}: {result}") ready = True elif msg.oscpath == answerPath: result = msg.params logger.info(f"[wait from {path}] Received {answerPath}: {result}") ready = True else: logger.warning(f"Waiting for {answerPath} from nsmd, but got: {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. continue except socket.timeout: continue self._setPause(False) return result def _waitForPingResponseBlocking(self): self._blockingRequest(path="/osc/ping", arguments=[], answerPath="/reply", answerArguments=["/osc/ping",], repeat=True) def _initial_announce(self)->pathlib.Path: """nsm/gui/gui_announce triggers a multi-stage reply. First we get "hi", then we get the session root. We wait for session root and then clean 'hi' from the queue. When we connect to a running nsmd we also receive /nsm/gui/session/name with the current session (or empty string for no current). If in a session we will receive a list of clients which ends the gui_announce stage. Returns session root as pathlib-path.""" resultArguments = self._blockingRequest(path="/nsm/gui/gui_announce", arguments=[], answerPath="/nsm/gui/session/root", answerArguments=[]) if len(self._queue) == 1 and self._queue[0].oscpath == "/nsm/gui/gui_announce" and self._queue[0].params == ["hi"]: logger.info("Got 'hi'. We are now the registered nsmd GUI as per our initial /nsm/gui/gui_announce") self._queue.clear() #this is safe because we tested above that there is exactly the hi message in the queue. else: logging.error(f"For ValueError below: {[(m.oscpath, m.params) for m in self._queue]}") raise ValueError("We were expecting a clean _queue with only 'hi' as leftover, but instead there were unhandled messages. see print above. Better abort than a wrong program state") #all ok return pathlib.Path(resultArguments[0]) #General Commands def send(self, arg): """ Intended for a text input / command line interface. Sends anything to nsmd, separated by semicolon. First part is the message address, the rest are string-parameters.""" args = arg.split() msg = _OutgoingMessage(args[0]) for p in args[1:]: msg.add_arg(p) self.sock.sendto(msg.build(), self.nsmOSCUrl) def gui_announce(self): """This is just the announce without any answer. This is a last-resort method if another GUI "stole" our slot. For our own initial announce we use self._initial_announce()""" msg = _OutgoingMessage("/nsm/gui/gui_announce") self.sock.sendto(msg.build(), self.nsmOSCUrl) def ping(self): msg = _OutgoingMessage("/osc/ping") self.sock.sendto(msg.build(), self.nsmOSCUrl) def list(self): msg = _OutgoingMessage("/nsm/server/list") self.sock.sendto(msg.build(), self.nsmOSCUrl) def _updateSessionListBlocking(self): """To ensure correct data on session operations we manage ourselves, like copy, rename and delete. Ask nsmd for projects in session root and update our internal state. This will return None without doing anything when we are already in a session. This will wait for an answer and block all other operations. First is /nsm/gui/server/message ['Listing sessions'] Then session names come one reply at a time such as /reply ['/nsm/server/list', 'test3'] Finally /nsm/server/list [0, 'Done.'] , not a reply """ #In the past we only regenerated if we are not in a session. However, that was overzealous. #Some GUI functions did not work. Better regenerate that list as often as we want. logger.info("Requesting project list from session server in blocking mode") self._setPause(True) msg = _OutgoingMessage("/nsm/server/list") self.sock.sendto(msg.build(), self.nsmOSCUrl) #Wait for /reply ready = False while True: try: data, addr = self.sock.recvfrom(1024) except socket.timeout: continue msg = _IncomingMessage(data) if not ready and msg.oscpath == "/nsm/gui/server/message" and msg.params == ["Listing sessions"]: self.internalState["sessions"].clear() # new clients are added as /reply /nsm/server/list callbacks ready = True else: if len(msg.params) != 2: logger.warning(f"Expected project but got path {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) continue #This is what we want: elif msg.oscpath == "/reply" and msg.params[0] == "/nsm/server/list": #/reply ['/nsm/server/list', 'test3'] for a real session or #/reply ['/nsm/server/list', ''] as "list ended" marker if msg.params[1]: self.internalState["sessions"].add(msg.params[1]) logger.info(f"Received session name: {msg.params[1]}") else: #empty string break elif msg.params[0] == 0 and msg.params[1] == "Done.": # legacy nsmd sent the wrong message. Fixed in new-session-manager june 2020 break else: logger.warning(f"Expected project but got path {msg.oscpath} with {msg.params}. Adding to queue for later.") self._queue.append(msg) continue self._setPause(False) def quit(self): """Called through atexit. Thanks to start.py sys exception hook this will also trigger on PyQt crash""" if self.ourOwnServer: msg = _OutgoingMessage("/nsm/server/quit") self.sock.sendto(msg.build(), self.nsmOSCUrl) returncode = self.ourOwnServer.wait() logger.info("Stopped our own server with return code {}".format(returncode)) def broadcast(self, path:str, arguments:list): """/nsm/server/broadcast s:path [arguments...] http://non.tuxfamily.org/nsm/API.html 1.2.7.1 /nsm/server/broadcast s:path [arguments...] /nsm/server/broadcast /tempomap/update "0,120,4/4:12351234,240,4/4" All clients except the sender recive: /tempomap/update "0,120,4/4:12351234,240,4/4" """ logger.info(f"Sending broadcast with path {path} and args {arguments}") message = _OutgoingMessage("/nsm/server/broadcast") message.add_arg(path) for arg in arguments: message.add_arg(arg) #type autodetect self.sock.sendto(message.build(), self.nsmOSCUrl) #Primarily Without Session def open(self, nsmSessionName:str): if nsmSessionName in self.internalState["sessions"]: msg = _OutgoingMessage("/nsm/server/open") msg.add_arg(nsmSessionName) #s:project_name self.sock.sendto(msg.build(), self.nsmOSCUrl) else: logger.warning(f"Session {nsmSessionName} not found. Not forwarding to nsmd.") def new(self, newName:str, startClients:list=[])->str: """Saves the current session and creates a new session. Only works if dir does not exist yet. """ basePath = pathlib.Path(self.sessionRoot, newName) if basePath.exists(): return None self._addToNextSession = startClients msg = _OutgoingMessage("/nsm/server/new") msg.add_arg(newName) #s:project_name self.sock.sendto(msg.build(), self.nsmOSCUrl) #Only with Current Session def save(self): msg = _OutgoingMessage("/nsm/server/save") self.sock.sendto(msg.build(), self.nsmOSCUrl) def close(self, blocking=False): if not blocking: msg = _OutgoingMessage("/nsm/server/close") self.sock.sendto(msg.build(), self.nsmOSCUrl) else: msg = _OutgoingMessage("/nsm/server/close") self.sock.sendto(msg.build(), self.nsmOSCUrl) #Drive the process loop ourselves. This will still trigger updates but the mainloop will wait. while self.internalState["currentSession"]: self.process() def abort(self, blocking=False): if not blocking: msg = _OutgoingMessage("/nsm/server/abort") self.sock.sendto(msg.build(), self.nsmOSCUrl) else: msg = _OutgoingMessage("/nsm/server/abort") self.sock.sendto(msg.build(), self.nsmOSCUrl) #Drive the process loop ourselves. This will still trigger updates but the mainloop will wait. while self.internalState["currentSession"]: self.process() def duplicate(self, newName:str)->str: """Saves the current session and creates a new session. Requires an open session and uses nsmd to do the work. If you want to do copy of any session use our owns self.sessionCopy""" msg = _OutgoingMessage("/nsm/server/duplicate") msg.add_arg(newName) #s:project_name self.sock.sendto(msg.build(), self.nsmOSCUrl) #Client Commands for Loaded Session def clientAdd(self, executableName:str): """Adds a client to the current session. executable must be in $PATH. We do not trust NSM to perform the right checks. It will add an empty path or wrong path. """ if not pathlib.Path(executableName).name == executableName: logger.warning(f"{executableName} must be just an executable file in your $PATH. We expected: {pathlib.Path(executableName).name} . We will not ask nsmd to add it as client") return False allPaths = getenv("PATH") assert allPaths, allPaths binaryPaths = allPaths.split(":") #TODO: There is a corner case that NSMD runs in a different $PATH environment. executableInPath = any(pathlib.Path(bp, executableName).is_file() for bp in binaryPaths) if executableInPath: msg = _OutgoingMessage("/nsm/server/add") msg.add_arg(executableName) #s:executable_name self.sock.sendto(msg.build(), self.nsmOSCUrl) return True else: logger.warning("Executable {} not found. We will not ask nsmd to add it as client".format(executableName)) return False def clientStop(self, clientId:str): msg = _OutgoingMessage("/nsm/gui/client/stop") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientResume(self, clientId:str): """Opposite of clientStop""" msg = _OutgoingMessage("/nsm/gui/client/resume") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientRemove(self, clientId:str): """Client needs to be stopped already. We will do that and wait for an answer. Remove from the session. Will not delete the save-files, but make them inaccesible. There is never a point in nsmservercontrol where self.internalState["clients"] is emptied. nsmd actually sends a clientRemove for every client at session stop. """ #We have a blocking operation in here so we need to be extra cautios that the client exists. if not clientId in self.internalState["clients"]: return False self.clientStop(clientId) #We need to wait for an answer. #Drive the process loop ourselves. This will still trigger updates but the mainloop will wait. logger.info(f"Waiting for {clientId} to be status 'stopped'") while not self.internalState["clients"][clientId]["lastStatus"] == "stopped": self.process() msg = _OutgoingMessage("/nsm/gui/client/remove") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) #Flood lazy lagging nsmd until it removed the client. #We will receive a few -10 "No such client." errors but that is ok. while True: if not clientId in self.internalState["clients"]: break if self.internalState["clients"][clientId]["lastStatus"] == "removed": break self.sock.sendto(msg.build(), self.nsmOSCUrl) self.process() def clientSave(self, clientId:str): """Saves only the given client""" msg = _OutgoingMessage("/nsm/gui/client/save") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientHide(self, clientId:str): """Hides the client. Works only if client announced itself with this feature""" msg = _OutgoingMessage("/nsm/gui/client/hide_optional_gui") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) def clientShow(self, clientId:str): """Hides the client. Works only if client announced itself with this feature""" msg = _OutgoingMessage("/nsm/gui/client/show_optional_gui") msg.add_arg(clientId) #s:clientId self.sock.sendto(msg.build(), self.nsmOSCUrl) #Callbacks def _reactCallback_guiAnnounce(self, parameters:list): """This should not happen, but let's keep it in in case of edge-case multi GUI scenarios""" assert parameters == ["hi"], parameters logger.info("We got an unexpected 'hi', as if requesting gui_announce. Our own initial GUI announce as received and processed silently earlier already.") def _reactCallback_error(self, parameters:list): logger.error(parameters) def _reactCallback_reply(self, parameters:list): """This is a difficult function because replies arrive for many unrelated things, like status. We do our best to send all replies on the right way""" success = False l = len(parameters) if l == 2: originalMessage, data = parameters logger.info(f"Got reply for {originalMessage} with {data}") reply = { "/nsm/server/list" : self._reactReply_nsmServerList, "/nsm/server/new" : self._reactReply_nsmServerNew, "/nsm/server/close" : self._reactReply_nsmServerClose, "/nsm/server/open" : self._reactReply_nsmServerOpen, "/nsm/server/save" : self._reactReply_nsmServerSave, "/nsm/server/abort" : self._reactReply_nsmServerAbort, "/nsm/server/duplicate" : self._reactReply_nsmServerDuplicate, "/nsm/server/add" : self._reactReply_nsmServerAdd, } if originalMessage in reply: reply[originalMessage](data) success = True elif l == 3: originalMessage, errorCode, answer = parameters logger.info(f"Got reply for {originalMessage} with code {errorCode} saying {answer}") if originalMessage == "/nsm/server/add": assert errorCode == 0, parameters self._reactReply_nsmServerAdd(answer) success = True elif l == 1: singleMessage = parameters[0] """For unknown reasons these replies do not repeat the originalMessage""" if singleMessage == "/osc/ping": logger.info(singleMessage) success = True elif singleMessage == "Client removed.": self._reactReply_nsmServerRemoved() success = True elif singleMessage == "Client stopped.": self._reactReply_nsmServerStopped() success = True #After all these reactions and checks the function will eventually return here. if not success: raise NotImplementedError(parameters) def _reactCallback_serverMessage(self, parameters:list): """Messages are normally harmless and uninteresting. Howerver, we need to use some of them for actual tasks. In opposite to reply and status this all go in our function for now, until refactoring""" if parameters == ["Listing session"]: #this feels bad! A simple message is not a reliable state token and could change in the future. #we cannot put that into our own /list outgoing message because other actions like "new" also trigger this callback self.internalState["sessions"].clear() # new clients are added as /reply /nsm/server/list callbacks if parameters[0].startswith("Opening session"): #This gets send only when an existing session starts loading. It will not trigger on new sessions, be it really new or duplicate. #e.g. /nsm/gui/server/message ["Opening session FOO"] nsmSessionName = parameters[0].replace("Opening session ", "") logger.info(f"Starting to load clients of session: {nsmSessionName}") self.sessionOpenLoadingHook(self.sessionAsDict(nsmSessionName)) #notify the api->UI else: logger.info("/nsm/gui/server/message " + repr(parameters)) def _reactCallback_broadcast(self, parameters:list): """We have nothing to do with broadcast. But we save them, so they can be shown on request parameters[0] is an osc path:str without naming constraints the rest is a list of arguments. Attention: a broadcast is not saved by the server. You either are in the session to receive it or you will miss it. If we run Agordejo as attached GUI (incl. --load-session) a broadcast after the session was loaded, where programs announce themselves to all other clients, will not be received here. Such is the case with our data-client. """ logger.info(f"Received broadcast. Saving in internal state: {parameters}") self.internalState["broadcasts"].append(parameters) #Our little trick. We know and like some clients better than others. #If we detect our own data-storage we remember our friends. #It is possible that another datastorage broadcasts, then we overwrite the URL. if parameters and parameters[0] == "/agordejo/datastorage/announce": path, clientId, messageSizeLimit, url = parameters assert "osc.udp" in url logger.info(f"Got announce from agordejo datastorage clientId {clientId} @ {url}") o = urlparse(url) self.dataStorage = DataStorage(self, clientId, messageSizeLimit, (o.hostname, o.port), self.sock) def _reactCallback_serverList(self, parameters:list): """This finalizes a new session list. Here we send new data to the GUI etc.""" l = len(parameters) if l == 2: errorCode, message = parameters assert errorCode == 0, errorCode assert message == "Done.", message #don't miss the dot after Done logger.info("/nsm/server/list is done and has transmitted all available sessions to us") else: raise NotImplementedError(parameters) def _reactCallback_activeSessionChanged(self, parameters:list): """We receive this trough /nsm/gui/session/name This is called when the session has already changed. This also happens when you connect to a headless nsmd with a running session. We expect two parameters: [session name, session path] both of which could be "". If we start nsmd ourselves into an empty state we expect session name to be empty Session path is the subdirectory relative to session root. The session root is not included. !The unqiue name is the session path, not the name! Shortly before we received /nsm/gui/session/session which indicates the attempt to create a new one, I guess! :) If you want to react to the attempt to open a session you need to use /nsm/gui/server/message ["Opening session FOO"] OR creating a new session, after which nsmd will open that session without a message. Empty string is "No session" or "Choose A Session" mode. """ l = len(parameters) if l == 2: nsmSessionName, sessionPath = parameters if not nsmSessionName and not sessionPath: #No session loaded. We are in session-choosing mode. logger.info("Session closed or never started. Choose-A-Session mode.") self.internalState["currentSession"] = None #sessionCloseHooked triggers rebuilding of the session list, which will not work when there is a current session. self.sessionClosedHook() else: #Session path is the subdirectory relative to session root. The session root is not included. sessionPath = sessionPath.lstrip("/") #we strip for technical reasons. logger.info(f"Current Session changed. We are now {nsmSessionName} in {sessionPath}") self.internalState["currentSession"] = sessionPath #This is after the session, received after all programs have loaded. #We have a counterpart-message reaction that signals the attempt to load. self.sessionOpenReadyHook(self.sessionAsDict(sessionPath)) #notify the api->UI for autoClientExecutableInPath in self._addToNextSession: self.clientAdd(autoClientExecutableInPath) self._addToNextSession = [] #reset elif l == 0: #Another way of "no session". self.internalState["currentSession"] = None #sessionCloseHooked triggers rebuilding of the session list, which will not work when there is a current session. self.sessionClosedHook() else: raise NotImplementedError(parameters) def _initializeEmptyClient(self, clientId:str): """NSM reuses signals. It is quite possible that this will be called multiple times, e.g. after opening a session. This is not a reaction callback, we call this ourselves only in _reactCallback_ClientNew """ #if not self.internalState["currentSession"]: # logger.warning(f"We received a clientNew for ID {clientId} but no session open was received." # "This would happen in an old nsmd version. If you see the GUI with an open session and a client list you can ignore this warning") if clientId in self.internalState["clients"]: return logger.info(f"Creating new internal entry for client {clientId}") client = { "clientId":clientId, #for convenience, included internally as well "dumbClient":True, #Bool. Real nsm or just any old program? status "Ready" switches this. "executable":None, #Every client announces to the GUI with the exectuable name. True nsm clients later overwrite with a pretty name which we save as "reportedName" "reportedName":None, #str . The reported name is first the executable name, for status started. But for NSM clients it gets replaced with a reported name. "label":None, #str "lastStatus":None, #str "statusHistory":[], #list "hasOptionalGUI": False, #bool "visible": None, # bool "dirty": None, # bool } self.internalState["clients"][clientId] = client def _setClientData(self, clientId:str, parameter:str, value): if clientId in self.internalState["clients"]: self.internalState["clients"][clientId][parameter] = value return True else: logger.warning(f"Client {clientId} not found in internal status storage. If the session was just closed this is most likely a known race condition. Everything is fine in this case.") return False def _reactCallback_ClientLabelChanged(self, parameters:list): """osc->add_method( "/nsm/gui/client/label", "ss", osc_handler, osc, "path,display_name" ); """ l = len(parameters) if l == 2: clientId, label = parameters logger.info(f"Label for client {clientId} changed to {label}") self._setClientData(clientId, "label", label) self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientPid(self, parameters:list): clientId, pid = parameters self._setClientData(clientId, "pid", pid) def _reactCallback_SessionSession(self, parameters:list): """This is received only when a new session gets created and followed by /nsm/gui/client/new and then a reply for /reply /nsm/server/new Session created""" #This is the counterpart to Message "Opening Session", but for really new or freshly duplicated session. logger.info(f"Attempt to create session: {parameters}") self.sessionOpenLoadingHook(self.sessionAsDict(parameters[0])) #notify the api->UI def _reactCallback_ClientNew(self, parameters:list): """/nsm/gui/client/new ['nBAVO', 'jackpatch'] This is both add client or open. The message comes twice. Once when you add a client, then parameters will contain the executable name. If the client reports itself as NSM compatible through announce we will also get the Open message through this function. Then the name changes from executableName to a reportedName, which will remain for the rest of the session. Executable name is still important to look up icons in the GUI. This message is usually followed by /nsm/gui/client/status """ l = len(parameters) if l == 2: clientId, name = parameters if not clientId in self.internalState["clients"]: self._initializeEmptyClient(clientId) self._setClientData(clientId, "executable", name) logger.info(f"Client started {name}:{clientId}") else: self._setClientData(clientId, "reportedName", name) logger.info(f"Client upgraded to NSM-compatible: {name}:{clientId}") self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientDirty(self, parameters:list): """/nsm/gui/client/dirty ['nMAJH', 1] """ l = len(parameters) if l == 2: clientId, dirty = parameters dirty = bool(dirty) self._setClientData(clientId, "dirty", dirty) logger.info(f"Client {clientId} save status dirty is now: {dirty}") self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientGuiVisible(self, parameters:list): """/nsm/gui/client/gui_visible ['nMAJH', 0] """ l = len(parameters) if l == 2: clientId, visible = parameters visible = bool(visible) self._setClientData(clientId, "visible", visible) logger.info(f"Client {clientId} visibility is now: {visible}") self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) def _reactCallback_clientHasOptionalGui(self, parameters:list): """/nsm/gui/client/has_optional_gui ['nFDBK'] nsmd sends us this as reaction to a clients announce capabilities list """ l = len(parameters) if l == 1: clientId = parameters[0] self._setClientData(clientId, "hasOptionalGUI", True) logger.info(f"Client {clientId} supports optional GUI") else: raise NotImplementedError(parameters) def _reactCallback_statusChanged(self, parameters:list): """ Handles all status messages. Some changes, like removed and quit, are only available as status. This means that status removed is the opposite of /nsm/gui/client/new, even if it doesn't read like it. /nsm/gui/client/status ['nFDBK', 'open'] /nsm/gui/client/status ['nMAJH', 'launch'] /nsm/gui/client/status ['nLUPX', 'ready'] /nsm/gui/client/status ['nLUPX', 'save'] /nsm/gui/client/status ['nFHLB', 'quit'] /nsm/gui/client/status ['nLUPX', 'removed'] /nsm/gui/client/status ['nLUPX', 'stopped'] /nsm/gui/client/status ['nLUPX', 'noop'] #For dumb clients! no nsm support! /nsm/gui/client/status ['nLUPX', 'switch'] /nsm/gui/client/status ['nLUPX', 'error'] """ l = len(parameters) if l == 2: clientId, status = parameters logger.info(f"Client status {clientId} now {status}") r = self._setClientData(clientId, "lastStatus", status) if r: #a known race condition at quit may delete this in between calls self.internalState["clients"][clientId]["statusHistory"].append(status) if status == "ready": #we need to check for this now. Below in actions is after the statusHook and too late. self._setClientData(clientId, "dumbClient", False) self.clientStatusHook(self.internalState["clients"][clientId]) else: raise NotImplementedError(parameters) #Now handle our actions. For better readability in separate functions. actions = { "open": self._reactStatus_open, "launch": self._reactStatus_launch, "ready": self._reactStatus_ready, "save": self._reactStatus_save, "quit": self._reactStatus_quit, "removed": self._reactStatus_removed, "stopped": self._reactStatus_stopped, "noop": self._reactStatus_noop, "switch": self._reactStatus_switch, "error": self._reactStatus_error, }[status](clientId) actions #pylint does not like temporary dicts for case-switch def _reactStatus_removed(self, clientId:str): """Remove the client entry from our internal state. This also covers crashes.""" if clientId in self.internalState["clients"]: #race condition at quit del self.internalState["clients"][clientId] if self.dataStorage and clientId == self.dataStorage.ourClientId: #We only care about the current data-storage, not another instance that was started before it. self.dataClientNamesHook(None) self.dataClientDescriptionHook(None) self.dataStorage = None def _reactStatus_stopped(self, clientId:str): """The client has stopped and can be restarted. The status is not saved. NSM will try to open all clients on session open and end in "ready" """ if self.dataStorage and clientId == self.dataStorage.ourClientId: #We only care about the current data-storage, not another instance that was started before it. self.dataClientNamesHook(None) self.dataClientDescriptionHook(None) self.dataStorage = None def _reactStatus_launch(self, clientId:str): """ Launch is a transitional status for NSM clients but the terminal status for dumb clients """ pass def _reactStatus_open(self, clientId:str): """ """ pass def _reactStatus_ready(self, clientId:str): """ This is sent after startup but also after every save. It signals that the client can react to nsm signals, not that it is ready for something else. Note that this is *After* the clientStatusHook, so any data changed here is not submitted to the api/GUI yet. E.g. you can't change dumbClient to True here if that is needed directly after start by the GUI. """ pass def _reactStatus_save(self, clientId:str): """ """ pass def _reactStatus_quit(self, clientId:str): """ """ pass def _reactStatus_noop(self, clientId:str): """ Dumb clients, or rather nsmd, react with noop on signals they cannot understand, like saving. """ pass def _reactStatus_switch(self, clientId:str): """ """ pass def _reactStatus_error(self, clientId:str): """ """ logger.error(f"{clientId} has error status!") def _reactReply_nsmServerOpen(self, answer:str): assert answer == "Loaded.", answer def _reactReply_nsmServerSave(self, answer:str): assert answer == "Saved.", answer def _reactReply_nsmServerClose(self, answer:str): assert answer == "Closed.", answer def _reactReply_nsmServerAbort(self, answer:str): assert answer == "Aborted.", answer def _reactReply_nsmServerAdd(self, answer:str): """Reaction to add client""" assert answer == "Launched.", answer def _reactReply_nsmServerRemoved(self): pass def _reactReply_nsmServerStopped(self): pass def _reactReply_nsmServerDuplicate(self, answer:str): """There are a lot of errors possible here, reported through nsmd /error, because we are dealing with the file system. Our own GUI and other safeguards should protect us from most though Positive answers are 'Duplicated.' when nsmd finished copying and 'Loaded.' when the new session is loaded. Or so one would think... the messages arrive the other way around. Anyway, both are needed to signify a succesful duplication. """ assert answer == "Loaded." or answer == "Duplicated.", answer #We don't need any callbacks here, nsmd sends a session change on top of the duplicate replies. def _reactReply_nsmServerNew(self, answer:str): """Created. arrives when a new session is created for the first time and directory is mkdir Session created arrives when a session was opened and nsm created its internal "session". We do not need to react to the new signal because we watch the dir for new sessions ourselves and the currently active session is send through "/nsm/gui/session/name" : self._reactCallback_activeSessionChanged, """ assert answer == 'Created.' or answer == "Session created", answer def _reactReply_nsmServerList(self, nsmSessionName:str): """Session names come one reply at a time. We reacted to the message /nsm/gui/server/message ['Listing sessions'] by clearing our internal session status and will save the new ones here /reply ['/nsm/server/list', 'test3'] Do not confuse reply server list with the message /nsm/server/list [0, 'Done.'] The latter is a top level message :( """ self.internalState["sessions"].add(nsmSessionName) #Our own functions def allClientsHide(self): for clientId, clientDict in self.internalState["clients"].items(): if clientDict["hasOptionalGUI"]: self.clientHide(clientId) def allClientsShow(self): for clientId, clientDict in self.internalState["clients"].items(): if clientDict["hasOptionalGUI"]: self.clientShow(clientId) def clientToggleVisible(self, clientId:str): if self.internalState["clients"][clientId]["hasOptionalGUI"]: if self.internalState["clients"][clientId]["visible"]: self.clientHide(clientId) else: self.clientShow(clientId) #data-storage / nsm-data def clientNameOverride(self, clientId:str, name:str): """An agordejo-specific function that requires the client nsm-data in the session. If nsm-data is not present this function will write nothing, not touch any data. It will still send a callback to revert any GUI changes back to the original name. We accept empty string as a name to remove the name override """ if self.dataStorage: assert clientId in self.internalState["clients"], self.internalState["clients"] self.dataStorage.setClientOverrideName(clientId, name) #triggers callback #data-storage / nsm-data def setDescription(self, text:str): if self.dataStorage: self.dataStorage.setDescription(text) def _checkDirectoryForSymlinks(self, path)->bool: for p in path.rglob("*"): if p.is_symlink(): return True return False def _checkIfLocked(self, nsmSessionName:str)->bool: basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() lockFile = pathlib.Path(basePath, ".lock") return lockFile.exists() def forceLiftLock(self, nsmSessionName:str): """Removes lockfile, no matter if session is actually open or just a remainder from a crash. If no lock exist it does nothing.""" self._updateSessionListBlocking() if self._checkIfLocked(nsmSessionName): basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() #implied by _checkIfLocked lockFile = pathlib.Path(basePath, ".lock") lockFile.unlink(missing_ok=True) logger.info(f"{nsmSessionName} was forced to unlock by us.") else: logger.info(f"Tried to unlock, but {nsmSessionName} is not locked") def getSessionFiles(self, nsmSessionName:str)->list: """Return all session files, useful to present to the user, e.g. as warning before deletion""" self._updateSessionListBlocking() basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() return [f.as_posix() for f in basePath.rglob("*")] #Includes directories themselves #Only files, no directories themselves. #result = [] #for path, dirs, files in walk(basePath): # for file in files: # result.append(pathlib.Path(path, file).as_posix()) #return result def deleteSession(self, nsmSessionName:str): """Delete project directory with all data. No undo. Only if session is not locked""" self._updateSessionListBlocking() if not nsmSessionName in self.internalState["sessions"]: logger.warning(f"{nsmSessionName} is not a session") return False basePath = pathlib.Path(self.sessionRoot, nsmSessionName) assert basePath.exists() if not self._checkIfLocked(nsmSessionName): logger.info(f"Deleting session {nsmSessionName}: {self.getSessionFiles(nsmSessionName)}") shutilrmtree(basePath) else: logger.warning(f"Tried to delete {basePath} but it is locked") def renameSession(self, nsmSessionName:str, newName:str): """Only works if session is not locked and dir does not exist yet""" self._updateSessionListBlocking() newPath = pathlib.Path(self.sessionRoot, newName) oldPath = pathlib.Path(self.sessionRoot, nsmSessionName) assert oldPath.exists() if self._checkIfLocked(nsmSessionName): logger.warning(f"Can't rename {nsmSessionName} to {newName}. {nsmSessionName} is locked.") return False elif newPath.exists(): logger.warning(f"Can't rename {nsmSessionName} to {newName}. {newName} already exists.") return False else: logger.info(f"Renaming {nsmSessionName} to {newName}.") tmp = pathlib.Path(oldPath.name+str(uuid4())) #Can't move itself into a subdir in itself. move to temp first. We don't use tempdir because that could be on another partition. we already know we can write here. oldPath.rename(tmp) pathlib.Path(newPath).mkdir(parents=True, exist_ok=True) tmp.rename(newPath) assert newPath.exists() def copySession(self, nsmSessionName:str, newName:str): """Copy a whole tree. Keep symlinks as symlinks. Lift lock""" self._updateSessionListBlocking() source = pathlib.Path(self.sessionRoot, nsmSessionName) destination = pathlib.Path(self.sessionRoot, newName) if destination.exists(): logger.warning(f"Can't copy {nsmSessionName} to {newName}. {newName} already exists.") return False elif not nsmSessionName in self.internalState["sessions"]: logger.warning(f"{nsmSessionName} is not a session") return elif not source.exists(): logger.warning(f"Can't copy {nsmSessionName} because it does not exist.") return False #All is well. try: shutilcopytree(source, destination, symlinks=True, dirs_exist_ok=False) #raises an error if dir already exists. But we already test above. self.forceLiftLock(newName) except Exception as e: #we don't want to crash if user tries to copy to /root or so. logger.error(e) return False #Export to the User Interface def sessionAsDict(self, nsmSessionName:str)->dict: assert self.sessionRoot entry = {} entry["nsmSessionName"] = nsmSessionName entry["name"] = pathlib.Path(nsmSessionName).name basePath = pathlib.Path(self.sessionRoot, nsmSessionName) sessionFile = pathlib.Path(basePath, "session.nsm") if not sessionFile.exists(): #This is a reason to let the program exit. logger.error("Got wrong session directory from nsmd. Race condition after delete? In any case a breaking error (please report). Quitting. Project was: " + repr(sessionFile)) sysexit() #return None switch to return None to let it crash and see the python traceback timestamp = datetime.fromtimestamp(sessionFile.stat().st_mtime).isoformat(sep=" ", timespec='minutes') entry["lastSavedDate"] = timestamp entry["sessionFile"] = sessionFile entry["lockFile"] = pathlib.Path(basePath, ".lock") entry["fullPath"] = str(basePath) entry["sizeInBytes"] = sum(f.stat().st_size for f in basePath.glob('**/*') if f.is_file() ) entry["numberOfClients"] = len(open(sessionFile).readlines()) entry["hasSymlinks"] = self._checkDirectoryForSymlinks(basePath) entry["parents"] = basePath.relative_to(self.sessionRoot).parts[:-1] #tuple of each dir between NSM root and nsmSessionName/session.nsm, exluding the actual project name. This is the tree entry["locked"] = self._checkIfLocked(nsmSessionName) #not for direct display return entry def exportSessionsAsDicts(self)->list: """Return a list of dicts of projects with additional information: """ logger.info("Exporting sessions to dict. Will call blocking list sessions next") results = [] #assert not self.internalState["currentSession"], self.internalState["currentSession"] #Do not request session list while in active session self._updateSessionListBlocking() for nsmSessionName in self.internalState["sessions"]: result = self.sessionAsDict(nsmSessionName) results.append(result) return results class DataStorage(object): """Interface to handle the external datastorage client url is pre-processed (host, port) Our init is the same as announcing the nsm-data client in the session. That means everytime nsm-data sends a new/open reply we get created. Thus we will send all our data to parent and subsequently to GUI-callbacks in init. Keys are strings, While nsmd OSC support int, str and float we use json exclusively. We send json string and parse the received data. Try to use only ints, floats, strin gs, lists and dicts. Client pretty names are limited to 512 chars, depending on our OSC message size. nsm-data will just cut to 512 chars. So a GUI should better protect that limit. """ def __init__(self, parent, ourClientId, messageSizeLimit:int, url:tuple, sock): logger.info("Create new DataStorage instance") self.parent = parent self.messageSizeLimit = messageSizeLimit # e.g. 512 self.ourClientId = ourClientId self.clients = parent.internalState["clients"] #shortcut. Mutable, persistent dict, until instance gets deleted. self.url = url self.sock = sock self.ip, self.port = self.sock.getsockname() self.data = self.getAll() #blocks. our local copy. = {"clientOverrideNames":{clientId:nameOverride}, "description":"str"} self.namesToParentAndCallbacks() self.descriptionToParentAndCallbacks() def namesToParentAndCallbacks(self): self.parent.dataClientNamesHook(self.data["clientOverrideNames"]) def descriptionToParentAndCallbacks(self): """Every char!!!""" self.parent.dataClientDescriptionHook(self.data["description"]) def _waitForMultipartMessage(self, pOscpath:str)->str: """Returns a json string, as if the message was sent as a single one. Can consist of only one part as well.""" logger.info(f"Waiting for multi message {pOscpath} in blocking mode") self.parent._setPause(True) jsonString = "" chunkNumberOfParts = float("+inf") #zero based currentPartNumber = float("-inf") #zero based while True: if currentPartNumber >= chunkNumberOfParts: break try: data, addr = self.sock.recvfrom(1024) except socket.timeout: break msg = _IncomingMessage(data) if msg.oscpath == pOscpath: currentPartNumber, l, jsonChunk = msg.params jsonString += jsonChunk chunkNumberOfParts = l #overwrite infinity the first time and redundant afterwards. else: self.parent._queue.append(msg) self.parent._setPause(False) logger.info(f"Message complete with {chunkNumberOfParts} chunks.") return jsonString def getAll(self): """Mirror everything from nsm-data""" msg = _OutgoingMessage("/agordejo/datastorage/getall") msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) jsonString = self._waitForMultipartMessage("/agordejo/datastorage/reply/getall") return json.loads(jsonString) def setClientOverrideName(self, clientId:str, value): """We accept empty string as a name to remove the name override""" assert clientId in self.clients, self.clients msg = _OutgoingMessage("/agordejo/datastorage/setclientoverridename") msg.add_arg(clientId) msg.add_arg(json.dumps(value)) self.sock.sendto(msg.build(), self.url) self.getClientOverrideName(clientId) #verifies data and triggers callback def getClientOverrideName(self, clientId:str): msg = _OutgoingMessage("/agordejo/datastorage/getclientoverridename") msg.add_arg(clientId) msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) #Wait in blocking mode self.parent._setPause(True) while True: try: data, addr = self.sock.recvfrom(1024) except socket.timeout: break msg = _IncomingMessage(data) if msg.oscpath == "/agordejo/datastorage/reply/getclient": replyClientId, jsonName = msg.params assert replyClientId == clientId, (replyClientId, clientId) break else: self.parent._queue.append(msg) self.parent._setPause(False) #Got answer answer = json.loads(jsonName) if answer: self.data["clientOverrideNames"][clientId] = answer else: #It is possible that a client not present in our storage will send an empty string. Protect. if clientId in self.data["clientOverrideNames"]: del self.data["clientOverrideNames"][clientId] self.namesToParentAndCallbacks() def _chunkstring(self, string): return [string[0+i:self.messageSizeLimit+i] for i in range(0, len(string), self.messageSizeLimit)] def setDescription(self, text:str): """This most likely arrives one char at time with the complete text""" chunks = self._chunkstring(text) descriptionId = str(id(text))[:8] for index, chunk in enumerate(chunks): msg = _OutgoingMessage("/agordejo/datastorage/setdescription") msg.add_arg(descriptionId) msg.add_arg(index) msg.add_arg(chunk) msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) #No echo answer. #We cheat a bit and inform parents with the new text directly. self.data["description"] = text self.descriptionToParentAndCallbacks() #and back #Generic Functions. Not in use and not ready. def _test(self): self.readAll() self.setDescription("Ein Jäger aus Kurpfalz,\nDer reitet durch den grünen Wald,\nEr schießt das Wild daher,\nGleich wie es ihm gefällt.") self.read("welt") self.create("welt", "world") self.read("welt") self.create("str", "bar") self.create("int", 1) self.create("list", [1, 2, 3]) self.create("tuple", (1, 2, 3)) #no tuples, everything will be a list. self.create("dict", {1:2, 3:4, 5:6}) self.update("str", "rolf") self.delete("str") def read(self, key:str): """Request one value""" msg = _OutgoingMessage("/agordejo/datastorage/read") msg.add_arg(key) msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) def readAll(self): """Request all data""" msg = _OutgoingMessage("/agordejo/datastorage/readall") msg.add_arg(self.ip) msg.add_arg(self.port) self.sock.sendto(msg.build(), self.url) def create(self, key:str, value): """Write/Create one value.""" msg = _OutgoingMessage("/agordejo/datastorage/create") msg.add_arg(key) msg.add_arg(json.dumps(value)) self.sock.sendto(msg.build(), self.url) def update(self, key:str, value): """Update a value, but only if it exists""" msg = _OutgoingMessage("/agordejo/datastorage/update") msg.add_arg(key) msg.add_arg(json.dumps(value)) self.sock.sendto(msg.build(), self.url) def delete(self, key:str): """Delete a key/value completely""" msg = _OutgoingMessage("/agordejo/datastorage/delete") msg.add_arg(key) self.sock.sendto(msg.build(), self.url)