././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6426377 qutebrowser-3.6.1/0000755000175100017510000000000015102145351013546 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/LICENSE0000644000175100017510000010451515102145205014557 0ustar00runnerrunner 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 . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/MANIFEST.in0000644000175100017510000000252415102145205015305 0ustar00runnerrunnerrecursive-include qutebrowser *.py recursive-include qutebrowser/img *.svg *.png recursive-include qutebrowser/javascript *.js graft tests graft qutebrowser/html graft qutebrowser/3rdparty graft qutebrowser/icons graft doc/img graft misc/apparmor graft misc/userscripts graft misc/requirements recursive-include scripts *.py *.sh *.js include qutebrowser/utils/testfile include qutebrowser/git-commit-id include LICENSE doc/* README.asciidoc include misc/org.qutebrowser.qutebrowser.desktop include misc/org.qutebrowser.qutebrowser.appdata.xml include misc/Makefile include requirements.txt include qutebrowser.py include misc/cheatsheet.svg include qutebrowser/config/configdata.yml include pytest.ini prune www prune scripts/dev prune scripts/testbrowser/cpp prune .github exclude scripts/asciidoc2html.py recursive-exclude doc *.asciidoc include doc/qutebrowser.1.asciidoc include doc/changelog.asciidoc prune qutebrowser/3rdparty exclude mypy.ini exclude pyrightconfig.json exclude tox.ini exclude qutebrowser/javascript/.eslintrc.yaml exclude qutebrowser/javascript/.eslintignore exclude doc/help exclude .* exclude misc/qutebrowser.spec exclude misc/qutebrowser.rcc exclude tests/unit/scripts/test_run_vulture.py exclude tests/unit/scripts/test_check_coverage.py prune doc/extapi prune misc/nsis prune **/.mypy_cache global-exclude __pycache__ *.pyc *.pyo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6426377 qutebrowser-3.6.1/PKG-INFO0000644000175100017510000003662115102145351014653 0ustar00runnerrunnerMetadata-Version: 2.4 Name: qutebrowser Version: 3.6.1 Summary: A keyboard-driven, vim-like browser based on Python and Qt. Home-page: https://www.qutebrowser.org/ Author: Florian Bruhin Author-email: mail@qutebrowser.org License: GPL-3.0-or-later Keywords: pyqt browser web qt webkit qtwebkit qtwebengine Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: X11 Applications :: Qt Classifier: Intended Audience :: End Users/Desktop Classifier: Natural Language :: English Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: MacOS Classifier: Operating System :: POSIX :: BSD Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Browsers Requires-Python: >=3.9 Description-Content-Type: text/plain License-File: LICENSE Requires-Dist: jinja2 Requires-Dist: PyYAML Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: requires-dist Dynamic: requires-python Dynamic: summary // SPDX-License-Identifier: GPL-3.0-or-later // If you are reading this in plaintext or on PyPi: // // A rendered version is available at: // https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc qutebrowser =========== // QUTE_WEB_HIDE image:qutebrowser/icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on Python and Qt.* image:https://github.com/qutebrowser/qutebrowser/workflows/CI/badge.svg["Build Status", link="https://github.com/qutebrowser/qutebrowser/actions?query=workflow%3ACI"] image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=main["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=main"] link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | https://github.com/qutebrowser/qutebrowser/blob/main/doc/faq.asciidoc[FAQ] | https://www.qutebrowser.org/doc/contributing.html[contributing] | link:https://github.com/qutebrowser/qutebrowser/releases[releases] | https://github.com/qutebrowser/qutebrowser/blob/main/doc/install.asciidoc[installing] // QUTE_WEB_HIDE_END qutebrowser is a keyboard-focused browser with a minimal GUI. It's based on Python and Qt and free software, licensed under the GPL. It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. // QUTE_WEB_HIDE **qutebrowser's primary maintainer, The-Compiler, is currently working part-time on qutebrowser, funded by donations.** To sustain this for a long time, your help is needed! See the https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] or https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc#donating[alternative donation methods] for more information. Depending on your sign-up date and how long you keep a certain level, you can get qutebrowser t-shirts, stickers and more! // QUTE_WEB_HIDE_END Screenshots ----------- image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"] image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"] image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"] image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"] Downloads --------- See the https://github.com/qutebrowser/qutebrowser/releases[GitHub releases page] for available downloads and the link:doc/install.asciidoc[INSTALL] file for detailed instructions on how to get qutebrowser running on various platforms. Documentation and getting help ------------------------------ Please see the link:doc/help/index.asciidoc[help page] for available documentation pages and support channels. Contributions / Bugs -------------------- You want to contribute to qutebrowser? Awesome! Please read link:doc/contributing.asciidoc[the contribution guidelines] for details and useful hints. If you found a bug or have a feature request, you can report it in several ways: * Use the built-in `:report` command or the automatic crash dialog. * Open an issue in the Github issue tracker. * Write a mail to the https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[]. Please report security bugs to security@qutebrowser.org (or if GPG encryption is desired, contact me@the-compiler.org with GPG ID https://www.the-compiler.org/pubkey.asc[0x916EB0C8FD55A072]). Alternatively, https://github.com/qutebrowser/qutebrowser/security/advisories/new[report a vulnerability] via GitHub's https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability[private reporting feature]. Requirements ------------ The following software and libraries are required to run qutebrowser: * https://www.python.org/[Python] 3.9 or newer * https://www.qt.io/[Qt], either 6.2.0 or newer, or 5.15.0 or newer, with the following modules: - QtCore / qtbase - QtQuick (part of qtbase or qtdeclarative in some distributions) - QtSQL (part of qtbase in some distributions) - QtDBus (part of qtbase in some distributions; note that a connection to DBus at runtime is optional) - QtOpenGL - QtWebEngine (if using Qt 5, 5.15.2 or newer), or - alternatively QtWebKit (5.212) - **This is not recommended** due to known security issues in QtWebKit, you most likely want to use qutebrowser with the default QtWebEngine backend (based on Chromium) instead. Quoting the https://github.com/qtwebkit/qtwebkit/releases[QtWebKit releases page]: _[The latest QtWebKit] release is based on [an] old WebKit revision with known unpatched vulnerabilities. Please use it carefully and avoid visiting untrusted websites and using it for transmission of sensitive data._ * https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 6.2.2 or newer (Qt 6) or 5.15.0 or newer (Qt 5) * https://palletsprojects.com/p/jinja/[jinja2] * https://github.com/yaml/pyyaml[PyYAML] On macOS, the following libraries are also required: * https://pyobjc.readthedocs.io/en/latest/[pyobjc-core and pyobjc-framework-Cocoa] The following libraries are optional: * https://pypi.org/project/adblock/[adblock] (for improved adblocking using ABP syntax) * https://pygments.org/[pygments] for syntax highlighting with `:view-source` on QtWebKit, or when using `:view-source --pygments` with the (default) QtWebEngine backend. * On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log output. * https://asciidoc.org/[asciidoc] to generate the documentation for the `:help` command, when using the git repository (rather than a release). See link:doc/install.asciidoc[the documentation] for directions on how to install qutebrowser and its dependencies. Donating -------- **qutebrowser's primary maintainer, The-Compiler, is currently working part-time on qutebrowser, funded by donations.** To sustain this for a long time, your help is needed! See the https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more information. Depending on your sign-up date and how long you keep a certain level, you can get qutebrowser t-shirts, stickers and more! GitHub Sponsors allows for one-time donations (using the buttons next to "Select a tier") as well as custom amounts. **For currencies other than Euro or Swiss Francs, this is the preferred donation method.** GitHub uses https://stripe.com/[Stripe] to accept payment via credit cards without any fees. Billing via PayPal is available as well, with less fees than a direct PayPal transaction. Alternatively, the following donation methods are available -- note that eligibility for swag (shirts/stickers/etc.) is handled on a case-by-case basis for those, please mailto:mail@qutebrowser.org[get in touch] for details. * https://liberapay.com/The-Compiler[Liberapay], which can handle payments via Credit Card, SEPA bank transfers, or Paypal. Payment fees are paid by me, but they are https://liberapay.com/about/faq#fees[relatively low]. * SEPA bank transfer inside Europe (**no fees**): - Account holder: Florian Bruhin - Country: Switzerland - IBAN (EUR): CH13 0900 0000 9160 4094 6 - IBAN (other): CH80 0900 0000 8711 8587 3 - Bank: PostFinance AG, Mingerstrasse 20, 3030 Bern, Switzerland (BIC: POFICHBEXXX) - If you need any other information: Contact me at mail@qutebrowser.org. - If possible, **please consider yearly or semi-yearly donations**, because of the additional overhead from many individual transactions for bookkeeping/tax purposes. * PayPal: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=CHF&source=url[CHF], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=EUR&source=url[EUR], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=USD&source=url[USD]. **Note: Fees can be very high (around 5-40%, depending on the donated amounts)** - consider using GitHub Sponsors (credit card), Liberapay (credit cards, PayPal, or bank transfer) or SEPA bank transfers instead. * Cryptocurrencies: - Bitcoin: link:bitcoin:bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00[bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00] - Bitcoin Cash: link:bitcoincash:1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N[1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N] - Ethereum: link:ethereum:0x10c2425856F7a8799EBCaac4943026803b1089c6[0x10c2425856F7a8799EBCaac4943026803b1089c6] - Litecoin: link:litecoin:MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2[MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2] - Others: Please mailto:mail@qutebrowser.org[get in touch], I'd happily set up anything link:https://www.ledger.com/supported-crypto-assets[supported by Ledger Live] Sponsors -------- Thanks a lot to https://www.macstadium.com/[MacStadium] for supporting qutebrowser with a free hosted Mac Mini via their https://www.macstadium.com/opensource[Open Source Project]. (They don't require including this here - I've just been very happy with their offer, and without them, no macOS releases or tests would exist) Thanks to the https://www.hsr.ch/[HSR Hochschule für Technik Rapperswil], which made it possible to work on qutebrowser extensions as a student research project. image:doc/img/sponsors/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"] image:doc/img/sponsors/hsr.png["HSR Hochschule für Technik Rapperswil",link="https://www.hsr.ch/"] Authors ------- qutebrowser's primary author is Florian Bruhin (The Compiler), but qutebrowser wouldn't be what it is without the help of https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contributors]! Additionally, the following people have contributed graphics: * Jad/link:https://yelostudio.com[yelo] (new icon) * WOFall (original icon) * regines (key binding cheatsheet) Also, thanks to everyone who contributed to one of qutebrowser's link:doc/backers.asciidoc[crowdfunding campaigns]! Similar projects ---------------- Various projects with a similar goal like qutebrowser exist. Many of them were inspirations for qutebrowser in some way, thanks for that! Active ~~~~~~ * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2) * https://luakit.github.io/[luakit] (C/Lua, GTK+ with WebKit2) * https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebEngine or GTK+/WebKit2 - note there was a https://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution in 2019] which was handled quite badly) * https://vieb.dev/[Vieb] (JavaScript, Electron) * https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) * https://github.com/jun7/wyeb[wyeb] (C, GTK+ with WebKit2) * Chrome/Chromium addons: https://vimium.github.io/[Vimium] * Firefox addons (based on WebExtensions): https://tridactyl.xyz/[Tridactyl], https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] * Addons for Firefox and Chrome: https://github.com/brookhong/Surfingkeys[Surfingkeys] (https://github.com/brookhong/Surfingkeys/issues/1796[somewhat sketchy]...), https://lydell.github.io/LinkHints/[Link Hints] (hinting only), https://github.com/ueokande/vimmatic[Vimmatic] Inactive ~~~~~~~~ * https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1, https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] - main inspiration for qutebrowser) * https://github.com/parkouss/webmacs/[webmacs] (Python, Emacs-like with QtWebEngine, https://github.com/parkouss/webmacs/issues/137[unmaintained]) * https://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with WebKit1) * https://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1) * http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko) * https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2) * https://github.com/conformal/xombrero[xombrero] (C, GTK+ with WebKit1) * https://github.com/linkdd/cream-browser[Cream Browser] (C, GTK+ with WebKit1) * Firefox addons (not based on WebExtensions or no recent activity): http://www.vimperator.org/[Vimperator], http://bug.5digits.org/pentadactyl/index[Pentadactyl], https://github.com/akhodakivskiy/VimFx[VimFx] (seems to offer a https://gir.st/blog/legacyfox.htm[hack] to run on modern Firefox releases), https://github.com/shinglyu/QuantumVim[QuantumVim], https://github.com/ueokande/vim-vixen[Vim Vixen], https://github.com/amedama41/vvimpulation[VVimpulation], https://krabby.netlify.app/[Krabby] * Chrome/Chromium addons: https://github.com/k2nr/ViChrome/[ViChrome], https://github.com/jinzhu/vrome[Vrome], https://github.com/lusakasa/saka-key[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]), https://github.com/1995eaton/chromium-vim[cVim], https://github.com/dcchambers/vb4c[vb4c] (fork of cVim, https://github.com/dcchambers/vb4c/issues/23#issuecomment-810694017[unmaintained]), https://glee.github.io/[GleeBox] * Addons for Safari: https://televator.net/vimari/[Vimari] License ------- 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 . pdf.js ------ qutebrowser optionally uses https://github.com/mozilla/pdf.js/[pdf.js] to display PDF files in the browser. Windows releases come with a bundled pdf.js. pdf.js is distributed under the terms of the Apache License. You can find a copy of the license in `qutebrowser/3rdparty/pdfjs/LICENSE` (in the Windows release or after running `scripts/dev/update_3rdparty.py`), or online https://www.apache.org/licenses/LICENSE-2.0.html[here]. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/README.asciidoc0000644000175100017510000003367215102145205016214 0ustar00runnerrunner// SPDX-License-Identifier: GPL-3.0-or-later // If you are reading this in plaintext or on PyPi: // // A rendered version is available at: // https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc qutebrowser =========== // QUTE_WEB_HIDE image:qutebrowser/icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on Python and Qt.* image:https://github.com/qutebrowser/qutebrowser/workflows/CI/badge.svg["Build Status", link="https://github.com/qutebrowser/qutebrowser/actions?query=workflow%3ACI"] image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=main["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=main"] link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | https://github.com/qutebrowser/qutebrowser/blob/main/doc/faq.asciidoc[FAQ] | https://www.qutebrowser.org/doc/contributing.html[contributing] | link:https://github.com/qutebrowser/qutebrowser/releases[releases] | https://github.com/qutebrowser/qutebrowser/blob/main/doc/install.asciidoc[installing] // QUTE_WEB_HIDE_END qutebrowser is a keyboard-focused browser with a minimal GUI. It's based on Python and Qt and free software, licensed under the GPL. It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. // QUTE_WEB_HIDE **qutebrowser's primary maintainer, The-Compiler, is currently working part-time on qutebrowser, funded by donations.** To sustain this for a long time, your help is needed! See the https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] or https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc#donating[alternative donation methods] for more information. Depending on your sign-up date and how long you keep a certain level, you can get qutebrowser t-shirts, stickers and more! // QUTE_WEB_HIDE_END Screenshots ----------- image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"] image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"] image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"] image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"] Downloads --------- See the https://github.com/qutebrowser/qutebrowser/releases[GitHub releases page] for available downloads and the link:doc/install.asciidoc[INSTALL] file for detailed instructions on how to get qutebrowser running on various platforms. Documentation and getting help ------------------------------ Please see the link:doc/help/index.asciidoc[help page] for available documentation pages and support channels. Contributions / Bugs -------------------- You want to contribute to qutebrowser? Awesome! Please read link:doc/contributing.asciidoc[the contribution guidelines] for details and useful hints. If you found a bug or have a feature request, you can report it in several ways: * Use the built-in `:report` command or the automatic crash dialog. * Open an issue in the Github issue tracker. * Write a mail to the https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[]. Please report security bugs to security@qutebrowser.org (or if GPG encryption is desired, contact me@the-compiler.org with GPG ID https://www.the-compiler.org/pubkey.asc[0x916EB0C8FD55A072]). Alternatively, https://github.com/qutebrowser/qutebrowser/security/advisories/new[report a vulnerability] via GitHub's https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability[private reporting feature]. Requirements ------------ The following software and libraries are required to run qutebrowser: * https://www.python.org/[Python] 3.9 or newer * https://www.qt.io/[Qt], either 6.2.0 or newer, or 5.15.0 or newer, with the following modules: - QtCore / qtbase - QtQuick (part of qtbase or qtdeclarative in some distributions) - QtSQL (part of qtbase in some distributions) - QtDBus (part of qtbase in some distributions; note that a connection to DBus at runtime is optional) - QtOpenGL - QtWebEngine (if using Qt 5, 5.15.2 or newer), or - alternatively QtWebKit (5.212) - **This is not recommended** due to known security issues in QtWebKit, you most likely want to use qutebrowser with the default QtWebEngine backend (based on Chromium) instead. Quoting the https://github.com/qtwebkit/qtwebkit/releases[QtWebKit releases page]: _[The latest QtWebKit] release is based on [an] old WebKit revision with known unpatched vulnerabilities. Please use it carefully and avoid visiting untrusted websites and using it for transmission of sensitive data._ * https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 6.2.2 or newer (Qt 6) or 5.15.0 or newer (Qt 5) * https://palletsprojects.com/p/jinja/[jinja2] * https://github.com/yaml/pyyaml[PyYAML] On macOS, the following libraries are also required: * https://pyobjc.readthedocs.io/en/latest/[pyobjc-core and pyobjc-framework-Cocoa] The following libraries are optional: * https://pypi.org/project/adblock/[adblock] (for improved adblocking using ABP syntax) * https://pygments.org/[pygments] for syntax highlighting with `:view-source` on QtWebKit, or when using `:view-source --pygments` with the (default) QtWebEngine backend. * On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log output. * https://asciidoc.org/[asciidoc] to generate the documentation for the `:help` command, when using the git repository (rather than a release). See link:doc/install.asciidoc[the documentation] for directions on how to install qutebrowser and its dependencies. Donating -------- **qutebrowser's primary maintainer, The-Compiler, is currently working part-time on qutebrowser, funded by donations.** To sustain this for a long time, your help is needed! See the https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more information. Depending on your sign-up date and how long you keep a certain level, you can get qutebrowser t-shirts, stickers and more! GitHub Sponsors allows for one-time donations (using the buttons next to "Select a tier") as well as custom amounts. **For currencies other than Euro or Swiss Francs, this is the preferred donation method.** GitHub uses https://stripe.com/[Stripe] to accept payment via credit cards without any fees. Billing via PayPal is available as well, with less fees than a direct PayPal transaction. Alternatively, the following donation methods are available -- note that eligibility for swag (shirts/stickers/etc.) is handled on a case-by-case basis for those, please mailto:mail@qutebrowser.org[get in touch] for details. * https://liberapay.com/The-Compiler[Liberapay], which can handle payments via Credit Card, SEPA bank transfers, or Paypal. Payment fees are paid by me, but they are https://liberapay.com/about/faq#fees[relatively low]. * SEPA bank transfer inside Europe (**no fees**): - Account holder: Florian Bruhin - Country: Switzerland - IBAN (EUR): CH13 0900 0000 9160 4094 6 - IBAN (other): CH80 0900 0000 8711 8587 3 - Bank: PostFinance AG, Mingerstrasse 20, 3030 Bern, Switzerland (BIC: POFICHBEXXX) - If you need any other information: Contact me at mail@qutebrowser.org. - If possible, **please consider yearly or semi-yearly donations**, because of the additional overhead from many individual transactions for bookkeeping/tax purposes. * PayPal: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=CHF&source=url[CHF], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=EUR&source=url[EUR], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=USD&source=url[USD]. **Note: Fees can be very high (around 5-40%, depending on the donated amounts)** - consider using GitHub Sponsors (credit card), Liberapay (credit cards, PayPal, or bank transfer) or SEPA bank transfers instead. * Cryptocurrencies: - Bitcoin: link:bitcoin:bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00[bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00] - Bitcoin Cash: link:bitcoincash:1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N[1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N] - Ethereum: link:ethereum:0x10c2425856F7a8799EBCaac4943026803b1089c6[0x10c2425856F7a8799EBCaac4943026803b1089c6] - Litecoin: link:litecoin:MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2[MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2] - Others: Please mailto:mail@qutebrowser.org[get in touch], I'd happily set up anything link:https://www.ledger.com/supported-crypto-assets[supported by Ledger Live] Sponsors -------- Thanks a lot to https://www.macstadium.com/[MacStadium] for supporting qutebrowser with a free hosted Mac Mini via their https://www.macstadium.com/opensource[Open Source Project]. (They don't require including this here - I've just been very happy with their offer, and without them, no macOS releases or tests would exist) Thanks to the https://www.hsr.ch/[HSR Hochschule für Technik Rapperswil], which made it possible to work on qutebrowser extensions as a student research project. image:doc/img/sponsors/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"] image:doc/img/sponsors/hsr.png["HSR Hochschule für Technik Rapperswil",link="https://www.hsr.ch/"] Authors ------- qutebrowser's primary author is Florian Bruhin (The Compiler), but qutebrowser wouldn't be what it is without the help of https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contributors]! Additionally, the following people have contributed graphics: * Jad/link:https://yelostudio.com[yelo] (new icon) * WOFall (original icon) * regines (key binding cheatsheet) Also, thanks to everyone who contributed to one of qutebrowser's link:doc/backers.asciidoc[crowdfunding campaigns]! Similar projects ---------------- Various projects with a similar goal like qutebrowser exist. Many of them were inspirations for qutebrowser in some way, thanks for that! Active ~~~~~~ * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2) * https://luakit.github.io/[luakit] (C/Lua, GTK+ with WebKit2) * https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebEngine or GTK+/WebKit2 - note there was a https://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution in 2019] which was handled quite badly) * https://vieb.dev/[Vieb] (JavaScript, Electron) * https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) * https://github.com/jun7/wyeb[wyeb] (C, GTK+ with WebKit2) * Chrome/Chromium addons: https://vimium.github.io/[Vimium] * Firefox addons (based on WebExtensions): https://tridactyl.xyz/[Tridactyl], https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] * Addons for Firefox and Chrome: https://github.com/brookhong/Surfingkeys[Surfingkeys] (https://github.com/brookhong/Surfingkeys/issues/1796[somewhat sketchy]...), https://lydell.github.io/LinkHints/[Link Hints] (hinting only), https://github.com/ueokande/vimmatic[Vimmatic] Inactive ~~~~~~~~ * https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1, https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] - main inspiration for qutebrowser) * https://github.com/parkouss/webmacs/[webmacs] (Python, Emacs-like with QtWebEngine, https://github.com/parkouss/webmacs/issues/137[unmaintained]) * https://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with WebKit1) * https://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1) * http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko) * https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2) * https://github.com/conformal/xombrero[xombrero] (C, GTK+ with WebKit1) * https://github.com/linkdd/cream-browser[Cream Browser] (C, GTK+ with WebKit1) * Firefox addons (not based on WebExtensions or no recent activity): http://www.vimperator.org/[Vimperator], http://bug.5digits.org/pentadactyl/index[Pentadactyl], https://github.com/akhodakivskiy/VimFx[VimFx] (seems to offer a https://gir.st/blog/legacyfox.htm[hack] to run on modern Firefox releases), https://github.com/shinglyu/QuantumVim[QuantumVim], https://github.com/ueokande/vim-vixen[Vim Vixen], https://github.com/amedama41/vvimpulation[VVimpulation], https://krabby.netlify.app/[Krabby] * Chrome/Chromium addons: https://github.com/k2nr/ViChrome/[ViChrome], https://github.com/jinzhu/vrome[Vrome], https://github.com/lusakasa/saka-key[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]), https://github.com/1995eaton/chromium-vim[cVim], https://github.com/dcchambers/vb4c[vb4c] (fork of cVim, https://github.com/dcchambers/vb4c/issues/23#issuecomment-810694017[unmaintained]), https://glee.github.io/[GleeBox] * Addons for Safari: https://televator.net/vimari/[Vimari] License ------- 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 . pdf.js ------ qutebrowser optionally uses https://github.com/mozilla/pdf.js/[pdf.js] to display PDF files in the browser. Windows releases come with a bundled pdf.js. pdf.js is distributed under the terms of the Apache License. You can find a copy of the license in `qutebrowser/3rdparty/pdfjs/LICENSE` (in the Windows release or after running `scripts/dev/update_3rdparty.py`), or online https://www.apache.org/licenses/LICENSE-2.0.html[here]. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.4836395 qutebrowser-3.6.1/doc/0000755000175100017510000000000015102145350014312 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/doc/changelog.asciidoc0000644000175100017510000074252515102145205017757 0ustar00runnerrunnerChange Log =========== // https://keepachangelog.com/ All notable changes to this project will be documented in this file. This project adheres to https://semver.org/[Semantic Versioning], though minor breaking changes (such as renamed commands) can happen in minor releases. // tags: // `Added` for new features. // `Changed` for changes in existing functionality. // `Deprecated` for once-stable features removed in upcoming releases. // `Removed` for deprecated features removed in this release. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. [[v3.6.1]] v3.6.1 (2025-11-03) ------------------- Fixed ~~~~~ - A regression in v3.6.0 where the page didn't have keyboard focus after closing the completion, so e.g. typing in an input field after hinting didn't work. (#8750) [[v3.6.0]] v3.6.0 (2025-10-24) ------------------- Added ~~~~~ - The `:version` info now shows additional information: * The X11 window manager / Wayland compositor name (mostly useful for bug/crash reports). * Loaded WebExtensions (partial support landed in QtWebEngine 6.10, no official qutebrowser support yet). - Support for hinting elements which are part of an (open) shadow DOM. Changed ~~~~~~~ - The `qutedmenu` userscript now sorts history by the last access time. - Hardware accelerated 2D canvas is now enabled by default on Qt 6.8.2+, as graphic glitches with e.g. PDF.js and Google Sheets should be fixed nowadays. If you still run into issues, please report them and set `qt.workarounds.disable_accelerated_2d_canvas` to `always` to disable it again. - Changes to binary releases: * Windows and macOS releases are now built with Qt 6.10.0, which is based on Chromium 134.0.6998.208 with security patches up to 140.0.7339.207. * Windows and macOS releases are now built with Python 3.14. * Windows releases are now built on Windows Server 2022 (previously 2019), which might break compatibility with older Windows releases (untested). * If using `mkvenv.py` on Linux, note that Qt now requires glibc v2.34 (v2.28 previously). This is available down to Ubuntu 22.04 LTS and Debian Bookworm (oldstable), so this should not affect most users of desktop distributions. Fixed ~~~~~ - Fixed crash if two new downloads start while a download prompt is already open (#8674). - Fixed exception when closing a qutebrowser window while a download prompt is still open. - Hopefully proper fix for some web pages jumping to the top when the statusbar is hidden (#8223). - Fix for the page header being shown on YouTube after the fullscreen notification was hidden (#8625). - Fix for videos losing keyboard focus when the fullscreen notification shows (#8174). - The workaround for microphone/camera permissions not being requested with QtWebEngine 6.9 on Google Meet, Zoom, or other pages using the new `` element now got extended to Qt 6.9.1+ as it's still not fixed upstream. (#8612) - The package version for Jinja 3.3+ is now correctly displayed in `:version`. - Fixed crash with Qt 6.10 (and possibly older Qt versions) when navigating from a `qute://` page to a web page, e.g. when searching on `qute://start`. - On Wayland with Qt <= 6.9, `EGL_PLATFORM=wayland` is now set by qutebrowser to get hardware rendering. Qt 6.10 includes an equivalent fix (#8637). - Added workaround for per-domain User-Agent header not being used on redirects (#8679). - Added site-specific quirk for gitlab.gnome.org agressively blocking old Chromium versions (and thus QtWebEngine) (#8509). - Using `:config-list-remove` with an invalid value for the respective option type now corrently displays an error instead of crashing. [[v3.5.1]] v3.5.1 (2025-06-05) ------------------- Deprecated ~~~~~~~~~~ - QtWebKit (legacy) support got removed from CI and is now untested. If it breaks, it's not going to be fixed, and support will be removed over the next releases. - Qt 5 support is currently still tested, but is also planned to get removed over the next releases. Same goes for support for older Qt 6 versions (likely 6.2/6.3/6.4 and perhaps 6.5, see https://github.com/qutebrowser/qutebrowser/issues/8464[#8464]). Changed ~~~~~~~ - Windows/macOS releases now bundle Qt 6.9.1, including many graphics-related bugfixes, as well as security patches up to Chromium 136.0.7103.114. Fixed ~~~~~ - A bogus "wildcard call disconnects from destroyed signal" warning from Qt is now suppressed. - PDF.js now loads correctly on Windows installations with broken mimetype configurations. - A "Ignoring new child ..." debug log message which got spammy with Qt 6.9 is now removed. - A unknown crash (possibly related to using devtools) due to weird (Py)Qt behavior now has a workaround. - No "QtWebEngine version mismatch" warning is now logged anymore with newer Qt 5.15 releases (but you should still stop using Qt 5). - The PDF.js version can now correctly be extracted/displayed with newer PDF.js versions. - The `qute-bitwarden`, `-lastpass` and `-pass` userscripts now properly avoid a `DeprecationWarning` from the upcoming 6.0 release of `tldextract`. The previous fix in v3.5.1 was insufficient. [[v3.5.0]] v3.5.0 (2025-04-12) ------------------- Changed ~~~~~~~ - Windows/macOS releases are now built with Qt 6.9.0 * Based on Chromium 130.0.6723.192 * Security fixes up to Chromium 133.0.6943.141 * Also fixes issues with opening links on macOS - The `content.headers.user_agent` setting now has a new `{upstream_browser_version_short}` template field, which is the upstream/Chromium version but shortened to only major version. - The default user agent now uses the shortened Chromium version and doesn't expose the `QtWebEngine/...` part anymore, thus making it equal to the corresponding Chromium user agent. This increases compatibilty due to various overzealous "security" products used by a variety of websites that block QtWebEngine, presumably as a bot (known issues existed with Whatsapp Web, UPS, Digitec Galaxus). - Changed features in userscripts: * `qute-bitwarden` now passes your password to the subprocess in an environment variable when unlocking your vault, instead of as a command line argument. (#7781) - New `-D no-system-pdfjs` debug flag to ignore system-wide PDF.js installations for testing. - Polyfill for missing `URL.parse` with PDF.js v5 and QtWebEngine < 6.9. Note this is a "best effort" fix and you should be using the "older browsers" ("legacy") build of PDF.js instead. Removed ~~~~~~~ - The `ua-slack` site-specific quirk, as things seem to work better nowadays without a quirk needed. - The `ua-whatsapp` site-specific quirk, as it's unneeded with the default UA change described above. Fixed ~~~~~ - Crash when trying to use the `DocumentPictureInPicture` JS API, such as done by the new Google Workspaces Huddle feature. The API is unsupported by QtWebEngine and now correctly disabled on the JS side. (#8449) - Crash when a buggy notification presenter returns a duplicate ID (now an error is shown instead). - Crashes when running `:tab-move` or `:yank title` at startup, before a tab is available. - Crash with `input.insert_mode.auto_load`, when closing a new tab quickly after opening it, but before it was fully loaded. (#3895, #8400) - Workaround for microphone/camera permissions not being requested with QtWebEngine 6.9.0 on Google Meet, Zoom, or other pages using the new `` element. (#8539) - Resolved issues in userscripts: * `qute-bitwarden` will now prompt a re-login if its cached session has been invalidated since last used. (#8456) * `qute-bitwarden`, `-lastpass` and `-pass` now avoid a `DeprecationWarning` from the upcoming 6.0 release of `tldextract` [[v3.4.0]] v3.4.0 (2024-12-14) ------------------- Removed ~~~~~~~ - Support for Python 3.8 is dropped, and Python 3.9 is now required. (#8325) - Support for macOS 12 Monterey is now dropped, and binaries will be built on macOS 13 Ventura. (#8327) - When using the installer on Windows 10, build 1809 or newer is now required (previous versions required 1607 or newer, but that's not officialy supported by Qt upstream). (#8336) Changed ~~~~~~~ - Windows/macOS binaries are now built with Qt 6.8.1. (#8242) - Based on Chromium 122.0.6261.171 - With security patches up to 131.0.6778.70 - Windows/macOS binaries are now using Python 3.13. (#8205) - The `.desktop` file now also declares qutebrowser as a valid viewer for `image/webp`. (#8340) - Updated mimetype information for getting a suitable extension when downloading a `data:` URL. - The `content.javascript.clipboard` setting now defaults to "ask", which on Qt 6.8+ will prompt the user to grant clipboard access. On older Qt versions, this is still equivalent to `"none"` and needs to be set manually. (#8348) - If a XHR request made via JS sets a custom `Accept-Language` header, it now correctly has precedence over the global `content.headers.accept_language` setting (but not per-domain overrides). This fixes subtle JS issues on websites that rely on the custom header being sent for those requests, and e.g. block the requests server-side otherwise. (#8370) - Our packaging scripts now prefer the "legacy"/"for older browsers" PDF.js build as their normal release only supports the latest Chromium version and might break in qutebrowser on updates. **Note to packagers:** If there's a PDF.js package in your distribution as an (optional) qutebrowser dependency, consider also switching to this variant (same code, built differently). Fixed ~~~~~ - Crash with recent Jinja/Markupsafe versions when viewing a finished userscript (or potentially editor) process via `:process`. - `scripts/open_url_in_instance.sh` now avoids `echo -n`, thus running correctly on POSIX sh. (#8409) - Added a workaround for a bogus QtWebEngine warning about missing spell checking dictionaries. (#8330) [[v3.3.1]] v3.3.1 (2024-10-12) ------------------- Fixed ~~~~~ - Updated the workaround for Google sign-in issues. [[v3.3.0]] v3.3.0 (2024-10-12) ------------------- Added ~~~~~ - Added the `qt.workarounds.disable_hangouts_extension` setting, for disabling the Google Hangouts extension built into Chromium/QtWebEngine. - Failed end2end tests will now save screenshots of the browser window when run under xvfb (the default on linux). Screenshots will be under `$TEMP/pytest-current/pytest-screenshots/` or attached to the GitHub actions run as an artifact. (#7625) Removed ~~~~~~~ - Support for macOS 11 Big Sur is dropped. Binaries are now built on macOS 12 Monterey and are unlikely to still run on older macOS versions. Changed ~~~~~~~ - The qute-pass userscript now has better support for internationalized domain names when using the pass backend - both domain names and secret paths are normalized before comparing (#8133) - Ignored URL query parameters (via `url.yank_ignored_parameters`) are now respected when yanking any URL (for example, through hints with `hint links yank`). The `{url:yank}` substitution has also been added as a version of `{url}` that respects ignored URL query parameters. (#7879) - Windows and macOS releases now bundle Qt 6.7.3, which includes security fixes up to Chromium 129.0.6668.58. Fixed ~~~~~ - A minor memory leak of QItemSelectionModels triggered by closing the completion dialog has been resolved. (#7950) - The link to the chrome https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns/[URL match pattern] documentation in our settings docs now loads a live page again. (#8268) - A rare crash when on Qt 6, a renderer process terminates with an unknown termination reason. - Updated the workaround for Google sign-in issues. [[v3.2.1]] v3.2.1 (2024-06-25) ------------------- Added ~~~~~ - There is now a separate macOS release built for Apple Silicon. A Universal Binary might follow with a later release. Changed ~~~~~~~ - Windows and macOS releases now bundle Qt 6.7.2, which includes security fixes up to Chromium 125.0.6422.142. Fixed ~~~~~ - When the selected Qt wrapper is unavailable, qutebrowser now again shows a GUI error message instead of only an exception in the terminal. [[v3.2.0]] v3.2.0 (2024-06-03) ------------------- Deprecated ~~~~~~~~~~ - This will be the last feature release supporting macOS 11 Big Sur. Starting with qutebrowser v3.3.0, macOS 12 Monterey will be the oldest supported version. Added ~~~~~ - When qutebrowser receives a SIGHUP it will now reload any config.py file in use (same as the `:config-source` command does). (#8108) - The Chromium security patch version is now shown in the backend string in `--version` and `:version`. This reflects the latest Chromium version that security fixes have been backported to the base QtWebEngine version from. (#7187) Changed ~~~~~~~ - Windows and macOS releases now ship with Qt 6.7.1, which is based on Chromium 118.0.5993.220 with security patches up to 124.0.6367.202. - With QtWebEngine 6.7+, the `colors.webpage.darkmode.enabled` setting can now be changed at runtime and supports URL patterns (#8182). - A few more completions will now match search terms in any order: `:quickmark-*`, `:bookmark-*`, `:tab-take` and `:tab-select` (for the quick and bookmark categories). (#7955) - Elements with an ARIA `role="switch"` now get hints (toggle switches like e.g. on cookie banners). - The `tor_identity` userscript now validates that the -c|--control-port argument value is an int. (#8162) Fixed ~~~~~ - `input.insert_mode.auto_load` sometimes not triggering due to a race condition. (#8145) - Worked around qutebrowser quitting when closing a KDE file dialog due to a Qt bug. (#8143) - Trying to use qutebrowser after it's been deleted/moved on disk (e.g. after a Python upgrade) should now not crash anymore. - When the QtWebEngine resources dir couldn't be found, qutebrowser now doesn't crash anymore (but QtWebEngine still might). - Fixed a rare crash in the completion widget when there was no selection model when we went to clear that, probably when leaving a mode. (#7901) - Worked around a minor issue around QTimers on Windows where the IPC server could close the socket early. (#8191) - The latest PDF.js release (v4.2.67) is now supported when backed by QtWebEngine 6.6+ (#8170) [[v3.1.0]] v3.1.0 (2023-12-08) ------------------- Removed ~~~~~~~ - The darkmode settings `grayscale.all`, `grayscale.images` and `increase_text_contrast` got removed, following removals in Chromium. Added ~~~~~ - New `smart-simple` value for `colors.webpage.darkmode.policy.images`, which on QtWebEngine 6.6+ uses a simpler classification algorithm to decide whether to invert images. - New `content.javascript.legacy_touch_events` setting, with those now being disabled by default, following a Chromium change. Changed ~~~~~~~ - Upgraded the bundled Qt version to 6.6.1, based on Chromium 112. Note this is only relevant for the macOS/Windows releases, on Linux those will be upgraded via your distribution packages. - Upgraded the bundled Python version for macOS/Windows to 3.12 - The `colors.webpage.darkmode.threshold.text` setting got renamed to `colors.webpage.darkmode.threshold.foreground`, following a rename in Chromium. - With Qt 6.6, the `content.canvas_reading` setting now works without a restart and supports URL patterns. Fixed ~~~~~ - Some web pages jumping to the top when the statusbar is hidden or (with v3.0.x) when a prompt is hidden. - Compatibility with PDF.js v4 - Added an elaborate workaround for a bug in QtWebEngine 6.6.0 causing crashes on Google Mail/Meet/Chat, and a bug in QtWebEngine 6.5.0/.1/.2 causing crashes there with dark mode. - Made a rare crash in QtWebEngine when starting/retrying a download less likely to happen. - Graphical glitches in Google sheets and PDF.js, again. Removed the version restriction for the default application of `qt.workarounds.disable_accelerated_2d_canvas` as the issue was still evident on Qt 6.6.0. (#7489) - The `colors.webpage.darkmode.threshold.foreground` setting (`.text` in older versions) now works correctly with Qt 6.4+. [[v3.0.2]] v3.0.2 (2023-10-19) ------------------- Fixed ~~~~~ - Upgraded the bundled Qt version to 6.5.3. Note this is only relevant for the macOS/Windows releases, on Linux those will be upgraded via your distribution packages. This Qt patch release comes with https://code.qt.io/cgit/qt/qtreleasenotes.git/tree/qt/6.5.3/release-note.md[various important fixes], among them: * Fix for crashes on Google Meet / GMail with dark mode enabled * Fix for right-click in devtools not working properly * Fix for drag & drop not working on Wayland * Fix for some XKB key remappings not working * Security fixes up to Chromium 116.0.5845.187, including https://chromereleases.googleblog.com/2023/09/stable-channel-update-for-desktop_11.html[CVE-2023-4863], a critical heap buffer overflow in WebP, for which "Google is aware that an exploit [...] exists in the wild." [[v3.0.1]] v3.0.1 (2023-10-19) ------------------- Fixed ~~~~~ - The "restore video" functionality of the `view_in_mpv` script works again on webengine. - Setting `url.auto_search` to `dns` works correctly now with Qt 6. - Counts passed via keypresses now have a digit limit (4300) to avoid exceptions due to cats sleeping on numpads. (#7834) - Navigating via hints to a remote URL from a file:// one works again. (#7847) - The timers related to the tab audible indicator and the auto follow timeout no longer accumulate connections over time. (#7888) - The workaround for crashes when using drag & drop on Wayland with Qt 6.5.2 now also works correctly when using `wayland-egl` rather than `wayland` as Qt platform. - Worked around a weird `TypeError` with `QProxyStyle` / `TabBarStyle` on certain platforms with Python 3.12. - Removed 1px border for the downloads view, mostly noticeable when it's transparent. - Due to a Qt bug, cloning/undoing a tab which was not fully loaded caused qutebrowser to crash. This is now fixed via a workaround. - Graphical glitches in Google sheets and PDF.js via a new setting `qt.workarounds.disable_accelerated_2d_canvas` to disable the accelerated 2D canvas feature which defaults to enabled on affected Qt versions. (#7489) - The download dialog should no longer freeze when browsing to directories with many files. (#7925) - The app.slack.com User-Agent quirk now targets chromium 112 on Qt versions lower than 6.6.0 (previously it always targets chromium 99) (#7951) - Workaround a Qt issue causing jpeg files to not show up in the upload file picker when it was filtering for image filetypes (#7866) [[v3.0.0]] v3.0.0 (2023-08-18) ------------------- Major changes ~~~~~~~~~~~~~ - qutebrowser now supports Qt 6 and uses it by default. Qt 5.15 is used as a fallback if Qt 6 is unavailable. This behavior can be customized in three ways (in order of precedence): * Via `--qt-wrapper PyQt5` or `--qt-wrapper PyQt6` command-line arguments. * Via the `QUTE_QT_WRAPPER` environment variable, set to `PyQt6` or `PyQt5`. * For packagers wanting to provide packages specific to a Qt version, patch `qutebrowser/qt/machinery.py` and set `_WRAPPER_OVERRIDE`. - Various commands were renamed to better group related commands: * `set-cmd-text` -> `cmd-set-text` * `repeat` -> `cmd-repeat` * `repeat-command` -> `cmd-repeat-last` * `later` -> `cmd-later` * `edit-command` -> `cmd-edit` * `run-with-count` -> `cmd-run-with-count` The old names continue to work for the time being, but are deprecated and show a warning. - Releases are now automated on CI, and GPG signed by `qutebrowser bot `, fingerprint `27F3 BB4F C217 EECB 8585 78AE EF7E E4D0 3969 0B7B`. The key is available as follows: * On https://qutebrowser.org/pubkey.gpg * Via keys.openpgp.org * Via WKD for bot@qutebrowser.org - Support for old Qt versions (< 5.15), old Python versions (< 3.8) and old macOS (< 11)/Windows (< 10) versions were dropped. See the "Removed" section below for details. Added ~~~~~ - On invalid commands/settings with a similarly spelled match, qutebrowser now suggests the correct name in its error messages. - New `:prompt-fileselect-external` command which can be used to spawn an external file selector (`fileselect.folder.command`) from download filename prompts (bound to `` by default). - New `qute://start` built-in start page (not set as the default start page yet). - New `content.javascript.log_message.levels` setting, allowing to surface JS log messages as qutebrowser messages (rather than only logging them). By default, errors in internal `qute:` pages and userscripts are shown to the user. - New `content.javascript.log_message.excludes` setting, which allows to exclude certain messages from the `content.javascript.log_message.levels` setting described above. - New `tabs.title.elide` setting to configure where text should be elided (replaced by `…`) in tab titles when space runs out. - New `--quiet` switch for `:back` and `:forward`, to suppress the error message about already being at beginning/end of history. - New `qute-1pass` userscript using the 1password commandline to fill passwords. - On macOS when running with Qt < 6.3, `pyobjc-core` and `pyobjc-framework-Cocoa` are now required dependencies. They are *not* required on other systems or when running with Qt 6.3+, but still listed in the `requirements.txt` because it's impossible to tell the two cases apart there. - New features in userscripts: * `qutedmenu` gained new `window` and `private` options. * `qute-keepassxc` now supports unlock-on-demand, multiple account selection via rofi, and inserting TOTP-codes (experimental). * `qute-pass` will now try looking up candidate pass entries based on the calling tab's verbatim netloc (hostname including port and username) if it can't find a match with an earlier candidate (FQDN, IPv4 etc). - New `qt.chromium.experimental_web_platform_features` setting, which is enabled on Qt 5 by default, to maximize compatibility with websites despite an aging Chromium backend. - New `colors.webpage.darkmode.increase_text_contrast` setting for Qt 6.3+ - New `fonts.tooltip`, `colors.tooltip.bg` and `colors.tooltip.fg` settings. - New `log-qt-events` debug flag for `-D` - New `--all` flags for `:bookmark-del` and `:quickmark-del` to delete all quickmarks/bookmarks. Removed ~~~~~~~ - Python 3.8.0 or newer is now required. - Support for Python 3.6 and 3.7 is dropped, as they both reached their https://endoflife.date/python[end of life] in December 2021 and June 2023, respectively. - Support for Qt/PyQt before 5.15.0 and QtWebEngine before 5.15.2 are now dropped, as older Qt versions are https://endoflife.date/qt[end-of-life upstream] since mid/late 2020 (5.13/5.14) and late 2021 (5.12 LTS). - The `--enable-webengine-inspector` flag is now dropped. It used to be ignored but still accepted, to allow doing a `:restart` from versions older than v2.0.0. Thus, switching from v1.x.x directly to v3.0.0 via `:restart` will not be possible. - Support for macOS 10.14 and 10.15 is now dropped, raising the minimum required macOS version to macOS 11 Big Sur. * Qt 6.4 was the latest version to support macOS 10.14 and 10.15. * It should be possible to build a custom .dmg with Qt 6.4, but this is unsupported and not recommended. - Support for Windows 8 and for Windows 10 before 1607 is now dropped. * Support for older Windows 10 versions might still be present in Qt 6.0/6.1/6.2 * Support for Windows 8.1 is still present in Qt 5.15 * It should be possible to build a custom .exe with those versions, but this is unsupported and not recommended. - Support for 32-bit Windows is now dropped. Changed ~~~~~~~ - The qutebrowser icons got moved from `icons/` to `qutebrowser/icons` in the repository, so that it's possible for qutebrowser to load them using Python's resource system (rather than compiling them into a Qt resource file). Packagers are advised to use `misc/Makefile` if possible, which has been updated with the new paths. - The `content.javascript.can_access_clipboard` setting got renamed to `content.javascript.clipboard` and now understands three different values rather than being a boolean: `none` (formerly `false`), `access` (formerly `true`) and `access-paste` (additionally allows pasting content, needed for websites like Photopea or GitHub Codespaces). - The default `hints.selectors` now also match the `treeitem` ARIA roles. - The `:click-element` command now can also click elements based on its ID (`id`), a CSS selector (`css`), a position (`position`), or click the currently focused element (`focused`). - The `:click-element` command now can select the first found element via `--select-first`. - New `search.wrap_messages` setting, making it possible to disable search wrapping messages. - The `:session-save` command now has a new `--no-history` flag, to exclude tab history. - New widgets for `statusbar.widgets`: * `clock`, showing the current time * `search_match`, showing the current match and total count when finding text on a page - Messages shown by qutebrowser now don't automatically get interpreted as rich text anymore. Thus, e.g. `:message-info

test` now shows the given text. To show rich text with `:message-*` commands, use their new `--rich` flag. Note this is NOT a security issue, as only a small subset of HTML is interpreted as rich text by Qt, independently from the website. - Improved output when loading Greasemonkey scripts. - The macOS `.app` now is registered as a handler for `.mhtml` files, such as the ones produced by `:download --mhtml`. - The "... called unimplemented GM_..." messages are now logged as info JS messages instead of errors. - For QtNetwork downloads (e.g. `:adblock-update`), various changes were done for how redirects work: - Insecure redirects (HTTPS -> HTTP) now fail the download. - 20 redirects are now allowed before the download fails rather than only 10. - A redirect to the same URL will now fail the download with too many redirects instead of being ignored. - When a download fails in a way it'd leave an empty file around, the empty file is now deleted. - With Qt 6, setting `content.headers.referer` to `always` will act as if it was set to `same-domain`. The documentation is now updated to point that out. - With QtWebEngine 5.15.5+, the load finished workaround was dropped, which should make certain operations happen when the page has started loading rather when it fully finished. - `mkvenv.py` has a new `--pyqt-snapshot` flag, allowing to install certain packages from the https://www.riverbankcomputing.com/pypi/[Riverbank development snapshots server]. - When `QUTE_QTWEBENGINE_VERSION_OVERRIDE` is set, it now always wins, no matter how the version would otherwise have been determined. Note setting this value can break things (if set to a wrong value), and usually isn't needed. - When qutebrowser is run with an older QtWebEngine version as on the previous launch, it now prints an error before starting (which causes the underlying Chromium to remove all browsing data such as cookies). - The keys "" and "" are now named "" and "", respectively. - The `tox.ini` now requires at least tox 3.20 (was tox 3.15 previously). - `:config-diff` now has an `--include-hidden` flag, which also shows internally-set settings. - Improved error messages when `:spawn` can't find an executable. - When a process fails, the error message now suggests using `:process PID` with the correct PID (rather than always showing the latest process, which might not be the failing one) - When a process got killed with `SIGTERM`, no error message is now displayed anymore (unless started with `:spawn --verbose`). - When a process got killed by a signal, the signal name is now displayed in the message. - The `js-string-replaceall` quirk is now removed from the default `content.site_specific_quirks.skip`, so that `String.replaceAll` is now polyfilled on QtWebEngine < 5.15.3, hopefully improving website compaitibility. - Hints are now displayed for elements setting an `aria-haspopup` attribute. - qutebrowser now uses SPDX license identifiers in its files. Full support for the https://reuse.software/[REUSE specification] (license provided in a machine-readable way for every single file) is not done yet, but planned for a future release. Fixed ~~~~~ - When the devtools are clicked but `input.insert_mode.auto_enter` is set to `false`, insert mode now isn't entered anymore. - The search wrapping messages are now correctly displayed in (hopefully) all cases with QtWebEngine. - When a message with the same text as a currently already displayed one gets shown, qutebrowser used to only show one message. This is now only done when the two messages are completely equivalent (text, level, etc.) instead of doing so when only the text matches. - The `progress` and `backforward` statusbar widgets now stay removed if you choose to remove them. Previously they would appear again on navigation. - Rare crash when running userscripts with crashed renderer processes. - Multiple rare crashes when quitting qutebrowser. - The `asciidoc2html.py` script now correctly uses the virtualenv-installed asciidoc rather than requiring a system-wide installation. - "Package would be ignored" deprecation warnings when running `setup.py`. - ResourceWarning when using `:restart`. - Crash when shutting down before fully initialized. - Crash with some notification servers when the server is quitting. - Crash when using QtWebKit with PAC and the file has an invalid encoding. - Crash with the "tiramisu" notification server. - Crash when the "herbe" notification presenter doesn't start correctly. - Crash when no notification server is installed/available. - Warning with recent versions of the "deadd" (aka "linux notification center") notification server. - Crash when using `:print --pdf` with a directory where its parent directory did not exist. - The `PyQt{5,6}.sip` version is now shown correctly in the `:version`/`--version` output. Previously that showed the version from the standalone `sip` module which was only set for PyQt5. (#7805) - When a `config.py` calls `.redirect()` via a request interceptor (which is unsupported) and supplies an invalid redirect target URL, an exception is now raised for the `.redirect()` call instead of later inside qutebrowser. - Crash when loading invalid history items from a session file. [[v2.5.4]] v2.5.4 (2023-03-13) ------------------- Fixed ~~~~~ - Support SQLite with DQS (double quoted string) compile time option turned off. [[v2.5.3]] v2.5.3 (2023-02-17) ------------------- Added ~~~~~ - New `array_at` quirk, polyfilling the https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at[`Array.at` method], which is needed by various websites, but only natively available with Qt 6.2. Fixed ~~~~~ - Crash when the adblock filter file can't be read. - Inconsistent behavior when using `:config-{dict,list}-*` commands with an invalid value. Before the fix, using the same command again would complain that the value was already present, despite the error and the value not being actually changed. - Incomplete error handling when mutating a dict/list in `config.py` and setting an invalid value. Before the fix, this would result in either a message in the terminal rather than GUI (startup), or in a crash (`:config-source`). - Wrong type handling when using `:config-{dict,list}-*` commands with a config option with non-string values. The only affected option is `bindings.commands`, which is probably rarely used with those commands. - The `readability` userscript now correctly passes the source URL to Breadability, to make relative links work. - Update `dictcli.py` to use the `main` branch, fixing a 404 error. - Crash with some notification servers when the server did quit. - Minor documentation fixes [[v2.5.2]] v2.5.2 (2022-06-22) ------------------- Fixed ~~~~~ - Packaging-related fixes: * The `install` and `stacktrace` help pages are now included in the docs shipped with qutebrowser when using the recommended packaging workflow. * The Windows installer now more consistently uses the configured Windows colors. * The Windows installer now bases the desktop/start menu icon choices on the existing install, if upgrading. * The macOS release hopefully doesn't cause macOS to (falsely) claim that it "is damaged and can't be opened" anymore. - The notification fixes in v2.5.1 caused new notification crashes (probably more common than the ones being fixed...). Those are now fixed, along with a (rather involved) test case to prevent similar issues in the future. - When a text was not found on a page, the associated message would be shown as rich text (e.g. after `/

`). With this release, this is fixed for search messages, while the 3.0.0 release will change the default for all messages to be plain-text. Note this is NOT a security issue, as only a small subset of HTML is interpreted as rich text by Qt, independently from the website. - When a Greasemonkey script couldn't be loaded (e.g. due to an unreadable file), qutebrowser would crash. It now shows an error instead. - Ever since the v1.2.0 release in 2018, the `content.default_encoding` setting was not applied on start properly (only when it was changed afterwards). This is now fixed. [[v2.5.1]] v2.5.1 (2022-05-26) ------------------- Fixed ~~~~~ - The `qute-pass` userscript is marked as executable again. - PDF.js now works properly again with the macOS and Windows releases. - The MathML workaround for darkmode (e.g. black on black Wikipedia formula) now also works for display (rather than inline) math. - The `content.proxy` setting can now correctly be set to arbitrary values via the `qute://settings` page again. - Fixed issues with Chromium version detection on Archlinux with qt5-webengine 5.15.9-3. - Fixed a rare possible crash with invalid `Content-Disposition` headers. - Fixes for various notification-related crashes: * With the `tiramisu` notification server (due to invalid behavior of the server, now a non-fatal error) * With the `budgie` notification server when closing a notification (due to invalid behavior of the server, now worked around) * When a server exits with an unsuccessful exit status (now a non-fatal error) * When a server couldn't be started successfully (now a non-fatal error) * With the `herbe` notification presenter, when the website tries to close the notification after the user accepting (right-clicking) it. - Fixes in userscripts: * The `qute-bitwarden` userscript now correctly searches for entries for sites on a subdomain of an unrecognized TLD. subdomain names. Previously `my.site.local` would have searched in bitwarden for `my.sitelocal`, losing the rightmost dot. [[v2.5.0]] v2.5.0 (2022-04-01) ------------------- Deprecated ~~~~~~~~~~ - v2.5.x will be the last release of qutebrowser 2. **For the upcoming 3.0.0 release**, it's planned to drop support for various legacy platforms and libraries which are unsupported upstream, such as: * Qt before 5.15 LTS (plus adding support for Qt 6.2+) * Python 3.6 * The QtWebKit backend * macOS 10.14 (via Homebrew) * 32-bit Windows (via Qt) * Windows 8 (via Qt) * Windows 10 before 1809 (via Qt) * Possibly other more minor dependency changes - The `:rl-unix-word-rubout` command (`` in command/prompt modes) has been deprecated. Use `:rl-rubout " "` instead. - The `:rl-unix-filename-rubout` command has been deprecated. Use either `:rl-rubout "/ "` (classic readline behavior) or `:rl-filename-rubout` (using OS path separator and ignoring spaces) instead. Changed ~~~~~~~ - Improved message if a spawned process wasn't found and a Flatpak container is in use. - The `:tab-move` command now takes `start` and `end` as `index` to move a tab to the first/last position. - Tests now automatically pick the backend (QtWebKit/QtWebEngine) based on what's available. The `QUTE_BDD_WEBENGINE` environment variable and `--qute-bdd-webengine` argument got replaced by `QUTE_TESTS_BACKEND` and `--qute-backend` respectively, which can be set to either `webengine` or `webkit`. - Using `:tab-give` or `:tab-take` on the last tab in a window now always closes that window, no matter what `tabs.last_close` is set to. - Redesigned `qute://settings` (`:set`) page with buttons for options with fixed values. - The default `hint.selectors` now match more ARIA roles (`tab`, `checkbox`, `menuitem`, `menuitemcheckbox` and `menuitemradio`). - Using e.g. `:bind --mode=passthrough` now scrolls to the passthrough section on the `qute://bindings` page. - Clicking on a notification now tries to focus the tab where the notification is coming from. Note this might not work properly if there is more than one tab from the same host open. - Improvements to userscripts: * `qute-bitwarden` understands a new `--password-prompt-invocation`, which can be used to specify a tool other than `rofi` to ask for a password. * `cast` now uses `yt-dlp` if available (falling back to `youtube-dl` if not). It also lets users override the tool to use via a `QUTE_CAST_YTDL_PROGRAM` environment variable. * `qute-pass` now understands a new `--prefix` argument if used in gopass mode, which gets passed as subfolder prefix to `gopass`. * `open_download` now supports Flatpak by using its XDG Desktop Portal. * `open_download` now waits for the exit status of `xdg-open`, causing qutebrowser to report any issues with it. - The `content.headers.custom` setting now accepts empty strings as values, resulting in an empty header being sent. - Renamed settings: * `qt.low_end_device_mode` -> `qt.chromium.low_end_device_mode` * `qt.process_model` -> `qt.chromium.process_model` - System-wide userscripts are now discovered from the correct location when running via Flatpak (`/app/share` rather than `/usr/share`). - Filename prompts now don't display a `..` entry in the list of files anymore. To get back to the parent directory, either type `../` manually, or use the new `:rl-filename-rubout` command, bound to `` by default. Added ~~~~~ - New `input.match_counts` option which allows to turn off count matching for more emacs-like bindings. - New `{relative_index}` field for `tabs.title.format` (and `.pinned_format`) which shows relative tab numbers. - New `input.mode_override` option which allows overriding the current mode based on the new URL when navigating or switching tabs. - New `qt.chromium.sandboxing` setting which allows to disable Chromium's sandboxing (mainly intended for development and testing). - New `QUTE_TAB_INDEX` variable for userscripts, containing the index of the current tab. - New `editor.remove_file` setting which can be set to `False` to keep all temporary editor files after closing the external editor. - New `:rl-rubout` command replacing `:rl-unix-word-rubout` (and optionally `:rl-unix-filename-rubout`), taking a delimiter as argument. - New `:rl-filename-rubout` command, using the OS path separator and ignoring spaces. The command also gets shown in the suggested commands for a download filename prompt now. Fixed ~~~~~ - When `search.incremental` is disabled, searching using `/text` followed by a backwards search via `?text` (or vice-versa) now correctly changes the search direction. - Elements getting a hint due to a `tabindex` now are skipped if it's set to `-1`, reducing some false-positives. - The audible indicator (`[A]`) now uses a 2s cooldown when the audio goes silent, equivalent with the behavior of older QtWebEngine versions. - With `confirm_quit` set to `downloads`, the confirmation dialog is now only shown when closing the last window (rather than closing any window, which would continue running that window's downloads). Unfortunately, more issues with `confirm_quit` and multiple windows remain. - Crash when a previous crash-log file contains non-ASCII characters (which should never happen unless it was edited manually) - Due to changes in Debian, an old workaround (for broken QtWebEngine patching on Debian) caused the inferior qutebrowser error page to be displayed, when Chromium's would have worked fine. The workaround was now dropped. - Crash when using `` (`:completion-item-del`) in the `:tab-focus` list, rather than `:tab-select`. - Work around a Qt issue causing `:spawn` to run executables from the current directory if no system-wide executable was found. The underlying Qt bug is tracked as https://lists.qt-project.org/pipermail/announce/2022-February/000333.html[CVE-2022-25255], though the impact with typical qutebrowser usage is low: Normally, qutebrowser is run from a fixed location (usually the users home directory), and `:spawn` is not typically used with executables that don't exist. The main security impact of this bug is in tools like text editors, which are often executed in untrusted directories and might attempt to run auxiliary tools automatically. - When `:rl-rubout` or `:rl-filename-rubout` (formerly `:rl-unix-word-rubout` and `:rl-unix-filename-rubout`) were used on a string not starting with the given delimiter, they failed to delete the first character, which is now fixed. - Fixes in userscripts: * `ripbang` now works again (it got blocked due to a missing user agent and used outdated qutebrowser commands before) * `keepassxc` now has a properly working `--insecure` flag - Speculative fix for an immediate crash at start with the macOS/Windows binaries (in certain rare environments). - Speculative fix for a qutebrowser crash when the notification daemon crashes while showing the notification. - Fix crash when using `:screenshot` with an invalid `--rect` argument. - Added a site-specific quirk to make cookie dialogs on StackExchange pages (such as Stack Overflow) work on Qt 5.12. [[v2.4.0]] v2.4.0 (2021-10-21) ------------------- Security ~~~~~~~~ - **CVE-2021-41146**: Fix arbitrary command execution on Windows via URL handler argument injection. See the https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-vw27-fwjf-5qxm[security advisory] for details. Added ~~~~~ - New `content.blocking.hosts.block_subdomains` setting which can be used to disable the subdomain blocking for the hosts-based adblocker introduced in v2.3.0. - New `downloads.prevent_mixed_content` setting to prevent insecure mixed-content downloads (true by default). - New `--private` flag for `:tab-clone`, which clones a tab into a new private window, mirroring the same flags for `:open` and `:tab-give`. Fixed ~~~~~ - Switching tabs via mouse wheel scrolling now works properly on macOS. Set `tabs.mousewheel_switching` to false if you prefer the previous behavior. - Speculative fix for a crash when closing qutebrowser while a systray notification is shown. Changed ~~~~~~~ - Typing in the filename prompt now filters matching directories. - When opening a file qutebrowser can't handle from a `file:///` directory listing, qutebrowser now opens it with the default application rather than displaying a download prompt. - In Greasemonkey scripts, using "overrideMimeType" with GM_xmlhttpRequest is now supported. - `:hint --rapid` is now supported for the `tab` hinting target no matter what `tabs.background` is set to, as there are various scenarios where tabs can open in the background. - New flags for the `qute-pass` userscript: * `--unfiltered` to show all secrets, not just the one matching the current URL. * `--always-show-selection` to confirm the password to be entered even if there's only a single match. - In insert mode, `` is now bound to `fake-key ` by default, i.e., sends an Escape keypress to the website. - Using `GM_setClipboard` in Greasemonkey scripts is now supported. [[v2.3.1]] v2.3.1 (2021-07-28) ------------------- Fixed ~~~~~ - Updated the workaround for Google Account log in claiming that this browser isn't secure. For an equivalent workaround on older versions, run: `:set -u https://accounts.google.com/* content.headers.user_agent "Mozilla/5.0 ({os_info}; rv:90.0) Gecko/20100101 Firefox/90.0"` - Corrupt cache file exceptions with `adblock` 0.5.0+ are now handled properly. - Crash when entering unicode surrogates into the filename prompt. - `UnboundLocalError` in `qute-keepass` when the database couldn't be opened. [[v2.3.0]] v2.3.0 (2021-06-28) ------------------- Added ~~~~~ - New `content.prefers_reduced_motion` setting to request websites to reduce non-essential motion/animations. - New `colors.prompts.selected.fg` setting to customize the text color for selected items in filename prompts. Changed ~~~~~~~ - The hosts-based adblocker (using `content.blocking.hosts.lists`) now also blocks all requests to any subdomains of blocked hosts. - The `fonts.web.*` settings now support URL patterns. - The `:greasemonkey-reload` command now shows a list of loaded scripts and has a new `--quiet` switch to suppress that message. - When launching a userscript via hints, a new `QUTE_CURRENT_URL` environment variable now points to the current page (rather than the URL of the selected element, where `QUTE_URL` points to). Fixed ~~~~~ - Crash on macOS 10.14+ when logging into Google accounts -- the previous fix was incomplete due wrong information in Apple's documentation. - Crash when two Greasemonkey scripts have the same name (usually happening because the same file is in both the data and the config directory). - Deprecation warnings when using the `link_pyqt.py` script on Python 3.10 (e.g. via `tox` or `mkvenv.py`). [[v2.2.3]] v2.2.3 (2021-06-01) ------------------- Fixed ~~~~~ - Logging into Google accounts or sharing the camera on macOS 10.14+ crashed, which is now fixed. - The Windows installer now correctly aborts the installation on Windows 7 (rather than attempting an install which won't work, since Windows 7 is unsupported since the v2.0.0 release). - Using `--json-logging` without `--debug` caused qutebrowser to crash since the v1.13.0 release. It now works correctly again. - Mixing Qt 5.14+ with QtWebEngine 5.12 caused a crash related to qutebrowser's notification support, which is now fixed. - The documentation now points to the new IRC channels on irc.libera.chat instead of the defunct Freenode channels (due to a hostile takeover by Freenode staff). - Setting `content.headers.user_agent` or `.accept_language` to a value containing non-ascii characters was permitted by qutebrowser, but resulted in a crash when loading a page. Such values are now rejected properly. - When quitting qutebrowser on the `qute://settings` page, a crash could happen, which is now fixed. - When `:edit-text` is used, but the existing text in the input isn't representable in the configured encoding (`editor.encoding`), qutebrowser would crash. It now shows a proper error instead. - The testsuite should now work properly on aarch64. - When QtWebEngine is in a "stuck" state while `:selection-follow` was used, this could cause a crash in qutebrowser. This is now fixed (speculatively, due to lack of a reproducer). - When the brave adblock data (`adblock-cache.dat`) got corrupted, qutebrowser would crash when trying to load it. It now displays an error instead. - Combining `/S` (silent) and `/allusers` when uninstalling via the Windows installer now works properly. [[v2.2.2]] v2.2.2 (2021-05-20) ------------------- Fixed ~~~~~ - When awesomewm's "naughty" notification daemon was used with a development version of AwesomeWM and an unknown version number, qutebrowser would crash when trying to parse the version string. This is now fixed. - Due to a bug with QtWebEngine 5.15.4, old Service Worker data could cause renderer process crashes. This is now worked around by qutebrowser. - When an (broken) binding to `set-cmd-text` without any argument existed, using `:` would crash, which is now fixed. - New site-specific quirk (again) working around not being able to type accented/composed characters on Google Docs. - When running with `python -OO` (which is not recommended), a notification being shown would result in a crash, which is now fixed. [[v2.2.1]] v2.2.1 (2021-04-29) ------------------- Changed ~~~~~~~ - When an error occurs in a notification presenter, qutebrowser now shows that error in the statusbar instead of just logging it. - New site-specific-quirk for Discord logging users out when using vertical tabs (yes, really) Fixed ~~~~~ - Certain errors from notification daemons are now displayed as non-fatal errors instead of qutebrowser crashing: * With the legacy GNOME Flashback notification daemon (not GNOME Shell), when more than 20 notifications are currently shown. * With the KDE Plasma notification daemon, when the same notification is shown twice (with <1s delay). - The `mkvenv.py` script now works when `ldconfig -p` is failing. - Running `:spawn -u -o` broke in v2.2.0 and now works properly again. - Fixes in userscripts: * The `qute-bitwarden` userscript now still consumes returned data if the Bitwarden CLI showed a warning but exited with a 0 (successful) exit code. * The `qute-pass` userscript now doesn't try to match a username with `--password-only`, and error messages with invalid patterns are improved. * The `qute-pass` userscript now avoids running `pass` twice when `--otp-only` is used. [[v2.2.0]] v2.2.0 (2021-04-13) ------------------- Deprecated ~~~~~~~~~~ - Running qutebrowser with Qt 5.12.0 is now unsupported and logs a warning. It should still work - however, a workaround for issues with the Nvidia graphic driver was dropped. Newer Qt 5.12.x versions are still fully supported. - The `--force` argument for `:tab-only` is deprecated, use `--pinned close` instead. - Using `:tab-focus` without an argument or count is now deprecated, use `:tab-next` instead. Added ~~~~~ - New dependency on the `QtDBus` module. If this requirement is an issue for you or your distribution, please open an issue! Note that a DBus connection at runtime is still optional. - New `input.media_keys` setting which can be used to disable Chromium's handling of media keys. - New `:process` command (and associated `qute://process` pages) which can be used to view and terminate/kill external processes spawned by qutebrowser. - New `content.site_specific_quirks.skip` setting which can be used to disable individual site-specific quirks. - New `--pinned` argument for `:tab-only`, which replaces `--force` (with `--pinned close`), but also can take `--pinned keep` to keep pinned tabs without prompting. - New `fileselect.folder.command` which can be used with `fileselect.handler = external` to customize the command to use to upload directories (`` elements, which are non-standard but in wide use). - New `content.notifications.presenter` setting with various new ways to show web notifications: * `auto` (default): Automatically detect the best available option * `qt`: Use Qt's built-in mechanism (like before this release) * `libnotify`: Use a libnotify-compatible notification server (i.e. native notifications on Linux) * `systray`: Use a systray icon (very similar to `qt` but without some of its drawbacks) * `messages`: Use qutebrowser messages * `herbe`: Use https://github.com/dudik/herbe[herbe] - New `content.notifications.show_origin` setting, which can be used to decide for which notifications to show the origin (the URL the notification was sent from). Changed ~~~~~~~ - The `content.ssl_strict` setting got renamed to `content.tls.certificate_errors`, with new values: * `ask`: Prompt on overridable certificate errors (`ssl_strict = 'ask'`) * `ask-block-thirdparty`: See below * `block`: Block the page load (`ssl_strict = True`) * `load-insecurely`: Load the page despite the error (`ssl_strict = False`) - The new `content.tls.certificate_errors` setting now also understands the value `ask-block-thirdparty`, which asks for page loads but automatically blocks resource loads on TLS errors. This behavior is consistent with what other browsers do. - The prompt text shown on certificate errors has been improved to make it clearer what kind of error occurred exactly. - The `content.site_specific_quirks` setting got renamed to `content.site_specific_quirks.enabled`. - The `content.notifications` option got renamed to `content.notifications.enabled`. - The completion now also shows bindings starting with `set-cmd-text` in its third column, such as `o` for `:open`. - When `:spawn` is used with the `-m` / `--output-messages` flag, the output now appears live, while the process is running. - When a shown message replaces an existing related one (e.g. for zoom levels), the replacing now also works even if a different message was shown in between. - The `.redirect(...)` method on interceptors now supports an `ignore_unsupported=True` argument which suppresses exceptions if a request could not be redirected. Note, however, that it is still not public API. - When the `--config-py` argument is used, no warning about a missing `config.load_autoconfig` is shown anymore, as the argument is typically used for temporarily testing a config. - The internal `_autosave` session used for crash recovery is now only saved once per minute, since saving it for every page load is a noticeable performance issue. - The `readability-js` userscript now displays a small header with page information. - When an external file selector is used, some additional validation is done on the picked files now, so that errors are shown if e.g. a directory is selected when a file was expected. - The default binding for `T` (`:tab-focus`) got changed so that it fills the command line with `:tab-focus` if used without a count (instead of being equivalent to `:tab-next` in that case). - The `:config-unset` command now understands the `--pattern` (`-u`) flag to unset options customized for a given URL pattern (such as after answering a prompt with "always"/"never"). - The `:config-unset` command now shows an error when used on an option which is valid, but was never customized. - The `statusbar.widgets` setting now understands `text:...` entries which allows adding a hard-coded text to the statusbar. - The polyfill for `String.replaceAll` (required for Nextcloud Calendar < 2.2.0 with QtWebEngine < 5.15.3) is now disabled by default, as it's not fully compliant to the ECMAScript spec and might cause issues on other websites. If you still need it (e.g. if you're still on an old Nextcloud Calendar version), remove `js-string-replaceall` from `content.site_specific_quirks.skip`. Fixed ~~~~~ - When an editor exits with a != 0 exit status, the temporary editor file is now persisted. This already was the case when the editor crashed. - When a nonexistent file gets passed to `--config-py`, qutebrowser now complains instead of silently not loading it. - With some (rare) setups, opening the report dialog or using a PAC proxy with QtWebKit could result in qutebrowser hanging due to a PyQt bug. There's now a workaround which prevents the hang. - QtWebEngine version detection (influencing things like dark mode settings or certain workarounds) now works correctly on OpenBSD. - Certain version number formats in `/etc/os-release` caused qutebrowser to crash. Those are now handled correctly. - The macOS releases now properly support Dark Mode for UI elements by setting `NSRequiresAquaSystemAppearance` to false. Removed ~~~~~~~ - The `qute://spawn-output` page used by `:spawn -o` is now removed, as it's replaced by the new `qute://process` pages. [[v2.1.1]] v2.1.1 (2021-04-01) ------------------- Added ~~~~~ - Site-specific quirk for krunker.io, which shows a "Socket Error" with qutebrowser's default Accept-Language header. The workaround is equivalent to doing `:set -u matchmaker.krunker.io content.headers.accept_language ""`. Changed ~~~~~~~ - Clicking the 'x' in the devtools window to hide it now also leaves insert mode. Fixed ~~~~~ - The workaround for black on (almost) black formula images in dark mode now also works with Qt 5.12 and 5.13. - When running in Flatpak or with the Windows/macOS releases, the QtWebEngine version is now detected properly. Before, a wrong version was assumed, breaking dark mode and certain workarounds (resulting in crashes on websites like LinkedIn or TradingView). - When the metainfo in the completion database doesn't have the expected structure, qutebrowser now tries to gracefully recover from the situation instead of crashing. - When qutebrowser displays an error during initialization, opening a second instance would lead to a crash. Instead, qutebrowser now ignores the attempt to open a new page as long as it's not fully initialized yet. - When the Brave adblock cache folder was unreadable, qutebrowser crashed. It now displays an error instead. - Fixes in the `qute-pass` userscript for `gopass`: * Generating OTP tokens now works correctly. * Storing the username as part of the secret broke in v2.0.0 and now works again. - When using `bindings.key_mappings` to map a key to multiple other keys, qutebrowser would crash. This is now handled correctly - however, note that it's usually better to map keys to commands instead. - When a minimized window is selected via `:tab-select`, it's now un-minimized properly. - When a format string in the config (e.g. `tabs.title_format`) used a value like `{current_url.host}` (instead of `{current_url:host}`), qutebrowser would crash. It now correctly reports an invalid config value instead. - In rare circumstances, sending URLs/commands to existing instances would result in a crash, which is now fixed. - Running the testsuite should now fully work without internet access again. - The `--asciidoc` script for `mkvenv.py` broke with v1.14.0. It now works correctly again. - Various other fixes for running in Flatpak (backported in the Flatpak release even before this qutebrowser release). - We are the Knights Who Say... ':Ni!' [[v2.1.0]] v2.1.0 (2021-03-12) ------------------- Removed ~~~~~~~ - The following command aliases were deprecated in v2.0.0 and are now removed: * `run-macro` -> `macro-run` * `record-macro` -> `macro-record` * `buffer` -> `tab-select` * `open-editor` -> `edit-text` * `toggle-selection` -> `selection-toggle` * `drop-selection` -> `selection-drop` * `reverse-selection` -> `selection-reverse` * `follow-selected` -> `selection-follow` * `follow-hint` -> `hint-follow` * `enter-mode` -> `mode-enter` * `leave-mode` -> `mode-leave` Added ~~~~~ - New `:screenshot` command which can be used to screenshot the visible part of the page. - New optional dependency on the `importlib_metadata` project on Python 3.7 and below. This is only relevant when PyQtWebEngine is installed via pip - thus, this dependency usually isn't relevant for packagers. - New `qute-keepassxc` userscript integrating with the KeePassXC browser API. Changed ~~~~~~~ - Initial support for QtWebEngine 5.15.3 and PyQt 5.15.3/.4 - The `colors.webpage.prefers_color_scheme_dark` setting got renamed to `colors.webpage.preferred_color_scheme` and now takes the values `auto`, `light` and `dark` (instead of being `True` for dark and `False` for auto). Note that the `light` value is only supported with Qt 5.15.2+, falling back to the same behavior as `auto` on older versions. - On Linux, qutebrowser now tries harder to find details about the installed QtWebEngine version by inspecting the QtWebEngine binary. This should reduce issues with dark mode (and some workarounds) not working when using differing versions of QtWebEngine/PyQtWebEngine/Qt. This change also prepares qutebrowser for QtWebEngine 5.15.3, which will get released without an updated Qt. - When PyQtWebEngine >= 5.15.3 is installed via `pip` (as is e.g. the case with `mkvenv.py`), qutebrowser now queries the associated metadata to find out the QtWebEngine version. - When doing `:hint links yank --rapid`, the messages shown now replace each other, thus being less noisy. - Newlines in JavaScript messages (`confirm`, `prompt` and `alert`) are now preserved. - Messages in prompts are now word-wrapped rather than displaying them in one long line. - If a command stats with space (e.g. `: open ...`, it's now not saved to command history anymore (similar to how some shells work). - When a tab is pinned, running `:open` will now open a new tab instead of displaying an error. - The `fileselect.*.command` settings now support file selectors writing the selected paths to stdout, which is used if no `{}` placeholder is contained in the configured command. - The `--debug-flag` argument now understands a new `log-sensitive-keys` value which logs all keypresses (including those in insert/passthrough/prompt/... mode) for debugging. - The `readability` and `readability-js` userscripts now add a `qute-readability` CSS class to the page, so that it can be styled easily via a user stylesheet. Fixed ~~~~~ - With QtWebEngine 5.15.3 and some locales, Chromium can't start its subprocesses. As a result, qutebrowser only shows a blank page and logs "Network service crashed, restarting service.". This release adds a `qt.workarounds.locale` setting working around the issue. It is disabled by default since distributions shipping 5.15.3 will probably have a proper patch for it backported very soon. - The `colors.webpage.preferred_color_scheme` and `colors.webpage.darkmode.*` settings now work correctly with QtWebEngine 5.15.3 (and Gentoo, which at the time of writing packages 5.15.3 disguised as 5.15.2). - When dark mode settings were set, existing `blink-features` arguments in `qt.args` (or `--qt-flag`) were overridden. They are now combined properly. - On QtWebEngine 5.15.2, auto detection for the `prefers-color-scheme` media query is broken and always returns `no-preference`, which was removed from the CSS WG Specification. This release contains a workaround to always return `light` instead (as per the spec). - When an external file selector deletes the temporary file (like `nnn` does when quitting the terminal), qutebrowser would crash. It now displays an error instead. The same applies if the temporary file is unreadable for any other reason. - On macOS, a change in v2.0.x caused certain shortcuts to not work with Cmd anymore, using Ctrl instead. They now work correctly using Cmd (like usual on macOS) again. - On macOS, using `F` (`hint all tab`) sometimes would open a context menu instead of following a link. This is now fixed. - The quirk added for a missing `String.replaceAll` did not handle special regexp characters correctly, thus breaking some sites. It now handles them properly. - The "try again" button on error pages now works correctly with JavaScript disabled. - If a GreaseMonkey script doesn't have a "@run-at" comment, qutebrowser accidentally treated that as "@run-at document-idle". However, other GreaseMonkey implementations default to "@run-at document-end" instead, which is what qutebrowser now does, too. - The `hist_importer.py` script didn't work correctly after qutebrowser v2.0.0 and resulted in a history database qutebrowser couldn't read properly. It now works properly again. - With certain QtWebEngine versions (5.15.0 based on Chromium 80 and 5.15.3 based on Chromium 87), Chromium's dark mode doesn't invert certain SVG images, even with `colors.wegpage.darkmode.policy.images` set to `smart`. Most notably, this causes formulae on Wikipedia to display black on (almost) black. If `content.site_specific_quirks` is enabled, qutebrowser now injects some CSS as a workaround, which inverts all math formula images on Wikipedia (and potentially other sites, if they use the same CSS class). - When a hint label text started with an apostrophe, it would show an escaped text until the hints first character has been pressed. It now shows up correctly. [[v2.0.2]] v2.0.2 (2021-02-04) ------------------- Fixed ~~~~~ - When right-clicking an empty part of the downloads bar, qutebrowser v2.0.x would crash. This is now fixed. - Setting `content.cookies.store` to `false` only worked properly when this was done after qutebrowser was already started due to a regression in v2.0.0. It now works as expected again. - If qutebrowser was installed as a Python egg with Python 3.8 or 3.9, requesting unavailable resource files (such as PDF.js not being bundled, or a missing changelog file) caused in a crash due to an inconsistent behavior in those versions of Python. This is now handled properly by qutebrowser. - In v2.0.0, support for importing the `sip` dependency as `sip` rather than `PyQt5.sip` was dropped, since upstream claims it should be used as `PyQt5.sip` ever since PyQt 5.11. However, some distributions still package sip as a global `sip` package. Thus, support for a global `sip` package is now reintroduced. - The changelog for v2.0.0 claimed that `hints.leave_on_load` was set to `true` by default. However, the `input.insert_mode.leave_on_load` setting was instead set to `true` accidentally. This is now fixed by actually setting `hints.leave_on_load` to `true`, and reversing the change to `input.insert_mode.leave_on_load` so it is set to `false` by default again. - When the `importlib_resources` package is required but was missing, users would get a Python stacktrace rather than a proper error message. This is now fixed. - Site-specific quirk JavaScript files were loaded lazily rather than preloaded at the start of qutebrowser, causing a crash when e.g. switching between versions while qutebrowser is open. Now they are preloaded at the start of qutebrowser again. - The link to the keybinding cheatsheet on the internal `:help` page wasn't displayed correctly. This is now fixed. - When the completion rebuilding process was interrupted, qutebrowser did not detect this condition on the next start, thus resulting in a completion with inconsistent data. This is now fixed, with another rebuild being forced with this update, to ensure the data is consistent for all users. - In certain scenarios, qutebrowser v2.0.x warned about `config.load_autoconfig(...)` being missing when loading a secondary config (e.g. via `config.source(...)`). It now only shows those warnings for the main `config.py` file. - The `--enable-webengine-inspector` flag is now accepted again, however it's unused and undocumented. It purely exists to make it possible to use `:restart` between pre-v2.0.x and v2.0.2+ versions. - When `hints.dictionary` pointed to a file not encoded as UTF-8, this resulted in a crash (also in versions before v2.0.0). It now properly displays an error instead. - When running qutebrowser with a single empty commandline argument, such as done by `open_url_in_instance.sh`, this would result in a partially initialized window. Interacting with that window results in a crash (also in versions before v2.0.0). Instead, the startpage is now shown properly. [[v2.0.1]] v2.0.1 (2021-01-28) ------------------- Fixed ~~~~~ - If qutebrowser was installed as a Python egg (similar to a .zip file, via `setup.py install` under certain conditions), a change in v2.0.0 caused it to not start properly. This is now fixed. - If qutebrowser was set up (or packaged) in an unclean environment, this could result in a stale `qutebrowser/components/adblock.py` file being picked up. That file is not part of the release anymore, but if an old version is still around, causes qutebrowser to crash. It's now explicitly blocked inside qutebrowser so it gets ignored even if it still exists. - When the adblocking method was switched using `:set`, and the `adblock` dependency was unavailable when qutebrowser started (but was installed while qutebrowser was open), this resulted in a crash. Now a warning prompting for a restart of qutebrowser is shown instead. Changed ~~~~~~~ - The `format_json` userscript now uses sh instead of bash again. - The `add-nextcloud-bookmarks`, `add-nextcloud-cookbook`, `readability` and `ripbang` userscripts now use a `python3` rather than plain `python` shebang. - When `QTWEBENGINE_CHROMIUM_FLAGS` is set in the environment, this causes flag handling (including workarounds for QtWebEngine crashes) inside qutebrowser to break. This will be handled properly in a future version, but this release now shows a warning on standard output if this is the case. - The config completion for `fileselect.*.command` now also includes the "nnn" terminal file manager. [[v2.0.0]] v2.0.0 (2021-01-28) ------------------- Major changes ~~~~~~~~~~~~~ - If the Python `adblock` library is available, it is now used to integrate Brave's Rust adblocker library for improved adblocking based on ABP-like filter lists (such as EasyList). If it is unavailable, qutebrowser falls back to host-blocking, i.e. the same blocking technique it used before this release. As part of this, various settings got renamed, see "Changed" below. **Note: If the `adblock` dependency is available, qutebrowser will ignore custom host blocking** via the `blocked-hosts` config file or `file:///` URLs supplied as host blocking lists. You will need to either migrate those to ABP-like lists, or set `content.blocking.method` to `both`. - Various dependency upgrades - a quick checklist for packagers (see "Changed" below for details): * Ensure you're providing at least Python 3.6.1. * Ensure you're providing at least Qt 5.12 and PyQt 5.12. * Add a new optional dependency on the Python `adblock` library (if packaged - if not, consider packaging it, albeit optional it's very useful for users). * Remove the `cssutils` optional dependency (if present). * Remove the `attrs` (`attr`) dependency. * Remove the `pypeg2` dependency (and perhaps consider dropping the package if not used elsewhere - it's https://fdik.org/pyPEG2/[inactive upstream] and the repository was removed by Bitbucket). * Move the `pygments` dependency from required to optional. * Move the `setuptools` dependency from runtime (for `pkg_resources`) to build-time. * For Python 3.6, 3.7 or 3.8, add a dependency on the `importlib_resources` backport. * For Python 3.6 only, add a dependency on the `dataclasses` backport. - Dropped support for old OS versions in binary releases: * Support for Windows 7 is dropped in the Windows binaries, the minimum required Windows version is now Windows 8.1. * Support for macOS 10.13 High Sierra is dropped in the macOS binaries, the minimum required macOS version is now macOS 10.14 Mojave. - Various renamed settings and commands, see "Deprecated" and "Changed" below. Removed ~~~~~~~ - The `--enable-webengine-inspector` flag (which was only needed for Qt 5.10 and below) is now dropped. With Qt 5.11 and newer, the inspector/devtools are enabled unconditionally. - Support for moving qutebrowser data from versions before v1.0.0 has been removed. - The `--old` flag for `:config-diff` has been removed. It used to show customized options for the old pre-v1.0 config files (in order to aid migration to v1.0). - The `:inspector` command which was deprecated in v1.13.0 (in favor of `:devtools`) is now removed. Deprecated ~~~~~~~~~~ - Several commands have been renamed for consistency and/or easier grouping of related commands. Their old names are still available, but deprecated and will be removed in qutebrowser v2.1.0. * `run-macro` -> `macro-run` * `record-macro` -> `macro-record` * `buffer` -> `tab-select` * `open-editor` -> `edit-text` * `toggle-selection` -> `selection-toggle` * `drop-selection` -> `selection-drop` * `reverse-selection` -> `selection-reverse` * `follow-selected` -> `selection-follow` * `follow-hint` -> `hint-follow` * `enter-mode` -> `mode-enter` * `leave-mode` -> `mode-leave` Added ~~~~~ - New settings for the ABP-based adblocker: * `content.blocking.method` to decide which blocker(s) should be used. * `content.blocking.adblock.lists` to configure ABP-like lists to use. - New `qt.environ` setting which makes it easier to set/unset environment variables for qutebrowser. - New settings to use an external file picker (such as ranger or vifm): * `fileselect.handler` (`default` or `external`) * `fileselect.multiple_files.command` * `fileselect.single_file.command` - When QtWebEngine has been updated but PyQtWebEngine hasn't yet, the dark mode settings might stop working. As a (currently undocumented) escape hatch, this version adds a `QUTE_DARKMODE_VARIANT=qt_515_2` environment variable which can be set to get the correct behavior in (transitive) situations like this. - New `--desktop-file-name` commandline argument, which can be used to customize the desktop filename passed to Qt (which is used to set the `app_id` on Wayland). - The `:open` completion now also completes local file paths and `file://` URLs, via a new `filesystem` entry in `completion.open_categories`. Also, a new `completion.favorite_paths` setting was added which can be used to add paths to show when `:open` is used without any input. - New `QUTE_VERSION` variable for userscripts, which can be used to read qutebrowser's version. - New "Copy URL" entry in the context menu for downloads. - New `:bookmark-list` command which lists all bookmarks/quickmarks. The corresponding `qute://bookmarks` URL already existed since v0.8.0, but it was never exposed as a command. - New `qt.workarounds.remove_service_workers` setting which can be used to remove the "Service Workers" directory on every start. Usage of this option is generally discouraged, except in situations where the underlying QtWebEngine bug is a known cause for crashes. - Changelogs are now shown after qutebrowser was upgraded. By default, the changelog is only shown after minor upgrades (feature releases) but not patch releases. This can be adjusted (or disabled entirely) via a new `changelog_after_upgrade` setting. - New userscripts: * `kodi` to play videos in Kodi * `qr` to generate a QR code of the current URL * `add-nextcloud-bookmarks` to create bookmarks in Nextcloud's Bookmarks app * `add-nextcloud-cookbook` to add recipes to Nextcloud's Cookbook app Changed ~~~~~~~ - `config.py` files now are required to have either `config.load_autoconfig(False)` (don't load `autoconfig.yml`) or `config.load_autoconfig()` (do load `autoconfig.yml`) in them. - Various host-blocking settings have been renamed to accommodate the new ABP-like adblocker: * `content.host_blocking.enabled` -> `content.blocking.enabled` (controlling both blockers) * `content.host_blocking.whitelist` -> `content.blocking.whitelist` (controlling both blockers) * `content.host_blocking.lists` -> `content.blocking.hosts.lists` - Changes to default settings: * `tabs.background` is now `true` by default, so that new tabs get opened in the background. * `input.partial_timeout` is now set to 0 by default, so that partially typed key strings are never cleared. * `hints.leave_on_load` is now `false` by default, so that hint mode doesn't get left when a page finishes loading. This can lead to stale hints persisting in rare circumstances, but is better than leaving hint mode when the user entered it before loading was completed. * The default for `tabs.width` (tab bar width if vertical) is now 15% of the window width rather than 20%. * The default bindings for moving tabs (`tab-move -` and `tab-move +`) were changed from `gl` and `gr` to `gK` and `gJ`, to be consistent with the tab switching bindings. * The text color for warning messages is now black instead of white, for increased contrast and thus readability. * The default timeout for messages is now raised from 2s to 3s. - On the first start, the history completion database is regenerated to remove a few problematic entries (such as long `qute://pdfjs` URLs). This might take a couple of minutes, but is a one-time operation. This should result in a performance improvement for the completion for affected users. - qutebrowser now shows an error if its history database version is newer than expected. This currently should never happen, but allows for potentially backwards-incompatible changes in future versions. - At least Python 3.6.1 is now required to run qutebrowser, support for Python 3.5 (and 3.6.0) is dropped. Note that Python 3.5 is https://www.python.org/downloads/release/python-3510/[no longer supported upstream] since September 2020. - At least Qt/PyQt 5.12 is now required to run qutebrowser, support for 5.7 to 5.11 (inclusive) is dropped. While Debian Buster ships Qt 5.11, it's based on a Chromium version from 2018 with https://www.debian.org/releases/buster/amd64/release-notes/ch-information.en.html#browser-security[no Debian security support] and unsupported upstream since May 2019. It also has compatibility issues with various websites (GitHub, Twitch, Android Developer documentation, YouTube, ...). Since no newer Debian Stable is released at the time of writing, it's recommended to https://github.com/qutebrowser/qutebrowser/blob/main/doc/install.asciidoc#installing-qutebrowser-with-virtualenv[install qutebrowser in a virtualenv] with a newer version of Qt/PyQt. - New optional dependency on the Python `adblock` library (see above for details). - The (formerly optional) `cssutils` dependency is now removed. It was only needed for improved behavior in corner cases when using `:download --mhtml` with the (non-default) QtWebKit backend, and as such it's unlikely anyone is still relying on it. The `cssutils` project is also dead upstream, with its repository being gone after Bitbucket https://bitbucket.org/blog/sunsetting-mercurial-support-in-bitbucket[removed Mercurial support]. - The (formerly required) `pygments` dependency is now optional. It is only used when using `:view-source` with QtWebKit, or when forcing it via `:view-source --pygments` on QtWebEngine. If it is unavailable, an unhighlighted fallback version of the page's source is shown. - The former runtime dependency on the `pkg_resources` module (part of the `setuptools` project) got dropped. Note that `setuptools` is still required to run `setup.py`. - A new dependency on the `importlib_resources` module got introduced for Python versions up to and including 3.8. Note that the stdlib `importlib.resources` module for Python 3.7 and 3.8 is missing the needed APIs, thus requiring the backports for those versions as well. - The former dependency on the `attrs`/`attr` package is now dropped in favour of `dataclasses` in the Python standard library. On Python 3.6, a new dependency on the `dataclasses` backport is now required. - The former dependency on the `pypeg2` package is now dropped. This might cause some changes for certain corner-cases for suggested filenames when downloading files with the QtWebKit backend. - Windows and macOS releases now ship Python 3.9 rather than 3.7. - The `colors.webpage.darkmode.*` settings are now also supported with older Qt versions (Qt 5.12 and 5.13) rather than just with Qt 5.14 and above. - For regexes in the config (`hints.{prev,next}_regexes`), certain patterns which will change meanings in future Python versions are now disallowed. This is the case for character sets starting with a literal `[` or containing literal character sequences `--`, `&&`, `~~`, or `||`. To avoid a warning, remove the duplicate characters or escape them with a backslash. - If `prompt(..., "default")` is used via JS, the default text is now pre-selected in the prompt shown by qutebrowser. - URLs such as `::1/foo` are now handled as a search term or local file rather than IPv6. Use `[::1]/foo` to force parsing as IPv6 instead. - The `mkvenv.py` script now runs a "smoke test" after setting up the virtual environment to ensure it's working as expected. If necessary, the test can be skipped via a new `--skip-smoke-test` flag. - Both qutebrowser userscripts and Greasemonkey scripts are now additionally picked up from qutebrowser's config directory (the `userscripts` and `greasemonkey` subdirectories of e.g. `~/.config/qutebrowser/`) rather than only the data directory (the same subdirectories of e.g. `~/.local/share/qutebrowser/`). - The `:later` command now understands a time specification like `5m` or `1h5m2s`, rather than just taking milliseconds. - The `importer.py` script doesn't use a browser argument anymore; instead its `--input-format` switch can be used to configure the input format. The help also was expanded to explain how to use it properly. - If `tabs.tabs_are_windows` is set, the `tabs.last_close` setting is now ignored and the window is always closed when using `:close` (`d`). - With the (default) QtWebEngine backend, if a custom `accept` header is set via `content.headers.custom`, the custom value is now ignored for XHR (`XMLHttpRequest`) requests. Instead, the sent value is now `*/*` or the header set from JavaScript, as it would be if `content.headers.custom` wasn't set. - The `:tab-select` completion now shows the underlying renderer process PID if doing so is supported (on QtWebEngine 5.15). - If `tabs.favicons.show` is set to `never`, favicons aren't unnecessarily downloaded anymore. Thus, disabling favicons can help with a possible https://www.ghacks.net/2021/01/22/favicons-may-be-used-to-track-users/[fingerprinting vector]. - "Super" is now understood as a modifier (i.e. as alias to "Meta"). - Initial support for Python 3.10 (currently in Alpha stage). - Various performance improvements, including for the startup time. Fixed ~~~~~ - With interpolated color settings (`colors.tabs.indicator.*` and `colors.downloads.*`), the alpha channel is now handled correctly. - Fixes to userscripts: * `format_json` now uses `env` in its shebang, making it work correctly on systems where `bash` isn't located in `/bin`. * `qute-pass` now handles the MIME output format introduced in gopass 1.10.0. * `qute-lastpass` now types multiple `<` or `>` characters correctly. - The `:undo` completion now sorts its entries correctly (by the numerical index rather than lexicographically). - The `completion.web_history.ignore` setting now works properly when set in `config.py` (rather than via `:set`). Additionally, a `:config-source` will not result in a history rebuild if the value wasn't actually changed. - When downloading a `data:` URL, the suggested filename is now improved and contains a proper extension. Before this fix, qutebrowser would use the URL's data contents as filename with QtWebEngine; or "binary blob" with the Qt network stack. - When `:tab-only` is run before a tab is available, an error is now shown instead of crashing. - A couple of long URLs (such as `qute://pdfjs` URLs) are now not added to the history database anymore. - A bug in QtWebEngine 5.15.2 causes "renderer process killed" errors on websites like LinkedIn and TradingView. There is now a workaround in qutebrowser to prevent this from happening. - Nextcloud Calendars started using `String.replaceAll` which was only added to Chromium recently (Chrome 85), so won't work with current QtWebEngine versions. This release includes a workaround (a polyfill as a site-specific-quirk). [[v1.14.1]] v1.14.1 (2020-12-04) -------------------- Added ~~~~~ - With v1.14.0, qutebrowser configures the main window to be transparent, so that it's possible to configure a translucent tab- or statusbar. However, that change introduced various issues, such as performance degradation on some systems or breaking dmenu window embedding with its `-w` option. To avoid those issues for people who are not using transparency, the default behavior is reverted to versions before v1.14.0 in this release. A new `window.transparent` setting can be set to `true` to restore the behavior of v1.14.0. Changed ~~~~~~~ - Windows and macOS releases now ship Qt 5.15.2, which is based on Chromium 83.0.4103.122 with security fixes up to 86.0.4240.183. This includes CVE-2020-15999 in the bundled freetype library, which is known to be exploited in the wild. It also includes various other bugfixes/features compared to Qt 5.15.0 included in qutebrowser v1.14.0, such as: * Correct handling of AltGr on Windows * Fix for `content.cookies.accept` not working properly * Fixes for screen sharing (some websites are still broken until an upcoming Qt 5.15.3) * Support for FIDO U2F / WebAuth * Fix for the unwanted creation of directories such as `databases-incognito` in the home directory * Proper autocompletion in the devtools console * Proper signalisation of a tab's audible status (`[A]`) * Fix for a hang when opening the context menu on macOS Big Sur (11.0) * Hardware accelerated graphics on macOS Fixed ~~~~~ - Setting the `content.headers.referer` setting to `same-domain` (the default) was supposed to truncate referrers to only the host with QtWebEngine. Unfortunately, this functionality broke in Qt 5.14. It works properly again with this release, including a test so this won't happen again. - With QtWebEngine 5.15, setting the `content.headers.referer` setting to `never` did still send referrers. This is now fixed as well. - In v1.14.0, a regression was introduced, causing a crash when qutebrowser was closed after opening a download with PDF.js. This is now fixed. - With Qt 5.12, the `Object.fromEntries` JavaScript API is unavailable (it was introduced in Chromium 73, while Qt 5.12 is based on 69). This caused https://www.vr.fi/en and possibly other websites to break when accessed with Qt 5.12. A suitable polyfill is now included with qutebrowser if `content.site_specific_quirks` is enabled (which is the default). - While XDG startup notifications (e.g. launch feedback via the bouncy cursor in KDE Plasma) were supported ever since Qt 5.1, qutebrowser's desktop file accidentally declared that it wasn't supported. This is now fixed. - The `dmenu_qutebrowser` and `qutedmenu` userscripts now correctly read the qutebrowser sqlite history which has been in use since v1.0.0. - With Python 3.8+ and vertical tabs, a deprecation warning for an implicit int conversion was shown. This is now fixed. - Ever since Qt 5.11, fetching more completion data when that data is loaded lazily (such as with history) and the last visible item is selected was broken. The exact reason is currently unknown, but this release adds a tentative fix. - When PgUp/PgDown were used to go beyond the last visible item, the above issue caused a crash, which is now also fixed. - As a workaround for an overzealous Microsoft Defender false-positive detecting a "trojan" in the (unprocessed) adblock list, `:adblock-update` now doesn't cache the HTTP response anymore. - With the QtWebKit backend and `content.headers` set to `same-domain` (the default), origins with the same domain but different schemes or ports were treated as the same domain. They now are correctly treated as different domains. - When a URL path uses percent escapes (such as `https://example.com/embedded%2Fpath`), using `:navigate up` would treat the `%2F` as a path separator and replace any remaining percent escapes by their unescaped equivalents. Those are now handled correctly. - On macOS 11.0 (Big Sur), the default monospace font name caused a parsing error, thus resulting in broken styling for the completion, hints, and other UI components. They now look properly again. - Due to a Qt bug, installing Qt/PyQt from prebuilt binaries on systems with a very old `libxcb-utils` version (notably, Debian Stable, but not Ubuntu since 16.04 LTS) results in a setup which fails to start. This also affects the `mkvenv.py` script, which now includes a workaround for this case. - The `open_url_instance.sh` userscript now complains when `socat` is not installed, rather than silencing the error. - The example AppArmor profile in `misc/` was outdated and written for the older QtWebKit backend. It is now updated to serve as an useful starting point with QtWebEngine. - When running `:devtools` on Fedora without the needed (optional) dependency installed, it was suggested to install `qt5-webengine-devtools`, which does not, in fact, exist. It's now correctly suggested to install `qt5-qtwebengine-devtools` instead. - With Qt 5.15.2, lines/borders coming from the `readability-js` userscript were invisible. This is now fixed by changing the border color to grey (with all Qt versions). - Due to changes in the underlying Chromium, the `colors.webpage.prefers_color_scheme_dark` setting broke with Qt 5.15.2. It now works properly again. - A bug in the `pkg_resources` module used by qutebrowser caused deprecation warnings to appear on start with Python 3.9 on some setups. Those are now hidden. - Minor performance improvements. - Fix for various functionality breaking in private windows with v1.14.0, after the last private window is closed. This includes: * Ad blocking * Downloads * Site-specific quirks (e.g. for Google login) * Certain settings such as `content.javascript.enabled` [[v1.14.0]] v1.14.0 (2020-10-15) -------------------- Note: The QtWebEngine version bundled with the Windows/macOS releases is still based on Qt 5.15.0 (like with qutebrowser v1.12.0 and v1.13.0) rather than Qt 5.15.1 because of a https://bugreports.qt.io/browse/QTBUG-86752[Qt bug] causing frequent renderer process crashes. When Qt 5.15.2 is released (planned for November 3rd, 2020), a qutebrowser v1.14.x patch release with an updated QtWebEngine will be released. Furthermore, this release still only contains partial session support for QtWebEngine 5.15. It's still recommended to run against Qt 5.15 due to the security patches contained in it -- for most users, the added workarounds seem to work out fine. A rewritten session support will be part of qutebrowser v2.0.0, tentatively planned for the end of the year or early 2021. Changed ~~~~~~~ - The `content.media_capture` setting got split up into three more fine-grained settings, `content.media.audio_capture`, `.video_capture` and `.audio_video_capture`. Before this change, answering "always" to a prompt about e.g. audio capturing would set the `content.media_capture` setting, which would also allow the same website to capture video on a future visit. Now every prompt will set the appropriate setting, though existing `content.media_capture` settings in `autoconfig.yml` will be migrated to set all three settings. To review/change previously granted permissions, use `:config-diff` and e.g. `:config-unset -u example.org content.media.video_capture`. - The main window's (invisible) background color is now set to transparent. This allows using the alpha channel in statusbar/tabbar colors to get a partially transparent qutebrowser window on a setup which supports doing so. - If QtWebEngine is compiled with PipeWire support and libpipewire is installed, qutebrowser will now support screen sharing on Wayland. Note that QtWebEngine 5.15.1 is needed. - When `:undo` is used with a count, it now reopens the count-th to last tab instead of the last one. The depth can instead be passed as an argument, which is also completed. - The default `completion.timestamp_format` now also shows the time. - `:back` and `:forward` now take an optional index which is completed using the current tab's history. - The time a website in a tab was visited is now saved/restored in sessions. - When attempting to download a file to a location for which there's already a still-running download, a confirmation prompt is now displayed. - `:completion-item-focus` now understands `next-page` and `prev-page` with corresponding `` / `` default bindings. - When the last private window is closed, all private browsing data is now cleared. - When `config.source(...)` is used with a `--config-py` argument given, qutebrowser used to search relative files in the config basedir, leading to them not being found when using a shared `config.py` for different basedirs. Instead, they are now searched relative to the given `config.py` file. - `navigate prev` (`[[`) and `navigate next` (`]]`) now recognize links with `nav-prev` and `nav-next` classes, such as those used by the Hugo static site generator. - When `tabs.favicons` is disabled but `tabs.tabs_are_windows` is set, the window icon is still set to the page's favicon now. - The `--asciidoc` argument to `src2asciidoc.py` and `build_release.py` now only takes the path to `asciidoc.py`, using the current Python interpreter by default. To configure the Python interpreter as well, use `--asciidoc-python path/to/python --asciidoc path/to/asciidoc.py` instead of the former `--asciidoc path/to/python path/to/asciidoc.py`. - Dark mode (`colors.webpage.darkmode.*`) is now supported with Qt 5.15.2 (which is not released yet). - The default for the darkmode `policy.images` setting is now set to `smart` which fixes issues with e.g. formulas on Wikipedia. - The `readability-js` userscript now adds some CSS to improve the reader mode styling in various scenarios: * Images are now shrunk to the page width, similarly to what Firefox' reader mode does. * Some images are now displayed as block (rather than inline) which is what Firefox' reader mode does as well. * Blockquotes are now styled more distinctively, again based on the Firefox reader mode. * Code blocks are now easier to distinguish from text and tables have visible cell margins. - The `readability-js` userscript now supports hint userscript mode. Added ~~~~~ - New argument `strip` for `:navigate` which removes queries and fragments from the current URL. - `:undo` now has a new `-w` / `--window` argument, which can be used to restore closed windows (rather than tabs). This is bound to `U` by default. - `:jseval` can now take `javascript:...` URLs via a new `--url` flag. - New replacement `{aligned_index}` for `tabs.title.format` and `format_pinned` which behaves like `{index}`, but space-pads the index based on the total numbers of tabs. This can be used to get aligned tab texts with vertical tabs. - New command `:devtools-focus` (bound to `wIf`) to toggle keyboard focus between the devtools and web page. - The `--target` argument to qutebrowser now understands a new `private-window` value, which can be used to open a private window in an existing instance from the commandline. - The `:download-open` command now has a new `--dir` flag, which can be used to open the directory containing the downloaded file. An entry to do the same was also added to the context menu. - Messages are now wrapped when they are too long to be displayed on a single line. - New possible `--debug-flag` values: * `wait-renderer-process` waits for a `SIGUSR1` in the renderer process so a debugger can be attached. * `avoid-chromium-init` allows using `--version` without needing a working QtWebEngine/Chromium. Fixed ~~~~~ - A URL pattern with a `*.` host was considered valid and matched all hosts. Due to keybindings like `tsH` toggling scripts for `*://*.{url:host}/*`, invoking them on pages without a host (e.g. `about:blank`) could result in accidentally allowing/blocking JavaScript for all pages. Such patterns are now considered invalid, with existing patterns being automatically removed from `autoconfig.yml`. - When `scrolling.bar` was set to `overlay` (the default), qutebrowser would internally override any `enable-features=...` flags passed via `qt.args` or `--qt-flag`. It now correctly combines existing `enable-feature` flags with internal ones. - Elements with an inherited `contenteditable` attribute now trigger insert mode and get hints assigned correctly. - When checkmarks, radio buttons and some other elements are styled via the Bootstrap CSS framework, they now get hints correctly. - When the session file isn't writable when qutebrowser exits, an error is now logged instead of crashing. - When using `-m` with the `qute-lastpass` userscript, it accidentally matched URLs containing the match as substring. This is now fixed. - When a filename is derived from a page's title, it's now shortened to the maximum filename length permitted by the filesystem. - `:enter-mode register` crashed since v1.13.0, it now displays an error instead. - With the QtWebKit backend, webpage resources loading certain invalid URLs could cause a crash, which is now fixed. - When `:config-edit` is used but no `config.py` exists yet, the file is now created (and watched for changes properly) before spawning the external editor. - When hint mode was entered from outside normal mode, the status bar was empty instead of displaying the proper text. This is now fixed. - When entering different modes too quickly (e.g. pressing `fV`), the statusbar could end up in a confusing state. This is now fixed. - When qutebrowser quits, running downloads are now cancelled properly. - The site-specific quirk for `web.whatsapp.com` has been updated to work after recent changes in WhatsApp. - Highlighting in the completion now works properly when UTF-16 surrogate pairs (such as emoji) are involved. - When a windowed inspector is clicked, insert mode now isn't entered anymore. - When `:undo` is used to re-open a tab, but `tabs.tabs_are_windows` was set between closing and undoing the close, qutebrowser crashed. This is now fixed. - With QtWebEngine 5.15.0, setting the darkmode image policy to `smart` leads to renderer process crashes. The offending setting value is now ignored with a warning. - Fixes for the `qute-pass` userscript: * With newer `gopass` versions, a deprecation notice was copied as password due to `qute-pass` using it in a deprecated way. * The `--password-store` argument didn't actually set `PASSWORD_STORE_DIR` for `pass`, resulting in `qute-pass` finding matches but the underlying `pass` not finding matching passwords. [[v1.13.1]] v1.13.1 (2020-07-17) -------------------- Fixed ~~~~~ - With Qt 5.14, shared workers are now disabled. This works around a crash in QtWebEngine on certain sites (like the Epic Games Store or the Unreal Engine page). On older versions, you can get the same effect by doing `:set qt.args "['disable-shared-workers']"` and `:restart` (or set the setting in your `config.py`). - When a window is closed, the tab it contains are now correctly shut down (closing e.g. any dialogs which are still open for those tabs). - The Qt 5.15 session workaround now loads the correct (rather than the last) page when `:back` was used before saving a session. - In certain situations on Windows, qutebrowser fails to find the username of the user launching qutebrowser (most likely due to a bug in the application launching it). When this happens, an error is now displayed instead of crashing. - Certain `autoconfig.yml` with an invalid structure could lead to crashes, which are now fixed. - Generating docs with `asciidoc2html.py` (e.g. via `mkvenv.py`) now works correctly without Pygments being installed system-wide. - Ever since Qt 5.9, when `input.mouse.rocker_gestures` was enabled, the context menu still was shown when clicking the right mouse button, thus preventing the rocker gestures. This is now fixed. - Clicking the inspector switched from existing modes (such as passthrough) to normal mode since v1.13.0. Now insert mode is only entered when the inspector is clicked in normal mode. - Pulseaudio now shows qutebrowser's audio streams as qutebrowser correctly, rather than showing them as Chromium with some Qt versions. - If `:help` was called with a deprecated command (e.g. `:help :inspector`), the help page would show despite deprecated commands not being documented. This now shows an error instead. - The `qute-lastpass` userscript now filters out duplicate entries with `--merge-candidates`. [[v1.13.0]] v1.13.0 (2020-06-26) -------------------- Deprecated ~~~~~~~~~~ - The `:inspector` command is deprecated and has been replaced by a new `:devtools` command (see below). Removed ~~~~~~~ - The `:debug-log-level` command was removed as it's replaced by the new `logging.level.console` setting. - The `qute://plainlog` special page got replaced by `qute://log?plain` - the names of those pages is considered an implementation detail, and `:messages --plain` should be used instead. Changed ~~~~~~~ - Changes to commands: * `:config-write-py` now adds a note about `config.py` files being targeted at advanced users. * `:report` now takes two optional arguments for bug/contact information, so that it can be used without the report window popping up. * `:message` now takes a `--logfilter` / `-f` argument, which is a list of logging categories to show. * `:debug-log-filter` now understands the full logfilter syntax. - Changes to settings: * `fonts.tabs` has been split into `fonts.tabs.{selected,unselected}` (see below). * `statusbar.hide` has been renamed to `statusbar.show` with the possible values being `always` (`hide = False`), `never` (`hide = True`) or `in-mode` (new, only show statusbar outside of normal mode. * The `QtFont` config type formerly used for `fonts.tabs` and `fonts.debug_console` is now removed and entirely replaced by `Font`. The former distinction was mainly an implementation detail, and the accepted values shouldn't have changed. * `input.rocker_gestures` has been renamed to `input.mouse.rocker_gestures`. * `content.dns_prefetch` is now enabled by default again, since the crashes it caused are now fixed (Qt 5.15) or worked around. * `scrolling.bar` supports a new `overlay` value to show an overlay scrollbar, which is now the default. On unsupported configurations (on Qt < 5.11, with QtWebKit or on macOS), the value falls back to `when-searching` or `never` (QtWebKit). * `url.auto_search` supports a new `schemeless` value which always opens a search unless the given URL includes an explicit scheme. - New handling of bindings in hint mode which fixes various bugs and allows for single-letter keybindings in hint mode. - The statusbar now shows partial keychains in all modes (e.g. while hinting). - New `t[Cc][Hh]` default bindings which work similarly to the `t[Ss][Hh]` bindings for JavaScript but toggle cookie permissions. - The `tor_identity` userscript now takes the password via a `-p` flag and has a new `-c` flag to customize the Tor control port. - Small performance improvements. Added ~~~~~ - New settings: * `logging.level.ram` and `logging.level.console` to configure the default logging levels via the config. * `fonts.tabs.selected` and `fonts.tabs.unselected` to set the font of the selected tab independently from unselected tabs (e.g. to make it bold). * `input.mouse.back_forward_buttons` which can be set to `false` to disable back/forward mouse buttons. - New `:devtools` command (replacing `:inspector`) with various improved functionality: * The devtools can now be docked to the main window, by running `:devtools left` (`wIh`), `bottom` (`wIj`), `top` (`wIk`) or `right` (`wIl`). To show them in a new window, use `:devtools window` (`wIw`). Using `:devtools` (`wi`) will open them at the last used position. * The devtool window now has a "qutebrowser developer tools" window title. * When a resource is opened from the devtools, it now opens in a proper qutebrowser tab. * On Fedora, when the `qt5-webengine-devtools` package is missing, an error is now shown instead of a blank inspector window. * If opened as a window, the devtools are now closed properly when the associated tab is closed. * When the devtools are clicked, insert mode is entered automatically. Fixed ~~~~~ - Crash when `tabs.focus_stack_size` is set to -1. - Crash when a `pdf.js` file for PDF.js exists, but `viewer.html` does not. - Crash when `:completion-item-yank --sel` is used on a platform without primary selection support (e.g. Windows/macOS). - Crash when there's a feature permission request from Qt with an invalid URL (which happens due to a Qt bug with Qt 5.15 in private browsing mode). - Crash in rare cases where QtWebKit/QtWebEngine imports fail in unexpected ways. - Crash when something removed qutebrowser's IPC socket file and it's been running for 6 hours. - `:config-write-py` now works with paths starting with `~/...` again. - New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify. - When `;` is added to `hints.chars`, using hint labels containing `;;` now works properly. - Hint letters outside of ASCII should now work. - When `bindings.key_mappings` is used with hints, it now works properly with letters outside of ASCII as well. - With Qt 5.15, the audible/muted indicators are not updated properly due to a Qt bug. This release adds a workaround so that at least the muted indicator is shown properly. - As a workaround for crashes with QtWebEngine versions between 5.12 and 5.14 (inclusive), changing the user agent (`content.headers.user_agent`) exposed to JS now requires a restart. The corresponding HTTP header is not affected. [[v1.12.0]] v1.12.0 (2020-06-01) -------------------- Removed ~~~~~~~ - `tox -e mkvenv` which was deprecated in qutebrowser v1.10.0 is now removed. Use the `mkvenv.py` script instead. - Support for using `config.bind(key, None)` in `config.py` to unbind a key was deprecated in v1.8.2 and is now removed. Use `config.unbind(key)` instead. - `:yank markdown` was deprecated in v1.7.0 and is now removed. Use `:yank inline [{title}]({url})` instead. Added ~~~~~ - New `:debug-keytester` command, which shows a "key tester" widget. Previously, that was only available as a separate application via `python3 -m scripts.keytester`. - New `:config-diff` command which opens the `qute://configdiff` page. - New `--debug-flag log-cookies` to log cookies to the debug log. - New `colors.contextmenu.disabled.{fg,bg}` settings to customize colors for disabled items in the context menu. - New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode. - New `colors.webpage.darkmode.*` settings to control Chromium's dark mode. Note that those settings only work with QtWebEngine on Qt >= 5.14 and require a restart of qutebrowser. Changed ~~~~~~~ - Windows and macOS releases now ship Qt 5.15, which is based on Chromium 80.0.3987.163 with security fixes up to 81.0.4044.138. - The `content.cookies.accept` setting now accepts URL patterns. - Tests are now included in release tarballs. Note that only running them with the exact dependencies listed in `misc/requirements/requirements-tests.txt{,-raw}` is supported. - The `:tab-focus` command now has completion for tabs in the current window. - The `bindings.key_mappings` setting now maps `` to the tab key by default. - `:tab-give --private` now detaches a tab into a new private window. Fixed ~~~~~ - Using `:open -s` now only rewrites `http://` in URLs to `https://`, not other schemes like `qute://`. - When an unhandled exception happens in certain parts of the code (outside of the main thread), qutebrowser did crash or freeze when trying to show its exception handler. This is now fixed. - `:inspector` now works correctly when cookies are disabled globally. - Added workaround for a (Gentoo?) PyQt/packaging issue related to the `QWebEngineFindTextResult` handling added in v1.11.0. - When entering caret selection mode (`v, v`) very early before a page is loaded, an error is now shown instead of a crash happening. - The workaround for session loading with Qt 5.15 now handles `sessions.lazy_restore` so that the saved page is loaded instead of the "stub" page with no possibility to get to the web page. - A site specific quirk to allow typing accented characters on Google Docs was active for docs.google.com, but not drive.google.com. It is now applied for both subdomains. - With older graphics hardware (OpenGL < 4.3) with Qt 5.14 on Wayland, WebGL causes segfaults. Now qutebrowser detects that combination and suggests to disable WebGL or use XWayland. [[v1.11.1]] v1.11.1 (2020-05-07) -------------------- Security ~~~~~~~~ - CVE-2020-11054: After a certificate error was overridden by the user, qutebrowser displays the URL as yellow (`colors.statusbar.url.warn.fg`). However, when the affected website was subsequently loaded again, the URL was mistakenly displayed as green (`colors.statusbar.url.success_https`). While the user already has seen a certificate error prompt at this point (or set `content.ssl_strict` to `false` which is not recommended), this could still provide a false sense of security. This is now fixed. [[v1.11.0]] v1.11.0 (2020-04-27) -------------------- Added ~~~~~ - New settings: * `search.wrap` which can be set to false to prevent wrapping around the page when searching. With QtWebEngine, Qt 5.14 or newer is required. * `content.unknown_url_scheme_policy` which allows controlling when an external application is opened for external links (never, from user interaction, always). * `content.fullscreen.overlay_timeout` to configure how long the fullscreen overlay should be displayed. If set to `0`, no overlay is displayed. * `hints.padding` to add additional padding for hints. * `hints.radius` to set a border radius for hints (set to `3` by default). - New placeholders for `url.searchengines` values: * `{unquoted}` inserts the search term without any quoting. * `{semiquoted}` (same as `{}`) quotes most special characters, but slashes remain unquoted. * `{quoted}` (same as `{}` in earlier releases) also quotes slashes. Changed ~~~~~~~ - First adaptions to Qt 5.15, including a stop-gap measure for session loading not working properly with it. - Searching now wraps around the page by default with QtWebKit (where it didn't before). Set `search.wrap` to `false` to restore the old behavior. - The `{}` placeholder for search engines (the `url.searchengines` setting) now does not quote slashes anymore, but other characters typically encoded in URLs still get encoded. This matches the behavior of search engines in Chromium. To revert to the old behavior, use `{quoted}` instead. - The `content.windowed_fullscreen` setting got renamed to `content.fullscreen.window`. - Mouse-wheel scrolling is now prevented while hints are active. - Changes to userscripts: * `qute-bitwarden` now has an optional `--totp` flag which can be used to copy TOTP codes to clipboard (requires the `pyperclip` module). * `readability-js` now opens readability tabs next to the original tab (using the `:open --related` flag). * `readability-js` now displays a favicon for readability tabs. * `password_fill` now triggers a `change` JavaScript event after filling the data. - The `dictcli.py` script now shows better error messages. - Various improvements to the `mkvenv.py` script (mainly useful for development). - Minor performance improvements. Deprecated ~~~~~~~~~~ - A warning about old Qt versions is now also shown with Qt 5.9 and 5.10, as support for Qt < 5.11 will be dropped in qutebrowser v2.0. Fixed ~~~~~ - `unsafeWindow` is now defined for Greasemonkey scripts with QtWebKit. - The proxied `window` global is now shared between different Greasemonkey scripts (but still separate from the page's `window`), to match the original Greasemonkey implementation. - The `--output-messages` (`-m`) flag added in v1.9.0 now also works correctly when using `:spawn --userscript`. - `:version` and `--version` now don't crash if there's an (invalid) `/etc/os-release` file which has non-comment lines without a `=` character. - Scripts in `scripts/` now report errors to `stderr` correctly, instead of using `stdout`. [[v1.10.2]] v1.10.2 (2020-04-17) -------------------- Changed ~~~~~~~ - Windows and macOS releases now bundle Qt 5.14.2, including security fixes up to Chromium 80.0.3987.132. Fixed ~~~~~ - The WhatsApp workaround now also works when using WhatsApp in languages other than English. - The `mkvenv.py` script now also works properly on Windows. [[v1.10.1]] v1.10.1 (2020-02-15) -------------------- Fixed ~~~~~ - Crash when saving data fails during shutdown (which was a regression introduced in v1.9.0). - Error while reading config.py when `fonts.tabs` or `fonts.debug_console` is set to a value including `default_size`. - When a `state` file contains invalid UTF-8 data, a proper error is now displayed. Changed ~~~~~~~ - When the Qt version changes (and also on the first start of v1.10.1 on Qt 5.14), service workers registered by websites are now deleted. This is done as a workaround for QtWebEngine issues causing crashes when visiting pages using service workers (such as Google Mail/Drive). No persistent data should be affected as websites can re-register their service workers, but a (single) backup is kept at `webengine/Service Worker-bak` in qutebrowser's data directory. - Better output on stdout when config errors occur. - The `mkvenv.py` now ensures the latest versions of `setuptools` and `wheel` are installed in the virtual environment, which should speed up installation and fix install issues. - The default for `colors.statusbar.command.private.bg` has been changed to a slightly different gray, as a workaround for a Qt issue where the cursor was invisible in that case. [[v1.10.0]] v1.10.0 (2020-02-02) -------------------- Added ~~~~~ - New `colors.webpage.prefers_color_scheme_dark` setting which allows forcing `prefers-color-scheme: dark` colors for websites (QtWebEngine with Qt 5.14 or newer). - New `fonts.default_size` setting which can be used to set a bigger font size for all UI fonts. Changed ~~~~~~~ - The `fonts.monospace` setting has been removed and replaced by `fonts.default_family`. The new `default_family` setting is improved in various ways: * It accepts a list of font families (or a single font family) rather than a comma-separated string. As an example, instead of `fonts.monospace = "Courier, Monaco"`, use `fonts.default_family = ["Courier", "Monaco"]`. * Since a list is now accepted as value, no quoting of font names with spaces is required anymore. As an example, instead of `fonts.monospace = '"xos4 Terminus"'`, use `fonts.default_family = 'xos4 Terminus'`. * It is now empty by default rather than having a long list of font names in the default config. When the value is empty, the system's default monospaced font is used. - If `monospace` is now used in a font value, it's used literally and not replaced anymore. Instead, `default_family` is replaced as explained above. - The default `content.headers.accept_language` value now adds a `;q=0.9` classifier which should make the value sent more in-line with what other browsers do. - The `qute-pass` userscript now has a new `--mode gopass` switch which uses gopass rather than pass. - The `tox -e mkvenv` (or `mkvenv-pypi`) way of installing qutebrowser is now replaced by a `mkvenv.py` script. See the updated link:install{outfilesuffix}#tox[install instructions] for details. - macOS and Windows releases now ship with Qt/QtWebEngine 5.14.1 * Based on Chromium 77.0.3865.129 with security fixes up to Chromium 79.0.3945.117. * Sandboxing is now enabled on Windows. * Monospace fonts are now used when a website requests them on macOS 10.15. * Web notifications are now supported. Fixed ~~~~~ - When quitting qutebrowser, components are now cleaned up differently. This should fix certain (rare) segmentation faults and exceptions when quitting, especially with the new exit scheme introduced in in PyQt5 5.13.1. - Added a workaround for per-domain settings (e.g. a JavaScript whitelist) not being applied in some scenarios with Qt 5.13 and above. - Added additional site-specific quirk for WhatsApp Web. - The `qute-pass` userscript now works correctly when a `PASSWORD_STORE_DIR` ending with a trailing slash is given. [[v1.9.0]] v1.9.0 (2020-01-08) ------------------- Added ~~~~~ - Initial support for Qt 5.14. - New `content.site_specific_quirks` setting which enables workarounds for websites with broken user agent parsing (enabled by default, see the "Fixed" section for fixed websites). - New `qt.force_platformtheme` setting to force Qt to use a given platform theme. - New `tabs.tooltips` setting which can be used to disable hover tooltips for tabs. - New settings to configure the appearance of context menus: * `fonts.contextmenu` * `colors.contextmenu.menu.bg` * `colors.contextmenu.menu.fg` * `colors.contextmenu.selected.bg` * `colors.contextmenu.selected.fg` Changed ~~~~~~~ - The macOS binaries now require macOS 10.13 High Sierra or newer. Support for macOS 10.12 Sierra has been dropped. - The `content.headers.user_agent` setting now is a format string with the default value resembling the behavior of it being set to null before. This slightly changes the sent user agent for QtWebKit: Instead of mentioning qutebrowser and its version it now mentions the Qt version. - The `qute-pass` userscript now has a new `--extra-url-suffixes` (`-s`) argument which passes extra URL suffixes to the tldextract library. - A stack is now used for `:tab-focus last` rather than just saving one tab. Additionally, `:tab-focus` now understands `stack-prev` and `stack-next` arguments to traverse that stack. - `:hint` now has a new `right-click` target which allows right-clicking elements via hints. - The Terminus font has been removed from the default monospace fonts since it caused trouble with HighDPI setups. To get it back, add either `"xos4 Terminus"` or `Terminus` (depending on fontconfig version) to the beginning of the `fonts.monospace` setting. - As a workaround for a Qt bug causing a segfault, desktop sharing is now automatically rejected on Qt versions before 5.13.2. Note that screen sharing still won't work on Linux before Qt 5.14. - Comment lines in quickmarks/bookmarks files are now ignored. However, note that qutebrowser will overwrite those files if bookmark/quickmark commands are used. - Reopening PDF.js pages from e.g. a session file will now re-download and display those PDFs. - Improved behavior when using `:open-download` in a sandboxed environment (KDE Flatpak). - qutebrowser now enables the new PyQt exit scheme, which should result in things being cleaned up more properly (e.g. cookies being saved even without a timeout) on PyQt 5.13.1 and newer. - The `:spawn` command has a new `-m` / `--output-messages` argument which shows qutebrowser messages based on a command's standard output/error. - Improved insert mode detection for some CodeMirror usages (e.g. in JupyterLab and Jupyter Notebook). - If JavaScript is disabled globally, `file://*` now doesn't automatically have it enabled anymore. Run `:set -u file://* content.javascript.enabled true` to restore the previous behavior. - Settings with URL patterns can now be used to affect the behavior of the QtWebEngine inspector. Note that the underlying URL is `chrome-devtools://*` from Qt 5.11 to Qt 5.13, but `devtools://*` with Qt 5.14. - Improvements when `tabs.tabs_are_windows` is set: * Using `:tab-take` and `:tab-give` now shows an error, as the effect of doing so would be equal to `:tab-clone`. * The `:buffer` completion doesn't show any window sections anymore, only a flat list of tabs. - Improved parsing in some corner cases for the `QtFont` type (used for `fonts.tabs` and `fonts.debug_console`). - Performance improvements for the following areas: * Adding settings with URL patterns * Matching of settings using URL patterns Fixed ~~~~~ - Downloads (e.g. via `:download`) now see the same user agent header as webpages, which fixes cases where overly restrictive servers/WAFs closed the connection before. - `dictcli.py` now works correctly on Windows again. - The logic for `:restart` has been revisited, which should fix issues with relative basedirs. - Remaining issues related to Python 3.8 are now fixed (mostly warnings, especially on QtWebKit). - Workaround for a Qt bug where a page never finishes loading with a non-overridable TLS error (e.g. due to HSTS). - The `qute://configdiff` page now doesn't show built-in settings (e.g. javascript being enabled for `qute://` and `chrome://` pages) anymore. - The `qute-lastpass` userscript now stops prompting for passwords when cancelling the password input. - The tab hover text now shows ampersands (&) correctly. - With QtWebEngine and Qt >= 5.11, the inspector now shows its icons correctly even if loading of images is disabled via the `content.images` setting. - Entering a very long string (over 50k characters) in the completion used to crash, now it shows an error message instead. - Various improvements for URL/searchengine detection: * Strings with a dot but with characters not allowed in a URL (e.g. an underscore) are now not treated as URL anymore. * Strings like "5/8" are now not treated as IP anymore. * URLs with an explicit scheme and a space (%20) are correctly treated as URLs. * Mail addresses are now treated as search terms. * With `url.open_base_url` set, searching for a search engine name now works. * `url.open_base_url = True` together with `url.auto_search = 'never'` is now handled correctly. * Fixed crash when a search engine URL turns out to be invalid. - New "site specific quirks", which work around some broken websites: * WhatsApp Web * Google Accounts * Slack (with older QtWebEngine versions) * Dell.com support pages (with Qt 5.7) * Google Docs (fixes broken IME/compose key) [[v1.8.3]] v1.8.3 (2019-12-05) ------------------- Fixed ~~~~~ - Segmentation fault introduced in v1.8.2 when a tab gets closed immediately after it has finished loading (e.g. with certain login flows). [[v1.8.2]] v1.8.2 (2019-11-22) ------------------- Changed ~~~~~~~ - Windows/macOS releases now ship with Qt 5.12.6. This includes security fixes up to Chromium 77.0.3865.120 plus a security fix for CVE-2019-13720 from Chromium 78. Fixed ~~~~~ - Unbinding keys via `config.bind(key, None)` accidentally worked in v1.7.0 but raises an exception in v1.8.0. It now works again, but is deprecated and shows an error. Note that `:config-py-write` did write such invalid lines before v1.8.0, so existing config files might need adjustments. - The `readability-js` userscript now handles encodings correctly (which it didn't before for some websites). - can now be used to paste text starting with a hyphen. - Following hints via the number keypad now works properly again. - Errors while reading the state file are now displayed instead of causing a crash. - Crash when using `:debug-log-level` without a console attached. - Downloads are now hidden properly when the browser is in fullscreen mode. - Crash when setting `colors.webpage.bg` to an empty value with QtWebKit. - Crash when the history database file is not a proper sqlite database. - Workaround for missing/broken error pages on Debian. - A deprecation warning (caused by pywin32) about the imp module on Windows is now hidden. [[v1.8.1]] v1.8.1 (2019-09-27) ------------------- Changed ~~~~~~~ - No code changes - this release only repackages the Windows/macOS releases due to issues with the v1.8.0 release. - Updated dependencies for Windows/macOS releases: * macOS and Windows releases now ship with Qt/QtWebEngine 5.12.5. Those are based on Chromium 69.0.3497.128 with security fixes up to Chromium 76.0.3809.87. * Qt 5.13 couldn't be used yet due to various bugs in Qt 5.13.0 and .1. [[v1.8.0]] v1.8.0 (2019-09-25) ------------------- Added ~~~~~ - New userscripts: * `readability-js` which uses Mozilla's node.js readability library. * `qute-bitwarden` which integrates the Bitwarden CLI. Changed ~~~~~~~ - The statusbar text for passthrough mode now shows all configured bindings to leave the mode, not only one. - When `:config-source` is used with a relative filename, the file is now searched in the config directory instead of the current working directory. - HTML5 inputs with date/time types now enter insert mode when selected. - `dictcli.py` now shows where dictionaries are installed to and complains when running it as root if doing so would result in a wrong installation path. - The Makefile now can also run `setup.py build` when invoked without a target. - Changes to userscripts: * qute-pass: Don't run `pass` if only a username is requested. * qute-pass: Support private domains like `myrouter.local`. * readability: Improved CSS styling. - Performance improvements in various areas: * Loading config files * Typing without any completion matches * General keyboard handling * Scrolling - `:version` now shows details about the loaded autoconfig.yml/config.py. - Hosts are now additionally looked up including their ports in netrc files. - With Qt 5.10 or newer, qutebrowser now doesn't force software rendering with Nouveau drivers anymore. However, QtWebEngine/Chromium still do so. - The XSS Auditor is now disabled by default (`content.xss_auditing` = `false`). This reflects a similar change in Chromium, see their https://www.chromium.org/developers/design-documents/xss-auditor[XSS Auditor Design Document] for details. Fixed ~~~~~ - `:config-write-py` now correctly writes `config.unbind(...)` lines (instead of `config.bind(..., None)`) when unbinding a default keybinding. - Prevent repeat keyup events for JavaScript when a key is held down. - The Makefile now rebuilds the manpage correctly. - `~/.config/qutebrowser/blocked-hosts` can now also contain /etc/hosts-like lines, not just simple hostnames. - Restored compatibility with Jinja2 2.8 (e.g. used on Debian Stretch or Ubuntu 16.04 LTS). - Fixed implicit type conversion warning with Python 3.8. - The desktop file now sets `StartupWMClass` correctly, so the qutebrowser icon is no longer shown twice in the Gnome dock when pinned. - Bindings involving keys which need the AltGr key now work properly. - Fixed crash (caused by a Qt bug) when typing characters above the Unicode BMP (such as certain emoji or CJK characters). - `dictcli.py` now works properly again. - Shift can now be used while typing hint keystrings, which e.g. allows typing number hints on French keyboards. - With rapid hinting in number mode, backspace now edits the filter text after following a hint. - A certain type of error ("locking protocol") while initializing sqlite now isn't handled as crash anymore. - Crash when showing a permission request in certain scenarios. Removed ~~~~~~~ - At least Python 3.5.2 is now required to run qutebrowser, support for 3.5.0 and 3.5.1 was dropped. [[v1.7.0]] v1.7.0 (2019-07-18) ------------------- Added ~~~~~ - New settings: * `colors.tabs.pinned.*` to control colors of pinned tabs. * `hints.leave_on_load` which allows disabling leaving of hint mode when a new page is loaded. * `colors.completion.item.selected.match.fg` which allows configuring the text color for the matching text in the currently selected completion item. * `tabs.undo_stack_size` to limit how many undo entries are kept for closed tabs. - New commands: * `:reverse-selection` (`o` in caret mode) to swap the stationary/moving ends of a selection. - New commandline replacements: * `{url:domain}`, `{url:auth}`, `{url:scheme}`, `{url:username}`, `{url:password}`, `{url:host}`, `{url:port}`, `{url:path}`, `{url:query}` for the respective parts of the current URL. * `{title}` for the current page title. - The `{title}` field in `tabs.title.format`, `tabs.title.format_pinned` and `window.title_format` got renamed to `{current_title}` (mirroring `{current_url}`) in order to not conflict with the new `{title}` commandline replacement. - New `delete` target for `:hint` which removes the hinted element from the DOM. - New `--config-py` commandline argument to use a custom `config.py` file. - Qt 5.13: Support for notifications (shown via system tray). Changed ~~~~~~~ - Updated dependencies for Windows/macOS releases: - PyQt5 5.12.3 / PyQtWebEngine 5.12.1 - Qt 5.12.4, which includes security fixes up to Chromium 74.0.3729.157 - Python 3.7.4 - OpenSSL 1.1.1 - Note: This release includes Qt 5.12.4 instead of Qt 5.13.0 due to https://bugreports.qt.io/browse/QTBUG-76913[QTBUG-76913] causing frequent segfaults with Qt 5.13. After Qt 5.13.1 is released, qutebrowser v1.8.0 will be released with an updated Qt. - Completely revamped Windows installer which allows installing without admin permissions and allows setting qutebrowser as default browser. - The desktop file `qutebrowser.desktop` is now renamed to `org.qutebrowser.qutebrowser.desktop`. - Pinned tabs now always show a favicon (even if the site doesn't provide one) when shrinking. - Setting `downloads.location.directory` now changes the directory displayed in the download prompt even if `downloads.location.remember` is set. - The `yank` command gained a new `inline` argument, which allows to e.g. use `:yank inline [{title}]({url})`. - Duplicate consecutive history entries with the same URL are now ignored. - More detailed error messages when spawning a process failed. - The `content.pdfjs` setting now supports domain patterns. - Improved process status output with `:spawn -o`. - The `colors.tabs.bar.bg` setting is now of type `QssColor` and thus supports gradients. - The `:fullscreen` command now understands a new `--enter` flag which causes it to always enter fullscreen instead of toggling the current state. - `--debug-flag stack` is now needed to show stack traces on renderer process crashes. - `--debug-flag chromium` can be used to easily turn on verbose Chromium logging. - For runtime data (such as the IPC socket), a proper runtime path is now used on BSD; only macOS/Windows continue to use the temporary directory. - PDF.js is now also searched in `/app/share/pdf.js/` (for Flatpak) - Permission prompts can now be answered with `Y` (`:prompt-accept --save yes`) and `N` (`:prompt-accept --save no`) to save the answer as a per-domain setting. - `content.dns_prefetch` is now turned off by default, as it causes crashes inside QtWebEngine. - The (still unofficial) interceptor plugin API now contains `resource_type` for a request and allows redirecting requests. - `:bookmark-remove` now shows a message for consistency with `:bookmark-add`. - Very early segfaults are now also caught by the crash handler. - The appdata XML now contains proper release information and an (empty) OARS content rating. - Improved Linux distribution detection. - Qt 5.13: Request filtering now happens in the UI rather than IO thread. - Qt 5.13: Support for PDFium (Chromium's PDF viewer) is disabled for now so that PDFs can still be downloaded (or shown with PDF.js) properly. - Various performance improvements (e.g. for showing hints or the :open completion). Deprecated ~~~~~~~~~~ - `:yank markdown` got deprecated, as `:yank inline [{title}]({url})` can now be used instead. Fixed ~~~~~ - Various QtWebEngine load signals are now handled differently, which should fix issues with insert mode being left while typing on sites like Google Translate. - Race condition causing a colored statusbar in normal mode when entering/exiting caret mode quickly. - Using `100%` for a hue in a `hsv(...)` config value now corresponds to 359 (rather than 255), matching the fixed behavior in Qt 5.13. - Chaining commands with `;;` used to abort with some failing commands. It now runs the second command no matter whether the first one succeeded or not. - Handling of profiles and private windows (and resulting crashes with Qt 5.12.2). - Fixes for corner-cases when using `:navigate increment/decrement`. - The type for the `colors.hints.match.fg` setting was changed to `QtColor`. Gradients were never supported for this setting, and with this change, values like `rgb(0, 0, 0)` now work as well. - Permission prompts now show a properly normalized URL with QtWebKit. - Crash on start when PyQt was built without SSL support with Qt >= 5.12. - Minor memory leaks. [[v1.6.3]] v1.6.3 (2019-06-18) ------------------- Fixed ~~~~~ - Crash when hinting and changing/closing the tab before hints are displayed. - Crash on redirects with Qt 5.13. - Hide bogus `AA_ShareOpenGLContexts` warning with Qt 5.12.4. - Workaround for renderer process crashes with Qt 5.12.4. If you're unable to update, you can remove `~/.cache/qutebrowser` for the same result. [[v1.6.2]] v1.6.2 (2019-05-06) ------------------- Changed ~~~~~~~ - Windows/macOS releases now ship with Qt 5.12.3, which includes security fixes up to Chromium 73.0.3683.75. Fixed ~~~~~ - Crash when SQL errors occur while using the completion. - Crash when cancelling a download prompt started in an already closed window. - Crash when many prompts are opened at the same time. - Running without Qt installed now displays a proper error again. - High CPU usage when using the keyhint widget with a low delay. - Crash with Qt >= 5.14 on redirects. [[v1.6.1]] v1.6.1 (2019-03-20) ------------------- Changed ~~~~~~~ - Windows/macOS releases now ship with Qt 5.12.2, which includes security fixes up to Chromium 72.0.3626.121 (including CVE-2019-5786 which is known to be exploited in the wild). Fixed ~~~~~ - Crash when using `:config-{dict,list}-{add,remove}` with an invalid setting. - Functionality like hinting on pages with an element with ID `_qutebrowser` (such as qutebrowser.org) on Qt 5.12. - The .desktop file in v1.6.0 was missing the "Actions" key, which is now fixed. - The SVG icon now has a size of 256x256px set to comply with freedesktop standards. - Setting `colors.statusbar.*.bg` to a gradient now has the expected effect of the gradient spanning the entire statusbar. [[v1.6.0]] v1.6.0 (2019-02-25) ------------------- Added ~~~~~ - New settings: * `tabs.new_position.stacking` which controls whether new tabs opened from a page should stack on each other or not. * `completion.open_categories` which allows to configure which categories are shown in the `:open` completion, and how they are ordered. * `tabs.pinned.frozen` to allow/deny navigating in pinned tabs. * `hints.selectors` which allows to configure what CSS selectors are used for hints, and also allows adding custom hint groups. * `input.insert_mode.leave_on_load` to turn off leaving insert mode when a new page is loaded. - New config manipulation commands: * `:config-dict-add` and `:config-list-add` to a new element to a dict/list setting. * `:config-dict-remove` and `:config-list-remove` to remove an element from a dict/list setting. - New `:yank markdown` feature which yanks the current URL and title in markdown format. - Support for new QtWebEngine features in Qt 5.12: * Basic support for client certificates. Selecting the certificate to use when there are multiple matching certificates isn't implemented yet. * Support for DNS prefetching (plus new `content.dns_prefetch` setting). Changed ~~~~~~~ - Various changes to the Windows and macOS builds: * Bundling Qt 5.12.1, based on Chromium 69.0.3497.128 with security fixes up to 71.0.3578.94. * Windows: A 32-bit build is available again. * Windows: The builds now bundle the Universal CRT DLLs, causing them to work on earlier versions of Windows 10. * macOS: Support for OS X 10.11 El Capitan was dropped, requiring macOS 10.12 Sierra or newer. * macOS: The IPC socket path used to communicate with existing instances changed due to changes in Qt 5.12. Please make sure to quit qutebrowser before upgrading. - `:q` now closes the current window instead of quitting qutebrowser completely (`:close`), while `:qa` quits (`:quit`). The behavior of `:wq` remains unchanged (`:quit --save`), as closing a window while saving the session doesn't make sense. - Completion highlighting is now done differently (using `QSyntaxHighlighter`), which should fix some highlighting corner-cases. - The `QtColor` config type now also understands colors like `rgb(...)`. - `:yank` now has a `--quiet` option which causes it to not display a message. - The `:open` completion now also shows search engines by default. - The `content.host_blocking.enabled` setting now supports URL patterns, so the adblocker can be disabled on a given page. - Elements with a `tabindex` attribute now also get hints by default. - Various small performance improvements for hints and the completion. - The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those versions should work without any issues. - The JavaScript `console` object is now available in PAC files. - PAC proxies currently don't work properly on QtWebEngine (and never did), so an error is now shown when trying to configure a PAC proxy. - The metainfo file `qutebrowser.appdata.xml` is now renamed to `org.qutebrowser.qutebrowser.appdata.xml`. - The `qute-pass` userscript now understands domains in gpg filenames in addition to directory names. - The autocompletion for `content.headers.user_agent` got updated to only include the default and Chrome, as setting the UA to Firefox has various bad side-effects. - Combining Qt 5.12 with an older PyQt can lead to issues, so a warning is now shown when starting qutebrowser with that combination. Fixed ~~~~~ - Invalid world IDs now get rejected for `:jseval` and GreaseMonkey scripts. - When websites suggest download filenames with invalid characters, those are now correctly replaced. - Invalid hint length calculation in certain rare cases. - Dragging tabs in the tab bar (which was broken in v1.5.0) - Using Shift-Home in command mode now works properly. - Workaround for a Qt bug which prevented `content.cookies.accept = no-3rdparty` from working properly on some pages like GMail. However, the default for `content.cookies.accept` is still `all` to be in line with what other browsers do. - `:navigate` not incrementing in anchors or queries. - Crash when trying to use a proxy requiring authentication with QtWebKit. - Slashes in search terms are now percent-escaped. - When `scrolling.bar = True` was set in versions before v1.5.0, this now correctly gets migrated to `always` instead of `when-searching`. - Completion highlighting now works again on Qt 5.11.3 and 5.12.1. - The non-standard header `X-Do-Not-Track` is no longer sent. - PAC proxies were never correctly supported with QtWebEngine, but are now explicitly disallowed. - macOS: Context menus for download items now show in the correct macOS style. - Issues with fullscreen handling when exiting a video player. - Various fixes for Qt 5.12 issues: * A javascript error on page load was fixed. * `window.print()` works with Qt 5.12 now. * Fixed handling of duplicate download filenames. * Fixed broken `qute://history` page. * Fixed PDF.js not working properly. * The download button in PDF.js now works (it's not possible to make it work with earlier Qt versions). * Since Greasemonkey scripts modifying the DOM fail when being run at document-start, some known-broken scripts (Iridium, userstyles.org) are now forced to run at document-end. [[v1.5.2]] v1.5.2 (2018-10-26) ------------------- Changed ~~~~~~~ - The `content.cookies.accept` setting is now set to `all` instead of `no-3rdparty` by default, as `no-3rdparty` breaks various pages such as GMail. [[v1.5.1]] v1.5.1 (2018-10-10) ------------------- Fixed ~~~~~ - Flickering when opening/closing tabs (as soon as more than 10 are open) on some pages. - PDF.js is now bundled again with the macOS/Windows release. - PDF.js is now searched in the correct path (if not installed system-wide) instead of hardcoding `~/.local/share/qutebrowser`. - Improved logging for PDF.js resources which fail to load. - Crash when closing a tab after doing a search. - Tabs appearing when hidden after e.g. closing tabs. [[v1.5.0]] v1.5.0 (2018-10-03) ------------------- Added ~~~~~ - Rewritten PDF.js support: * PDF.js support and the `content.pdfjs` setting are now also available with QtWebEngine. * Opening a PDF file now doesn't start a second request anymore. * Opening PDFs on https:// sites now works properly. * New `--pdfjs` flag for `prompt-open-download`, so PDFs can be opened in PDF.js with `` in the download prompt. - New settings: * `content.mouse_lock` to handle HTML5 pointer locking. * `completion.web_history.exclude` which hides a list of URL patterns from the completion. * `qt.process_model` which can be used to change Chromium's process model. * `qt.low_end_device_mode` which turns on Chromium's low-end device mode. This mode uses less RAM, but the expense of performance. * `content.webrtc_ip_handling_policy`, which allows more fine-grained/restrictive control about which IPs are exposed via WebRTC. * `tabs.max_width` which allows to have a more "normal" look for tabs. * `content.mute` which allows to mute pages (or all tabs) by default. - Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only once), as support for those is going to be removed in a future release. - New t[iI][hHu] default bindings (similar to `tsh` etc.) to toggle images. - The qute-pass userscript now has optional OTP support. - When `:spawn --userscript` is called with a count, that count is now passed to userscripts as `$QUTE_COUNT`. Changed ~~~~~~~ - Windows and macOS releases now bundle Python 3.7, PyQt 5.11.3 and Qt 5.11.2. QtWebEngine includes security fixes up to Chromium 68.0.3440.75 and https://code.qt.io/cgit/qt/qtwebengine.git/tree/dist/changes-5.11.2/?h=v5.11.2[various other fixes]. - Various performance improvements when many tabs are opened. - The `content.headers.referer` setting now works on QtWebEngine. - The `:repeat` command now takes a count which is multiplied with the given "times" argument. - The default keybinding to leave passthrough mode was changed from `` to ``, which makes pasting from the clipboard easier in passthrough mode and is also unlikely to conflict with webpage bindings. - The `app_id` is now set to `qutebrowser` for Wayland. - `Command` or `Cmd` can now be used (instead of `Meta`) to map the Command key on macOS. - Using `:set option` now shows the value of the setting (like `:set option?` already did). - The `completion.web_history_max_items` setting got renamed to `completion.web_history.max_items`. - The Makefile shipped with qutebrowser now supports overriding variables `DATADIR` and `MANDIR`. - Regenerating completion history now shows a progress dialog. - The `content.autoplay` setting now supports URL patterns on Qt >= 5.11. - The `content.host_blocking.whitelist` setting now takes a list of URL patterns instead of globs. - In passthrough mode, Ctrl + Mousewheel now also gets passed through to the page instead of zooming. - Editing text in an external editor now simulates a JS "input" event, which improves compatibility with websites reacting via JS to input. - The `qute://settings` page is now properly sorted on Python 3.5. - `:zoom`, `:zoom-in` and `:zoom-out` now have a `--quiet` switch which causes them to not display a message. - The `scrolling.bar` setting now takes three values instead of being a boolean: `always`, `never`, and `when-searching` (which only displays it while a search is active). - '@@' now repeats the last run macro. - The `content.host_blocking.lists` setting now accepts a `file://` URL to a directory, and reads all files in that directory. - The `:tab-give` and `:tab-take` command now have a new flag `--keep` which causes them to keep the old tab around. - `:navigate` now clears the URL query. Fixed ~~~~~ - `qute://` pages now work properly on Qt 5.11.2 - Error when passing a substring with spaces to `:tab-take`. - Greasemonkey scripts which start with a UTF-8 BOM are now handled correctly. - When no documentation has been generated, the plaintext documentation now can be shown for more files such as `qute://help/userscripts.html`. - Crash when doing initial run on Wayland without XWayland. - Crash when trying to load an empty session file. - `:hint` with an invalid `--mode=` value now shows a proper error. - Rare crash on Qt 5.11.2 when clicking on ` {% endfor %} {% else %} {% endif %} {% endfor %} {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/html/startpage.html0000644000175100017510000000674015102145205021761 0ustar00runnerrunner{% extends "base.html" %} {% block style %} body { background-color: #263238; font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; } .wrapper { width: 800px; margin: 0 auto; } .header { margin-top: 120px; margin-bottom: 80px; } .title-container { display: flex; align-items: center; justify-content: center; max-width: 600px; margin: auto; padding: 15px; } .logo { width: 105px; height: 105px; margin-bottom: 20px; } input { font-size: 23px; outline: none; min-height: 50px; width: 600px; padding: 8px; box-sizing: border-box; border: 1px solid transparent; border-radius: 8px; background-color: #4e4f63db; color: #cce1f7; } input::placeholder { color: #bccbda; } .bookmarks { display: flex; flex-direction: column; } .bookmark-container { border-color: #222d32; background-color: #222d32; border-radius: 8px; border-width: 1px; border-style: solid; margin-bottom: 15px; padding: 10px; } .bookmark-container-title { margin: 0; font-size: 25px; font-family: monospace; padding: 10px 10px 0 10px; color: #84b4e6; } .link-container { margin: 10px 15px; font-size: 18px; } a { text-decoration: none; } a:link, a:visited { color: #bdc4d4; } @media (prefers-color-scheme: light) { body { background-color: #f9fcff; } input { background-color: #f4f7fffc; color: #4380c3; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } input::placeholder { color: #7b87a5; } .bookmark-container { margin-bottom: 20px; background-color: #f4f7ff66; border-color: #f4f7ff66; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } .bookmark-container-title { color: #4b91da; } a:link, a:visited { color: #4b5a80; } } {% endblock %} {% block content %}
{% if bookmarks %}

Bookmarks

{% for bookmark in bookmarks %} {% endfor %}
{% endif %} {% if quickmarks %}

Quickmarks

{% for quickmark in quickmarks %} {% endfor %}
{% endif %}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/html/styled.html0000644000175100017510000000142115102145205021262 0ustar00runnerrunner{% extends "base.html" %} {% block style %} a { text-decoration: none; color: #2562dc } a:hover { text-decoration: underline; } body { background: #fefefe; font-family: sans-serif; margin: 0 auto; max-width: 1280px; padding-left: 20px; padding-right: 20px; } h1 { color: #444; font-weight: normal; } h2 { font-weight: normal; } table { border-collapse: collapse; width: 100%; } tbody tr:nth-child(odd) { background-color: #f8f8f8; } td { max-width: 50%; padding: 2px 5px; text-align: left; } .hostname { color: #858585; font-size: 0.9em; margin-left: 10px; text-decoration: none; } .note { font-size: smaller; color: grey; } .mono { font-family: monospace; } {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/html/tabs.html0000644000175100017510000000170715102145205020716 0ustar00runnerrunner{% extends "styled.html" %} {% block style %} {{super()}} h1 { margin-bottom: 10px; } .url a { color: #444; } th { text-align: left; } .qmarks .name { padding-left: 5px; } .empty-msg { background-color: #f8f8f8; color: #444; display: inline-block; text-align: center; width: 100%; } details { margin-top: 20px; } {% endblock %} {% block content %}

Tab list

{% for win_id, tabs in tab_list_by_window.items() %}

Window {{ win_id }}

{% for name, url in tabs %} {% endfor %}
{{name}} {{url}}
{% endfor %}
Raw list {% for win_id, tabs in tab_list_by_window.items() %}{% for name, url in tabs %} {{url}}
{% endfor %} {% endfor %}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/html/version.html0000644000175100017510000000234015102145205021444 0ustar00runnerrunner {% extends "base.html" %} {% block script %} function paste_version() { const xhr = new XMLHttpRequest(); xhr.open("GET", "qute://pastebin-version"); xhr.send(); } {% endblock %} {% block style %} html { margin-left: 10px; } {% endblock %} {% block content %} {{ super() }}

Version info

{{ version }}

Copyright info

{{ copyright }}

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/ or open qute://gpl.

{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/html/warning-qt5.html0000644000175100017510000000155315102145205022140 0ustar00runnerrunner{% extends "styled.html" %} {% block content %}

{{ title }}

Note this warning will only appear once. Use :open qute://warning/qt5 to show it again at a later time.

qutebrowser now supports Qt 6.

However, in your environment, Qt 6 is not installed. Thus, qutebrowser is still using Qt 5 instead. Qt 5.15 based on a very old Chromium version (83 or 87, from mid/late 2020).

{% if is_venv %}

You are using a virtualenv. If you want to use Qt 6, you need to create a new virtualenv with PyQt6 installed. If using mkvenv.py, rerun the script to create a new virtualenv with Qt 6.

{% endif %}

Python installation prefix: {{ prefix }}

{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/html/warning-sessions.html0000644000175100017510000000331215102145205023270 0ustar00runnerrunner{% extends "styled.html" %} {% block content %}

{{ title }}

Note this warning will only appear once. Use :open qute://warning/sessions to show it again at a later time.

You're using qutebrowser with Qt >= 5.15. While this is the recommended Qt version to use (due to QtWebEngine security updates), qutebrowser only provides partial support for session files.

Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.

At the time of writing (January 2021), a new session format which stores part of the needed binary data in saved sessions is in development. However, it unfortunately wasn't ready in time for qutebrowser v2.0.0, as it's a rather big refactoring. It's currently expected to be released in a future v2.x.0 release.

As a stop-gap measure:

  • Loading a session with this release will only load the most recently opened page for every tab. As a result, the back/forward-history of every tab will be lost as soon as the session is saved again.
  • Due to that, the session.lazy_restore setting does not have any effect.
  • A one-time backup of the session folder has been created at {{ datadir }}{{ sep }}sessions{{ sep }}before-qt-515.
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/html/warning-webkit.html0000644000175100017510000000765015102145205022720 0ustar00runnerrunner{% extends "styled.html" %} {% block content %}

{{ title }}

Note this warning will only appear once. Use :open qute://warning/webkit to show it again at a later time.

You're using qutebrowser with the QtWebKit backend.

While QtWebKit has gained some traction again recently, its latest release (5.212.0 Alpha 3) is still based on an old upstream WebKit. It also lacks various security features (process isolation/sandboxing) present in QtWebEngine. From the QtWebKit release notes:

WARNING: This release [of QtWebKit] is based on [an] old WebKit revision with known unpatched vulnerabilities. Please use it carefully and avoid visiting untrusted websites and using it for transmission of sensitive data.

It's recommended that you use QtWebEngine instead.

(Outdated) reasons to use QtWebKit

Most reasons why people preferred the QtWebKit backend aren't relevant anymore:

PDF.js support: Supported with QtWebEngine since qutebrowser v1.5.0.

Missing control over Referer header: content.headers.referer is supported with QtWebEngine since qutebrowser v1.5.0.

Missing control over cookies: With Qt 5.11 or newer, the content.cookies.accept setting works on QtWebEngine.

Graphical glitches: The new values for the qt.force_software_rendering setting added in v1.4.0 should hopefully help.

Missing support for notifications: Supported since qutebrowser v1.7.0.

Resource usage: qutebrowser v1.5.0 added the qt.chromium.process_model and qt.chromium.low_end_device_mode settings which can be used to decrease the resource usage of QtWebEngine (but come with other drawbacks).

Not trusting Google: Various people have checked the connections made by QtWebEngine/qutebrowser, and it doesn't make any connections to Google (or any other unsolicited connections at all). Arguably, having to trust Google also is a smaller issue than having to trust every website you visit because of heaps of security issues...

Nouveau graphic driver: Should be handled properly in current versions of QtWebEngine.

Wayland: It's possible to use QtWebEngine with XWayland. With Qt 5.11.2 or newer, qutebrowser also runs natively with Wayland.

Instability on FreeBSD: Those seem to be FreeBSD-specific crashes, and unfortunately nobody has looked into them yet so far...

QtWebEngine being unavailable in ArchlinuxARM's PyQt package: QtWebEngine itself is available on the armv7h/aarch64 architectures, but their PyQt package is broken and doesn't come with QtWebEngine support. This has been reported in their forums, but without any change so far. It should however be possible to rebuild the PyQt package from source with QtWebEngine installed.

QtWebEngine being unavailable on Parabola: Claims of Parabola developers about QtWebEngine being "non-free" have repeatedly been disputed, and so far nobody came up with solid evidence about that being the case. Also, note that their qutebrowser package is usually very outdated (even qutebrowser security fixes took months to arrive there). You might be better off choosing an alternative install method.

White flashing between loads with a custom stylesheet: This doesn't seem to happen with qt.chromium.process_model = single-process set. However, note that that setting comes with decreased security and stability, but QtWebKit doesn't have any process isolation at all.

{% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1762183912.534639 qutebrowser-3.6.1/qutebrowser/icons/0000755000175100017510000000000015102145351017243 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-128x128.png0000644000175100017510000001443215102145205023270 0ustar00runnerrunnerPNG  IHDR>asBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDATx]x_}b)z|P)RPi*W\ @ZB !@B u7ے ȥ?wll}Ѵ nY] Vrjb-ŕB"r>D@\f`:M|BEBrea)[z6Yf}ix \꺶텿4P:.$²LJf4J]2Y%F Kb$<J#D$A|XQϔ X@qlm=kjO)pm)Fj"-+wGs=@J<2 Csd#33(o0ݙ Jhq­K0in%f"Hq%NPRDb'D1~?1"\A32 T ,g/:Μ:DF7!jBSeT\r]]%w: h6"g @n*Y,B@Qil_Q;b!o?H`Wݍ;Y9a bȞSBc|\1<>T)i5Ҝ<o_JЂnT–;lldJz!n]tLkh!J-}`m%ao. Vb"g^GGYVɰgZ.^Z;l)vlD/TO͇ 6r{>=]TJ >+'H@c3g *n;oеE'1}Frg;υk(qa4m:G ,o.ڲWTs m_ t6l|uE wh C)xPO5@9̥v?aaaCIvS0z{j64xibxqP8=8Pxt|8y dh+wBKCK4bU <}hy/5Ҡ)࿼/KHPzU{ }a@H WLpvwE9H'mmRP~w w| J2Pc lb8c[-/leG1~N;]| b^CAE*>O|竚-(\4Q?p}N"dWryϿs1~ߙ7o\B Zb ޡ~  e% T@OͿ£;DhߴΐgI&"=ԵU=k2{Zs5B$'I\Ǐ;evǨ0eyoǏ02K1X+r#RpgDZ0w829^:ii/M 6)&j:Mwxz`Lj[Пw8(BXNR@L!c}'1)VB_<% .b}OQo&N (x| ,8]ز(Y_#;o$?uecݝBtɣo@N\J}|1X1\ Ϣ;}-('/džĚх3{֘{Xo(ȶ',ΑZ#k(XPJ@JI}JyDaul r&Yԏ6<{ ,~@;1`aɒғ jZB'(AnrcO!62Z$twMDZP\zѧʀ,`w{c[,<ʨ\lŲ+ DnoVr*Dqx2)QBNWoR%/Y)D_Elan5)8o Npw%`\R5|r@op~J,=qS5r6TsiwHL>2AV[x40sT ÔB"Q͗3D`nڄ(_ԅO@~ ^.t JpADp}lTx.Rl_ViΪ=Rvc֟$D= ˕8F]@='~sV y ʈ RG#KYrE@ &견ÒEĘ`^o~_4aW)BvѮL?%b]Vf 0]ˍu4TߤHZԨy; 3>+7(`)õn:6_F#]P6*Ϻ:}iGQB'r=6QKÖHZ\n\(+h_3ޙF rIA9zVs|J^mrT蛢m0QL̟&Fr{NZR_BNWZ!i1sRu>hIuq]R c 9v}f 1MZ-bO=_`A^fݾKP4=xQ{2Iϻ7:a]},$8!۫@}.BQ3w+s( bB09gBYQ-" dlj_#SɼW0{g}*1.q/ay/|XK;Av;BAlqӱ)QB>Qq<,X5'C]G.Rƺ9|N4[8-9ހ[s# [ `{ iiZ)`e.dzm_Xga\yR'79@vE-:,9\vY0+ŠSxZۤK)X&{-oB{kL(R'= k^0 4e 37sxxTILA(xydA`DPnȜq%wZbA/^.e8&f~e3fHx>bűR(R! Cuv}?[6 D WDI>6j{-|8yI#((TDTff(Y/+Q Z- ~*`g>A*3࿿^q\Jh:Viاa%Gmkj"Eq#lJaTb)Y\1t*s/D/ڣ/'G=B0Z&WRo]o] nb̽[š?%I 6b"rY/d1B>Pzex3;oqw3ٮibb&zYkl)4Œy&-.~xtKczԺ>%RZQ]AOS:|V8A+<;IB.qs 1 !2f_Qr \zJi)k8Ng8i&-0DbNX*~;q`iϥ9FDmHm/Yhb(31T+N]+e (>R#;?()6*;8N6+gQ4Ι6cV`tSꚸJC'nG.0a~Ռ{b@|(m:5MF>Zj m(弿a=qUT'a]85w ٨>`WI<=88.gSVtciېhcW0(U\&Q==]0{ow9'؇RoƸ +1$*R8ZHr_f;?1̔H"aKScz w:`,]oI.x`th W+CLRmii,~w4A (%i?30Cތ=b!GZԫ/p]\[iUaÿr|/0IwrPͳA#UZ 7 $%sQcƏ["bDyI?hpO/Co~%ͣY.&kJUM"66oJ2EvYTٹc}g_(1Rw>Sivo\XT VX_E|9V 5F`N KW K;~/M젭& p0fk}5 Z+B1K!5:u-k 7Bd%ĥ9D9DWe:,A\˫ E/ ?:@k̯\tz?owz>%]5~_XD~:$92ml>@XHz'?IDJI-a8-g="udixH\OpM "V~ݶG+.y}d*Sv{pjyD38 'R)Ul/Cg/04Htˊ&ĭAPYu1g]ophd&+O5_x.U ]T+AS!= m4pa!'C0*/`!y6m# bLX!}s(|Nx =9%Gn[+3g %lC%J~]V%]XylR(U,ES<#`ʒ?MUd~q9w=UX&Fa檿oG&mfeP2rmiӟB ԪC0b&Vi~a*^aQ^0Ii@Ag"Rp"}K1피V`pl1P855{h pV,*"-%voQ2hl+ppRNk)fj҂RqG?gRIXUNs \} N(#ƈ<6M!, =Z)+^XfSt8C ]_@FmU0-I&l,QjA_ ' :+xe;&><, Ws-V*dF"jp$^>w)IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-16x16.png0000644000175100017510000000153015102145205023113 0ustar00runnerrunnerPNG  IHDR(-SsBITO pHYs B(xtEXtSoftwarewww.inkscape.org<PLTEݠܻ~zέrӐ@h_o都 9n ;o?t@uAtAuExEx"S$V'Y)\+^,W,W,\/_/a1e4^8a8a8l9m;d;gh>iAuBkEmKqP}PUWWX}X~Y~[[]^^exx|~~ŀ܃ʌ͌ΏғҘ֚ףߨl1tRNS *,=APTIDAT#BaS(+<ٲ2?Ȟߵ3Cf<2$W3vHܙ Df] @y?IZa*8eqC$~uZ\<(FXQIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-24x24.png0000644000175100017510000000247315102145205023120 0ustar00runnerrunnerPNG  IHDRשsBITO pHYs B(xtEXtSoftwarewww.inkscape.org<PLTEmug߃ 9n :o :o ;o ;o ;p tAvBkClF{H}NOvOwOzPvPwPzY[]^acekkmnoqrsttuvvvwxyy{||}}~~~׀͍ғԘ֘כڠCtRNS 04478:ILMNdijlmnpG!IDATM_q𯔽B>+27'I6gE5nz~KD=v#V4ZUrP2Xh{΍[Fդᐐ&EߗMҨ/FLDm W]DNR`,YP`jy_~(w q,Γ]t4\HdKj .EȤcݥ70Snwm0x,-g'iwgpٽIRLȓsɪTM~ 2gݟ&%G68&+HL_Q{fhJE4R'X GsHe' r,%: +ƭ;g!(qz0|O\ 2:f?$"黤B|%{Օr檠b-j-{ ҮaB܍$m+IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-256x256.png0000644000175100017510000003257215102145205023301 0ustar00runnerrunnerPNG  IHDR\rfsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<4IDATx]|TU]f(m׮W4EA)"$@@B@脔I&=B BfUI;?1}sNG-y-']dmٔd>F?EŒz8;#] HOC tß)N]XU+R2;9tV\Ѻ{RA \+n@h6P 40G.^)9zx""2Ӏ22ZFiw?躧xM3Yr1Lp+dp!zhn]3 |CP\gKh-]g0 :eTBiX^Bf'7tSӪ3٩n!;G2߭io { wҍm?3GߓNF n̡V0o|J֯ Ǩ:מqו0nQ>ݬJƋXNCq^۝z2t{՟r^ͪ m;Po*䘪;3|vu`?*v9ii@P#<I=CΧe++Up7Ap?s7h-?3!3@6աswf\5?,~C %`9Qws AE>LJ$@] #A3ZN6agYG) *"Mh-@l? TZ:m:غ_{{eAV&i,7U 3Hj1V|i2v;g Vdcsca!4NA Ɗj })z|C]J Z'|KcBrK*j;)tl ¡@Ywv 81=o.; ;x)h3Ap}12`fP8ga]{Bq] z π)Qy<&8b6"ke4Ajʒ"kRJP0u~cjЖАwV#K(=M7 ÀLo\ڕpU¸^{Wl4csI#mw24pP3-y"|Sbxsа&XV#(N)E^ ͪ,P2x1EWқ<8yՍ uG$$ BEֵ3͖ցݨ\V)]SGkVzWalnؽg~ 3̌:.tp'ݙ02s0YvǃوqEuml%.Q$Fߠ?9^ۓ VHE_!ڰ][+:QK0C YuoX9t<>)kb95÷}?,MPds "`I=BVa~6jd gǮ=p96Ǘ|z3(ɻ -. m* :Zrhnd?p?p5GT^xFׄ&;5 X\@h h.8umZ*ןKÀ˸fnw}>6]'5 O쁮Cq0$pXAn%tZwfQ\즀UYw?saqO֜A9=  hWp a'>bq[Ϩ5n397wNؖxV>V iÆAbh?6oQ,ЕB e=.ϨQ'o EMHW|A.ف};wCý]&r%:Z%֬x܁I?CxyXi&/5`aDg4(bw wK@S}J*kR$!f5zILj8-m<a~`f3^Qn<DEpfwg-vHHX,][LSwS,#TÿKHcJOKT&:jxP<%H5[Yi5l,]|nSa. X+mY ^wb^6}i ,*Jy!xgyp"[ Y&ާ 'PBʮZ #=B,Q7MGv;Wo ,P%N? D# hV Gp$g UoO`~m{ +ȸ hʖoR,۵<}cl{9g3$kn-2!BMv1Nh6}Ike%T8R|́7Պ? xx2Pk]ʍVŜ 4\CAϴ X7("{,b .3u>wc.BLHTT@1t>3?rM2C͆;(2 ;R~Sm@!uqPZ pgRra|>zCf$)d lI0Pfv̤S\2A>y[=߮Rm{Ed=,7wm( B1gmӄ@ eC ^~@S c5jl"j-uM ubE&x=f3t>708O=~XٮQ0F?#}RPt Gf!~p+OrB2^>N E2]#vHv}'kQZ?ɏA@.xv/G{Nd } LQ#UǑjB`s]@l/bIyZ%zwNwک7Dz"NO+%L=HD)<犹ͼl5oo׎$ ' [ix\&D>uJ@|x\h5X\U ȴxzy$ׁs-,?qKE@X>Y]F-"ot6Ps[J 糸J#WJ՟ ]utu瓺@޲m`LKčӦ<">WQOƂB"lO71m0/$k/̢]-_>FOv|W\мr)+m{xCG s«MrQj!{:gq01Oyq%f{_L-Q`F6eϬrj`! lrbc"@@B lnLVj NA%#@[S N@+twHC 󠪩2w}KMÑ+jd,|pjo}7$`=[K+0WO7L2{E'JiZ& &z~+ n>/Z0v2m7 ^v;e'`Er?pJh&0}r 9Ч eo$j&t1b+:&&8,-8f04("oT B%Eidަ$d$K([ |B:Q/Gg~seoAe5+ΔL8 VZSm6 AA@ @rf}<}|ٴj8Y'-Suo"xғZ,t!@\h03[w\Y/vLjۨSm>eu Fēfubz3 Kz{a^g}|iXid4,L[m1˘?@׉_W {h>eLO>dP=,z?2z}pD%芌yt ׳J|%&K(Vz!ʛ(]WCqMwߎ}4KG#`9J >laK(|Ȑ}G grꁀ>zK΍n1bPD) lCgQ2PC.> `%7@aU 7v[yD5.mAw`Q26Jav I%}w9֬Jluݦ&UJ7F2nɐ@T7~)ٯzN/L,^5s~fݝ,f M_F)JC 'fYaZ1?rbjL`5qopW #>GiJ}4A@ZԶE (k n-?n^AM9p^{:˟1ڗ! p vwBPY10`Mk:J3l|pڽ,~ !8<(,{BPFD]w-\AI.yf^_4OPšˤm되 ۄ E7-URj&̦rǧ>VC^*!)"9"I*%M.m7<^~)> G{BPЂW Gs;^w":x^ sasʲƆ$0eW-A)x]3ДNgfwj p4IEQQ$Ul"`%2U-#@u%@~XmnH8RֹD}~.*o27rPQT@q6՘Z9 t7<@<@ TuSl!)ߞy=s ͮd|ei!Z vI.dvHԚR ]3f=Mj.`"6%V8NVhv#8HQIe6lO<+3h{~T ~"h84۠Hh'͸Zl\'ZۍKPtܯ!TAPa@(=Jc]ň :+oKBa; &SmuۋCGzLyBu]Ma8/^yal>?> Vg? B(w&k ă`RBON`b.=eVW5ۀߔ_H-Kt %(2g{QNcГaj֮W1ö4[ZA- w9 e-VǀPv ^c N)J̈|f! y졖{ebԔ ==ItBWNlXs!`Ev)bG0ߩSpZ.'ܢs-bL֐ oI[.Մ0d,<_ӠAVUoԹZ!D\IZ +j1=iB=O|;-YlԲFWɚ4VwGE0=xǦ?M<}ri`Vj\#0١I([>ɼ*QNp_ְ?xgDBI-6xTVŸۆjŞVx%| @а%!>._ _?f@̉Kw|pG?/ m\L;҄ 4}:wf;&v+eTa{-~Lwo^ߙ w%:,~bO^ZYIjU߭9tay"}P }}%SF \[-VMiG ^<3L^RC]Rj%ttB-FOOƒI1+(U2i&仅B+-- iKvP&#/յ6L6jT-j7}Julh1go:4?U0[kJ9^Gm{πOvZaSB'4I*TT/ymN<[ >mW5TWsgm'4/jXElPoT >]r8RM_VHuV έ\= ]mJ+N,4G2v !f,"ԒВ mn@i <%JɎݱަE{Mz#[+5Gh{V(#;9*h+秌ǡA9'|yex{ZOI~l2|_f@W>Mmb޸J2j;:8Z RM9!78nQCXaDσc$y pw}*f-)əc±H[| ʆ>?P(GY&\ Uy&90Wg,V-W}X;W"g`IJ0u7qWat? ut/(ynJ] aA<9?cwط{[77+j`.-őDH7ttĖ{ܿKPdQAT tQ7б%xiuOIwN 7 G`0y5Ajܳ-Sp_geŝ=.{u Cɱ("6M.p/"gZto˵(X@AN%l$B0Itẇc!  ^[fD]!vuB ƕz!R=!B%{:S8CW'(? 1$pMщ=n1٨TSD `⼠ڽ׊(f>I1Tl/@_EA!-e/ c9rfxՀ Rr[tR {,eRbyb}G'0R@7l֞3UQœB+@+vkO\U|=HXS)v>xj0l)ZB\ ,H)+5[{d$zm*M&@|qpgKa9K:H A:Og:kA˅-*&@@r0dә]6NC^2PlKkBd}E <$&@@5֋kS܊IRbU!BJ7LQ6ʂҩaOTiBmfn_4?JR GABٝBFMԩm`y"F, M4!B2D#[2oЎ?M& xY,=#>שyHG,BL_M4bs iaTTZ6Obz^VR&on).7ɇi4!n+NKl2_b@1sJ',2U0aV7{upW ƌZ0_!6 Dӄ-߬q ٲ?L]f߂Q}KaQw{-n?բO1/6l ʾ)G8"&`F L ^!ξl6_z9^\Vzb0b ZQub-oW<{e(JvG  o>NXPЛ|HJ*0jil2$\4Lr%oO6<->Cb*`Pa;ozzy5| b7${r;`w|Ld y,w)x6j۞ iUCw`<`ʱhQaqoJ3Bl3g^̒ f Gx KMmgX1<xR/b}6SRaʮZf >G>_Lf#Wʅų)f8bE^[' fћZ= mXZ[鍚T@aR [*W2RkmfNR x%U^ۃ Pb1\Ao1lw\]7(f: fQZtji 6!Sj IW ajS6`0 =!e[d.o35An CbBGÂ+xI_ͷ6xMƤ+MuJ?;JVx HCpuo7b!A#rv(;$BQr2=|6UGѰyc4S\>GVNi3 ~x[LƲޓ tx^la2)0Q *࡮1]P3E᥃-1Im 4e Tt܊Fo3 `Vۭ<4P~@9mh0u~;Y" w}MS3 1G2[/&9jwF\}2D~l@\b~Wv*X/J4#<WHI_W´ݲSІH2d+L0dQ쟙֯tۨF|25&*.2idsmtr !; ŏ%bl.ݗ€VqEjG^Xf cNt2a<m!Cs#Fw,7R@Ѽlڼ-@)jc?=@v^5V퇶&j:w*՞Il1f C\GW[l/n6ؤA*'Q޻IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-32x32.png0000644000175100017510000000313015102145205023105 0ustar00runnerrunnerPNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDATXåWiLTWmK5G[֤vQƚ(6(jƢUdXaPUDED2, @ j16mQ:olMN2}{Bd;kf.Em@ˏ5| `g9i{-sirǠ` ؎[1]exWJ`_ 68lq KnixvWնq0wrȵPev{N% /?I+u1[-U@A3m+01"(<3gK;^7.I,jYliduG}5ވv o̤Jp?FOƀ#ڇ;so͉4U]H^3Lߦ_ŰY>TεHƹ1`ꆀw.9g$'Ul-9H=e&`dQ9sֈ)"ǹ)El5q2- RQxW,r\Ѥ2-x ίyĴkɁG. !Xk>7RF5Y* $T?Ʋ$O\Wv=&U@۽˽T,К4F|+q$ 剧$f:!i$µ7X%׸SVgJQL"=*.~?o̠"e_JyVOEj ǯ?Yء/7=xmf!EbJ vSF9Co6rnlˆ* @N}Lht>,!JDaA"+z;El9K#KHAՂK1[+Utc 5BK/Қ BdE 1%IJ]ЙQxg>1 O WlC~LB= )&LϿ S oſSOЅV "Kzz%)dV Br_i ]ωz)Si{j%ԫuʟ:E\N-a$IgR.9F/Ik<ҋN}lxɫ.dar=irg 4(@Dž)FǴ&;FU$I jYQ+ d^b->\E̎-8႐ )+[r6pvTbޢ*lCOx |p*{lۧσiĜ=vUm?l2d"-ׄ9FnA=^=#񎣞tV./( IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-48x48.png0000644000175100017510000000453715102145205023137 0ustar00runnerrunnerPNG  IHDR00WsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAThZ{PT?&5&:hgLf&&<*FcP2іFkJ# C"H. ]]1ic|u|v .{Hveswhs=1 (ǃR n-` KFU4mS](#s0`xŵHPK]_6f5*ί넨c *kU`PԶwcb~M%k -^l=?/foRܑNL^m5yݑVq >bw ‡xaU[ ]SKDgExThqH>~ ͂S3YMxr"JP?QY xc݈hNc@`_- 1jF"iˣY}M9II˱JS2U0"ykPm`IHv~ YKXK7W`G⛯v_!'٣8TjBLrW+lO },VmyL'G_CZY[RC`‚`7يƛSm]?ǝ׷W 0E W1`dExF/]!0j?2Ɋl<eD'!@YB9Ll@=Av6C!@BZLEK~ne2Im!]/DjzJ<ZZ)81m\|/fHqe.pydȩm, ϧ90N\%!S?0/ S鐃77)(Dʪex@}3o |RnMeUMWCM Ope9dwG.s)WGrJɥMTM^P>@flÐO~;)Q(hfS:P79/icfY'5],rxjݎX4d&gLw1C!/._{ =Q<ڣ(-#@ܺȵ{O:%$ҋ ocZ|Ws%oK (K{:F"pGo`yb sr$.r#$jP陥nqx"_)UCe К&)x^`tގ0S ~vD ڄ=]+b~=t%h(]p!0j:n: >%9"{Zq%@ljW^-tXo軄͹xH+w+ɵO+bfgXuO֔h#IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-512x512.png0000644000175100017510000007263515102145205023273 0ustar00runnerrunnerPNG  IHDRxsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<uIDATx]xUUletԱJQ(A!j*@$Bz/齽B h*ݻ{n$o}̨$ݻ^0Lնy3yY>>Yb,prZ?;OI;󇦤Q 85)?__L9qOK4v{ş?#ؿ'sr ]d?#$NMMONKc339{n.o]lmn]ίw-H"`0 cB76*[2;2TD`?xFZRrZNÙhqhsvg{^19>x ti1D.ͧᳳ32<-wq #`VO׆W`kq0l㾳[ZDt <`EڼlshuX:tNeR9WD@u$cqc9;`0Lv0eS94_ ix}M7DT wY6n 093a0 6c%vʟ% i"f 0bw~^ 겻\;ZK@Ƥdutvq$|>NLo; ;u#3,W J i;҃V0=D"}Ggmq q ,F6&'^;+ c3t w􏧣 wwFWB|D2]$Ep`u28j6>Anpg`Mu*)Dv󬯼_9ae˕cVJvg/$`5> ^D0UWCө^ \㏸[50)fGykD9 vl{hw,q luW wSRHrgA~BԃN1 GA"X YY<)\sz:/+1+KuUOK8Zz,wΟL))P08݁(賀:rVX=v-P)rHȄ'A bqF} fs-DlU\3U:G`b"lqPSDGLDDKNrJ9*\@GIDKLO6 ~K'F?n9+@8gsC iFn'607G;4kVȳ7Ea0}xGAL^܉E<5r6_ ݆ i+Tv%Ф'+ mظ{yKDWLf-FNrHU[Ǧy_Ah -̶ѽ0lwWĂX2# XΚÕ1\fBՁ@G,6gD\Lf)G,v~}EԮGlva:1a|e1X jf;tF;nq7Ah: zA`- XTOa0-ʪs[Yj#H=kY'C6na1:7EbhȮ5Vb@z::"4 &a]nYwjavФBrذbz!άf`^^BÅ`07Z؜IWS)+~^Ƃj`HUk=Kx-~.7 "8|= 5"s$ri85p"k6~0I"8 V[E{A=|5~\`u!3LA0D.vcĵ,E#V43X;w.*`@,6zLy6L,U\U8g?T+*']ݛXq; ]B+qE*J5#Ì ~ \Z{kP_+Bx:dMq&[xqY : -t3yZ*Xur|V PbM}j[`(NVpYy:5m{he B~Lcc$0ٌDzhr2X* p>˄hsӮ@zTkŰa7zkwFm ͻL-O,Q`Lz-0HKٝ42#!wZM5, uFAIz3س4xpXu>Ϧe6pW^S$rJ/DPP@AY`lj'o$v.#H!}ٳA5.ݴD{`w2paÚ6aׯtB׈@5(3`jmI~W %:#wg`Q.^ ƅYeA-732xZ/mGEFP:͍򉭚0=n?vc镯nvL}Ĥ¸B7֟^ cqv+ !4H;iӻ`jlV&M󮆄"j&yg-5~.rȞ]]dUjĮe~cz@8g)_I˼khRSe9Mģ`yT֘ ! S?kȕx FA09)iiZo>JKi:4O[O:ߌ_M. ȏͽ- Vg_nRyB(KisqJލ6[gP=L6MgGlw}xVŹ+wYw>sb6v4"D4bq=X$Z 7ȉ;{ңGsSaQµɫ, aX[Z2LGO. T x=5j2-@ Ibcдׄڿy!in[qMLv~L/^/ H_BMʡ^>韝f M { E,3d{o>C ls?J;BdMEH%MKs_Jͭc;@P0ub7Dңľ mzueqP$y/X 涴t*s[8KOp=:U@ Krm/9LM̡ƞ虮!tW! g~7!tqC kpcϪ/Ƒ8gBIuVV]۲X@XEa@{**JZuN';M! u?0 ٞ!6oX>`2XmF&Ū*X[֒ͥ"ϕ>gEzh5$  ҊQqe}fA@c _J-Gnw(ԃP* !41lfj3H>еg zg CLF)M@dvgq L+~Ege@J":uDTBcAMt,2^zL',!TI36jy98irk`<5 ϥO7 _N؇P[_OU3i0#<'IԖrrzw!s5Pj=zv`@#鼞6K@`cH>gG8`Bf3(~n)6y  Z,Dsg[,%? dlߤگ7xW#A|j@^u!+BR8="jrB3y`'63&bQ3] bzɚ}hCfe7Z?zSAn@ҞWNA?`JZD5#N?re?j RT/rKkv"BO fdcSDE+n 24!bKu ! DDjuDcp o"!$hB^ uY `set ԁMϠZ#o#гB@^&t}-Bkq8"A5l3iN>hFٺE} V0e`R-/ |УЊؾ7ݦ! 䜝C/QtA&Y-?` *@ PC=C w]B'|/`V-&'Ej'~G4%"#\geQU-Vu%j:x ݎt?98cg`,⯋ޱDK,VEݽcK}ѣM 530mJڽLBe'%jlO r`#cyH*6j*_ʫ}ħDM w czT. U—ccƃ@@CW1sȸx[j'vDd<6, l]{2sT_6DPOMHr@26V+AItqnfwfv'=eEV~ Ϣ`v,:!X^+GվD M#ܻ WSQT._'AoZl.7wb3AjIB&$֏oZ8/0}0n J/I;B jY<9bƿ.m-9Bw7Bp h=i-+rxxTGa\^_YGΊWBP */-MO`~vJcKC@ u4:FPW itL.6'q۽BH:&zAai4,<^xf@r#{LN_ M-lm|T@u OͧӷѴ|K.>CxlЗ}<34$18_3L'T!x#oE4|*P,YAt\)?N7qszȝ&f#%CP ,fLlcD*\y5 0k3zEd*sJ;pb0ABƉssr=z#)FM'CӬ[Z.ۂ\ʒ@sr<Ԏ^ UwTdQC@@!QPB|{erQv=>NEQT! Zők06% {ڃ%sJPKUlJ%/ewӣ#hINvvqC@@l(0T)tCr/i>³xe[T.gAzTVT>J.3LKiK_j c;|֯ ܲE`zE? hn3^Q:_ѝ"0cwR9X\䯙QEEgAlk꺟t%O_>Tt0`\rs"2QIe ~),ˆ ƂY]fJWe\ē)~IC}#G+ *+cfoUۅrJUF.QRl?.+wF=wX~]s\Bm@/}x檩/SjY@b 3S*?F / vD. t԰~ KMu> ⦯+ncңGOҫ=4Y["9[ɱHU,1xv=@ʲԺ;}yx<46xu ̤[ * 8J IXwǯb̯i RߴKQ\|P<{H>F G29,69 KmWl6FjYBS86@qr, v6'5Gc1a[ ~UVfQQKDߓOwB}@6W>#$! jHnxJή-{o|=kX&.?owc1%>tXp x*n '=4u{k L%=0,)kXX!73t]}o[پec~^\c>$:A[,Ϻ#lmt?;fw6L_GեCCq?QEa p9=ҔCwM *jBFu >r-3!!O^b3۽BM>+094 apr-OŴ'ZBN0/R&͗>mWW}|ѣ?w8;ȹPz*JjD'7!Q*>VX +g`~Ln 6O"Ѕ`g̝ iWQ[E 97˵`StJ1@ƞ%ʝsZv=P_MG@*gJw )]Q@GA@S{̴sw^לگ%ZP_yp<-~Фx2}R@kVchڳگ0s~ƍsH Д@1VtN}ob!Ro"5/³  bv8zk@)0Zf\!3bfhH24>~-B;%n?$ {Ő q #~ ]v  xcnu}_F{ak@ ׋s|lNHk9x"xG-Ԧ. ȵs4+ɺ^Phw:ɖ?#(QfHɯ؄ W|m8Px/M[/{dnC@ 6 3u \9eNy?c!T#ب\_溔p;@uxs&KXLeIt݇cp$@`K/rL~NxoZȮ9GSH*R. >\%wG[yCFȍfz⣱x_ϧk2 B\i 730`E  ۩9Ck?qR-ey|UWݡyCFtkxG4f)0#vP̖_KO1C `ccnm_۞bV!`*Fg݂z^iիVcޙ&(U @N lR iVN3`\@JY{ xl՝`յK_6=y@lsRLQFkf09TI:]7J9q\ <,U ЪX^Jo  7\ OOU7Г }O |g73 <qCO:8xA眼 uY.G; kjB,ȥ$j-|~ySXOADޱ@< n]e,?>:[_h36N 7.tbW6k3]>fj7[L?T6:1n#4q%pw2 ,}|O!iD|:;6 ý͆gV:tQg4}>v{n3_ "OILKJ}';M2  7>E[Yʜ=/p<F`z$&&e3@vI7{LA`a4b%u_Lב??? 6ӌ pN^%u~|&Nͦ@O}f nZq;B+<)YU? ~/kU&zw`>J~;8nIFdZ|Pg a?Dp i;&>:_E4xv&^i֝qxDNnnP<zxQ"`:^Y %?3]a傸!k(CܝKX6. xҋ]#FPjݧ)lQ<==nF)fU &&fd[0leR p0VuxF~`ĜD[]x⣱uL4MYeu ٻm;~=x۹9k 4 0f챡,mƽCieM3/n[v+2a@n:Dշ"(J%?s_YX DBlPl ~u]Ƌ=@MhK/)>P`V&>8((a^ahGaV@7H6'Xv_a JRt쳲"T]!ik`B"]Vð#;9I n ~ ={anUg\2!:PN_~q+a^neXx<`uaE]@.w(m!NPbElw"EU6Nc OSx&-5; nc wveI;\`6IKuX˟ԥ9Il_yѣ쥦se.ArvVbd'a n} F /̥}6WA4;]5<}y-xY]y> M5vrrǮIgʁY{FZg,3k=ͽh6^ J<b3K861HrfC; ;qOQ)bߓyn{`9ujT;Rg@MaMfkꮌ)|vAjKvR}wҳ`~ڼ\=K/j`^.7l㾫LeA^*ĭSiYjdm}?UW7 ~Om&h-pTaqp^5KAANĭmXR6C_u`qQr3/t?8Z`x/sB-Dq>HY{ 6QjiE$>k  ^&MM0W]j WP' ,W^U2w"/ ai?#*isq01X>di|,@x6oahW_"4"G l6SP<ҫ/f BT/u }XOՠڰRC7ཡT'f@;՗nq4'"`蚳wikXT.~o0I<곳hlE&KvuS7y{3\ }>]u`z1WD@T{Es62bN"@*]*xmt~uSW9!~UhMM ^חo'8}V)idad,w'<;h-d Bo)hI95ڳ]*a_T%&X *,-71F*KK9uh.~ x1,@e|CU`R2c`oC!mxZϳ4%ȧ)͈AEy|교vʲӻ;R r\4 kiL+p6U It R)uxŻ$^LAPQPD_$bmcYV~ MgrwWZyt cR; k)S Zmx#eíFHls҄l[ 2> 77JY|i1>(OƫJ& t\GEB3Oa< '2*2AEFnte^b2JoBӔMbo $0}"7-N yiUtA>AE&o,YTY/[DIX sh~Z|x?/=0.^‹<gZtL Z Z+J,ΤG[BSu ͻ$J6}<)B5 jpm;~ؚ+U~_)i7Ï%Mwlzc^>OR r58kɂN\+N>VCʀ]h\gW wK\ PRS.Epq`G*َdb& u"ynwh- gOA|W3U#&];XF=<%v`bms'Ɓ7v^C{xd">)Ak׋VGNř?-K+٧hrGwFig2C{O}Jw|Z~A &:h(;fXa&n5ggdNjW 洪ϩőBAe|)9Xԓz[%M 5@V[xņϧ嫪ojq vW*I[Q$_c}u |N5}LDfz cC{@/*u]4qjW7YhW @D_ݷ%À ٝU)Ž~sN`Rg&gc ,:th?>X~bI~#NqUP&A&7+>tZU *U@xKsO>X)'&!-$vWghJjݜ+44 X{aN!ѩ1:1^`ǂnp [wT5xU" V^!.wo)87ll<ŋ?' Si*/L-:K_ϛs 툾 9ռƋ])*vdz(}vnB#{ Ы}); 0?wM|EttCS0[vA1%vw3}*[ 'ى[Z. -=bTC5:Z9D>xg*Isomn?J%N#Q\IIg|(@}[a333@Z%yԄ̧K%m<}LoCQjiA~L5g^L6nEeɿع,-$'dqQ)KGp,ܐbݦV=5 ܋I1ƞnsŕ2Ώ,Ksݜet]/B hS v(֚ל> Ep}q6+4^B OII2^-5n! &n5gW,NQ{t[t)5;:LHOTbVScU;O{[3JqI8頡A3 2U.W-FaZАt?*ݟ _)֜A6[Jw,|z /F@~6M/iiI%.Ȅv@" .lb`|q7Pä $EPHH&n+6 2³@'@wd+%ҙ_vlbvI b<)_QzŠ^OvKU!Ijw@{>tYNH]$Эto!{E}Y#,>&`q߫i6'=n[_Oei% ~}VB *{?@>q_tzQp;:z-D'@M =aK0:Xw=vuۓjhOutWxآRY_HѲ|6s7 "Ex1<\=F+2PoYcŞ~XOkvUzٟUY<`4wzf7KK:[t%v+w20+CkfWk:}5E ؜" »C OsQ;q!@ohө!PojLEu'%x4 SLF=N*wYkNZn)^d05Fl$+_ yZ W l-Ugq+&Zt1p.@ux }DV2Y"kV  7? ,(zjf;7[Ε Kjh;?Stp[k`C@(a ;Y. Vl 㮦C͗e%ynj sڜ_,ЋZvf?DF];s tGAU~ 7^W.uy=`݂,VlK/to\lx}}5c/٪wB@ȋs(kDo>l> > 4zSVgEЕX΃—S߉d~h>$K=#hY}Au~W\yӔM%&;9YX;h'{j]wJ@rI%[Y;\uc8wyQ{\}QU v q?\3[Qur^nY\=88P3~ YsZtƫ U ! $ǂ# D&@ &)`d.uJ9;w?B/YWf07@xG8~탕uQdN%LΖ_4Ի&>MfUG~vNt%gU"Vp^:S ksr{ۄ<N42t>Vø`V?diz5=Bȍ}ʝE%SS.0(omnh|;A&z|-ͶnJΝ6;5:Oo#Ȟ5!PϦ.j?AN]Mo*O:Bf by2z XG '{ANk-P]@6߁A?l܀;d-ʬ6Fu ;6jD|Z?zz]iwlq(yygw@15~%:w b*c+a|UR'ګl Cf_nKz Jn)M]]Lpna΀}Dvg;J@TEFtY.!mw.p]ΞN.X܎2K}W1jZ(90z.Tɫ,`QzPeN:9kzpndj~A"=fB@q<'t_nr}rL6'+Ru ʢ>ǯGQYJ_o5@jߞSdzvl](<ƋDߦ+buj1)Q[%}5ud]! qUM2s}et?K9у븛\ j!C <^#;R!sDGTg^9b|c;;Ʊm# rg}EVT _Ee饞RaYZ EWGZ$SYrPƑ%Xzk&:ե]c}_* tZLw6l@ p7{LTAV #ti)5JML;k6׳&:wUzHoԀеr=0/3OOe{^N=Hm}"gNZR FXzScU?zC֞WFe@nl56"6F]ظJlg$_^\Jky;:O6iX1=ΰQ Wxe=4Cj!ɐ ^u2 XiFM'jJ7*Oy(my6zV0Aj1G Z) A@Y%?݂ӈErTJOy,vg"]  Ԏ./Y e;=bֈ B[Y?piє[*Cj@1]P(fgC@@@P&.Ix''LlfiQdq4.d Νa̯`֙ѕ6Cmf9L6J1 k ?o<?Gџx4YKazCf;S@sr@og:}4X7 K=/\gf! 鯣B:֣(wџyL_QX@~uGx?G Oj6֝H;ii VbKg9wqW }9o=,|KzZMC@̧"y2ٰ;.]9Fyh6nnr R0`%$(Dgrlbz^[Q&?C+frkzY)9*g3>h^-j/︞y-^攢? LL-yzWo0?Hw{hzLIB@@TeVg_6O<vg{ "Ezy(yyEf̋1c Vi>rT)Y/ά{m"}b\E˔$@@@4'Y@~e㘩)Wgkx CyojWb@%J &X! 6<,C“3.݄xoeoS{1 7D  !.w ߼k~wp `fTNG1px 6?A&(.*?{,v7z@@@ i*XdWWZ -O*|| ͂ nеg{Nzzgt;x*DK +B#ZqE`T0re}#Ыᶾxk~||q0=98 ^n!{ZLM7\B5<cwFMѲ8j{r?qQo5 B@K#.X09P]0ԍO͗?qMTR rr5_-[=fs:W 1lzgl]֛ߏ9 '^a.jhw~2ILKC$Ϙ_ ! 7P֡j:[#Ct+UKEetMpj t? g7+)myC&Fՠ <#!*O{%{YZI_^jJWq w=!)F}z؝( ׃JY !(x6t0Nlu8.я$3'{4Uu BSGsbZ&GAF v~I4z?@]L?J%:(=ԡ<~ozE4diŞ!PJjdzLz/jQu)D93j۽BhH-`H#s "? ҍ}<.lSm{mFz[l`FCW뎛t |M 5U !xh6Wƍ#̈@ΛQQXnhQoI* &`:X "7O:QAn!"!q}}VI*ʛ`.zWTqmAͥ!g-Rr? VTpr.-2x 56\3P?ć>&Ř)ii}pZn`4Xo؜=@W q uſ>u' ԇ;RGݍc wZZ`2X3v',6n5z{a9P)f@(|{0j;zеu&;# H'Ec!J7\t?S)v~X*N2'Ha悹v7"bG!~OO6UJi&4>NJ/M9DY%@=c/UiŔk(*7x7u @pbv^ ZKJ@ L&`B r j6|tpGoߒ#|j=&a1=e*=jX KcL$R΂1L^l&tD; =0] )Vg07dPxqb_&N跽IA~OOu,~~7QIILQhma,Ue?7ͬzP2J8_p[_"DFxgrđm}Xzzj:x o,W{G]˽Ћ1Wg o gBY"G~qBuj3 ϰ_h#Ϙ_k,dͲ`~lw#?9.SF[t9MOu%Zݿ"Hfh`UK30LLَ6c=ff,/ =͔bqjw~)_p3۹F!]H;?2l' g-`z2M15bL?đMSˑi ;J[/ ZUNq` p낃1ڢ,? WP#6P!U0 LunmpU]jF! p wA ;-z+(>H tZ:]MQ[Alˈ΃Pxk154c'[cK{"sr T`挈SNUz_GA_G~,v &iSlZ> @Xh+0L7f7gWXp8 bZV>; ^ūPe?p0Z1zbwfqAp0[""ǁDMcaq..,szYe8BDhDY dqA&֬6z}eQ&HmbNeD ޲mtoV` AIDfYͅet"? vX aq iyq/BeijXw "> VU]) D&8ev8;"`3f7@z,a:X1]X8sؓBKYp#t": V#Sa!ς)?5*[=j Y|\^= - ,pڝppE? &V ÆH*"4 &e_7ʽZ#j3HM}(d6)d?vg{DbL!3-v~ZB!@`cA nrQzSYm,` kb+N|q $61K?fJw"r`0YqU!R r JnD* &4w^A0LWf-FV;-_+%A~+ jIƍaS?`0b X87\Nl4+`u v֙U#~?X[_PZwџi`0m KK1ȸ΃hO`Z@T`y;ca.zkm*A`0-̦m9wCG>Ѕtg&n|HƺBab73诈06aKQP=I6TF/!`0#ܳ͘~0'3]59oX0 VɖZ99(.DIүu3X0 V[VE&nHX?x6GfE[hQ0 VO0]Q9Xj ENe)ct;V. JOb`~HU=V}@9#}٧ `0lI.snF}Q;0"V!ڽ `q]}֋`yl~T:r"4]Sod<=`: &۲<\JIS>Ym,=a0@6BDpcgd򙓒:5 f9;j`lzpȦ)_ pw',v~+g?Ba0 ҟn^X:{R2Gve:\b…f=OD`0mEw͡/#32hGp FJ?#Y]7 `0ELJnʡwfp33!it`b5ޭEE{; `0[L ݺB KadI%?6,Jdkw+?'XֲmR0 ӭ-Ȧgrl.0"_gOOHtj#_#vNaq,]/qŸߛmΏLe`0 | z#*LBT6MC"hL>kF휖Ɨ\`5 ܄DxO|eX&,$x.0.OLRsSS?Ae33"w\n< .2soaNIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-64x64.png0000644000175100017510000000615315102145205023127 0ustar00runnerrunnerPNG  IHDR@@iqsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATx[iTWhqb6L/f8-*,krqs1 8 ~;0w+\"3?8 hEЮo~R^ƒ4^WU ܷz^qǖb8FAQZΖ[*|.}|[!zF@, lrj`ō g/Yc4o_&4`X@.܅ǯG7x5nφRD>Έ E8|VM݆buXOn,-w`jNg |Aoi6: ZhG$\@Ds;Ÿ>N[#*y[@Ihw!s |},΢Fc- 4u(nʖx($?Oq-E2NeM>)- |K@1i"n$ԓxșx7)@^}6b溃xoR&<ˏKil,Fɱg~8;5(>btkp3!GOt mCd;F`QK2>]D [ K;}[Z(qEHq)08Nїꀧ 9]uN\2UәW-{LKh,m/ߖiuN=3n5;o~Es%(.CZկܨ dCmLG 1$Iَ^m?-1"JLErH3%_K5QT{YhlL >`]+bĜ-.댧uώ{K6=wIYL~%Y"7+*Y+:16jK_g|iJx4id 7pZM*%ҝ|-r2TgwDD wER+Rh\Nȳ$ )Ķ9M& vB}'3BwBqO[u"/klzHGwy'@pW 4G ![Glߵ MFI!no~YHg켌^b|v6 I9Ry&Xi x.h$ Vy魹h؀C=B%0O!%zє^(//Wr8u'Wܹrb1`[:Ϻ:97Dk*H<mД][(SuƉ%bQQ YKT=8_wJ#EuMK^љ dr<5ʞ*T9b*BQC$΄t4Ǭv9ڦCOƊQG`~aHKχ7uF vlhœo+_ JIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-96x96.png0000644000175100017510000001125015102145205023133 0ustar00runnerrunnerPNG  IHDR``w8sBIT|d pHYs  MtEXtSoftwarewww.inkscape.org<%IDATx] XT~}m{z*^Y.jճ &$(n:0:(mfj&ʽ̝;ܹ3 ;9TmJ-V`\R) 9r.EBӂlh\h\+iGjK333tvկ$@x|!ew!dbhL l5ijS3H+ZU%rwUrvcld4&33TwP΁`rW$_f \/lWLS=~aBBH):6& 5,,$?&98N ENP2rs:YX{3f 咿czU5P A_@;#<<` C˃W X;o}svieh wFIQ=HM)Ul]W5.'`m-{^IQJGDԡJ Ԧe=1ˆv@yPBlGAр'6٥6:#;@1@TCZCQ̱_K8~ffݥM\㒊J%"j$YR<}ѢV&AUWc?*sxɧ'CYlY=;j- ,q$ ޟQeQ+[$.8%gAxtPR`n輦M@QE6 e=q6C\ka(´C0!z7 Cl'abnY)y+q1{旦o()C.b-(lH8$*J< o:O}/i='Ih/ ^ ziCT&hO0p~><92n{elPT]^#MƇI'ٱ)ЩTEAJ@WGoÝ,ue{^lj?:2\<$$ee~ ?ewϏ# kE{᎞Ĝ' @p1Y #`yT8 B3ᶞӼ 7דQLV!G)]31^s["m~1O1;Q>@ ?b(%:EO>eZحiGNFҎHgtMD"$b?>$-_!+*kBL/%z!CV>V&>~Nzcɥl$S_MBP%7-;#K:}@ߝpj=ڿ_ j~!d9Q;[El(SlU17&"ss@Yw#9  Ʈ]Ķ~< >xBy1, g0o)uvS0ƯQ̖%v2*S}H@~%J^c 7y>%kMwx'?]УhiK[Rx5yk`4RP{ m#P/@Z5t Ugqz{AdkP!w -$r:2;6e' ??kn?S+փ~3q4 V? 6sg{,quEZчώN(rXؠ%0B ;ab(6.ϰ  ]'(vQqpT[_`,qwZ %_Q]3tBJ(UjKt:_4Fݨ xLP`7w6sqW nmB=?Ay[!Ǹ尽i6h&Y"C4 )eS2 ʲþ uU'/|%QVj+' $l.m 6*[11햗霔1Yl)}ǩݲKvqU%+s (.H>Al9dɲV>6d2/Ose^9/*N՝$X6ᆎeݎYюw]x(.-_-}XCnH3OeZIF #N L1a-\c0"38x?xsۣj.丠[nLx\wz-p䈁cSl!`k>#Չqbϙ3SÿuE"zzFz "w`N !k#n4dj]{:y$'+wz' B A _$wޤ&W+8Mwg ƌ}DU)oZݞL)Tba?PCOӣWAHG)0s i9<|sd f]HbRx|2/= AZq{qfa݋ׂvC{//sKq' 3T8 bḱ3͊-J<~n/D 'PiN+|ng[Ja2 uގ#i-nkK:O?&jX_{йw@Ć6/q޼ch:o38R1*s@gYݝ/D^P/!^VdF#=  IR0(C˞$vdƎDBQ/LJ=N/jf(O`>&XwSH:= image/svg+xml 16x16 (favicon) 24x24 (small icon) 32x32 (large favicon) 64x64 (medium icon, large favicon) 110x110 (Instagram) 128x128 180x180 (iphone retina icon, Facebook) 165x165 (Pinterest) 250x250 (Google+) 400x400 (Twitter, LinkedIn) 120x120 (iphone app icon) 76x76 (ipad icon) 152x152 (ipad retina icon) 256x256 (windows large icon) 48x48 (medium icon) 512x512 (OSX Large icon) 1024x1024 Broken Logo 96x96 qutebrowser-letterform-classical qutebrowser-planet-circle qutebrowser-planet-continents qutebrowser-letterform-favicon favicon version classical version Planet qutebrowser-colors-classical qutebrowser-colors-main-classical qutebrowser-colors-secondary-classical qutebrowser-colors-tertiary-classical qutebrowser-colors-favicon qutebrowser-colors-main-favicon qutebrowser-colors-secondary-favicon qutebrowser-colors-tertiary-favicon qutebrowser-negative-classical qutebrowser-negative-favicon Negative Letterform ColorScheme qutebrowser-planet-favicon qutebrowser-planet-circle-favicon qutebrowser-planet-continents-favicon qutebrowser-planet-classical qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-logo-favicon qutebrowser-planet-favicon-clone qutebrowser-planet-circle qutebrowser-planet-continents qutebrowser-letterform-favicon Logos qute-icon-planet-16x16 qutebrowser-logo-square-favicon qutebrowser-square-classical qutebrowser-letterform-classical qutebrowser-logo-square-favicon qutebrowser-square-favicon qutebrowser-letterform-favicon Square qute-icon-square-16x16 qutebrowser-square-favicon qutebrowser-letterform-favicon-clone-white qute-icon-planet-24x24 qute-icon-square-24x24 qutebrowser-square-favicon qutebrowser-letterform-favicon-clone-white qute-icon-planet-32x32 qute-icon-square-32x32 qutebrowser-square-favicon qutebrowser-letterform-favicon-clone-white qute-icon-planet-64x64 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-square-64x64 qutebrowser-square-classical qutebrowser-letterform-classical-clone-white qute-icon-circle-16x16 qute-circle-24x24 qute-icon-circle-32x32 qute-icon-circle-64x64 qute-icon-planet-76x76 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-110x110 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-120x120 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-128x128 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-square-128x128 qutebrowser-square-classical qutebrowser-letterform-classical-clone-white qute-icon-circle-128x128 qute-icon-planet-152x152 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-165x165 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-180x180 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-square-180x180 qutebrowser-square-classical qutebrowser-letterform-classical-clone-white qute-icon-planet-250x250 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-google-plus qutebrowser-planet-180x180 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-facebook qutebrowser-planet-180x180 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-256x256 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-400x400 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-512x512 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-1024x1024 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-planet-48x48 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone qute-icon-square-48x48 qutebrowser-square-classical qutebrowser-letterform-classical-clone-white qute-icon-circle-48x48 qute-icon-planet-96x96 qutebrowser-planet-classical-clone qutebrowser-planet-circle-classical qutebrowser-planet-continents-classical qutebrowser-letterform-classical-clone ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser-favicon.svg0000644000175100017510000003716615102145205024004 0ustar00runnerrunner image/svg+xml qutebrowser-logo-favicon qutebrowser-planet-favicon-clone qutebrowser-planet-circle qutebrowser-planet-continents qutebrowser-letterform-favicon ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser.icns0000644000175100017510000057715515102145205022525 0ustar00runnerrunnericnsmis32؁у ~µՀ ^<;;΍X"  B+/ ,; Ył B K8Ax~ BB84 #)r EΨX x o |] X/ _= 2$8B> zP'1WU[e Bη Σ~~9 PP ,, @ʇ~+ UP 4ξ8W~^'>[̀α~̃΁ мՀ Ɣngd~S99k+_9Wd99~ϼ9Ak9qauƣ9kka^9#\9m};EԮ?99@E;}a9f92Vakh9}Ye9kA9κm99\W9h㾺^99^lYìֹ̃ҨΧnn nגnnnun͝nߍn#noxtnnuxonn2鐗nntnnnևnnƁnדs8mkTԐ, =P,*AӍ*il32 # ď ~~ϋƝ~ͣͿ~Ƃ́̆ x`KZeob}̈́xL 4͈ y^ 4O yƁL 44 yɢb  Na 44 yˉ' y 44 3~~ȥ y 44Ȝwdpny~~s 1y 4|r \~~Q ny `'  b~6 y ,δG ~/ zl T1 eC t$ A} Iq ,ƢA $I 6΢ 2dq^5 lzi 0+ :Ν, y 3Ν ]~` yl U H{vN~4 y/ uՀ XH@Ka|}P#~~$ y ~l" #~~# mΘ 4ˏ~L #~~# ?I  ˀο~L #~~#  ~΁~L 2~~# ۂ ǐ~~L U~#  .{ЄͳN>x~YG@Wz̆}~ʈΖ~ۋʠ~ˏ Շ ¿ ǻтʻƂ߈ ˼Ưۆ˽O9]I9ނE9]v9Ƃ:9]]9΀Л:9;t9]]9U9A囀9]]99]ɂԺ99]]Gṙ啀9\9]G9Ey99R99n9@˻I9ߕA99@ۆʼA9P9@Մºg99@乃ɺd99@ʂѽy9d=99@ƺ́Ǻ<9[À@99@݀ǀt9n@99@ƀ=9[@99dԽڽ婂9:@99JψR9I兂9]@99<ۄ<9[e9@9dI9aP9@9_9ͼJ9„@9T;9RO9˄@99BF9fƙE9ݽQ9@:x\9X<9\9az:9Cc9o~9 qC9:zֽa9[9=R9\x:@9R̃9BY9FB99@˂9MY9;~T9@‚9Jʁ9V̦9@9Xn9EZ9@9he9㽆>99>ۀk9y>99>X9=ὅ>99<|a9Ӆ>99q́>99z̄ۄ>9J9EɃ>99>u9PځÁ>9a9A`ʀQօ܀҆⌺䊺Փ潈ф»ۃ Ԉسsnɀntntnntwnぁntnɂntnɂntnrnɂntqntnɂntntnɂntqntnɂn˂no܃tnɀn|밃nntnnpqnntnɑ{nnӄtnᎌnπntnpnntnnuynxnnۄto݋nقqnn˄onwnnwnoې˂nnq߂ۈnotۂnnxƒnyunntnnp允nt߂nΌnɂntӂnn{ɂntnnqpnɂntnn|nʂntonnʂntnstnʂntntnʂntވnqtnʂnr˸ntnʍntnʌntnʊnxǀtnnsʈntnʄntՂh8mk 0鹑Y&m9 G&vQhm\CB ,)n(ZݑYVmjws$'QNfd`gPN%$o k(޷Wit321~~~ʊ~~‘ǐ~~ͦ~ā͞~~̓ͽ~ε͜~ѹ~ǃ~ƶΎ~̥~~ͳ̏~ͽ̰~~‹Ю ~Ϳ~ɦϫ ǝ~÷~ ~~êLj~Σ~Λ~ʕ̰ϿΝ~~~â~͖̀6`Ȥʁ~~~ȒX `Ѣ~͛~~}m[I7.-  $ w `~_ α+ `~|: ΐ `å̓~v' ΐ `Β~k ΐ `~m ΐ `ɍ~q ΐ `~~u ΐ `~~~}' $ ΐ `͝~~D D|` ΐ `~~i $|` ΐ `Ȃ~~# ˅` ΐ `~~X M` ΐ `~~#  x` ΐ `̌~ ` ΐ `͜~σ4  ` ΐ `~ς v` ΐ `~̃c D` ΐ Ĵ̀Ƅ~̓% ` ΐ bĄa> 4X~ς v` ΐ <x6 9k~z ǎ` ΐ  G  6v~̓O r` ΐ eĂ; a~$  ` ΐ ?X  P~ C` ΐ !| N~Ѓ z` ΐ _Ă4  [~̓ ` ΐ CR p~d ͐` ΐ$w (~@ 6` μǂ+ W~~7 R` M |~́ˋ~3 l` r U~ ~~1 x` ;( "~~. `  H j~~0 ` hŀl Gjh; A~~2 ` D$ TɄM ~~3 ~` &B e q~ξ~8 u` qȀg  XQ S~κ~@ c` N 4~, :~~~I F` (A x̎m #~˵^ (` {ˀb  ]́~+ }~  `X͈ 8č~S r~ ͅā8 ̀X`~s b~ Dȷ~] bĀ{ ` T~2  ~~~¾θ ;- `- J~X C~~~f$  N `Y B~ͅ N~~u8 hŀq `t :~  Q~t? C& `} 4~& 1m~{[5 $D ` 2~d &@PUNA- mȀh ` 0~ H ` .~͇0 (; `v 1~ˇ I `k 4~# 2r~~ΐ `P Tǀ a~zΐ `4 i͇< K|~ZYΐ ` {ψ 5s~m, Yΐ `   b~zD Y `v ʀ} N}~X Y~ `?  ǃ΁͊  8u~m) Y~~ ` (́͋ (f~x@ Y~~t `p R΁یE  1a~V Y~~Y ` }Ղǀl;  'Ms~k' Y~~Y `q ΃ϋ~|[9$ *?\y~x> Y~~Y ` (Ѓ̏~}plgaiqy~^ Y~~Y `@ h̄~= Y~~Y `o υ͊ɑ~= Y~~Y \ 8ˆ~= Y~~Y A ·͉~= Y~~Y =v (͈̂~= Y~~Y =~H ωψ˒~= Y~~Y =~| 8͂ϊ͡~= Y~~Y =~x! ͉͏~= Y~~Y =~~zeMC s͌~= Y~~Y  @̃΍͊~= Y~~Y +ЎωƂ~= Y~~Y  ΏƊ~= Y~~Y ͉̐̉~= Y~~Y '͉͒~= Y~~Y :͓ۊ~= Y~~Y  `˅̔ЉɆ~= m~~Y 'і͉~= J|~Y eLJΘ€~= -q~Y QΙ~= b~Y Ỷ~= K|~Y 1Ċۜĉ̫~= 6t~Y =x՞͉˰~= !d~Y =iĎӠϋ̤~= O}~k6( -E\w̒΢Ɍ~= 8v~ {wolnpwy~~͖ѤՌ~=!d~ȦՋ~l}~ωǨ~~Ϯχ~ϰ͆~̳~ŐͶ͂~ːչ~̽~Σ~~~~ȊͿ~~̂~β¼σކȾͽ֎ż¼ϻٔǾ߁̺فʂ˕ŻЃЕÖބ̘ÖŊΙڤ ɺ͓½ ʻ؎ͽߩ῀͍ʺ˔ߦʁـͅ՗_ʾ㻁Հ~;9̾ʀԼ~l_XI<9;DPe{ȊE9пŻvY?9[9˾X;9z:9ߜ‰W9G9ۚÈj>9X9Ňl;9x:9ʘʆs;9C9̖ϻJ9V9Ҽϔɿo:9寍9̓ƄY9寍9λJ9寍9ڐE9寍9ďȃI9寍9̺ǎлL9寍9Ǻ΍ĂZ9HPC9寍9ʂŒσz9>l儍9寍9҄͊<9Pڃ9寍9⽄Š΂U9G9寍9Іɉ‚9t؇9寍9ˆO9;9寍9‡ˇӨ9=9寍9ɈĆ]9;9寍9ʼn΅Ǐ99寍9ƻȄ̃9l9富9I÷ὍP9Dԍ9寊9<ۄڰgI9?]~̃ʎ99寉9dȃә_;9Bmǂ߃9Iߎ9寇9Ln;9:ju99寅9=݂d9FɁP9<ԏ9寄9h˂};9;Ɂ:9j9寂9ME9:Í99寀9<݂]9:9Ȑ999j͂x:9A䏌9E9PC9Zv9_9߂V9ٺl9x9t9Jg99A9 c99dсS9S˼a99Lo9Ʉc99=݀ߏ?9Cnd;9vńe99ĺP9Azt9Mнg99Qk9A׉;9ݼl99A݋=9;}׋ᆎ9ڻv99tҁM9]ӹ^9o Ļ9n9Tj9Eߺ:9U͏9S9Dۆ<9;ڑ^9>9:Ր}׀I9bƁݍ仺9Ѝ9ψ݁b9I}Ϻ:9=9kźڇڂ;9<ۀD9кM9\9:ަH9dǁX9`9~9kƄȺU9Mt99x9φl:9=݀A99oώ9:t?9j́Q99iQ9di>9Pl99e9:Xuw`@9?݌=99cȇį9pρL99aZ9Sd99d9?~ځ99iO9 e寍9w9 |9O寍9^9d9AJ寍9C9C9:i^99寍9ƍ9;9Qz=9گ99ǃ:9BH99h9;߃<9:l\99;9SJ9Zu<999xr:9;cG99I9ՂỦoC9;XY999΃܀nU?9I]us<99C9TÃE99h9̄ږq999ʂŖq99:9aʗq99s܉<9ɗq99q܄;9Sחq99q܂o9Ŗq99qˀD9a͕q99qؙM9=ȂÔq99qj=9ݻq99AJB9i̓q99Vكὒq99Mуђq99G˄̐俑q99R˄֑q99cׄۊɐq99:̔㾏q9N9Rҏq9@9<߇ߏq9_9EZmֻq9:l ʅqR˖Ջ؅˔ߨ᩺ܓȓ§ܑæŒ̳äáչȟړ֓ؑ͝͝ĻȾƽǿއý ᎬonӼ{pnoxxnҲtnщ݊n輑pnчonŐnхynsnфۉnpnтonpnрvn؁n膌nonэnnэn灧nэn{nэnnэnnэnn{wnэnnsnэnqnnэnnynэn̑nnэnnpnэnˏnqΊnэnnoыnэnnnэnnnьn|ֈnxnъnqѭ|nsnnщnonxn|nчn~ؚ̓pnpnnхnqߒn|ڋnpnфnonpƋonnтnςxnoÉnnрnqߍnoӊ͍nnnnonwnxnс҂vnnnنnΈnnф힨nnnтtnˈnnքnnőn~́뚯nnˑnrsnwŲonnɑnҁnuꞏnnÑnӁ藓nuonnntrnoŽnȆnn~nߍnnnց疓nxonȌnnxqnpntٌno|nɍnnɃ␓n|񦭎on܅rnpnqxnnʅno{nڈnnnፓn~΁ퟂnnƎnÂonrtnnnoƉunӂnŒnnӤtnҁ阇nɌnnoóvnsrnˌnn~nČnnց풌nnɪnuэnnnэnn˦nۂэnnnwӁэnwnvnonnэnnσpn݂snэnnonxnэnnopno镄nэnpn|nqnэnnpnp܃~nҍn{nƒynp瓉nҍnnөtnqnҍnvn|nҍnnnҍnnnҍnonnҍnqnЂnҍnpnnҍnnςnҍnxnnҍnnrnҍnrnnҍnwxnnҪnnҩn~nҨnznҧnnҦnnҤnonңnنnvҡnqnҟnqn݄ҝnunwқnnoҗnqn߉ғnunx禔xpns|noߞt8mk@8x髍mK")ŐU x؏G {K.+yMOGR, DB N+_!5< 1'P;rL G?yquYf%>Mw Hw+W$8M}ߍmLl"UQGCKF$"B=OJGB$r B=+QN   DJ ?=oCB>JZM8#yG uԌD,ÎR?|ȧiHic08! jP ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cOQ2d#Creator: JasPer Version 1.900.1R \@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP ߂`LmD W0 [i.@ȶ`̈, )NsF_^};t3Y XFW"߂`m" B",<@W_"~ $?M |Vi2D&y;! 6󹙠F/C@ތ\ l;Fɐ<_ˊNK֡ˏ,c-<'u1EYjN/b">j j]'=S6T/T +I]eᒩ6r jrzUiO68C]&OGDXϷ h~ 3J6N&=9ρuX3ţuEU|pbǼ;mU4j}@$Waw'T $h> 8*h )[OginOmw)͞J!!KZ ؗ+:wq vRynqڤ ވ練whpqztHfN/rIKKZ)oaʏ~6ӉϜ <,lms;{ߧ屘q3b[_";,LlJbi"?so\ ,cVW3j(6j+nOg!o? AY<'-t2y5荝Koͻ6\bcF ms -?;9HRJszi+Ǡ ƕk9\zeS+ ݅Z &%!c ٗv2W n H2 Q.8E}.y]uqVeso|xYهCU^^X9>ߝ? ? 8$G٪87ŗTupSw&FlUFt|}[2R3;?d?f0fR1I M?{ q=_Oq̮VɍC{'Q'+wFaP.r 0{wӻCSvK=V vaR~BMY%ۉsB$Ր`a0͋I^m S͆3 >l,$);^ʅȷ/g3wTuGZF1uɘ_mBY+Nپ?q1a^"TfTe~F! qT3xkQ6(2@{.c>UI wck3@Q',*˽VX3n+}A/c^,ԕ;<> m/sҤ:!~ޘuk~M%\p0R1IsXfi &. +-AF*!:rIm3%:,r d;2zMyCD^~Jp33Dd7j/Qq= (UCVKfYԅpkh'0!|&P*_eOye̋2G9mAJ@럹K`T`|7tHn7L+C {mLM`- Q[h'.p @|%o"\$}P߲b,J^ ?ߝ~wp-oZzuFX* ]hdGhnQ4NlI"uU;Z `/غS hJ0M$CpfPYe TGmSl8M!i鏘I9FzL9dt_iUӼD'n;J] "L4nbr,k@ÓcD"Ǡ NZp פIN6O!&1Zz81`OCՔ3TZbW{v K_4=tNj~TG?Vw쪛o*ikĂ^9Uǖ;dE1O$F+{=|4htVXd@22W':dd/(&BahWgI+!/nS_O;[5URCV[Y$m1V|b-#sUh,k# 4pD հԿ[c,{Z3>#}+kZ]۰NTVb5g* ^+Dl"Qe6|)񏴋{*.XIgX[+IEv,>jg H _lmeR#G8P/l6S>!c'6c3TDߛЊ81E&+Tm[T5Jp˙iʡx%E3WrI. "daŹ3֯ahF]k&RXun* _ )?30,ĕi joYIۆy8̙C ̽JLob$L8_#_&Va*}, 2(ulJ4=ga6JK8Oq8D_;'V[Y[bNĞփ+ Ro.YM9jH5-40Β >0ې ;0\qiUj.M "[=)z>MS2?>Q⠺SJ2\\MɌދGo t 3)܃o^2`2 3|f/7`<:(^j0T0)6SODl?/('LfixO)w:#8&>Kd`ݳ<šxainѺ"LxYYo[!/DjL(}:.\WVGX~ U]ҍ=B`%fzF]'%9.)4r2i Ʀj.-/b*NJ9vF㥂$7*aڻȅst?H%ݐּQ&Χ] p.K1>yQ=[rHy9$Cއ JǽS\EIKsɯk=z!F xP~O-t%#Ϸ٥+ F(EROd}\_8yӏBtc>|ʤ͡*ټĔPIR)fm0"mZ-HҪ_%butw?o44s uCc9JQX9@4Nۈ F gA#|>s_h}qkS'˹yt"vXz؂xѩzȒq47,JS`poˀ;. 2&U_?NfA=bDh>ViߟT|~>aڢ.fiԑemgOsECLmR X.ݍѯIr^fL&\$vf=m 7&Rc`+ya0goӾ6}j8zsжĹ 7 :H cX^[T}|Q3y헉{uՇ]H57 N[1{#,鴐k|sJ*@OS j.&[q)'p-EI͸"PQ)s{/Eܻ+9И'P bse3nr*ȑLmuF`o0釣gF; @o sigうHMG47vv('z?^m>„4\&o6WO:ʆ-km^Z;8D6A+gD{\2=hn*.`S)etx[:DA[4Xu/ Sv='ǙJi^GYHyAlf8SH}O,FvYz?-iClc8+jtrrFV~r=|4[R]m*JB'| B$]bœޱs} vWHOVf &>mvZ[ E2929< &Xhx@fN,QN7(|JY}HKX8Gxf@Ϩ(p>bWwcO fE2'dRf9g5kkF Fث(!폷*yݥ,LЬeUۈ-EBݖpU.#H a_aQɹ3)XfT́󻺈@RjB%:.l8;]{1G;:2뜾POOѨlk! '>jWsvzqt$#~&?ُػ*|dK oUkGj4\i U ʙ7?/{r){]"jc^HcBӌ^6ƖXx@kz&pSP5UcO;ʘ[P hc\L~dQ CKӒa[j~422GWoBQj{TlW?_VvH$ ڑ%ֻkY0YKdK \ ט w]E‘5-"5U'^IY|WXNBfн-PX B~6g_;b z Ҙ#:tgi]@{5 oCI;Ε=SA4ms2 Dz299i`l=(O6~iƊsCFlפ9Oh[pA['o6GJ$E0fymy'uijah/;A/[#d] ,UW{7$+vY1Ō;G~oX5H`ykM!\oWTڢbok`zVpzgXQP@suID:&pp݆ }-y;0'ӌ!\d1:%3] ˻DڛAL1f B#_ϷoM^h@ɉCiږ ,\4bswKR{PZ<f-f]גuhLL k`ȭp+M zt6{W)1oj (ۆYXoCSbKIDr:KQa|F,obӮ~2FVn/,cR՘ΌYhX2x]F1EpgX]/* +JtXH^V\{KUK([d/bMZ? g[pHh ."\ \q0Fz,!@Y6{LJ6\1Z~aAGaDB;֣2{;ާO+[X.~ڏ:cS"!"_1DeqF˧Z~zݏ;x*#.]W'X+&W׊;|\oNfT"9HwxQW r贫I-=nF'$::ܘ UTߒPڡ,r-C7+U5 swɵbTf!uOꜽOp_7gq붯I[E贓+eC SS,vs1Eqlˇ+)'C?M钳fZ. =sc7(Mlg_5zDPpo}Zzv@{oV[/~<ސV0;fA9U*]li4bwo3v9Q!U8B J6Z$rM{g @.XR(;ץ]~9*8~~ K16Ĝk.dȴ .et\KX]Pl%1cGtcW ʑ&w`=JPOPgs949ZT3x"zs"{(Ar lugVAz#r^.4)Jpga:|CJ=T[Ղf.i@⛧_jK(_b[o# 9|-6*]GQ1ю@-,/mq^=dr5s MgVDk^KE\WG @Fid&4*|ͥod !"ErNNQ,r3dj_*|2{Ȕ>giy#!Bk,Aj݃Q%sC1ǸC4kG"YUVFSDK8L&> ]ڣGZ@Ҭ6cml1hKoͪ7WR&L~q<DzlMMy1E[J+ġ>1#4 Bm>g9x{//T~0'm¢g9ٻļw}LLa^+aI=6%n+4HZ40NTg<ॵOBңE,2(eh5?#}tXhU a2t2NHF9fr86|0fph9:5G*vzf8R.T%@f%)fV w]=̎Y9[}Ӡ3m^(7 tAJIF#6Wu)Jrzn:}tI_;JEhv>0$SuvGYUoYpuobQG=ycd?VAa|FzvyPA et| M@D嚺db% 2+VLiV4Q~NIIw>Lhݷ U)2-Vu _{>X@ }+m糭3E~tFH~"{^UŤ*W.g&j{d`!HŬE{Ak4騃FʠT@+N %{5H4{/y5S)Ghðnm8۴^05B3ӯ7~0 k/)|s/|Np_bkkV?'jPRK̼01%ɄW0dIWݛup9!FlE IfL{~Tcf:F2oBòс3}6?|wA]l|2o4Z"? 5 ioѮo5:x|q^lAZug%_d&%݀XzzTyl-jА)ZI5I@j(?Ӡ!Xka H-fY{\c/El W/ che|l sB+)ZLl  Z5 ɭDcEP0+2 ƽklM(z3"4`ZSB"LD$ɸ2<9m;dssX﷘Bw?#N&*5tٗ/TE  J8}q-N'"-Q9 GH#w䅰`ap&Nٯ|(zdv(ͱ7T]`)E_KIk'0qGpU{xu¤ #P7q@f| CA>Og)SQ'YڪDLC~{ڱyv(tu)#S,E7Dv@zP-(Pq@P,ίsv$\ h}yjD1obSu¤[LcۜJgRljql sk; ':^W>d|hv##i(ѷ7eFrNgզTK?_"mT3XiEWh~IrLtm又D%MAX?VLm7h'"\2 (eey˫F~zĂiFK\*oqч?(6lv~Wjl>~Ã9R&)OP;a^U/m\CoUQOVH:2UZL?JNvO1 ByޚHW;, [PGXhͮmLS)YoeR "#:|mC{DC@uQ#J: @˩ߢ42077,>G:l؇'[ݔV4wH&b%t}EyÖ῏TO]5#:*L#dmʶ8 .zKGވY?ȿ#aa5WZmոFQ MlJOB(tߎq?9·4~ SrB?9PЖڙ3uVj?^hM©vr[BFA)x16 D($pXsJM_MlZ@l&_[ae[l&A%K/SeG/.,[#u68;1>;ܵD E=}<5(Hx)&n궶 ̐ h8!I*oBbk[' kyz!ilq,*il"آKS%_1#=`k%EUzYs2S&<0NQ(rރ:]yL/vM j]jeph:Rzf=@Л<-3K}gTZa\};$jzNk[ѹ^`Ei :Lu=wLrU;5foys*Dp_0-cŹRu\  C!ZpCUf/FGholk25Du?˵}тD3w+ЀwG?||P␍o`ONdsK!jx媷'bw 2;P8df2zɍP[6ZWy+ɞm$KvٕL, A4+g?Hq/'AKSsmf1nmUMp53>i14d68c#p{cq0KV L{&z ˝iLm#iĎؓwhB+I\%T4L"T)0'.,yԻ e~k&Sh1>!^Ȟ0[qFX U_l 3i֘݌}c3@-%ЖQv)I}(O!bbYQcOA ~E\vŕ*ݟ )uQ9sy+뤫+8&FtA0S9h~L~*"loNu_w HKs05\;6*-l+.ܵ2S@xy7HR+7Dsv 898yYC%nJUǏ!&NS0p% /X#WȄ7wlկRȬ]5wiU&=HU ρ! 5gQW-/Jܴ C[Էj! 7hKCG=zϊyRvŐ"c= @BpCR@Dһu!w"8@xfW^R'!bNT\Z@eV`'qF1'K Z=MƢv^ZR_CRcYN6nyW!?j/C /PM pjkG_XhƑi1$GXŢNכBRY^7vQ)244yt{y#x jb*)#TySf6p4װ;@`,MїDB01fL8 *G|A <\j/XgD5'UjܐT_-)M0VR ,>VB Nsz S]ͺL:80m ' h7dyw|4^!5h=CJIиt&M6| aC & E gd7Qk*5K eekk=vevgѮ&RFA+@:9Tb'4Oъl:_J7:[kI:a޺~*^^m"v@U=8}\@8]=ş;}? H" g\rv¼$.F~} gkpiBQg߬qFmax#8in0~VZn o%X*f?~g`SCŽ@>WRT`"*Dh݅^jO{c$ So!NA7V7ίJ_"'eze c탮AR J;!g Z$zqfB Lo?z3doXiFCC]zˎє1ǰ9":s3I=ߴsdd Fe_rrhTvtHAcQnF~3j3 3O[^/nߟ5w{5 03C-?t+'< 붒۾$cFq0M3fmˊ g|vbK37U8񢈆p%FiHnbM.|>ږMAI}wCw՘"iG k 8 UÊl8UpFd0} ĹفDVz|3&fc,@d }7nmC=b-IH2FO%zm-eg+uxHf ͑ crfp} qh4EbxMg4㠞-]i˅-_8!ץ1*IEޙXJu<Y.7z^ qubyV-}I)!WQBC:[7 WZW֢G./ u=9+P2յɄ7"9x0gRo#ľ# iZmx$vQ-^Jn D#!Tok6'w[Hj}uڞ–7Ô|\S#Б j)- Un%ldwf̀{t 4cke74>H<$47 Ѐ͔)}ܒY}ېG\}t?!<=l9k@B{4-:J*-nFS3MwgL<9| GhN$[7F 9(3 ގ+yO1sP )A7+BmR);*1w6tNmf=棅fǚG58TA%@ѧ bBPFO>">. eWJs16j{Կ~S} nGN&H] dUO݇o멡]7 dE6.?pp<D l2FpERDB}/&<(2=:| _Qxd.w6de vdh>oSky"yrtl4Gmvj"AMςHz;TPkC-P,`s>w_쁠ݽ;Az2ĈZqM-zҎuoYhAi>WQɯ\RYk(FmlFuI$V%n\˩dXIZ@{wbN{<7 5Le^x䷪nv1g;EQ"H7HUP x3FFNѪ^W?fhNԻchxa$vfTV3l@*5'b|])e O]dGۚNE).axs=ýKGj(kIBj:6:w ln{Xj.kВ%Q*?k$M$V-XֹmŬ(RwތC𕊬Lp*t)  Yo{{DA"yk K,@> hؽiol4r(o$u[?21 WѪh Ez 3 MG2̕8"xj&2?BzI-̅2cD*@4m$K[KҖψFS1rPNΥ(*1/ɇH)^eUュBKfabخзMӠpjK:o祂pinQ<t-8tĤ%mS՞HWd}C)z*oc=Y0 M z0xؒ4]&]C{bFXs-4ows-}y f]>1$Kev%SKx%Ҥ %L}=aV fcba8 l^jɌo!~HT%dJ:0?YUL" C_Ԓ}ީKda7 =8&K=}, dp~/R? Hj3oߞ_GὠSs$E %㋐,µe!ƮvXz] \U %7v!lLEhΌwCOa/ Х1hU ΄vWP^1^!HC~oþr $\ z{uL] e斏rlRdC"Sg I۩\@I l ޿;wLqG@pjg1s1|+wi]I2YIHHt=+u`ʀ3+AbƢZ?YEJp{DMӐ@#mC(Q%h_“AIF|K?V v}`!bleVkV+SSQS iB'AKCjx|Eo9-ι/j8wOwYs`,Nngo!fk|6,dmn| uNiƪa/m\jYR0 ,d &Vk)_g};] ٥\@*-!jڏHfFVd?=8E(OrnbI OCpqZ62`6_j~j<7PZ%pV^ɥ[]U[㠦(JxbBf ]qk82gc,iu]\ð1~þ9>%j2f ^. If-X=>AŤNJ#EQTo:UR=ж%-☎c-_h5OaUCp2ߺvolfϸ&Ꞧlp\TByXa2'+KՆLJoAh OJ 榀an|u`Ṭ{L@>BY/앟 :+*T?;KRcqҮUKae\g=Ra6`vHYӤ4<Nja4SHnvɝ:ȩ; eb;c'N9L1p{ r]4,U4V2h)XKS Ěw ITeB/7iL@ol<1ෛ/x(zX6Z0{+vN#K2&4IpCE]|bg@u1fr,3 `g|+8U%DYDw><,µ lQ, 4̏KS-T ދ^Ev;(Lt9X9Mp`ԅSn47ua:Ru:=uc:W4Gl`) 䰀5(50snʝj ¥A0MF2QVٍͰ!>rcXhwjFC=wѐ\ƛaCHuU{шyd>JRJwM\F+R{[%.  <LXJ]Zj?M9< I5&i3IcPXM;jSF։R?J2W{˼lgkӑr#R""X0F)`̯#[=Ha!{tQֹ~rԆGrsCX"q/Kt2\C}/b]/& K{u}^MH]MӐ7rGϧ(E0žL RTExhYv~oNѩdݥ٘ 6PSֺ7S!4tP v`- f/}x 6AqvqW8vk8Zx@Sl Jw b,M*uVQZ5lqyRwٴz3¥ռ!Ffl4/,\"7 8zS*1e#sؠ7%cBu_l"47&kxy?_q `1 ?8A8JFcaXf楞K%!n9 k+t*aZsxPqLn7$ X9N|)NJOr`-p5A, }hcPzJ&*h~~){zbLHob(u(wsQ'= nۍ@kFڼJkN ČoôA6gz\GdԐX ۨ]n?\wV,;j+rc$z鎑ި2NG^_)#"JQKiMs}Hr7V{:;Ah@% _tn+@;I~n(ݞR_D$T"?糐RVlF1!dl!E?ؘfUh6S}wVXZbXc#Jn7J#da_,1YtAxGxR7bC9c= "q!6!L8MD* InYcGF~?Ș''zdžFI1N^oM R| dC]V?Q@c@y kkB1ib\/sJ}ܮ,C]J#XqFQ* vR$':i~82"t {BK?ӣDTp(w҇@ Urߐ0wƼTM@k-,/gvŎb"AG^ ` Z茯݇/j0M}Z~x×=f {Kg_FHwnOul5?/5L9`_oW:,[W )Mu+U8C% )ۅH%qQڊi&c#>*~rd_z^cPK&3YId߇qp |qE+6V>yUXG-25J8&'>f%O T;״y@~q +#"XAa"ZPQ={&.)p˽}B6z `hO8lS r<;qMe' ؝I$D9 a8CI[I0c 9 :7᛽mQYdF&|q*vЩ߄^A6E^ *6\s='҄8J'O"zm{3jܛ,Wv?p;'1Jþ&Q0~a䚁m5NWY+/TnP葺 YiL"r>_~m5ڬ@W͇c3߾B{/`MfzJY=u< ^.qO5we&xb m)л\ nS9Jؖ+ c8Zd*ԣEO,?)BeC\#F' 9wŐ6*n>ߜj\a75QyRO6d1=oHy-srM&Sk @ݜs0 DXS\i7UKQT0 $ ӱ^ֳdyX1icCX͚…BƺS'F]%<~E6=^_Wrz5 >!WRkVI )eAĻX$ W[,TmK.Yaq?nZi4Iި2AHN*F&(ɳP f zN7ٻ/@S"ik>A"K`}gž xW) l8P7L{DϴwzƩFQ!;X3l=Z1umفx$|&Ǫ F<"^%Iv>$԰"#bGlVM1L UPg*ExZa2Qg󈰰Li׸W~sK 2)Wh |tw(MRdE* ,M\w&ka<߱| %cs9*G-d5|H^@XKgTRժSE89&F/.,ښM/J ´1XN.J2c…t Lj5]@EP`h! hBtW˳w[ en`IC@.00Y*ͫMw9 v^JyW{w}8ךȣ*5r{d _(HS7x޺Z@gUa1L>S$hv}baH.Ut`6LѕnZ2NzDؘ2)xUK?Vئcs[ `/J(]1$O z\Lt*݂ >`Zi$j<b)zl{IFQ /5H+3Ay ׋1 x$$tfriϮA ԞI)Xu0KIp0_z%Dd_8hk5^QetIwW7:h4ӭR^aRQID}DV {U)3|; FAU১1WN,gv7Ե'u t^yǹq KQ9L3^ c+)`X72DJc·@+шQl|yMmygv~ӥ XFu6RuQVzH݇ẽ kSavlT_c7L" d,1:/#qi!pc:%gAs]9kؽ(P&Ү}?; &抳a|daS9<>Ðj0P(=G7jGV Q_ |r#ʡrI9cP8U5cS3~TE|Q֌.d5'+nʁ>q;\H_ (({sx[}]sVK>O@5|Э:@4俷2б>i|.VF82":҂Y"qD`1h"[4[Vt3^b߱)]u#TMњ7ʘ>UwG5_#97?>J1hy9=QzB#)_j.>H2L µҁ%{Oka{v~# 0r{psd;#rhB]P+P-b.u$KexA/8-egZMY?o^ljUoԍ+ׯĤHu/,]~vJHDG6C,[]'V{PaV3 ҒѦ}O*`wKZ=3tл,G_IԘf>.VJ\Z qj;,4q+aV`;:]b7{{S,?J\k@ly$?*_e&$ 蝺')֝݀2ʒX`FYY z!]_}F ðon7BB7[ |>|BɶWkO$c 8,GNzKpMtlw %hFX0 B{*'uJiuVtpCVqw 6g?YΗ ^xEl`(~fn4E0 L+s|#=h6u*UZh&Fvte{w d,%.yN\\p揄-C̭A'a?3A#6sym:c1e7$?jt2 w`aqxyqq%Qgn1[#e\5LlO#З8 i7켖 (0 랊eICJY$)EoR6/ O=_Q }7SþѢUޫAϞ?%SǷVjx[V;(h^Jʼ*Sy!X KEFw:$EIԭO,}VgӐ 1tQPZD-=J6N+j|~\>ycy2^{V z Kb/bU}DW9H}WZ,2i|µCb[MY `˷N;Ҷy/`Pcd\%/ڀ0u2=l&U\+CM+m19;7mʖdU,μ+ߦv+usNxol]Zلs UI`0Rl͂vHa7=E"~g6.}NPM*(-iW ##=dh>7'DžE욣t^.3j[rQ5AV?T̰*ss~CfCC.e6N;/+0 {wr] ռ2UlX 7a5Hڢ84K-~9`xԳL Bg#H+.[pAQ( ''xgp[$2rRE 9P:O b18C}otHVŽsl/}baЯZkv.8Ĭ%Y(򟉬r[ֽqRu5n}QoD bT;r}%`hOB5m2 G@5F7h"t6L2aA0:^[1W덽DK#*cc3υح/῿c 0-=&B `%n#wiz]hD֕)CVr~i2)7xd_\NP 90Sik0MˆHx([R p 4We9q_(p>XcI]r 4{mUjRz.Ty@)ACeT@`Z6D3n%[;A~Y{V@GJmLK(8b#ˡk(fERISф=X% ؠl%鼮ofj錿n¦=)d=+HW6ucFMZ&}@`#"y7i2d%(@3lIz%(RBaLzP!YbXj[`@ǚwӳulc/vt=J2/#fĹ2R ay$)ѷ3<Iy0dɾJ(V^W,LAg'!+c*I5_B5 뤕>BGhb&cqV,ٹF3ǻ ,:߷(,NzEd'ro(-@f3k:w/`PMQJ>mo5/`ha#B@Ir2H ~".Yq4ЮJ@)1;GR6lpnCS+EsIF$N $d ,rJGI))ieܬ-+4QF!tO k LRR췩Zk6D"?ȇnqZ@s|I)O0Y1!i=;v)PoO4?ms2Y(>*,pҠοK?Bfަ81<ֺ5% M v9KlE9Y?u~/Ci:L*B|w0PtQh<-$[6S:\?H5!BKT41[Enq|YT >Fd"Gmj \'ppb;IP/fx3JN ,eH9 ך?vFҲ(F%OӸR'Dh<;d5w^}VA~IMbNn$[ 썸 a?s亼!'(L5JiZd_MLn@Oِ޿rp$~W0?W~hD$gB@~M_;; Tt-TvG]C&U u IpCJ^2 9PO M )i?hb"WGi1su Xxѐ1l篔6*̇L }엧sX!PWAXuO XX dxPE MբP׺Kt.>qg7#`; Ÿ5ṻc|bsp޴O s_q +8 14t/uΏ05ͅ xWswSUpT)OE[b4^Ɩ|eR_,G #32 GKPR*ʬ^)6_Si8]\ !)lbEw #8X Fke /jPe|Ǔj܆#sMgoEi$Q˕X7O>dѿäuh'(W=GlBAojێM*so0_Z-}1!4ذM6Nu;eckOVlH3vgI_,L`&&W]LDueֹ lA~>FrƬN%@⓵E*o#$u irVnr7̪@ wRlX)FGnqLێFEA %G_,RK2QQ*?Q Ӆke{`{YYo{vȍD3cbUZl֒#Gi}\:{qQCB.|l_IX)nAϨ  in?}>\"ul b ~GW W =Z%S++Y0.Lx%DIɱ\-<2H%j|Kg7dkMnr!`A_%Cς0C#gpթp @ዥ7nK?eFWrt6NX J݊:'(XХ9ߨ&"v Qe60GTK!5@RtKPV@8KTn:?tIL_D ]6nW)A>bX({6u㷬R.|zXnǜ^;b1=js<`[OhZ^Ou% ڝgK(j?m/R4 ytmwFWav[HYmB\Y9jLbDn=’gLV gqoo;׽Q5L*eu*zlM7I>'oDކH/ǫ6QlJO:#ȭ?pC^r' /DDLVb>i52kaB>?(I @)!vཾǏ-EZ1:ّi7 pKHVܥ|J PCYRGlg_ڗ¨ ڝ1;HW#Frn QH&mQрbҢXYo!JaHg'{dg 4G:@\@i-eo>lՉ(mlY''}ɴ2B22,FmLzN7""95Kl>[8\ltuevb&B -PoL萜u;dX/OA3Fg(bPPNXm ~m bXFA*͒)u9ljD^ K -J  }|ޙk'`GM&׸\Gw~}%z'⸌7V(gs5G4,<xgzU k]/9vtg=K0keîNtXI9_iձj& ʋ*t*{T MA'9 iqj=/M/,St|^i # t~Fi )W_cS i{O+uʂ~ VMSYu" PڛN+^>!5YOik:`hA񘇋\r.uԌ9luGbD.."k|AB `Ycwm׳#?d5G4d  Q@pK!uk2A:4VuZdۯXp||lLEٜ.lsRBьC,l+ߚk?:-F zkyjd͌i #z]0lA혺,yY"1O)Β`H@ԡ0sU&Wdq>KEB;U+) wKQ{dVxBQXy\F! .bV{iP 9Lur9ella%}*Jm.GNl05^FCXe\Fv='3|RlvjӢq# V\V7wUb\HGG1:ZP[m)ω^MEѢ13; I]dxe&'t@Dvqnpĕs}Vl%4 wЖߙrYIc6mk4W`^ZIPrLԎ)C}†L'%q`R5{U`SLdMHtC2HG>)ZTǠN4]MBGfeS!^j9OQ~^GIy]9xQHNߔk3tR5?Srw4| I <ѿg ]YN[1:le-L@+C7-hsx<=[2;Bo899_@ZէkҶJx&U~ZRàCr;  Y)L*[;Т5cGٝ`k%PHp=酆 bW Jxtq,ޒHFp$ױGPx3gRNF.)I9vv?nI̘p4P&Jܙ?쥅g(@Z&@W&V(cpou&4&S YCD^cULN5`rFJMȏҋC@(h2;? `WJ,0!")c =}ewRfB6k(n-FFcAkn6+62VSXo!<-->DZ2^&mΛk)('ݵ̓0yj Y .U̻Qe"57o̓`"d>ͼ9u 4%wlʿ:w`̳_JRo{R8;b$*JH& @<y5a9FN{({tP1˪Ṃ~ߵPB"MTas-zQj&z󪸲Mq-?)Zļf`H%M%N"K p4- woU76d?@&G@Չá:#ENq4r&jh+BSݸsO\lc"2#T̴9$Ttf"f;Bc:6ܮA[B)n~-kC.H-Wږɤ!Ug8 ޘ^1[\/ ݷo)1Nǹtfk\an15ݥN[\WYl{D|θ=ºEuZyNzy t;S9NߚvG0ݤY_rkuħ({<ƼTjUD+SO֊"8F;R!O"xq*,`#͒i9{]/rĊ_k>I6rzS΀LT?7roM/P(%I1} TydY`.ͻ2i$|g兟'du\:NWP9pͶhʬ8)Ky%yB/%L@i5TwGg2NG)581_+cZ5?oGq+b_GpϽrF8D{iu'O. w< 0ԉ"qSz-vՙ;_%`Ƥ*ĐYPZ[#[l3Zc7~*kw[àpV"1"nMC [96.tTv+|m-Gش3π)mhx2?)>7(n2'rq/ӇrOhRi@ފhtӷ 1z1H#)+1yiaX6P9[zwMӮJc/<="nx.⥔_Su\m`4)xP&JK v D*Ti_GB'c3Q?-0Ucm)\f.QPtںrԧ I* `ZΈ˗Ir3/uϫd yU׏#bV|Y~^Q!=clX8PG7<:O΍>o^OPD2k(,fAP~@ k!ؠiYy!v,%Q%CVA[C;WҞ\r\BjPCqQEr=$I9ƈ7%mPSA"@He:S#ݝ!_XY @6$71 ԟV8:Ȼ{J@Ylquzi_9H_QŒm@43?Y.sY%|f*g+`:Oc;jfDq]Z90ňϖOkDè%+:OӸ7Se Z(%b݀8G"ZJ4ذ6lmf SYvAI W6AD%3g[enU "A tC)P vߌۢ XD1s 褝DtNPAz 4cy{+1 1ө! n&˔m! b.qIqCtP6IϹN28mE)>c0K@&F:p8Yt 7B ePYPJA'DlnL1h~FnpeMK?a*V@/N=9mZu 5iQ0` Pw6r=5!Nl7hg7SXyRBTds9R5SfDU>+3|!\"iVbCnz*I56^] a޵ة u8Mx{nZR,ep4럃Mvq) kIW?*`81Y?}JnWBv< L_7Ǜhe$˫EKУ*$@Kxå+pq99sDRU>=V>k}]UpB/)I!۹Ai20IٟU Hp.OW2}n3pr-o1K JQXӴJ8"Ofk `#Ml/^qzF aC+{ ,Vu/"{vg+ˋs5v+ FyH<3z/lh0>Ixӻx4_͝ތA沇L`r~lpZT:GݽJ K =c@7pfkP|v~#У?\&# []~B Cph%>2s;pذ߭X_#ڂeK=|1˕SM ]Pie a1dmkP6?<ԛamZ8DĶh6Q,U_-O6j _{?~ޑV̮,Wx ~;?$C>DA<|[a@<NTBRs,[5UQІie .om\=Y$&*WļKn/ٯп4ȥVse4"2j`!۵uv PM'I-lIᗄߗam`]8Gw+:Vڦ55[ˉ= ZѧêLO[>=U ĆtH*zy#DjqDg!mosQ-m$vARe) =VR%ztxz<lQ T `kPESUB;O#+O$ϯ[ |ʻe2!q*vWB:6jE+! v^ ;U~s_F'\nxdiO:" ;ˎSpb r[G;ߺ !1}Ͷ0MH)Z>PA9Ȇn]AFM1$B \qH?š Ϸ$tn#'^p[A}!䒩: Au 9 <|":׬qy݀ ;% F"ɾ$ V˻b9$Yz>X/(7}:< wu n^ڠMKjXX+?j<^#$vTjKL t_2C>GL(އ>\͉61^5)3EXpj G- 7i*;zL7OT?$.6WUIRmVBNIr"ՅqY=&rU@z"j Mo $BȜVy!G4MWLѭ #Зk@.|̸mi`7Gj _[qsWaqݗq϶w& nq ׾2l@FѧӁ "CRgu@Aud7ӥ v@һ+~p" MN # 9-#O +8#÷ĩռ>US35hbeSqe FAC$Ȃ8DAȤweG^zlk6)u3Z7uLeX'?poM=V{\yRM>UxkWUuP Ituf >au 'Udݗ 6Cs͢,۬h|cM)byc7؄.?\I^H}ib TF\ K0ϣ ~ =\c KRp pU@t̿le=Ӆk9ܰu ȐmG`qVL`ʎ >ב=iS qJ29 ~:vՀx>ק֝;s=O x jˣbw Gl]χ[#.)'OYv84+ Ğh]^;lR 1Y3 S\oS9PʛJt`4aE%zrzfPdZ٪M $S9i]?ŮeOrX- q (:b$D|;~2DwP{՗CFOWLCnBw 2ʖ 2}y ҃y?O y 8 n)ʚDaQ+j!?8gUK+^H6Ơ~kʞe~P>W'=G==' z獇ء1k砩 X]^%b{I[mwQQJƦP$⍅u7}{DS̯?vf*Zzi&wCs ԁT RׁIKooG7#z|xM:]Cl <|(QG6=:s!]PJ;<5ExfZiFJug$p5eHypDWۄ``MUՓ!QmfAK;0[[66O  peLogA[ (OœDu>J>_n5QMUu2kҠ2]ub(^sy=lhxpg{ʩ]vbI*eK5p!1LOYQuULu7,?yF{uubkq Ei`i{&|%XR4YJG>KS t43O0K Pv`BA U5+[/Bhwe}0%{6K;j5(ǧ)Xg2)/gvp4&o9زO@cR3]\9mN~Z}5H-owZ0f0K{Ȋ-] B٤,#%g iq! -چ3ORk;*W8㐇 >YS'-0ij#G,B6y$uuc K"E[rVMҳd<4r20 rgvt_ea̿ic09N jP ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cOQ2d#Creator: JasPer Version 1.900.1R \@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP  ߆3y)tQWk$st`gJ zsh,\ R' sm?cz8ƙO>SoZ{ XvF~N*3"KpIHB5H)*^[qHxH:33/AGz;*uv~wImlHw A>,"7@=r|座vIY%;,7]/Z2 R}vDh;l߆#n%2i+GN6{Whc=Kd.( Qʯ3kVTxF**E6,ss Eqp$3K?\l$vFF[#Ԕ,:Kt|:TX;EmX 1&9򣲳x4 uO?m_@b)sX*".I&WL':BZ܂F0[lLl%[KŒ?;sSiz}pR}pB pf4IDﱎJ+vX(NB/@ĻoFZO1Y ^ނO3~D2kcaKe0Ï| icوoi wr RFn?k /9΁sk튬ZLT 1ld c tXT7퍪2;ߝ? ? 8$G٪87CUZB^_*>RG)_/ WtLG9$qص+=sbqJodj0ƈc(PAC7zadFr(y12dLHs㾠M0Y}^hh0Tϩ~'~ ҦSG IӦ4Κ="%/F6 Ǽ#xrUIÁ!|zCw.=6Q19@gm[N;C A+30-R7VFTN"ݏ\FOXGL!<6h"{"Vr܏^ k؇ G᦯f gѿ]>IHM/$ґP4m)2%W*HAzRd80U01`ԋvN]prFyE5 jcæMeևGԶ/3AR',x+ ZP_'FDV[Y$=Y1h٩4Xdo;ʪ$q擑, aDm:kN/%--Bw/|sTZ 8tn]s{;ӪRY ۦ;%{3 Wvoܓ8Θ^Fvڗ R` Yw5vWL}&uEuɒP23EӒ[$=ĕ]V{3Z^ɼ ߛO؊81MA\(.}ɣ#h6X6flC9\ӵO@@c5T9ԒhB,cIԱoToL&NV O֏_]Wրå 30-R7pݕ^ #"R">]R^nș'P+(!f/Ew_ @:(\jLA%_dE6|+XI&OChTJJ:5#OC9p)yʁީ5qՋ@΍&؅_? e /IbaYt.|+`/ygJYO"=esAu =AQ%v%c?5MaH@7|1=hSKn TH-Xc̱ǎ2pq 7%6vFTƷHIB+L(UXEx|Mլ.? J*cUS&PT$j"sGJ|DZhPvdKa?Mq-']>+>2_ήaU/Ci'F+a ж=eNӳעv/~yZniC*4.e9:P5t߸dpV}'Ǧ}y!5CRE\*"__tP.~͝z854%wy{_ yignpatSaw\mC-;kN*rLfZ(b"wF| k_jkBpZ${@?f*zp@]G¼fKܥXb⇬$ :iA.pz? R(P?v?msplOi]?|~ 6*Oz6vJ[UҿsDl_P* bPK>tF]ٺ(< *bONA3%+e!-zYqy'.hHK 2/3LnY\ )Z;rD ]xqy dߵV^Υ"O G 0Ob7}z^䯸G,Ti%cTԊð_gZ%g]``a=ejjadZ=GRNM[e/+h. ɗڧJ(.@g2}b7y˸)XSG{|}Əmԩ\E*ݘ}AJJ+&" ľ}!MKesxOZ1T ?(W۪?gDX'ŷ,+:VP~I\kbyN8F35F3j螇ܮHoT {ZLd/̈́%<#ٟ#4Hͺ 6ND1h#-1!K5M]H5nis_VUʶ`)2 ˡ+m%%"cUlz %RTdhHk^}G2*0Mfc3%Ľ/L9'EBu8E,6 u74=) v87JxD%ݰUfUPh9P]uv'Cw9,c6oFٍ zߟTW~@a[ kF!>} Yx-e`hQ+ IEĞTo(WzCImKXu"9V\5VM\1 X3^}6Rcߞuv̂L][ STiyk*~im׺^{@„<[tP8עP@왷?2 tLH;k bwpx6j?UkHQG/I>5Ie]ک<2Ҷ0 (VַͱqǤi 0嘦ΖIx"K5;\ܫ S|gUV/evh^gFbGO*qFe= t\] ̕7#*@pwmtQ t7s|hg̮K4F%qhoݢ0gO㈜ p*pJdi {zr\Uz;'RߺZ!J.#n{(z>*y[YS7\aH U&^le_$;1fBv#l>CDw)axX"CH*x˱i>i`g\ySɃɝH3>;bsrϑD {c?}a$0c 6 K«ۍ2]}j,̸0RKxTU5ڥ^g̸74Aj:<7q$P$[F.W 1f̩iU7ǜi* Sp5W&Y9BQj B2K{g3!f6UYYt:a>f0qpYtpZfENҢ۴>7 kaȾlqxl|[05'ib`S}VK⏍K!YGR_WBsPCC>`, xI<.ӈts5F1xܫ,UW}_UכCsXWyvp$'i5"Ƴwc,.Fm['uaO3[r /)Z5ŝ4usf}x+M՜ը޷44_j=1Kp 9:*3}sQ Zɬq^ nP3Ac1ZP ނ(;7,mM:>|% y`Efw{7LZU۴VH-櫢stI& BUҨEP'EIqߟO҄R4瑍1 ^wOH@ļ:}jiQfE幗}gah#qے' Bm5g8$ b$9y،Hd%NMr*#Z#֪w@(`b_5&~^•glڛh6髃a hSnR},UH֑ö$Eu燊`z`1)wB*D3k`%^M{b.$Yqqݢd~8K CDP.elx}2kSocUڑ\b]d +oV"+sѡ a/4s l;a΢} :q/LGϖ[ R >ZB.%Zc7Lyhh%>.Z攉5Ya´uk yM3qP.M4 MS .mؕ9ZDwki.sN '`3nWtZ\ܢs?>ga_,nNE )ɽxk:ziLi p2ɲOƘRrJ%a%qF$qܲ$}m KŶKSUQ?:ʱ!ˣP5(}@E@cd5;eH6,Vf\l1b^,Y?ߝ~w4yA*i O?AtFbyf#B]dr'\}shH -%Λ0`Eө!"3@**6gJ75'x8ѿɶ8Gx<.?B@g$mCcF2Fs+fޯwGO^hB B=>[BJd ϢM" ~xϲ-ZWܻ `KXMPgY)=HK#鉏ꓛuK\;ꯁΫ%<ܓv#j[w0>iI!^Rȋ6aPc*E6 Niȋ4W_:z፲/v( * 34OƐ%ɋtd.D gA|O,8` w5"0GCx)IP~~4n.Is_uNoa!AzFF$_ԪyʦOɝLmߟFѱEN*[KYM̱X#/i}MÝ[5Np{c'ΖR`&ߑUBw99/d<B]aaJp^`*SLq&zt+ 璲6̋t-p ݰzF4JdEa9J-`$=zrU ɊP,k T@^ "nTYi$j:# f|k;⻥`v qFqUvJXM<B3at6KxS\R/,S&&3zL; gNffix{ AV7mBK1!+\ XgKtT ɇ(tO:cy7{d14@o"A :F:nMDoC=OzV*ӌ4J?SqIHJ|vhlK3,fX]4'W|C(٨2 G=j8ҁ7ɡM84V.YQZDt/rz cJw i⬸!xB :C Exx LpjL!j](Eqf,'7_k91(]ٔmTY¯D8,+dJ<&тU4GWGjsz,/1AuJ!" n@V|~Lպ0O_Ofy+" B o_^ ?wւיJ( eiVʥ|G;nD=Cpar j5%\B^ۻJD{"x-HGD%T\ 8XXLLse>xKHN57LKkIx,Uԥ%` |W>-WS;Jjr(o6 װQ"{qq"1 ՊAF(+G/s9Ǧ0n0uWZQ4΁6Ťc=ͮ4OaCPygu.X?54 eZ1#xo)$ fƖ%y>ݠ8y캚4z6Ɵg28J,ZN'J1) W]y,T+%u񽲒Amt ]H |H}xl;zXf\-ۢC':ۤ'ќL=s~mü3&whV ISTj#>zIE8h=Ѵl\>AqU&CpD 91#Fh0wz(҉ _.g9npn+޽c( c1 {x2|⋤k뀔|hkHO>J.d.Oè#dlk+y(PF:]}{&_P<%dab0✲<vE@C0ϞQЈ!w{ERئEM$qkv-aB :7RSIz>F42XftTHQJm#fA-c8:w5G#sM?W>87NxnC`maЮHs6LYSÊI-ҳucj2U)Yt5 ĊDiLœ{Ɓ G aeꔠ]?]]1jİfu#wC%)e-2S7Ӿָ5M1vAYq@ &rEX1Ji(+d 4];*E:^؃"B="Qnoo,WMS8x*G>ݞj:IsI%YƑP+Lk$F}BW}4Nd$Yx:wVs-{L˝S;Jq$dyF4z|1,5$0 ]Mwl)u'N9N=ضKp1I~Ң৺xJ`}r9!qKvClG-xJ~EB(~̘H>}uZ  p;vL{`z_I~E- "&uFkNc`eCIZe+hlE#v|uYaB$3u3<\ k*н-K 0{44kP$ή!.mX n'هVfn8:(G3mCljWݺR?EkKJLiɉ}HX\"]#EE "F!|GI7UYy`jIv|MA+v4)3'0 r2EǮ.Mp[[C,s0oPna_`K"#tl̊fij?{Z~A114\ { ڢ}l,`† rשCcm迯]~k ?%0bX>__^r $lUoPiƒeU1ai&gPi`;\:jғРCGtP G_'ѡy2T:ϥ_s:`/j";FMķ.xɸ-U7݇ Ng LI0 T1h51]FSy|RMd٣Oa 8l&K7!dSvu>[.ޞP);JI@n' y`A22.9hф _? #ɑ X n%!{PHe٪w$  JQ+@Bf;1iX$RNCųױFR1v\82Ύm% M}p͹;\XWqQ:5WW`aρl=iEt⃆an(?Ӿe0 8D53eg3q,zK< ;I+#/Fq ˙6 `+J3~A)Le1>J_&-꬙cbBY|8LKn[mYa혤[?Wi6BV݈m6DN-7]Oݯsiq( D#?Z[F>Y :Z]7ۧz J@ˁ! ۡBb5岗7Ewr  q.1 2d@̆t|Ěhhdp߬{V!S88\blNaY! "Yrǵ>˓@._)D#Q/׶C2p=V&,/L)"=.Nn՘A\8j}˾baTǞf7Ghեpc~vŜr6fĠF>gGj*~i>P'b YJzƆ6-jA<8]Ve?j%64_KhРdx4T Qy97xH솳[bY/jcL7Mm.(CԳcgP2#GXjw2fn%G-cE,xbǶ""GZ _9h)H8%ʆ@:>~⍺/LMiAw/ovm,(dN6Ӌ#s!/$Y '9 e Y?$׊ՅW0Rp+۵Y}zr1LsZ8 PІ AlG%U%&dbބ1ar7q'=+5CDV>9an+^{UE2 gYgiOHKX38uf4}o;/ iuFIPc~zVQ%mU",bsGE+Y5HfVc}j# L'6h"uPTؾ]N7AҮKnb',F!11U%C"NBɪ:@Cch-qMP^S $v ] CǩӬ=]!N1fF}֍H -ZI , @D|vm4_s-hD5|?hkdUU}ߕkG%^-WNzz^7 Lelqt>09.%Lq&CO4-i-z"K=h4 )gQneC)?991(8!Yay;HKQŁ:zvSoƀ`D'k@xک6p-XC5ՖΞ3NfӍ50%?V -Uj ω#l5? r4Ku Hf-q'VUnìsqJFSft!߳&(xCeތDVFՓɺO buOb)@o.9w'Y*H}Vɔ}[>6+HFg$^|K_EP%Oj/EcvjHf{<:$9Rg}hM=Qk_ д~\%j,lߊI>ZTi%G#@8C*okY&I.$FW_ΥXUr U_ȭ \#K}q!oEܭL;զ}i m!=AV aX 뱼9:~k? eE6APfXVEꒊdGvLJ^L<2OדI/~Oc[`}{u`&>?͋:@l6 /2_c:$]NOu {o8BZ 6)^RvD5AK-nds MEd ~El3`h)uZ4{u=ZHR1hObN~"r/Fz{X;d1}@oPؙ:+QUf~!K|GѠ; 5'1Ct)bS?l܇=?*S=?]rU4o֮.f(%"tLy%IBQZ 2Dk{`k[~XmA|#8cgK_>!b腻"*.+$ڱQF3q;5 ŋ #nuBp}/<'69Z LqF=DIbI5_y)L\eYyY1 @»|WF k)Ήg[Ypǣ ^?Xm\iNuܝ)}͇j4K%E8YZ{z~pX kU9(2tT0"+lcG 3rv0Vf݊2[Lg-҄_ΫQϒB{g -5!.ME;W) bi( oT`WKq#&$Ņq zɾp8;t%cy g ; UO` ,WS ns_-KV,f >F(龢0Z9R/ Iq!o`p` %砤6 1&Z-U ēIS|V@M'o<#:zrb3ݞYI2zV=3V,mYᶯr.z%6t~V3"Lq<ݯ$P>F:֤O>58r's;k%ҩٸ1HdiT'/w0=8:FB,@q5jh <HM  P]r0΅p!waSxE@$PJQi ȄSDFGÈ;(XT+UÙ ol֧,YDk759 jQTߟO{5 yG/ܑa#Gǰ-/iCI8ncgQ 쇪 TVA\~]<޹&nFtӰ>qoC1e-Pju fG2Qn 'Ae}WZNrB#\5!Hc; ՘v|[ L|5y벇_oa4{ XAo LyJA/ 8V%QjɌ?  8ѳt 2I#$gPPzqYra06h9iQڨ>w{`3,v~]thVB,D60thQZ܅"X(ƫvLu ׌,Za7ψ ^D -Dbdm9U[$*x96/Rʛ\em;J^GZTm q+}O#<9az&dg@Te ilQQ؝F0-Co.2zx8X68'b:   YYu눑H{<ΫLJ!>N;%0^sE5`TwhA] ^IIŢڭx]-YMBT94QQF+tH'(I!q* 4nw<&;hFe\_Ҝa*OێPãu\jIpԙ8U9!:Pz9U ё.EAqJqNtŠ%_}aS)j!\yqfKLREeiS[ް>Xh}q" 2?mf=rۜkp^ 6lyn3 G.U?cX[;|S#D=?UCL,w\,!ng~8=0/Q_B_]j2篰8=2#& r3ibdtC|N:N`9R+;d>6X us0'jR <.S8N后~A6eyɑ:G(Fh?8$F^C-PV" 6(8ܚ9ӧ-0* : &x@>-0 (QCb_ O,{UE!R'C ˛kBJT_bl^ǍlGi,aSe|NǾҽK faam mb'bVZvڴ5Zh|7u!JVn&/Cv!gi}D1VxVPyPB*r- ]ׅKϤWftJ.ήetJD {ˬb X[J9i/fn܎l=sP2SJA* [/Oe~LG6`_YYSȸަ4,7\jQ!LFd.b^y@ܼl F~oZҟ[8DQb$B^PV mļX*Ҧ&H K[ݿdjͅ4%%WYKi_O[BN]9qhr9g{nfq %Z&/QXW$b}?˥V4R_Ǎ!خ1/.!CE48 * xT*iCW`6l̮8Ny_L yD{0 M ?4wr+wsmqMo=4rem۩Z,^ר<;.ANL3I%ؑGēN#VG5E*Ƃ̱R~SE%LzRzhz-{UlK 6w_cxU3fi]%&T+n>-LC緋Db qW2>P!t*_Ov /uBF3y>%oIaM/q?gP.<`5ruʸ5{ [M]c8}.[JDJz21002}W⿋.(#ӒKk EzElK0fT]g%):;Yl- )/zCv5=t N'G<swsƽqWؖ򚬥"GfJqZkWh;QH['/uo=re$BL^ԬQ QtZbg'GW%goߘL#p.@ M⏃_" Fhժ"hEjbC8}, cViL1@5 cm39n;wpmlctR6VȢh$78hu> SsO8T0zIx]]E|ЇU!-r!},ꄎnGT>iYI‰۲.Ed$MW2m!l~;Wb$Zl2mg>7oZB4l.X8, xh_ 'LnYکeL.]=.z[x @Wwe+sڞv,La3-_';p;1csWݷ·6 X`zPBCLN4zI{Pz5"#7!Wp7I pϙƌŲ6l3̉70}5.fMj`$&4"P|O/UI+pc8sg3b{nIbwo]}pj[Gm$=L]sX_sTB>_11bٷcT ~`˂2$O ?tD7I&9ˬ38AYPT&krl(U"~Gͣ+{{Rd#i~㓀`x M/7Ojah+=:D+@"Aŕ75`}~h3`d:h߅/IP8-2cn2gi{yewŋu< Ѷ0@*qH)`ZY4K{/~ "YPnJc5j3L{ٜAkt݋58wɻYuNBB|- j9fpWREgyp̔'G9K8I)Feh;tqN8lJt. PX{~eν/C2i W.|2oX Ck A<soHZQB9/z5G+@#a4eiSwp}hْo22tPgNi&cL쩉wM[~mveA ;!} Ýs;x;H@Q,H5=5>uxϼpԘ´3;DSܤNc F">Icu=*"Y1? KHnаAW?['8h!W8{ 9Ǻ j!_Fh9% n/5 i4BjnVUHUE3%tR㩿 '=Ff}=P!+Z2 <ϟ: #v];4u;scv]:|or.4 Ĩ/9E*ء*Ul 7f ibO(Ƴp43 `Oޗ}p&X~66{'X Th;^pmvz*a$||+f;S̵n@et(*FTtt`)8Ƭ-Ɏp{X?mF{'0@sd$ nV-P)"[Ie,ŖP[)xЪJ 22 *O٫%q"fz+RH}UH Ҕ&&E"vU|])o&&|=Y%{Ȇ5z3_[RV ØAOE٤@O -h|O_|X^YG%(LJ8 )6E|CF4R*gL%{N~[B!ea51=-D<KWjpCS1B]z@@wt'/hb"P=ft YVEE)gG<ј't XXe-B4=F6SD~CӼykgh@zJ~"3kbgoE(47/P<0L77G{BQ/ Ε#x2snq)W`WY})/j?oH/F^l~: fgݴOֹdީTg24Rs ρ')ao Hl'nO|h`rfvQUqx [x\1sSUy*mU׸kt/l'?NY_`c?Lܴh~5bᬖWٱ$ppu7I~Is*}Q l݇Dy^{Oiˀ[(txnbvȕW`&jCʚ2Š]f*A3FvQ`i9颗fb&0f!s]U4EcHCN7i[d;*y{VQ͞YH[0ɾ0JBӞbANm1.b7S/m%i Lue)Yʆˤ5X`z82)Ŋ` 6>A?`̿g%_¤P-@#);q,y!QdyJ+O\8vG7fBU Yʔ]nU2҉K4ՙVfVS=/j WMPc&5@R# wP?́,Ukr;枣WIȰ)FIi gՔmS z3#. vjn| NEL"R', ő,)/)3$8Rbuǃ h8@9ޔll;œ:gu"lO:Zf6\/$t.l4n^.2uL y;klK6oۺ. k)]q̞ ~W\T >M;pm5CHdߌ7G-ԁ^jVFG]]W~7 -Qh18(zD 9 Фm>Ҩ[qp(G8pj]q=ŪȜo1tuv0Ă uW}q 3ʘf̓yzQ{e|1֞bdRBkJ]W$!y;íd$bDKEQJn.pvȅ9+J7-08`%J) J`/ )7N2'GAJwW:g(/.SgKn4Nly~%. hCjI\ $Cʾr*,C{Y&(rNZJ4=$8D&]"nX/M?_5$z2   nɛfpx ZG8 I1wnfy- zev~ϖi:UFTcf3gl|A҉re Tp!^f5H}ǡ`F4&3^Kz4}q-OJRDz#bĵ aE!N>D69ONdI<P?oHj$aÅ3֦d %]3{gU9>;A\掺L27Odw6+[P$$u#K3ęTo79b,G`R&_v^8rUPPgɨp+9MH=D<_;3$0%Ol=>3"ocZm 4m LoݏZ .J^ ,5FE Sxfs;]p luz߂Ie[JO(Kfe R"~ET&4`bMS) ̅r"C0+S 0I\Sx}kC]#i!XOrYm20{Ѿ Zp^H D#>qcޅWډ5_'tCy[Ld6/E.̟Z iwa8 \\p28E5K`X{'j>74FWf"xDfDM_\o73b*y2p~lslK;Hf bH)4=5DY%2g"a9Ў2׵%FRÀ b^jBk!.Ux9z6&STS"f;@\z:Ď%slK҅%Oь0v7L 7t%ېM*֗ADB5B UyD֙y050!]FKSkHmvC82nt"CJbe6P]'v Ddnˈ"U0xW3];u.I=+0pdɜ'Z +!V=^˫ah.hnh30tF50'8?F@fJy,I2۟xjOqX `j_½QW[>{|%9Hhv&c7I? kN?fDL Lg߬f4}zeHIq*?URB^S0mzRl^};/{DU:g ;Ξ]l Ts _IO|VҊ"6A tsBsˑ޿|td>ďFHI/ZFћ}ŷ=MߍMs__ 0}cH`O! 3ebpnxoi]k&ү&;eoU<v yiP [mQ.RsyhR⍭yO`Gu1~GuXa#("=uO6G8V|!eJilîKhMiS֔ndo!_rXAU7UK2Q!0A䈎e龏{:h\r6l$GKaג<4,ICYS-si`da#{?v5d!l eP1)%\ZIN|f81Ē)Cy18yrzj\ۈ5C%ƸoD,yS?s}ЀtISkOL xKK5;' \Y`FN᭄Cd_ȏGb4A tH f#F@1U0tz%Zl_PJ=7NZMj`T>rūZ7Ct|7̺D CuxCÆ||ٓ(jʛ9KmRSrsH୲M.%{yB; j]tVoާZ ~@|A]IcoXi=Xˋy[GsQV݉e:?Fsno7C#N7j80VAw\~70#-:וZg6X)stHY3HvǏs9;U@17#v^-D3۞jlx!J9ok^Qo͏zBf!sz$^ 3] V^KhIiX"5^6y7\\l_85"zwخu61[ DckhGO_*doU`BshqHJ>;ٗl ÕAVyx}Jv3E""q pZ\ %H^:;_UҲʾ娍ez#Fc'ct <뢆"5VnQ2I ȀV~r0  En77ȖU7fS[RϭvJO.B65Fڧf1+i7Rh5ӧcMo}0u{x/'RiFkoKP Qn rz NR, rF-},jDv1n~|nlmumt{Ak[ }^N!Ϛ_3 FhĆkB9[{ pvpྰ|<6滔h&sbkc-gIX'w?竞0^ُ/'H>3El khwo@,^e@הv, qB!Nֵ .D~ '݁]89LN+3 w>]QUpΈx8HHwC_~_\GZXr!;x9Ng1uGؿZˮ8\ѽ-/Lx{pk/ֶW7;аiZ%(^h2{LI16EO+l(Ԑ$yxPXέdN0Գz w9'Wbdԃ%!t- A43u7_tJ d@ z =ARma2Ng|2\ &Zv+E۲ +ύ?RF mrO5GFI_N<ٻ}*w3g8L5[y>K^1De(]HXmx*tӞQhRTdH_2;C+5oD.٪[Lv1H6MrBf0BɰD`WXi bz(}n+O|i%ծ*ہ'}ÌkOь/eqK9 6Fgͮ6Vgm)=A n #6AC}p LL?6tbI s[wΌ~*(~cUOc>!Pnt;,/+LK&7a{1ݭÈct d1lGA2/OsQ:|AUnQ28S2)FT/D͟APrѸAMvgE-$H[+/P/,_؎-:!LM#V1Odq:Iɝq{rrV(Aц ~7Pm+q .3ͩ~3+ *KѝFw xu?K C/9P0 -7 7#y -#u~1!|+A(#j"k\Sѓ9;D\N[?>r g&6(*Z*;0 y3v@m/S:*TUWVoǽA?5tbOvA;1u;hcv':xW 3Up.) ?|ڹ m$NV9̴X; pAp0~¥۝s D<.=;*WqE>JAr~T"5 BJyȍ% h[Ưi ԝ@wzgH +?O}]nA&f3C3þ8xAw[T|H=: ^MЅKœݣP)*fMiL/~~BM-xf ~/rhGW ( ¯I/X|'JO" rswV?\ȉxޮh7_[M yםlj$n3KhKݝ ua< z %#`/]{4zHbH>܅gZ}e*hvPJ{tD!zQpP %&߶񫜴j5ٴZص!/'}E!3Zr0/ 14U2?7̣?gݱ92yNU <>8t˾eAazp<- )gM}jzŞ:J?$xh A?Μ=*cA!!0p3Z׸8K3TDpT^m9mܙ^\6ү_D 68Jbwϝ8SqC0 UcCfeo' {Fͣĉ knc(&d' ϗ_qIp`YA=,wK;v ~u:|rv шgng&lKf.-,tljW!>DqKԔ0\xCm)gW7nԕ5Q1֧KZv\7JC5U:'m2o6[C}(b0fI{|)C1KVg('.#re r̞ ?Xo|(QIΪZl捥;4MV>뉣oI,+T*yO WRB;!nb6^؃([#sunv|u3/B q, $* 9gӽEH&Y-[ th/ͼcӜ\8)nP(òk[:|7c0 t-vl1od.Q[\ٶc#GH{t? ,/ګ-[Te 銷$\;I'.~CdvkrZۑvʻ^bnvlIJ4JRٱHU V<{oշb ;p1' |8z\"o-Z,>$ 7M6=X)@!(^uZP|=潵5]O֣ J9Æ2H=y3uuHپyMN(3J춓3˔k΅s <0*|MroM/PD՝GҶ}~|X5&؉WcLS^ z! . eh/ Z-'Y  a%qd; ^48 M{PʹeAӷ1(Ub6jna(N0/AQl5ߛzdPNМ.Uw=y6eHsz¯.q?٭550Q9Nj0qlja?Skz"61)K`M zȳbE-݁.G=oB^MP,d!.YnF履Xe@4xic4Ld ]>zҳ8 'McR9 3d)|E`ΌJyN: 1FB; ˻$ \l[Ȏ2|ӅoAٽ`4Z{!ޚO&|HNuflCzbLlFG‽Ӣ8.- cz$|g.aR>qk믙~'OTNt|e0@Yog`l`=6E-b}sƬ)Sb]_(ZkL^6ZGStq<]΢\P?fRPH7ML\/Nu;uT;FRo1gd z%@P(i %&ᘜic7SUOW٣5 K{HaTB6<@e3j,Qz%'M'=__|=Z뗷`|ڄ''ں6\mnK㬗b깶+^*_T=Pepi<:;쫷tLu !6nڸ"&^ӎ x8 iz&,j$ *DoyBP'Dq{a>Ll#RY}, dٜ2HȪWXl-0f @PO{7V$r)kc#RtɊ05O;!0E.n34 f)Daw5"7մiC kr _P %&!3 S;xkr诔94}(jߦ0"]pkHVo&?=8L;Ss}TI* סzOJەe6da*ox<ٍ&sh"+ 0VI!9 h^dPBs$Iv h_CxgY-=#HaCXm7l~ |x=Pij<@,0<Dd:AOE̅h(W,pOPvn/FO|uND`K0Wފ'xl!t*;ܡ;S?ͽt9ÇvV[D8֭ci ̮q|ap8}}"aSSxnDr7hDׄjOУ]Mmѥ#]3+ygxR/wL ,fgM&=^TSzH7j[~?zA=O,wDY+z~Иms!vdwt>t|k1OV4ZTQW(Dw>j$l!8.q(m|vapi"\N?0?7qgZh@/e*kJc[\a$ M$'_xP´fiΩpe'nTl7i8B:CBƑFHY5̾0 H'XzU -eG[vaи&YkV@XN29ѴΙR| ×Tc@Y_ޚǗa"zmGI{l6C.b-gvdS$sSJ8Z hJd?r5-猔^JsÅ?UhjlXI97F56J'Ǧ@\ :t2U;=r*{añ]r)o#,fP j2'_S`-a1ԏ'tV\(?~pr X]t9Q W47 en{pˁ6W N%E3s!/¼dj-sIl|QCS+B;\2Ðh?fZ@^I8hHfaOAB62H 7WOkμ@vz$_acyY)>}} B2d`Jr|%ViSܷ\^%::n6D܎&D{`o⺟j<ɏa@ArZIOyWsPbugvN@u³͓)2QSD?m:GIϱcAd' ^([tDyxҶ*=`tK:Y,3)81O<'^W)a_`7.H*&V@hne˺^*a>`&Q",]^nupDiQjna-e8?}TbxKd.:c ^؈~擓U O?zya%!DgIm 26k]ոr."S3UcNc0xP}")%@U7B2!(T|]&Tm悈xM'R|Ofjj‡>Sj_  u#$h,ŘXZ2iqq3Ш<)&`K2ef=?vξ9W-81a8_Mؿa"*O)_W$$VfWY0XCzuo=^%u>ys0dX91_Ktk[giI0y^i|:"(.Nz&("]:5U伓c_NȬơ^]DYϻXXY1%'͕5 *` 'OҲ`Ya|Q:~MbTH5r)b-J#S!BhH+(QcwĪ iD&NpZ,OyB) 4|QI(S{Ãd'z I`*y@J$ =iG>a4FZsYd,t>:JQuٽ/oUàq$?VD>J +O AnosXS'\&97j#{fvR=wG~v5Av'mn@\ZRz2) 'ҴKe(fX?h!wрSRs!1A d#ٞb&;xBr]Q_GmR -]**x`"<#LݫEX?6/y#`.W>~|_WFZ鸳aG{K:Ev9ZOϫtc4F@˺D5%ew}Xʗ-rFoʃKA]% m;N:-8tCsW ZJk".cl'-ݚ=(t:jDQeBR+HQРhE-soRSx@o9sG] iZ^wI:#gv8:S?ez:.z03 \jt&jE:|aL2Ojd/hũ?.K<7,*W>C7IER{PV*glQTOZzۣSQXE){+C"ZSjAp0KKQ~a;.ClWkUpR:)dw77w04뽠 T{erE`/# kbIQʮk]yYV҄)e^F %v+|@n1x|J3r'3-$ pm%:M64)H'bCm^"}*o+C[?2R $MKHuFm G}"í@urvSUNTӍmq4pG50;[HC[SQda\M=NW|'Yl"@ɫ-z7mm7՛| QtB8HreOQ|BZACG;Yڒ%9'1Les:3Ȳ7}jcuN4tq0Ц&FOm{݃y03Q>%1(VZ"W%'ms&Cjb`C~g`:O_V?Réj&ɚgY ʝX W uX,ۊxe|Rc^gql9xƀޒF` <g8u; + 1Hیs±4{L'%amO2C_U:U1m=u_nied7YqazX14)D~ߡ׊t|7DJ], YdeoJ,|p,߱d~"3EX1ô!AIL')q%qU`nЦw>^??N^qGJxqd5m9 oJObsɞ°TR :8&ɝМ>&R8^Y K~:Gya1' QP7C+Esoi*lUvBOi+oҮQQ5L(w fs{J#^)l V#c2P#'Q>ĕţ鮂.wCݢf))w}àXѸ>xr%*HNJYEՀ4MՁg/,W DR( ?Uhm-C*#!jQKQM܆?ˠA HR Ƃ vHȗL05i{JOou^^0#J<}T|>rVlq|ecmp=“ J|^+!:ؠ5=眿 hzuN!"p 5oWye'([s0 Q(t۷xe&@\@OvJp&΋k^-Wb.C+ B 1/eT<<!~``fHw.-ҝ= A@M;lKm/%Nt/~0ѕӷriޭ8̯0MN(e`r C88Ҽun|Iqh,}ۥ3_ԩfҧ`E%A#܏nb-4%8z(B}bZוF[ lbQN F"^pE ]T+(*~C+;0K+ /+3Rサ,\R?Ϳv"aw;͌/=Uq>Gr ɭ8䧐3Rwگ-uw%"wW ;6ֵs]o":}´COC`&jx8?wR(8k2Eq;m[ʠщaClQ\PBL]dƻ6Ϫ#bEe?N+g"m(!;=j(󺙉vHsPw*ǝj*٭]z#9M'1;kilp7y\RxOcP ǘCZ]4o5-r M![!?U,jq`(|bBDk"-mHKG`u1Q*c'RT3]뾷. h h-jPK&-LēGw^-\+CЊ z*>)XqX4|go='3D'Y+gښ" c!-7ɱ3Y9eN宄S7c5"G[ UtBq)xҐBg!8#OT,?z#eETKцl YmeUY,9gVf PM{j:4ۯjt=]&x <EJ Hs YY aF' ZmՒa[u~ZؠYQW&Q}(=@{ :֪l1:Jp& ܹ@*gI}m:Qno؛zn*EGry^h/遹Ҥ^ TEzKſpk&;^uފ];ӄ# =-UrC6mK G]wð9w="(jfWYU^50*o_@}x*{9L ]a&"Hnҕ"^Q+hpsH&Eiy6~,S$`vΧ9pGcZ8gG)5pB!uZ9FtIf2= {:Z%E |%1-my &0+X[@vjΏ)u`` At<+PIlG.X?m6a:řquƤOh)neֻT^u΀aƩ ~iFpbͩk>K3kDG kfjkh߽:ʻ`n bٻ5@:N.Oj66p1H1HT-A|pH63Be|r-Em;]VEC"ߥJsI5XyX#>T/jM$%\(Ǘ1nVJ-L; ҟ%Q]: <NҕF3{)`@ )LNtqo学{6t?ávݱ!q214 P`5#(;{mLL#;$0cP 3=}o!"t8iexcU,Orx 'nݱӷW1~OY/4nb~7QK=&OMQ]%(h-JÂ? ;{hCd]/˷](-xp#|k-anT"7sPJ, 㶶c4sw|q TnU_|yl v^)y:bvt1=DGazLGAB2MFpO8p׵ӠQ{b$G(g52H@X}a Ays6ϱlPK#`&|5@SloԾ' QXQl2 +,,2`є5əxn{J?|Q4LQ)DJVo T&[Xv _SH:YJ4 QPe<ݫ\({TzW"0xњ:adblH,ƎIQ?_=@1U8{><*1W!RyI@s*ډNA R wܚF ( MX<ʿM\x^.R\ҪV9ǗR]W@_0܁OM<1uk~^| ,mr+B)١/{B:vftq{ M 1m-1\]vY\߫F A>xx}FwZ/ nP'҅#M) B nN'}UHV6xY-UpH ^;FZAZ;m(&{ɤAUYjzEaoBCPVR>="ުf'a&KXؾ=3++ZA!j#rp(zMf%(h\DccF}\K0;sfw~hrxDyy"p;:mZ76˽R_>81=Q&6\vlyY|j}B=E&+ӌ[ku>O@jĀ{F1ɖ{d4~g]ls'4ݐdx>Kwq,<ێ#9;woxhmtV/OF&TȎI8C#h\~JQhy3}l"?[57D{]T=$&4Y?Y]ZW:-O餦DԻK;%#$on%-'E=B{Aԩ(08@9z&hťwʦ|wTPː8ۜ{ WxQ>ld0b!Uϭ,r: 3dX A`;4(aȳETOH=,n#N>DکX7?cʓQB+ү:XgnsZxڅ̄x1˜M7ö)IxrBnzWRЊqƂu8ML3V顰9 ^HVP?5g Ԋ*@K\[oj:q>,A9UV)Z!Ei +-[< s#wܘ6=V|L▫  ocua> T~<,5̎4lQዋ;YrhYU'sW.Bxς!= Tr+qb"E^4YOm1iQDܤLY_UJLs! $5xF8SS iߢS`-a1ȪZ'uɝt{+^O2=QCsrcpi|ԉG4jk WٱannSh_WN<A吖?9EBxZH3!;8pM7^ȭ\/8n[0__ᡁ[tGF:(Bע  ;LH}NʨձJU)+m)|!fEslzE6!5B1'}9%bR&œ𵆲S]o+4xsjĤVQ CN!F~|kɵťЫQ{/ک7ʛxy@SJq0g݌vn|颲)0tVRnBx^Qf &%I8j͔+qP7E6D&dFYz %QqHV;MX7Cgk+MzI{Aoi݀#}+ONLo-:A|#UvB9U@lxV׹KAquc;&ziג{p5(cƓ_\oE\cHО^wN֢XF [SmQc꠿en20AV386R4 "J큇Gaǻ}mVlxC~v/^KX߰Gmp3,5AD?VX1e#AL@Ђ3FgɆg@Ue\r1^ABܙ~/5h#_q*ẚ) 0M"m-r΍Gj7C-],' ]p8c'FMDMXq>,f[?j|"~}Y$-. `*LL*ulBkhzNɛ#-OXeZ~\-Own`F4ږX. N{j^qm"$+N~ѸGoq SYC}9.Q曕s(CI@$,C~/ҥu-B!r#mg}LLBc:Y8nith9}߹K=S@k,u.(GCHCY,[Y{#V2q)18,3646d-n9GvIoX'O]}.`wCk*EN)"5MN硗@hMhGЏ?4*g{WȔG0NNi<$Xw|.˵x^+m,`~!8Xo>TZ {ui5,&߶VW8b+C|=@’3H70&\+nk}FAV ?YBc0g;Tv͕4z &60RH=[6̤m'RRea'.+za͇R(YrPAkRFr3bMd^>A`EkkcSy2&-]*QP_o_#3 V2 T)%{b,).h"[\eiXޠY2^EJEܡvF58`bZa ;FpMQ G&C[IVzS ÷Qح荟t^^eD¹t1aE ](lsBzcKjy6,$=NE~#RaEF 7Vg\?R>}B, h+j4v" NJu KyA~/UR4(ߵlf[<0Y3ؔ[}[FŰ@^EZo"rNDR@T( \*u _m *fq cj+[eW:[cACG$œAMq=?f8A}ޕ5kڄ~CS~h1JLltu.\z}MPLݿ4B,UpC79jmfU̸hS` 04gpHx3 d5[FH0a-BRwgK'cឬt8z훣 دL{/okrTE!}gx@Y⟐@{ _kAɹG0|S*/^KfKr,sݲ7ܶ))UZޝq!u/2-1 PvWxQ*b+c\4Z J(W=),Uc]O5X|.b{G/MB@!:{8(]Z//EuKَJVOH;e8*`U̺ W5KgkqK?ݥX&.E SX&}]1SGq`9-[S6u; oaW^nOlL۸nVˮҼG" t dgNNU_)P0ϡAm%Ao)Y{ڿ1 ٽgUwT6[[]=h2m젉42NIC݊!o '#rl0JODEwEFz 2\Y1&眴rX x|j 2 7[G-b [^=‹rG1Ѵ z(_ےN)%k2Ǽ_g0Y1zmg0UBoG0Q:5~Sh'h=pk( k^L:c `_NCxt_Nqh|t{'[WzZ_lHCwBۉ"ǿp,ّ#7 c< RrEY<~!a7%iz&џ>2_-4FGQ2H;/vcrE ska\pE{Y yS$A,-lbM. fJ~~qGX|S& \&Q]͢[..^UBdD(L/ۋ@t_,{a"*&GnYi'H YӪz9\Upbd3ڌ8z,-w}4Z|{~5;mR|fbcGV&\Yk^mӌH|qTڦ"h󱏬;}Qal ܫr]aoގ|J! 1h!C_:ybPdWCaSߪ*zv\b~# 0j+cXEģ:.jR ]WKꧪHRUJi[r-eY2~ge㺝-bЦ19̊k`&}yc G KF!qvu &Z#i[,I=<-'kƞm.em*^X_ ϨYY?3ZXVݭLKiq  $t10Uz=:',J^HT= U۵mH 0-ta!4uc%Tq8HX&=]gZ %* uuj$8ڄmn`I݊&O)ힼ^|LgKESxα4aFűň_4'Gƿ7OӚ γܓ?,^%w}FG uthu`wK:3x nF !ʛ'67THOۻ@o(6Vw1!B,a86$`Ww^Z 4>Iot%]RBɎ &Uv.h`/v ޶2a{Y ew2hQ4ٔ=W(͚j&mY+o#@ eUJMm ]Y'̊^'T;U|~^"6m&D8!pAڑsn [zmqE{Yp`IK2 3II=NӱQEZOwm+HVFS? IghS WYIlG Sp>#4p죬6R׎6e$bVr;C_Q wQ1Q10>Lll})}On6}Z=}G6o>aCn}}}}7koei݅enѻ֯x }lU5#W۰7fn_na7۰=V?}/z9+pŃ A~^rG_W=Dc 6@tmajWZMC A-M4M *=xW>:;C)B4+$Owd96wIpS9/oȧnR <.]tƶe s ̛BUmƒةT!%B×g>4tw^pngBW >#\QBY~j|D@6n# op"^So1,h{k޹`r}kV_ \g^ʷt͑mV*0w:V[J/l?X"*$"GL*x V&aְU\4 d^4U2G,pǍ|Au/}`FQ.!wv6OS @?DDb2wV|9b4ެk!Цw;Sg,'"+ZWA1w𶽁O` Ɣ Rr- Eβׯň`+^,o)[3WeH8*> 's-_5x`}aNC,.+|UQ\M>PW؏YXFߑ>ϺnGVʐhA'dP!L3%=?1i]{ʾpK qEtԊ4q!vu'V|emD{O=wx7_>&@:ɂ;Ko$; b@tsb=@Gu:z7Zʋ?B0zdmߊd@'(j=Wc+vn0y2(DrN?YNw׹;r@vk1]Wp/pfj%.N۫D|T Aú5% ~qsNNo1Ð\ͧ=ء)QWb$(Oӻ*m֝2JԚ1Tn]xh\1u7#kXݗCy\w@~v9:n !vGrҮy $մ #b7ry*8u˷Pj ]HqGZ1/eG28π ӼXH. iJ Nh/f2+%9-$6RHԆӠrT=ʋ}F=MJݦ8KQ^ѻtSF%qqvޱ;Re}Ar Ϊ %]4~ =)XxeQ,p(gV hU}&WGC< q)h>f,3hjhbԮcL-O}θHt"Gm1=v~)8(.8?ח2xaO`U'OoĞ|{k]IY5Xr}͡_?mNी S}l")Iq.ɚ'4ڞV.@rgqzx@"H5y "o!nY :3U#5}$ ]'0_ "DL P9 Kreo;H`ɴ~G&ijuA|lo20@慻O X8XUbQ?x[ B9*D=8ź{u\x~>@me}VvcȒ16 "s^REr Jn3Cjɟvŷ_ǿ 8૬M3iM((9}#'toՎ -U^A1 ϥQK?*kkA2YP^׀>-q?T߳զT9 |.+AןH(G@BFtU0˟ĹĴ<6 UV2NaGdw[pwHc_ٷ;zMP[~EGPGJ t]Z{c<?: %|̱>cRKuYaeGnwĊ/KUPݚHb{zFưF{={gzHl/OʣVݍQ)>EXHYY?v*^,wrǕTJ^v_>iWf2hFsazk#o}%CCfp@G 셣{yڻZm*LvG.,EYM/-'om\xr*C$+ѧ1z__~0H o>-;f%8CWAxW@c:~d,(,)wӪӡ9) %,[sNGhrzIۡ Qπ E$8$ eGF K[jc3S&<m;DxUGcI+̶ ESH^F:R {ZF/$?" 7TQ>/YB(D3[h*xSk#+ 8@_by⩂!ghIwiBwAmGfc'ӳ41JԷ`w܀xú4Bκ왳4[/,$bu]%u͛,rBN; lHZ1mfq2P /GüA/Gw39(F;[{M۪gdaHmBOy;#ujʞHO/c8`ցU%NI[rpjw_5-nA"M0qt~0/SCxP=WpsI=ks;c]O9.= Y,/(vشa40$lݍ5]RA4 ZD,d>֚wc9'r~E=]pem fgjy[rrMm4qL=R-2NsHa^ .|o&Wfdg:0R: ښRv\vχ/P:YR5m83Ju!P$`5ڈJ= Uz cL=% j6oE-~8Z\DvX +_{u=Gĵ,L9+yg3~s~Xɲa+N*?1p Y5ckEbufN'yg{2jZ^0v;9xt?p 2F(k;韆0GtR/ lۚM+BsnE}h (MʠV]7+I,7HM(,sXɐ30RoG^_F/Urh[B\aFL{}Ve:^(HoLb-Q.YX@g^mwz @5;R-k>%'$e3h2g}$^ {l|+T}nrzD-_/E ܧs`y1׾xW"ٷQGpVD/~gߦ]* rY QFbT|*R0Tb>ߩη׳jELwPaDo_#Ϫ @~ў%l/@ H_; ?iD =P](@|0nK&ZoQ‚^E8ъ e낗azxg=T65ׇSܿՉJ>Z8d.=_4M;L M*ѣX 4ɂsރiLrU׏Ҡʽm<76+M_f|~?Ht_/P;.A1ε''ݰ < x6(zv}I1'495h(Sy,7hhU5kĤ10wހ*D~ɔ`9vDWn[2B~+%sLe474GP" }-!&.5G<$M̼V"CBAѮ3Z=F 3PK nV] <`#|鞵p) )y?3KyfFM5̦a˲,SK ܇U#dWum JY(Y V+s wᎹXÅs @cbRK)ۈ[򬖰 {̜OH'%yoЍfԒ,I?—QS|frN݇fwJ<{nV Zn 0`~p tV^"!O<_)Xe-Ty!3\B" VoWEu+d{?yO Rd6ËڐWD:!zN w}:jPqzؼT!AoT(Yc3J&*<_KSF)]h`(-Q7cޮE]Tc;dUS%3_^jw$m ;KLB}Z/ܥT7XZ?@j{VBN3e- ,3PEJ^Q}(l.MGbPIUbDʔ(/vrݡ=fv#?*E &8>tХvS3rW{[\پ+^1~s"I%p֓'X IFFO)7ێ cfvZP vbvH_ՑFF8qSmP] pDht8)=?gY2g5X^ܿ/ V Q9vB27!~ 4"D`uȥ'~֬^7քvo|wȣq%jb Q֒68'S"KY޼li4~]({+m ;dNbqMQZi͋FӦuqQAvX=DDԊ1seʼ6'Z3܌9P")\6 MT^ X(ObJd4+;ߵ@_/^%x%x>D W[_>s^- ȵ+rޮ1zAAޓ0"L8]¿Ē[J0G=@=_̚sdEJa 4)ekŴIIf<)!7_О ѱDp晗a#qܟNb@GĽ8.P+~(#b QxJm.x,l۱f<,?hϘ7dY?,BMͣBD~b=b.R#hgR`-~nzYVoiy,)s ftl.Sh32PGY.` 8',J* xڵY!b6|$^铰H&if`E<ahEܠ ;vLmn!v/%G:b tMJӊC-PҀZd[SB\dVB[=܀(Tؿp+g0Io*.VJ FlS+΂M$GíLIY}&2^~zձ|4`/x7D8&,6> b`S(0> ?!Gaٯt;!7ijE1,:_-m֩kA:_ցnM8/6w qx $+ jF?é ^ΌO b=bݣ 1e@rʨ߼F i<[KN@HG' ݱ~Hi MD թIt62y*X&azkqQ/c޶&جg6/tdZ GZĴ%# .^tl7u(kt6뒂c5&oɼXV}{ gVM]~6yTTM G~;. ]'); }/ms&τp2BPžJl?\ w3ygY+aGCK<;b29gc ;PLԀ8?]\9er4s\*uuy[$ꂇqD:Rm^7m#M&%+ѭ;'QoJԵ4#)2\}sϩ"yH# !/ pL^P-'q6@Ԟ=-|`}drMopY+"~08e+~?wmW`s}*Ĝx qA!%>v/JVN9f◎"MY.r͐YʽqJ{} qɒ_"XzHGyE)vю&%(sʰ3 N)M}~Z +̀[ecI_ 9\1ns3 k̺<*+pNw`[g/`czd1MF{bvDΐo^njedpr*m)ꑒ!ye4)$)G*Q7%3'ɚƜx Oˌswm8iۙMd4sew Av+3Fu^/T YW,L{JT Ͻ֫V)-89gɷY.zϿHf} 9TriM< JUjA ծI6ȝ<':.5=mj*%O9$ەH4da_n(yg,VO⩀綹chi[yI_i7d}v `6U )(먐6P8:N`ׁXd2h>"ku>0L_PӉ$/j#BNhEÍ7mޕcqM$dozv@}jT.Д1&zdI\HW%!>sjOFFF&!DZ7IQZ+m %{}+oTHdWĦnUoRBJjf !RTv"3 HW4wn7`I  MAZ(x*`Gww _ 4MLMPi/@=oNժj5ɒTh лەx(_)Iq@^̑'xO׎+Qza bw(H$9E2fOE@p3h݃V*!NUgo<'?D I^Y"%5S;"Hj4Vӟ-i02 Եӡc~aW,;%ea(몁v"Q5gDKٯm(G`1#n_YbC bi'`)}[5ʓ\4Ζ3J:wy`ϥ u HS38:Y;'U6[qsx m3`*圿Lʴ%yKtxJc9␭Ɍlj .U>3PV;KvFDmC8؄p!@A'[X,#{yi7@[ %(Y*fqu)P;:?s|gHN2 bN1dYЯ뱂"=:RwV"6vьX<}j+S}a+KׅVPJ*IWe[ V@9A^UD=j]d\pЦ `@T#Pwn!6Qف>iV~Q2d2(c4 *lK] WK2a{WvUcC&nǍ嗝f!Ha`2_ ;DOÑveP ɻ/%̍KX# g1U*t{MN*ٲtDghI!-ЫjA tMg@'= MQ 1HxyKmΡgv0u8_n%]TY AqKFm2 4Q~l Jмք(T­si͂kqNS@,8vPqbs tM Qƌ&ǟ_} Z-j?șژ<#[rҟS!..{H 5(JIlk9mU|ll\i6S>kLfD}@рX)GGfO~"äV~9`jT\/4@g.(=`]5h ])V>*tUӯ_wy<0p Ҹ,l\b'|r_Qd̰p@š?[9hlI]ҢӃ Vr3"/o{ַ[2qο2~V1}7ׁLtrv\!}튯) 2t 0m^*%= >uCJoN(3jfj2+p5hV <|ox#fv&{Y6Ewsr4?_րɱLb7)s 2vw?}Z3K-thܨGޖjCa'ޗ/Iʉݓ!CoT=:a] R0 xI~ֶZCHoؓs)i5d!RJݎa|U&2Uڅ $#;R$a\ ( ]$>2; iŐTi.@к9`?VΓy^6dN˯iJ^<X֐Bƨb2$y*6g.l77ݷP=M];V5)~GJj8\wYS |uwҤמ茖m&1C[b&SIAfltE'1:+ !@bl 8OgV/*_S8nɧ[BGLA3cLÍ}N/nsTݖ$~8cɞ^6##J|AձwZ|u VB#ϲ"O6~RN;MQA/]$V')( mqmE}w=\0QT!$]Ud=\W-H[!1uh SrIm/\2t" , Tc'yXd 0]C9IIaiU7 ](fhTOP*3yӆHZqZRwr[%OA%\="RC)!I:X6I$_C+: ,*ܯa[|\-VzNaRgv^T^Ju. !LVC4o=k>WH*֩wPnagI $)wDč"9 z:2 okk6 Eqz̞̱6h]uՊ s@ivx|9(WU-xRDkk"\!cEcB᪸ +vuq ISE7عSΪ#'M9qhCnQt p7{xtg=ix0̗ӝt诮R;I,c {Qc>zE=55396գL˸E0sר/voMuI,!(VOw!1}+j!/eo ?=$}#Zڇ ز;CK񣦀klP'[ s;㤹5ȗ8)!#\sX K[oЗs2lLwFgQW6匴.j&`@X 0 ˍizI=q?zz%Mp};(%nz ?r{¦鯾r|5퓓şe9e*n% g%$"Gn-1J8NZk_L8U hڳjP`Ћ;$TJg,zfF.մ,PQ{$dnt[pY<vK__tA`a{6)Ldzx0PؗQZ'fN' h$2 m ).Z+ƺ¸+*K`?Ǣ F&[ OKV(RqrSEUB .ʂ8ȴa @?:Nra{g0K,2  Th\I[V5M-<? 85<:^"cЩG`sA @cxC6UטwUt1Bڲjujڑګ_oPe6+n[\hG wO( r8c?K J|؂ړ$Gn.vn/cKL yX< }2ؓ kl5jDp_KT=(lj0ؐ(I-:>)c2Io;iR51zԏ$Str,cmO*]vyE<_8Uu`EY!]moB-{Q+tֽUx`M۷Ea'~$3`k3$vl?%mo v',1<9LiANHGe_4TS wSْ "Ass-90ApZDֿWAƽ&H5U.S7lf!eEn(#wq$]ޑ1F6ȥIq;aSvwqsBusل7G C;~BHX_TFQ?8̵m0{;\bcMAZA6lģs+7_oLcw296m~6Yb9jp"_6dIs,Xe1B.[,wPGzDӼ:i KH/Ԟ7\ w*qaYnb3sdӿ:Rt7L9HZ}~r?wwz)(c r5.l#FLWq;ypLf&k#AXPLNq*q!OPhmWLM b<]\i#yIɁTez+1oyB,F>Ş:reͥX_ncQ-Gc̹! %CA0Tife I^Uۺ*Iuzr| Bš m!BRZê iց?Bp_kԳ'ojo{ӻ~֒`>_~M{XqaUl, (blnB=2t f@8~3{.E}qrX®7eٔkOL'?ϗ}*#k f:bd` JuɳX}}R%dk Uz:9k$d.?:@jLMt21 HN= X/zeQaH8f!kAEtS}hADx)2g|d}(jrjKLT:n{<ɬ@"D0pp>wmRkŁcW="67!gzbQ8x|.xxLˡeƃ+kjfuSӗuoj'mn-+sP1`]gj?~_TJng,$6+! 'RjrPܟ `\KUbX^_;@a|h׊"Eo[l,? :Zmm<[a T؂ &0MĩrCѵW+JqW`Lw)Pv5(:Ã}4eE`,b V]ZXRGVIJr,f.21]x6*vݛ7hl ݓY.[ӻ_= W}}E5޾݉ݴۡs_q5o+/eONCX?׎ozT%nSF=FRZmE!ڲ_uJbhD ,@ 'oȬJfx˅)wy%gԕublkKze%2zmfKM0+"7ĄC KmͿ P٨O6xGR@@N#k1G"`W|kNhsm.m),HPɵfI-L5Ѿޚ#LsIdwsUt@ܫX%_>]֡/n&L/B5ުVBѽc ]`* J N㲉0on~'jz"9>ZrmrZ*r!S]G'.^tX"8h_2lݍpB{5vx :o[i!ڹ 2g2Fc޼yRu2lEDOmֵ2p¼G HZQI ̗hϗ'H4}rm9mڴSm-]D(~0ɰW>.82&Mpai LNSThF[BuO=Pm'I%ksl}$Ms ռ-qY<MSq+`RX"ͤ4&qހWE,Ҩ`t-qCS \+%lz`tTX9ξoamM{4AYbٿ/j>gm߈h Rpp9ɸ1Z˭@, MW)}> Y9WE){:-Y9t1]3²|g,X| WWÔ:v+K SsrHԿYl~'y&,YM2G_ JӶkFSAb|nYmxhb:^ WHPO<%dp (:>bK0y3 6'$'A 󳳴"lL$71xO#8r/F }nM.<]/ ZĮW\5ߥ7o#K?ʳ|'^.?{HFeW6î|5~%3Bx/gG<!e\\[tmLSDHcQ7r#\ {E[*7 /g2JU[Dmk)_7ӒVX&44={cBj;k3{Zy)[W\jaqp1Q퀏 tYm+7rґY_3*z"- <1r'׮kr.fi@i3I؅֜)F ^?CQC$R8 ;N>OX$0MlفaŭVn] Dif#9S.^QNdkO7^^^^^^^{W?ęt[-,;Ok=h~nr.;Y'omn_ݞ @VK;oTtK|ϾeG.BO龓(C*QԲ&վ[`U_ibZ.g@ԃ|Ɏ d͞]cׯҏ%G!n+Vdy2qϱYmd{3VR(iHJ ($G +jPL0g lVusEXqZi\t=3IbV^*uxKOqoo헃jkBdFy=a0NtrIkv?Ӫ>BR?- +H(>Y `?:}o޶M3|fYץR>_뤥)DJ*Bw:؀6|[@9#IN~_7|7ډ&kQP? $aF~ZtUDW:3>_7k$0eRnV?_,HOLGL~q:zxtE"GPx79:bKu)_ڜQ]{+NDWtW*j[⋧뾿.AA6]u#+  xJ͞uKqB6{-z(|OPcE:c"_AU :om!wX^6'eKQρ9J6ǰ,njI L&R'8X<)} @ol؎6}NUEI>'oa{If챮cM Fga+ A˜6`KSRgKV|09wQh`6[ &k +.IђtA|/dVeroP 1j\7c V&jRY}o*'EW€)FsgWYaC w?=}ԵIs-/j 'ͲkϨEJ uhX_KMte|L:f:K9?iUκ)kXT̝cD) z#Ya.;u!Ea'_ D/o)f//HtU;-w\VteV=JG|q=cdB*KBHQec5q3և5ґEe y&:C78뗦 LqW0]q^nʥpNuV?M@WS/O_#0y|Ӣ s,0c$4i@gk24=٫Zs=^'`@jwN^_[/nэPJAS4W1IHYD g7\FOdA+Df)'FN($:}<i:.D&` kDndQPdOFu4P#{ e2#]PZgso=ή1awK]{ 2J^iYp4yAZqJ19rԦ'vO%kUFI:Dp1XbfY 6MU`aQq0UZ8R +O){vl8AL]cA퐗AL,Q=N,H~أ+C%2& !FFэښiL4~P^:y}3g&;}ࢃϥ)msKwY\VA@Q^lr .:^>.( 0GE:[bۈBU= :t %̀|r!x(o% P(Q|tNR_-o|-p` 6upAQ#S I~ ُ(=EG`XBJ1w7!ۚ!֕ŬFT5Y̬[=uԲڍYN)`<FѢɄHr&0p.w0G9Kj$`>q)1؊+` G1 #)/|*58X3 eal󧤄QAɺi,(_xCWfF maMcLo!֛a{6_8[\xka.n9+\#4U\_X)^Q^+xSz+{n^"q&fBƃfQބe|kr{j &JoZig]o2฽>iCV4Yт(ѻyѷ(C)s5-uAJzzy.k=JԻ-\0R 9{km&: ::FHYg`gؐsuXx~PqD€s\1.ZusE[K9vDK$s-U޵vk,#2Q VB+1@.mS,}^?Fϥ I%x2UօD?w+~@&kC P_r8!vg.L>!@b{G9r`.8Uֽg?R u9bIQ#m9\)`ænK! ?2Ͼ mX ITXŗr:HfO oC] -Qa8ӝQ[tgR K _r6eR#( ˣy-} &n OI)IA/{$!qs[+K2ev$_u>DLvÌ8^~3v׿_[7 ؟hbHXoN/eB# ~Bmx Ն2 =ٟ3^(JL2ZEyZP]:qjW<| 9|㻷cS>sND'C pS%>Ņ'|*-.HkN6xF:şV a0-^XB!JZVa`ÊÇ293gWLa4a[B{s\%r:VM3_"nT[cM<ဆc~ 1pzLpSK-8HՎ1O醰ɯOZΞ|-x3mF8Vs,17;W\hnOQ;o8c?/[C;%FjC<5W\B?ŮgDJ7`*oPfDύ7pɰ;T*8^ϪGs >>3[:iFEXϝOcd}.qw )}5ltx5~Xrz&WH\i9A))+H(7Eؽy55B&#%"}t XSg jqfc=GB]su"axG(O-*8 G@YQYbX1Ó%lf;a+M߀kMZ(}(n*A;w45𔕉0ծ%jVDX#gw/~fn▼?Y 'm}^ԏL-9HVj^E/:mi ܑs7a+S_ ^b(;R99$< <J*ef).1]8@$Tazσi钷#Co4U#D ;DeHMq;+L-]c񎲳¾oc2'FzXM'4a,a<3.sWky #`}UQ^(oR^D>+1}u $gXsGXĦ[: Ș@tшqacT?/4\n seD)MgT(+8%&gp%L$|"c {P[nQb%~wǺN;q9.m\0 j 1gc]hw_P[g(XɹCc 8g?x xsݟ -1˜fTiF+u 0l -y1'x`@ϭϸy=UҀ&ⱐ84>/ .<)ko(c\I+Zf)q7\gv(yɱGd8pzF[Ԋ ''TCFeHԞܭ5s!*9~7Iύ)7$!zMp9l)P\9UEyv&#+kjﭐeYM$P pQnfP#ꃵdOnzP |O¬t?Wֿrf\T^<-b98}9% hE')ݸ'dzgI|,Aln*wM'nytQX+ t\l6w> cuIuX1%:E\W߅+`7zMqIG*"6/G+QVx.mY@n >[咅=ֹ_osxƋ4Ld-!#ϋnнyr@Beb+1݌>+R?-~^q j~_,uk2<v. va:a,j$v X$ofLAMD=.AOB eJx>sQ < ege.=|cIL ނ-Dz-!e c˳-54Z lPʳv0\:0m1QT:6v"̺<*0ޮC[.KR[1\x|,Y.5U8\d9ߩQiX^3SgK}f/Mc>6\HRSs=BU} QɊmڼj X$M=A7v/:#kPEaY%]6ȪPO"1O'PTȪjkftY&W!!Qp^pa DzhY+S-}TkfS0㏔V쳴Stc"HQ#deN͐310d_t1Z4q&sۏ$LKw9K!?fTJ/ѥ5#=@0E Yb4 ؘ$iKE4k'$RNKy֒Rϗ YLg0#q<>[ЗςO$ J"xɱf)!-ՈZ`^&U;q݂)+ U\pޥi>L23M`:(X%$ xEXHך?)ET7_PṞߓFbPFB 4+ZBi3doaB"Sry[VhJ6CM7f̊fŒ7L o:TY`\n+/@֦K]MNה)8[!; Aw$$QĉRJeupQ!h\>#.Kms~rli#MIGBN)L{sb#l*P[YJu ̀~ )W;'}S;| yxɦRA ?J;H%!tqy]y#mZ];u2>pPl!D<׷xmfRDO.rqȇ؅ŝ>wɅ΀GVhkX5C;l-e2moǨ;c^w;}˂V7)$-x_W?_Z0F֨t ^>3ۤP%L?_VۃSA!͹+owSQL@Xq)pZS}| 5ViRLqP0Fpͧ2:WPJ&+YKX;tKC,N'2x[:Jr,m&UK%%z=q^J+\䐢^N?a!_];pqE3:sa icj sO+\r!8Qׯ޼p};TY60?kҐ34z)ŠD&km_7q%m$/KsPR#hv +yF yp{gu`1}jBe>) ;$zl%Rz?(C`{Sr暅)+PXXƻy9:#"r)?%_q팲G$1[]8MT1I dZU`a$̞~@L\DQ0iz-GJ?JP5jv)Q]Bz z]gGXץB'l: >r+l=K$񉝡i.^]a׌ѫUy"iR׃wT5n,HП]_X-d d.\ދ= H>d#]\ӹkd|V w.et [Qsy?#Spom1,ri R.#yQGs$"%|rѡl4!@rE%`A(6py; STv11ğI~{C]7e{>h1 WЋҾj6{Ene 2>imr] /pE*ptj7B``NV^ؒՂE,ħWT|0K~Iٕ3V$6J.́go<յBSS] &!_4& b y"ASf!h'Vjo~Hdh NHVXstSєҼ~vVQʸ5DRI2,. zi39(%0eZ'rq煂a!gq^[[Xc \hʂUTlma_\p ) q1t̅wVYQN1&T>KE?sʸ} 99AFVU Ey-,H`~j`\"Nhg<(TH*Ŵ.$ ʱJŃ^`A1Jf' 0l3ENУa{P\NZfy2]ToP1} S!pV1"rϚ!^N .c? .Nv$9CErty[qmfLD{. Gxmf6mA!KϘOucM-.=@j n+ pUR̵?Wr?sƓ7M:,Z<8¢MlS厓ۤ+Fjn]AȰаwm&aj>H=26B%f hT¹n/\9{" ܯRk[E/nA߉nHq~M緋XՔ[EsqNAdaaKF ǧ:[ ɠQ_ۧ|gwQs$Ц- bm9;[h?.rχOQRs_ڝe7 hG1r8!2eMLofͦY12 V [ %"o\1> TW--OVF٫^ybM%49L`Gx5kJ(;9bUI2Ljs~_3FV#m FЕCm/= J Vܿ/1C ?:-7ӬzG~NM&cg_F΍+f_*:PGtkEOlP)KOyx{\ 꽐Oc f 14SJ+4v{ۢy^sڧ!t\ -u  絳eBq$<~ (D CT]*K8 *{zF*̡Ǥi&D8!2y@Sxd)(lE yS:`*lY}P5PmmfA}u=sy׽ s`r V7C3❯NkC.."}YXwqBb7+pSQlŭ* ɋZ)FR)z?݂=;Vu Ng[N5' y &7u:WOnVu?kCz XfUo#5IEzqQC:rj Q\qݴW)txmj%e_8"MTۭ];sIo  }baaeND][F|* ١MTώ+ߡx2ֻ_Z)LFZ7\sګ^=i\D))FS"x8XieClVjz h F]}́ZnyVZ:,QSֿeyq?.Em cL46 .0 ?rW_72?0!O kEH)!3 1ۂdUL&I`ՇO*U{,~p7wJSy6ȿѷ݇Cqhk0U޺RtgLM/ {jaQ(JHݼsW58B캀wx׆;zݫB\􋭰y7 3DDoQ5m_AShi]USw5Q1Xv^In 1H/|}} tSlm|FvO{ Z gh?rL8`-Bdԩi\e?h'Eޗ}wY8 D\$wyq{@d3\à鏵=ĂLuq'&+mx<@q*q!OPhmWLM beˉbu,b6Z.[-) V> do1AY ̯*pe{dzLFU@3NѠW߳?Bril'`w $aZ6zjʆewᒋj?*&abs^Yow=j@tx$@:N{\$vHj`RL܇vA[ڸ Mg'q4f䦓xyV hӝ`l#4tWW!'(f}"mI{ 7vڽ7~ ڊF&T: .CǦ[8Vq-ȕ C 8X,)Ͼ}-D+τIRHc֍ذS9sЋ!s=^1DX8UR9X0".GIz 秵`3*_TUEFih֚$n ds lzf³X.26`" Т&3C̘"ZSttyNJxiM[RRUSn qyZkhk*_j?~_TJng,$6*頙ЀFW"'%Ax Z }T_1p9nK?IBB9Wᥟjo>i?-_{By|]ybϵǥҙӛ/͑&0)C %iG)A!y0sDHѤ(k|mR\]5C|I;e8 ) (=Z=X Isύ5Hrc?*As3 Ok5hHR:]e4!s\lNe,//GuEO'{Ձ!l+nη/nicWJN@$;WDE/0oKl0}϶d{K$X - w6 37Luɘvp@/m19䃰Ǭy*kLhۈFp#euЇ4iɉdŋYr#1>żA>CgppMY t ].crNϝ<׸T@0igo+""B\o)s+CR Jz)J JpR ]?ۿd^!#]y 9 OqƎ~Ǧ:=(qD4z$ސm%1B7@ oCރ ܬ${S:!d&.4u:$IA%;T)1`g>~8V,&P7weu҅aq(x j/Ԉ<S Wÿ"2'=o(ly]"24d C#9o>.7Jvgj9'yxK;}nfv+\䣒I_;B"@f)0PP*?Q{\&cVm l|w$yuUĵY+tS njiޣ͎jc~N`skuS4F 81"ZCJ6 oF˔xJ(Yx;0EZu'n!SO 7V"O˯EĞWe01 *͟!O C^.ir(˅6%5V!nU ,RfV3JiF u9;$OxQ.qu)oڠ:QZifOS@"~GI\"N=ױ.6 ڣ'X鎩|7|72b'FWiGIz*t_yF2p)'n$2tyE2`%g{7e[n[+ ]x#_T\00CBS+v K8'pk=acV31>nZsG #C92n(>,> {|j@M< & p _;3ꨧT ZZA8bCXy4S.nN[nQ|Ɇ* c4[8D` !43si'(1<٨W]*jd*Z"cK6l* M;do^hob&ς~Q_ Z kw<576X~bxZHS w$U̥+vq..6:d ]1T,"܂s*nL][mÑyTL;{`npZk2GiNN6:F{a* ?IZoK!^uoAĜ_r2K$?YԫBTyU R?"Qr5`$G\wY@v2oQ:n!K8ÎaNɍY+'݉AޡoS&c2G|v @B*Z+#"NB"PԶ-ƍ6We$կ\k'z&񮯸\I`ꌠ/+<0n3u.NU)zRFh󹜃,R\ی\Ok2!Hz34V۝zFmvQH؉'JM]M!D`NEm|7͸PU70`ij3@qU >6dMQ'bZ)5e%d%<ؕb]jdчk.Gfnz>Y^pIU,/r8/l پGPQqػv"Jw])LODe@>{]?߷ڇF^hӐ=%׀(4H,1-UYSojd|9_ڷf-lV{ݐMŁrN}Fž:52ZM.U{rF[Am2A*M&:Vwv4Qa1$Ҿ,M`iԘMcTi GYmt,/,M3Fޝ#bѰ6Ѭψr.n j &JoZwQaFi>\[L97˳ ty' 2OmЦ~kga΅dSؤ{|2&@4؝0;uG0ɶ7eBcm0f=H /i,F|+5Dcft0⩅W9vDK$s-U0[QՈZ'kW(#rz:I=*"l[4nuXo(R_#DTBphCh[{Q%jn`bjUj"&n`!!d x &E,ce"t֑K^|@1<˒~ A*Ev%*8irdOnV-h~>3fԩ .YWr$ ߧR@q":v~$w|vw _9 I%%<:C\AlMitÙ,{[@+nqYPqje%ǜw5e -(:_H=]QL.@)%`5JBN &uH&_{aϏ͗>4 D.lnQ$Dm\v#TV,s"[֤7^td'EM9+jg3pDF;Yդk?ʣ.`~jKfTIсEa3\\2fʉim6S⒩O,\ tGoХ sY.sظK@`+A U;F\Ģ4i_MruIBy"4 bg{d DO-e# ka;rtlKi1Yܑ=q cF, x,,/hqF/e[3DvBYm _T*;!b I8Y{xStD~xV`>_6?Znz DK:S,dBXiuWJXR2q'c -Pי8('FN^PhA`6&bB`|Br/ue$?ЪLђS[W |߮ѡR^x\Yq墙h2qSŊ/ABaGUa]X9إd)XCR:=֓%.Q}sP dX^K"ף'LO 1]!?~Li24Sly'jj ƨ6ͰX U߬ fU`ip&dҖaVnAc$Yڄy/ܓmd,b?k)'`JvƄͷy,lkIe[)64$5̓H(`5jDxŨ~oGYb4^.D~Y8{@~0+:6^&rB'Вm*}\/PfDύίhJN@kBz}Ɠ'Wa$po pxxCQZ}S6 u0ճh\E5GXFwhsk2f Rͬ0DE7U߸|jֲ0IS40R9'yg@#=QؖvPk>N3)}B*T[^OuF?)ar5? zlY 'nEHw͡{MT焴ʻ+le (z 81:k(-p99cYs l3ve!Gsf^ H:nD /MRټ-x կp)}Yy_443 ?ѐUdlXmGB]]\wyszp}º>hO_a<3.sWky Dhok:![(OoG*U]>:'Ȟ-__1`Gzi7Osu|JubAxWH,=VnV;DA+X<9SK4ehV iR:LއL!.62$ƭرn'δKG ͧH!"UJ& 1d|6{fk83n;Y0 ]O8^g$[{m X*OF8;exsS٬>vE1:ɗ-ǎygZZv: X75:"l`N+8xM$(FFt\d_!KUvZAb&aQغFjaP5"VNh"Jw9L#lxQ݈F7@qaҪ9eXR􁃍= LLP͊p*ٜ ~@{ꚣhd6t%eKH(+7'0mkpA^V7^%^e /9=˗1xLi1>>yuLBcu/l1eukr5ݻ0EaHZ"/y,m, =\$~ѱݣxKy^ SP/kŁ 1rZc}=T-o@1Gp?0J|jNd"SKiWM8`JB>^𔘚Ա|yLyRWG/_*Hg\E.|`I\p&NofH]IG({0=E՛w(favI*"6'n̂RqMљSuٹMBZH*X%{7^XW@ߡ%NJZj:};:@`#bzIn4䔋GUlrFh{{ nkJ6l'Fjz+4jRii{/1>G}5XN<;4Gaq &:yKLft!JQ.4riCFU9CNxYf;5MNc}m/H$*EYVڌ[wJM>8R@sV?: "46aZ;#M-.a0̹JbY BbynTWS5 ^TCfKFv qJNy0X&U:k` 5R`!^rc&KS=SdQo LnzdH EAh0X.,( sq8n4U-s2T&}i [aJ_{-S e0m- `DnB 慑| 7&>|a?9YߪMa,uVmmikC 35k&`#ec@ R4@u|G#]a˸i+3T9lYaxK}N {Xi H04QXz֓} %hY KŎSPP|s={q/ZWߙ/&Gz"fpWp3A~f}sd߿}Wh<8.m/ٜ{!P>]w5\E+W QRN F*Տ?z6o.xFa1$ٸuD{kb:wc* N Y厘NBQHp/._~9–73NcUڨ 7D&܋r2}6 c l%:MZ}HZB,lg火!^!Af'FdlvuN׺ۭ?LJyWF@h0nh[,=PqhO~LfHɐBMV݄ NbKy!+ylGlAu2>pPIIZ'P= ˋE Rɂ7O?=a AC!G z}c/M\⛈*As&31>cEX8?U2hn*Su`)rQ&XIbn6WiǯJ"5G $(9Rc73(]'I'oفNf EM^WsYD ֱ0Q7Kxx]Iz0VNI9ɘbmq/)[wEvh4::(φ^ Ƚ@MIΛDˉ*V]&>dA~ә膁(,ԊDy7"M,us\+L:~7/ә:jۄZ4/c`xCs`]`왵G99vi[C sK;pq-P>!S/UHI3>O5:ŏv٭k( k91dQdYS~m0UC⮚(+*-mO$s@I}_/k ?o;'+JHZ)c>H쫊{?L<x }4 +YuݏoЈzluaCZ&-‚ Ϋ_~He-.Ypoa| =+UbR»gc_'UG6 NS8q{䐬G`5e2*;9\(wt| Xo&'Іu@3A sұg I]lI/l+yS=b+b^}-FS~.wyQB&JWa#n8f^\H`;X0|8¥VyA6Nq tXW@q)d{Qag]:p001A?߉_y]vY> 3已'KkW̢G,"#-I;1P6FqG:U =(Ag.m2. +8³_jp{&By* ͩ2 gLp ͅ_hNg|82h^YAN\ĪQP"ۏ(7z_QP@LGNw]ө^D-EqW6,B,az^t>b|1'ńfa-ax" ;H>Ӏ-MoKfQ&b^:63HRsvtedhwEWyj0rjĞӈȣp#+aKHRy's)ㅍ<\6k c^B yƽ<"'#C~fxz|$^ԋr.Qu\$BbvLGKYqSw 0,SS.} Ds'Wb0]dRZ HVc{[LuVu g=׹oDd_"4ɉ GVܱm=`uk_DЧ$zx p 0U=5+-؎"S?*NC" ő[Էd(D@UMr-;@aq~|aFjj^1<LA0;'<[~vx% _P:0@FX7: DyܮLTlk2:L=X\vH\Og]?`*nS'ClbRΩ9~e @g*lpbJLfJηNכvOk.q$:+Xf.`¬@ZexH)էlUor}!M9zتE>oݧpGIN9[EM^&l%p\l/w208胾v2g6z .P<ݕgGqJdο=HV߅FHpk$nwhm X/e+vO:1Qz},isj`MgVZcJ4]!XY "ЧR8fEXoWTzh" [7),ڥTS=آy,DьstB:~Ո2)ܗJ$)Xpzyjuu "{@U 7+$@v `5Q|] A\QqS1VJWQFv-L ?IM\uοRE8m%>FS`]y}ܬq+naEy!;`벟s%Tl,)G݆͏)ݳ"24Hݪ|z"݇d{^` xm4Lj_oGդNߏnXX5۝ <[[U{EԳ?E>Ö֠S *i.?gEVoMoxE+}y!"x##L:cӺ06؉T7,iOŞo"Q. r:g3> ұqN h4x<ϟD[ڟ9N٠: 5Zs/(J}/U'4ywb?̑AAk5RB8[,((gEPL%kꪳ҆1nV2QFg:ZӌoK%@/N8:\f~>=@^Qan z36Ʒ*ZvXiDqwZ:UDHݭ_CAzcg/ ,e+iuR)r<~PҴ ,N^L/p!h[`ecޞf IWTQ+`srDvEBi5 2l6 <'$Cji 8Ճl\alR4YK*O)J<ED[C"9>)&t#+tjEe 4Bz](jj?ѦCnӄx7GOax8Q]fN t""oqq{^tAMKl`3$ԫEҮ;pk[XAġܽκ4`>fFn^AU$O5.n?`w*iZi9<<*Yri4҂S s+<\7Mw﾿Զv R %'KD鯧>^kO\cABs:a}x|Œ?#W6[u}w/S>=QڧW/(S} [ yߵ__ڐ!1>~Ge o,EGcq 1x;̡̈́۹)Cc9`Km!R BoAa_Y4qM֔kZlz~0v>b޵][H<ƄV?'N_A0k@֌ XD?Zm+3l RjZj]1CFyhV`ӾOΧ_3LKsn T#u|l߁|`Zqӓh/EEyqB尠m$i—ph_n('L`Eq?`^1)c5X́5~B7M@tQPγz@ e-=:a \"Kdt+#'Ǘ|8&WlYI6u͐Ct Ӕ oБXhfOʙEt-"z, uUi!̘o @0_YH\/LSj5爊(oPAbԆ2ox]N{`2[r%Y;^V'ZTU*&"1z}p]Ci8q):imHQ)*E\e*6GLeĢ+tEuBn)7)ī@b%D;kDX̃`1JS*ʹRrq?S6΢~]kقc)E#+t }:./ *EƊZeM=Rck?,?2R1p}JrUeR=e\ͼac6]pUJyý]?KV&ϼqTe!44-)pAy>_>|~?KMmY#Z8^WRJL5[P=NJ:|̬vm!f2: ׉2G#'{/ɞLY!+LCxhuDߥ*E6ŷٴ![q'YCϯ\QC ԣ!kxjpˀToU(Q{4ƹ(T.bLF>N](Q`dYRYK조l.ݹ`P6oNJo鱦gI ־p`Vt[VBX8d0{$v3f8-+$<mt+//q3* P&Ho%d-StR;7 !Eq$oe/4sY2r $`'03R5z'+؅evkJ-M4v=/曢ߊ&g!qkLtP bؿmy2rZ,jFL'.n^/",Gz6pOm8>^jŻF@fNgқqG|!LOMɌ3\d?#Ro\S=h6yvnͶd9K&in&c n7*øA?ꂷɣRG(k4uGcZ,&4[\jɍQd¬]Mn;ZI2C 7$LK d6fR&Z:)ňЬOWyR=8i绒WAx@&7{&"+Wnnz#0TfY@$*/V!xbD$ܧ6+qVRK,-e;C/LjUeAz=zxZo,CM@r4GYm]+Ƿ=HJ ~[d W9C `̅X"- P/$d -@kJb#Xk!~S %c86ntkiDfC]ܒ1#ij7`l_[Lŭ'$鉰#uFg!WPB4Z WȑYtme|d&Yzyލ#:7: e[Cb |,-~Z>6NG҇ 1dv:V )-4H8>Ro1e. 5N ))Ȑ6ػs\DQfgJ}cr$ E.Vj$VWbEW$'\2f׆1ӯ^ރ  =iBDɞvv8^6iñu߾tV0)ٝ쫧:6Ma25s9JQ^Njn`|mYN29h(*ǶA-7l % "L vq4%jrX^|&XV6 U:Z>IDzh.F@SQkäϳcazzpx,O0%^hf'ىY.ƃ$0_O,^ avwMpM(wTR}^ QLl~7ObP 2~RFor_P Z^?&x3y684MD[TṀUO ֡C{BCm0[D'M@6HoSFJhf=/!GSWb`Р"^1F"*ݗ5sT&U2˹ڳŘsͫ3sa u.~_(p}lԀ*Oj};%'?.m)_X6Đ`yaEgօ)=E( pZmx6:\g B"qā(nz,Kj|S* + m;d}l$n\y &XݓPd&Efe"ʘJA!WTqGa;D)X$.r3,ܤgoxFjH%*e˳5"4#Q}, $*%КI[xH;_IaiUڷfVa@DžiwreۃưĴK@95(././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser.ico0000644000175100017510000034144215102145205022327 0ustar00runnerrunner($00$ V3; Hz?00 %j   h(C۹ ]ÎÏ̀ǖ΢Ph"xoΰ=0ΟTtbƔڷəŒΩɕȘW,̟˝͡|H(ŒN˞Eʜʛ̞vλϥ)Έ˜΢2ϣȘ̌ϐș΢[ȘɚΆΚďÏÏ̢iϵđٸ̢qÎtɝִϤ֮uͧ=Գ3n9 ~δzTj5no: p; t?r=ضܽÎѨʛǖֱ̱}wCO#_ז^Șèxd;gxBuBԭő|EW,ɦЏWΤݿ|Iɯհ\1s~LںЦß~ռt@h?ʊSҪ׶zzGjBS(ӹ|̌U׳ƣǬұkԴӓZyCƓάea8H˞̠l~XY'{ƇPW%LnG۽ju@tJa.Nrۛb͡tNh^5\lDR xQo;xbp<ںws=ٹU#Ю~Hh3qI|Fж]*d2qKoޜcp^,ySͪj6{U﹉XlݵʬЯݪ|bbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbblqaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbtaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbblaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbtnaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaybbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaqbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbblyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbhtqaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbblaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaavbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbvaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbblaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbhaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbwaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbqaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaayxbbbbbbbbbbbbbbbbf`ϸbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbf```fbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarbbbbbbbbbbbbbbbbbf````o͆bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapbbbbbbbbbbbbbbbbbbf``````ϸbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}fӂ҆bbbyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbf````````fbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbω````````````````````iفc~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaayvbbbbbbbbbbbbbbbbbbbbbf`````````k߆bbbbbbbbbbbbbbbbbbbbbbbbbbbbe``````````````````````````````````````oٽaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaanbbbbbbbbbbbbbbbbbbbbbbf```````````bbbbbbbbbbbbbbbbbbbbbbbbbbbe````````````````````````````````````````````kۡjaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaawbbbbbbbbbbbbbbbbbbbbbbbbf`````````````bbbbbbbbbbbbbbbbbbbbbbbbbe`````````````````````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarbbbbbbbbbbbbbbbbbbbbbbbbbbbf``````````````k͆bbbbbbbbbbbbbbbbbbbbbbbe`````````````````````````````````````````````````````衭aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaawzbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf````````````````bbbbbbbbbbbbbbbbbbbbbbe````````````````````````````````````````````````````````kל|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaanbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf``````````````````bbbbbbbbbbbbbbbbbbbbe````````````````````````````````````````````````````````````ݢaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaawsbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf```````````````````kbbbbbbbbbbbbbbbbbbe``````````````````````````````````````````````````````````````ojaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````}bbbbbbbbbbbbbbbbe`````````````````````````````````````````````````````````````````ݚaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf```````````````````````bbbbbbbbbbbbbbbe```````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaɋbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf````````````````````````kbbbbbbbbbbbbbe`````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaubbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf``````````````````````````}bbbbbbbbbbbe```````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf````````````````````````````bbbbbbbbbbe`````````````````````````````````````````````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaayxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````ibbbbbbbbe```````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf```````````````````````````````}bbbbbbe````````````````````````````````````````````````````````````````````````````kؚaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarlbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````bbbbbbe``````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe```````````````````````````````````````````````````````````````````````````````kaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe``````````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe```````````````````````````````````````````````````````````````````````````````````kaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````````````````````````````````````````````````````````jaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaawhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe``````````````````````````````````````````````````````````````````````````````````````ۦaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe```````````````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarlbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe````````````````````````````````````````````````````````````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````````````````````````````````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaqbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe``````````````````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagsbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe```````````````````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe```````````````````````````````````````````````````````````````````````````````````````````kaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe````````````````````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````ڐi``````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaqbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbb}fm``````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaybbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbbbbbbb````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaavbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbbbbbbbsaa~m`````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbbbbbbbvaaaaa````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbbbbbbbaaaaaaam``````````````````````````````````````````iaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbbbbbbhaaaaaaaaac``````````````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbbbbbbvaaaaaaaaaaaa`````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbbbbbhaaaaaaaaaaaaa````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbbbxtqaaaaaaaaaaaaaaajk```````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fbgaaaaaaaaaaaaaaaaaaaaa``````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````fzaaaaaaaaaaaaaaaaaaaaaaaa``````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaawxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````naaaaaaaaaaaaaaaaaaaaaaaaaa`````````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarpbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaa`````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaao````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`````````````````````````````````````|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf`````````````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaqlbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbΉ````````````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~```````````````````````````````````jaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaxbbbbbbbbbbbbbbሂƓ©bbbbbbbbbbbbbbbbbbbbbbbi``````````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|```````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbғlj``````````````iͯbbbbbbbbbbbbbbbbbbb`````````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbb`````````````````````````mbbbbbbbbbbbbbbbb}Ɖ```````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~``````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaqbbbb````````````````````````````````mЯbbbbbbbbbbbbbbbi`````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaai``````````````````````````````````|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaypbm`````````````````````````````````````oЯbbbbbbbbbbbbbb````````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa``````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam``````````````````````````````````````````bbbbbbbbbbbbb}Ɖ``````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak`````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa``````````````````````````````````````````````ibbbbbbbbbbbbbi````````````````````ebbbbbbe`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````bbbbbbbbbbbbb```````````````````ebbbbbl`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|i`````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|``````````````````````````````````````````````````````fbbbbbbbbbbbb}`````````````````ebbbbbd`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````obbbbbbbbbbbbk```````````````ebbbbbrd`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaao```````````````````````````````````````````````````````````ϸbbbbbbbbbbbbb``````````````ebbbbbqd`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaao``````````````````````````````````````````````````````````````bbbbbbbbbbbbb````````````ebbbbld`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````o~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak````````````````````````````````````````````````````````````````o͆bbbbbbbbbbbbk``````````ebbbbad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`````````````````````````````````|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````bbbbbbbbbbbbb`````````ebbbtyad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa``````````````````````````````````````````````````````````````````````bbbbbbbbbbbbb```````ebbpaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~````````````````````````````````````````````````````````````````````````kbbbbbbbbbbbbo`````ebbaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````}bbbbbbbbbbbbf````ebbqaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaao`````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbb``ebbaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````````kbbbbbbbbbbbboebnaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa``````````````````````````````````````````````````````````````````````````````````}bbbbbbbbbbbbfhqaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````k~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````Ljbbbbbbbbbbbbaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa``````````````````````````````````````````````````````````````````````````````````````ibbbbbbbbbaaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````aayyaaaaaaaaaaaaaaaaaaaaaaaaaaaam````````````````````````````````````````````````````````````````````````````````````````}bbbbbhnaaaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````thbhnaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````````````````````bbbsraaaaaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````bbbbbbbnaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````````````inaaaaaaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````````````````````````~aaaaaaaaaai````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbltaaaaaaaaaaaaaaaaaaaa`````````````````````````````````````````````````````````````````````````````````````````````````aaaaaaaaaa```````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````````````````````````````iۢaaaaaaaaa~`````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaa`````````````````````````````````````````````````````````````````````````````````````````````````````aaaaaaaaao```````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaa~```````````````````````````````````````````mщ````````````````````````````````````````````````````aaaaaaaaaa``````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaa````````````````````````````````````````m}bbbbbbbҸk````````````````````````````````````````````````iխaaaaaaaaaa````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaa``````````````````````````````````````Ȇbbbbbbbbbbbbbbbo```````````````````````````````````````````````aaaaaaaaao``````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaa````````````````````````````````````ibbbbbbbbbbbbbbbbbbbb```````````````````````````````````````````````aaaaaaaaaa|`````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaa````````````````````````````````````mbbbbbbbbbbbbbbbbbbbbbbbbb```````````````````````````````````````````````aaaaaaaaaa```````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaa```````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbb``````````````````````````````````````````````aaaaaaaaa`````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaa``````````````````````````````````obbbbbbbbbubbbbbbbbbbbbbbbbbbb``````````````````````````````````````````````aaaaaaaaaai```````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaa``````````````````````````````````bbbbbbbbbbuabbbbbbbbbbbbbbbbbbbbo``````````````````````````````````````````````|aaaaaaaaaa``````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaa``````````````````````````````````bbbbbbbbbbvtbbbbbbbbbbbbbbbbhhbbbf``````````````````````````````````````````````oaaaaaaaaa~````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak```````````````````````````````bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaa`````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbwayvbbbb``````````````````````````````````````````````aaaaaaaaaak``````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````i}bbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaa`````````````````````````````````anbbbbbbbbbbbbbbbbbbbbhqqraaaaa{st݊``````````````````````````````````````````````aaaaaaaaaa`````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````bbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaa````````````````````````````````aanuslbbbbbbbxzbbbbbbhqaaaaaaaaaaaaaaaaaa``````````````````````````````````````````````oaaaaaaaaaj```````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaqx```````````````````````````````bbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaylbbbanvbbbbraaaaaaaaaaaaaaaaaaaaa``````````````````````````````````````````````jaaaaaaaaao`````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalb```````````````````````````````bbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaa`````````````````````````````````aaaaaaaaaaɃbbsnaglpunaaaaaaaaaaaaaaaaaaaaaaa~``````````````````````````````````````````````aaaaaaaaaa````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagbbb```````````````````````````````bbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaa{baaanaaaaaaaaaaaaaaaaaaaaaaaaaaak`````````````````````````````````````````````kۢaaaaaaaaaa``caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagbbbo```````````````````````````````bbbbbbbbbbbbbbbbhaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaa``````````````````````````````````````````````~aaaaaaaaamcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb````````````````````````````````bbbbbbbbbbbbbbbbhaaaaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacmaaaaaaaaa~``````````````````````````````````````````````aaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaybbbb````````````````````````````````bbbbbbbbbbbbbbbbhaaaaaaaaaaaaaaaa```````````````````````````````maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac``aaaaaaaaaak`````````````````````````````````````````````iۢaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbb````````````````````````````````bbbbbbbbbbbbbbbbhaaaaaaaaaaaaay```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac````aaaaaaaaaa``````````````````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaghbbb````````````````````````````````ibbbbbbbbbbbbbbbbaaaaaaaaaathbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````oޅaaaaaaaaaj``````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb````````````````````````````````bbbbbbbbbbbbbbbbbaaaaaanhbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac```````jaaaaaaaaak``````````````````````````````````````````````խaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaybbm````````````````````````````````bbbbbbbbbbbbbbbbbaaaaawbbbbbbbbbm```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````ׄaaaaaaaaaa``````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb`````````````````````````````````bbbbbbbbbbbbbbbbbaaaaɏbbbbbbbbbi```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac``````````kâaaaaaaaaaa``````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasbbm`````````````````````````````````bbbbbbbbbbbbbbbbbaaaysbbbbbbbbbb```````````````````````````````k~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac````````````~aaaaaaaaao``````````````````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasbb``````````````````````````````````bbbbbbbbbbbbbbbbbaanbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac``````````````aaaaaaaaaa|``````````````````````````````````````````````maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{b``````````````````````````````````bbbbbbbbbbbbbbbbbbanbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac```````````````iխaaaaaaaaaa``````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaao``````````````````````````````````bbbbbbbbbbbbbbbbbbrtbbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````aaaaaaaaam``````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaa```````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac```````````````````aaaaaaaaaac```````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````|aaaaaaaaaa```````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaai````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac``````````````````````oaaaaaaaaa```````````````````````````````````````````````۷aaaaaaaaaaaaaaaݗ``````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac````````````````````````aaaaaaaaaa`````````````````````````````````````````````````oaaaaaaaj|ۊ````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac``````````````````````````aaaaaaaaaa````````````````````````````````````````````````````ʇ```````````````````````````````````````````}bbbbbbbbbbbbbbbbbbbxbbbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac```````````````````````````kñaaaaaaaaa~`````````````````````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbxbbbbbbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````~aaaaaaaaai```````````````````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbanuvpbbbbbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac```````````````````````````````aaaaaaaaaaˑ`````````````````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbaaaaaaabbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac````````````````````````````````iaaaaaaaaaa~```````````````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbaaaaaaaaawpbbb```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaaaaaaaak````````````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbaaaaaaaaaabbb```````````````````````````````iaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbaaaaaaaaansbb````````````````````````````````|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaaaaaaaaaaa~````````````````````````````````````````````````````````````````````````````````````````mbbbbbbbbbbbbbbbbbbbblaaaaaaaaaabb````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaaaaaaaaaaaaak``````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbaaaaaaaaaantb````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaawpi```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaacaaaaaaaaaaaaj``````````````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaz```````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaadmaaaaaaaaaaaak```````````````````````````````````````````````````````````````````````````````ڮbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaa```````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad``aaaaaaaaaaaaa`````````````````````````````````````````````````````````````````````````````obbbbbbbbbbbbbbbbbbbbbblaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad````aaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaad````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````oaaaaaaaaaaaao````````````````````````````````````````````````````````````````````````}bbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad```````aaaaaaaaaaaaa|``````````````````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaa````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````aaaaaaaaaaaaa```````````````````````````````````````````````````````````````````obbbbbbbbbbbbbbbbbbbbbbbblaaaaaaaaaaaa`````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad``````````oaaaaaaaaaaaao````````````````````````````````````````````````````````````````ibbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaak````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad````````````jaaaaaaaaaaaa|``````````````````````````````````````````````````````````````ibbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaa````````````````````````````````~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad``````````````aaaaaaaaaaaaa```````````````````````````````````````````````````````````kbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaa`````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad```````````````kaaaaaaaaaaaam````````````````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbxaaaaaaaaaaaa`````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````jaaaaaaaaaaaac``````````````````````````````````````````````````````fbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaa`````````````````````````````````kcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad```````````````````aaaaaaaaaaaaa˾```````````````````````````````````````````````````ebbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaa~`````````````````````````````````daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad````````````````````kرaaaaaaaaaaaaai``````````````````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbblaaaaaaaaaaaa`````````````````````````````````k|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad``````````````````````~aaaaaaaaaaaaa؊``````````````````````````````````````````mbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaa``````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad````````````````````````ׄaaaaaaaaaaaaaao`````````````````````````````````````mbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaa``````````````````````````````````kaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````kñaaaaaaaaaaaaaaa٣````````````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbblaaaaaaaaaaaa``````````````````````````````````jaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad```````````````````````````~aaaaaaaaaaaaaaaam`````````````````````````bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaa```````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````aaaaaaaaaaaaaaaaaaaـi``````````````aaqxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaa```````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad``````````````````````````````kaaaaaaaaaaaaaaaaaaaaaaa㒡aaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbhaaaaaaaaaaa~```````````````````````````````````jaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaa````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaa````````````````````````````````````iaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagqvxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaa````````````````````````````````````maaaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaa`````````````````````````````````````|aaaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasbbbbbbbbbbbbbbbbbbbbbbbbbhaaaaaaanɖa`````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaybbbbbbbbbbbbbbbbbbbbbbbbaaaaaɔbhw``````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaybbbbbbbbbbbbbbbbbbbbbbbqsbbbbbbx``````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaxbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbh```````````````````````````````````````k~aaaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb`````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb``````````````````````````````````````````aaaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb```````````````````````````````````````````ֱaaaaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagsbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb````````````````````````````````````````````aaaaaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbΐ`````````````````````````````````````````````o~aaaaaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaantbbbbbbbbbbbbbbbbbbbbbbbbbbbb````````````````````````````````````````````````aaaaaaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarhbbbbbbbbbbbbbblbbbbbbbbbbbb``````````````````````````````````````````````````jaaaac`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbb``````````````````````````````````````````````````````o`````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaytbbbbbbbbbbbbhbbbbbbbbbbbbm````````````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaayvlbbbbbbbbbhbbbbbbbbbbbbbi```````````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaavbbbbbbbbbbbbbbbbbbbb```````````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbb``````````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbb`````````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagbbhbbbbbbbbbbbbb````````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarlhbbbbbbbbbbbbbb```````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb``````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbb}`````````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbi```````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbӐ``````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaxbbbbbbbbbbbbbbbb`````````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaannabbbbbbbbbbbbbbbbbi```````````````````````````````````````````````````````````````````````````````daaaaaad`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabhbbbbbbbbbbbbbbbbbbӿ``````````````````````````````````````````````````````````````````````````````daaaaaa`````````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapbbbbbbbbbbbbbbbbbbbbi````````````````````````````````````````````````````````````````````````````daaaaaa~```````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb```````````````````````````````````````````````````````````````````````````daaaaaaaak`````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaayxbbbbbbbbbbbbbbbbbbbbȉ`````````````````````````````````````````````````````````````````````````daaaaaaaaaa````````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbo```````````````````````````````````````````````````````````````````````daaaaaaaaaaaj``````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapbbbbbbbbbbbbbbbbbbbbbbo`````````````````````````````````````````````````````````````````````daaaaaaaaaaaaak````````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbι```````````````````````````````````````````````````````````````````daaaaaaaaaaaaaaa```````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbb`````````````````````````````````````````````````````````````````daaaaaaaaaaaaaaaaj`````````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbb}i``````````````````````````````````````````````````````````````daaaaaaaaaaaaaaaaaao```````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbb````````````````````````````````````````````````````````````daaaaaaaaaaaaaaaaaaaa|``````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbb`````````````````````````````````````````````````````````daaaaaaaaaaaaaaaaaaaaaa````````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbfm`````````````````````````````````````````````````````daaaaaaaaaaaaaaaaaaaaaaao``````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbb{bbbbbbbbbbӹ`````````````````````````````````````````````````daaaaaaaaaaaaaaaaaaaaaaaaa|`````````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbsbbbbbbbbb{aaanqbbbbbbbbbbbbfi````````````````````````````````````````````daaaaaaaaaaaaaaaaaaaaaaaaaaa```````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbranvbbbbbbbbsunaaɃbbbbbbbbbbbbbbb̓m``````````````````````````````````````daaaaaaaaaaaaaaaaaaaaaaaaaaaam`````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbaanbbbbbbbbbbbhuqqtbbbbbbbbbbbbbbbbbbbb}fӯȁタ`````````````````````󒄚aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|````````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbzaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbqaaaaa|~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa``````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahbbraaabbbbbbbbbbppbbbbbbbbbxtbbbbbbbbbbbbbbbbpraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa````caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabtnaabbbbbbbbbsbbbbbbrnaaabbbbbbbbbbbbbbbhaaaaaaaaaaəaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa```caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaypbbbbbbbbuaahbbbbbb{qtbbbbbbbbbbbbbbbbbqnaaaaaaabhugaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabuaabbbbbbbbqaaaubbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbpvqnaaaatbbbsraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalaabbbbbbb{aaanzbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbxaabbbltgaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahraa{xbbbbbgaaanqqvbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbhzavbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasnaaxbbbxnaaaaaaa{bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbtuaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarbbbaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbhaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbsaarvhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbblzbqzbbxraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaawagbbvaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaathbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbzaaaaaaaaxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaagnaaaaaaaaaaaaaaaaguaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaanbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaubbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbnaaaaaaaaaaaaaaanlbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbvnaaaaaaaaaaaaapbbbbbbbbbbbbbyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaubbbbbbbbbbbbbsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaagvbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbvaaaaaaaaaaaaaanbbbbbbbbbbbbqaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbnaaauxxaaaaaaaaybbbbbbbbbbxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbpyaaaaaaawbxnaaaaaaaaanzbbbbbhzԌaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbtwaaaaaabqaaaaaaaaaaaanbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbhtvsbbhvyaaanbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbxyaaaxbtaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbgaabbsyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbt{rbbbraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbvaaaaaaaaaaaaaaaaaaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbraaaaaaaaaaaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbuaaaaaaaaaaaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbqaaaaaaaaaaaaaaaaalbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbhuaaaaaaaaaaaaahbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbyaaahbb???????????????????????(0`ϏPmѭ(Ī ύιYᾺֲ$gʛeÏ&NХB˞հɜɘǙnjVۿ\`w%С&ժǘ͢ʛϧ əƓ˟՟0ӬǖďΡ9˝sԫvΣХGѨ,ݽձʝn9 ~t@ɦʊTt>o: q< IX,p۽̰tA~s>őzTȤxE͢Qȭ[0nFںǫa8ЧƔֽ|z׵ʛÎu@ǖۺd;Z'\1gLjQˌVάS'q=exٺԖ`Z͢qIL{zTp; ˌUPX.ԯq״ĐֽϝnֳɯܿY/ۛaɦ͘iȗy˰նЮg2lܽ|I}GvO]3R'i5žѩ̠ۼWԔ\U'ԳnG|őT"ӮšδyQe6V+qqCa/U$Օ]LƇP{vʭg:c0j6c3{UX~RRRRRRQQQwRRRRRRRRQQQQQ}RRRRRRRRRQQQQQĿRRRRpr\QQQxnRRVP[|RRUPPPPPWQQRRRVPPPRRUPPPPPPPPi`QQ~RRRVPPPURRUPPPPPPPPPbQnRRRRVPPPURRUPPPaWPPPQQRRRRRPPURRUPPPQQPPPTQQprf}URRPPPSQQQaPPPPQQPPPPPRTPPPSQQQQPPPQ_PPPPPPPPRPPPSQQQQ^PPPPPPPPPPPPPPQQ_]PSQQQQPPPtPPPPdo֏PPP^Q\uQQQQPPPPPP]kׄPPPPQQQQQQkPPPYPPPQQQQQQQPPPPQQQePPPYPPPZQQQQu\Q^PPPtdPPPPPPPhQQQQSP]_QQPPPPPPPPPPPy|PPPQQQQSPPPQQQPPPPPPPPRPPPQQQQSPPPTQQs{PPPPP[RRPPPPZQQQSPPPTQQTm{gjRRPPP`QQSPPPTQQTPPQQQQQRRWPPPbZcPPPTQQTPPPSQQQQQezWPPPPPPPPPTQQTPPPSQQQQQQo[PPPPPPPPTQQPPPSQQQQQQVyPPPPPPTQQhiPSQQQQQYqfgjQQQQcsQQQQlRRRlɐxQQQQQmQQQXRRRRXޓvqQQQQQXRRwQQQYz??( ͐ȗϔ,*حAÐֱΓ΍ǗνΑμǗ˞T˼̳ ХͳڻϟƔh@հ=ܼӠrΣ~ɭϣPoΡz՗_Ɩn9 ~kBšƇPʧˌUֵY'd;o; ^4}Xa8׾è~~XϐWÏl8׵έז^Œܾ^+~Yұ¦m9Ұӓ[i>g;V$ΎWn<[Ͳ஀Œeͫe1xE\,ͱ׷Ҕ^Σ]qKxEW,ѷa8tAx}PW,S"Ѹh>ԼuAuAܩ|_/u@mEf=\)xܮt?a/C333GNOMHF3L:X`V\[3Q1861111=k33U1661g9o1u:e^8Wc142pr11111]bh?422z11~tlf<>@A2B1122A@>PY[hN+L Z-Y A!Rmj2:2+r;mo4KLdHBבF!5ѭ*Md{A$@X3q |q *1B2[< V_>^ ӔxB6ʭ (%+ QdӤ܆])l7{%_YAsIc:+x| k / ­^܇҄BF#go~Dh^wS6>cШx wLJ4;PH gnLlͲIk$jYSR ۃ@=L^A[œJ ^mQhI =VH`Wi\ļR%}(B0ItP SVD}Vj!lo#qbR0I[YPd$PCi3Ai*| 0 lטv-H5~r|8Ns+!V>ͭoK|Rb׈685A:6# f+wd 1cL%αZGxf/lo$؍JŏlW]pefeɜhva:ÓM']J-XX}6uˍF^y )f:bqm2&_(oa->qlB_"_$BDp80 Y>qdh4Z*zY/* y4Ia;fzղׇ\ AA (|,Zb>ڨht e4~,=ֽx ͝Ŏ W2 ^@:!50jz++wWӧljGp/!|ku,ڞ5 R-Jҝy h&;`!N`G^D%SS7;u"XȫzM*a+]ݹv 8 o xj`J أPb"Pa)ͺmG  {W[)L'[W R*l;)>N)Leb4s|ۛ|XOi𤡔)dՈ؀wxuo "J9& %{{}QUmC^C8 -!`q5  nB!-:]g1#`2o'kH7:<\;O`o|Y4%}p+4g@;>T+bឹK*܇HA|8<랠+!ci񤂏x4^Oӥ@1%ȗMvdPĝ+ɔ-"gDw~Tzˉ¶ LH/&\\y* OK&x:(:F&5ϥ_yU ?QaT}ό2%{R}~Gw+ Rkz( ~:MCdQL8gT H X}W) a 4[j0 $dMK[4sٌ_#գLTHw̬O b8d8Q#?&c,y/k,s&S_l)EtMtQ=xN]xdb587f8Y3z|@*- 䪙Wgz}l7Pᚮ$G7_lgwUS3 LuOsdϳ^{,V}|#h1) 3[,+= :"zY@S ?&Y[xkxߥ|Hx:=PxpöTQmٿ ^m~(aJ6p#QՌ+1aμ.$ ':xW 6?Wm# c@GvOYe͢y0ko>?'d{t Y  Sb^4?lUݒawA pj.kM+k?rU\^5B_1cma0ATS^Q&(4BɀapE?_hc3Bl$ ‚ $8,qVo ٹPJ`|x\U%.IKPESxg'hqYT@(3wi__JE(*,B(b#EpN)\V^ elM`?S MC &""AlT3HNb?^2sNs~u[" nWR rQShR|A\UlIu[*tb$c|S6츷ޕKlN`Zˁs(8 UU.b؀Kwp=Oa$ oSR /k͠90`0O"0f.'^Mj;G"ҳyRTǏ@Nvdew:KT `?ɼ>0^ЩBJa25 b.Т,bfmc gIq0:Ʈ(nܥNd9æxtj].fk'f.BIQ޿ lxJ+ oXJ-&Z0@}8%3.&3Vp~`v8o2QR,_ fY:w/W}һ'|Ewnp`vؽmd*O@s#μ*e|DTU_Oˇuho۸>2r* #|ݒbuܯFlpYZeZW[iXGݦ5a >v7/L7xb2|JY'##T_v.U4:>>$;奰vWa_HGx:s"+o6}QhQeߞCy:vx7m_;LDnyow_+zѝ>FўNt_w vXAݞvWL+h.ʆX+̞:lߴV;u6ÝvɿsշOJpZ;ݪlN+#pQDQ-V1SuظzX5ձG2щ~E|TG RN)r nS_nWd qQrN׳?K~,y60 v8Gu.&OE|F_MF煰$%#He1oH>ZW:O`Α2ۏ[[jE2s%N=f; *Ց)Aq5Q Rj"y&&) `&,iEAJ `.]ta}FPEtӌӔ?}㭤S^$mEh&%6/+b K*Gھ?Aw ;SwFx?XFp/Sjg YQLپ?Nn%,7Siۜ (l7,%80KQtt9mˢ)ZVdWRbWWӍ@g߯}!0(f\AlȮ #8؀e~]<GtttCBk~lȪLWe.:D?oA*r9P>, )ŋ8cg&jutt4 Ns\^@YKa\tO~ZؕCտ+nE,FN=\t -9 ߔIpa40bydQEFڷk+w.`,"kY$tY,Be:-k=WG-p8$@FQ] un:N(/uU5?U| jI.G::bVM|{\ӟ:pkm`kNm}$h2ڴȦ@0 zW͙ aɊ\RFT\b[6cMŖMʼn0Eq*N;6{Vw\7,L G''N]`klwSd+ǂN+ۥFE<+%Ϣ UgN#ԋۂbZ:^O .L$tQ+Wrne7[og=u/\ e׉`+?FJ~(s![ma%+_y``7BDx(EGup?T3TZIq9EpX`q2C}{.i>;—ӞQw\ a4lr*X 2_A{:\waPM|DmO_ ]#g?qTܾL|-h7)J)+]&\.Л3u<ٙ(M *{=pH,">_X vfǶ.R:СCa.U0ѱrWREpH{%SGES ~ZӱQ|D{û{n)^=,s+.V[2i`=6BfSF+ۊ.J]MЯ׬J=2^|ztmGtt{nJK%Fh:/ _p&c(\Վ)^>)XLm3^ ڶ#::ʃ12W/.1Ʋ kW@_&|V;]d4N?)6<"@^[͂rh9ozKZ2)?W/L~~H#::ʂ=v$M271;dp^TxG)E8 {ik/&LxvQ)AiR 2, *w璕l+.O咎2`XHWwƇS\Dx__mcϽ7_%-3"5 Fz=2i/e='i\R,1gU a  hf&gũ_Qi?Wmg'S{#r%?w^Kʊ2zf\EIR`$Ta>~.TR@+f׏z^+Xr׭Ϧ/RդL UlϤxE=sI]Mh. zoz,yz`O~w?,]?ዙoC-OpsUD=5#P#'#잛`3,2}Gر~q<'Ӟ{-P\T}1תXAoΥIN\/f~m=vH.A0`cm>j,}~xl҇}a>޺l#, p-p}>p_ˍQzM?X_Cǯxd0mGy'a'}KPb/Pp ;?X -ҩ }ix (б#08?_[dGbRe/yCݠ76+GtV!6if朆gL2<.T繵WF/pl?}Vu2{uW:J~ ,}]ضǻ܅j?~\ (p8 T_=R'D0VTQu~;3ɄG'N XR(q ]Y@,&Pd]W?[xp4إ<oՆq陶XؼgW=.NoO䙎̈"_^?&Z:^*_`UT϶Y1VuϭUFsajL`QϿS?Y % wԑ* m$mTlK2$`0X2q9nJqG#?}ٺa)(7rR%@ j X}ŪP"={p7$>: YkA_DghO/8ߣ,'l?IT\UN =\&>ysg;yOjRZ1oc'}J~%f+*J `1r|8"\[c⟏#8j=rOy'D;p8vQ"w2K`PWR$Hť^[;bgϧ>%e7zg} SU%RlΩ]Gm4)-)0@YR|H"#)=rKF]7S{lٚpط`˥c>O#9>ޝ0L~)EM[xЛ62[).!)ZlrNyONm[gMo#'N/πW͗{< KN)jڢYisI] PR{! l#Bx OյЫ@k2W |:Nu!{3ݞY > 95}}թ9S|Zȥ,׹|O%Ih+v"t5~'4k7<Ch0L'&K;|f<2P^Tb˾گUZu~C V޽m̛$LQ|/2W_v^5&E&TVnEGlqRX e[v%",$P:Ʈ;$J+> !Ą[v 7n50gR)rT]h嶋00EzˠT.E_{Ն1NX{LcrUl%WLs&M0+4X.#OeT6&G "׷͠e hFt&n/]o %}a/u:`+ER.+mx1^[%sߞc;{򵏥.Z YI9BȳVMjjF AN@[`0GlKYŲkՅ`x|_vP%gWp*{ےEI_9EEe! ښbI.<ϟ>',Ʌ1 Ĥvh`_ >l!_vR]1BJ Znlekʕ0ex%Ҟ_N| 9 c :=,ёRi'-afZofr%0߹#}x4Q x" 10G*ue朆g'Ͼb1rI|WkA$[ "9[7׭ttp/ࣷ_~r<6Fq>6ZgG׆;\?];|r MG'>*W1r-'iGv1A/6m:tm:@l|247 +?VqDb;|X뱑I ҵϜ 1靹rsIhrp#ݹ8m5dm DJAM$׸]ȉl60alXIax7tփKB$wl8LVOMz,HY'@\x 7g*Ͼ籅G\n*:o618'ɗBRB])HRYϬ=ts4f>FJŴ݆s8<0e")_nʍrҳ@@va}`nSIua{Hڥ V%B'(BHUodj@@ 3;vO<}.e6(>9(RYk`(DQ)o^; Y*=v\q pC`$ [vbwbh,bv:aCN1TjWUyItեwxm &xKhGm`d.G5GZjt I*Fe5ϸ{ZNt5#7hw2xIХ{o&ʁWɕנMHS"\^&3JvJtqvH@7 Bc$$?ތlˊeN}gBMӰ>6aNf~F Tw|#!0ƻBtceذM89"]:WYggH Rv7n~`OON.mk#Y̛1=(m }}('%L3!6'EܚX>iwj'Ԣ |0t84w };DV.lU"V>Gf4Td-hۖOrQvڶH{oK:*%|vt}aia'uV%*5g8x4K#'clݷi@Kr6!k"}`ԶFYLiNe{9`W߲$stܯ~(qR8w(PE:Բ 4eG8̛967X -NTVpr9 G0(,ք*/ݘ X"Ua)_PR{!C+K5*03r.)FU*G`8^9 k,W8 crڋM`ٞ}y-u VsA⸰َ/^9NNܑGΩq KE߫ 鄠ܪd_>f;f?7Yry2`T޸ :O}ܾ@*.嗊-^@X{FC(rN¢JL-Ʃ%@J$H4 ů?vCrQq S&<vtt0}πKEIˏXT~*~=*!֥2[uj̯D̂ (NYE3̞:Zzqlc\(=6oBRu !~Kq]8uNɲ4rOvnT L~wʩ(;|g&?~>K$ ] ʼn<شC?SsSokU~aOn?& Mzb"rrhBEp2[-_|9:; v5֩yEIAlj6H%ΧV-d%K^}@aS{Ǽ7|vY*`[7d^ ȁe#oXz(!ZZk0(``JV?\6h<4%H|}8+9i$<ԫ0pphmnvQxO Bƫ%,ZQzv_isN`eO>?UN(5$ 0_u#G.1\%{&;6d_"fKۈ{ 8~~EPQY"ώ3VVJ4?  k*Ӫ~6Q}d<=T[+xm tؚKvQc4'u6nɲ.dO$WF`qH(+9)񩉮M1/π{sky,1YYR4T_` m'<\eV}EK45!} l~Y' *`U":nf0e\W\ʯ@juҠ`Z2Ss.zNttf0E\].jp?9Is:5sot[(,R&8 /=t7]u9`zKt!DFep&H{{`f@q~kX<G5 v)DI%z|+m8 ${'_E% ~ Tx_6ݿ{~r.*;DSJ`!*wa>GPr a/ᒽd;!B]~1Asl;-8"!2-s9AX_)T/Y_;@Jwj}_مC {/[8v8 `7ŕ$=8AS"g}So|/M *Mrnv$ï±P:_*FBn ۃ # #ۮrw!t=G5qXL0,`E?M]}u/8;7_Z_6 -&xGp`:XB_"\B::DŽ  Ka_`ĸ)pߧGN¹EqX h06mЩ:Np Y&4@z%zq[.S%C8UQ+?S $'S 5JCo_͏.yk͚q~E>IN@RlgMR. (Ç~b5xU=jůS!A\nN+Hŧ(ƔN+$Á=a%R7_ςE"W~"ĵ Ɩ,iz-eS&bdU(b7_zxBAVg'UAyY)TJJN+K8nHv[ݔ$rRD@6&)K x{=\b.9D9@x_, (RJEE? lw0cmL"KH, I7c[ńP ԕg2x!|w)q\tE dtKruX,UY`uRBr|dSHXr=)!4 խ_783Px \YR/`ѐt,:~i*>ηV |eCv0HQ":e7i̝L~3P^[\ O.* ZM  3n(: [^tΟ#(nq[bG%%C݉W#B=lV+b iE@*%Bc%ttdEy@' 6u >EEPT >߱ioW:r|f:q+ξ30{ >wr|)6 -6I906ZI@P v?>#8R3ROfZQܢ$F pvoQ|t3 }ص?z"t%oιm xꡛoQ70O䲢mƓ;إ߰^@3=O@">0lݒ#!A0M9SZQY)nkeJlQ$'DCVеC+ܡ%Hq1<opY}hS~WK6}X;rlثл-2lՊe^pirLcXhɮ1_1j[d9&”sIm `[6Q{@ai3US`60΅B(+-B76鬬Q\@@` Bbt$DŽB492 KP^NM08RM[2p&t.`CfRSaM#`UáMD_22%̕dQ=fi~50J|u 5?+ڈ?.% {5_мJݚmR2]ý,)l aB/gtq,6*jmC|rYh>e&%+A.]Z@x)㔹![~^q>9QA^3Mxίɏh^ X0]0 2~Jl?Qp#{3_,jEiL!ūcUIϠm .amm#a '],-)[7B݌G;J_="(VݜM#j!9ObeF`} sԄi"1\Iȇ'\d;ze+N~J7Z =m#*l>=1{sJ`Оvw?zJJ]E<#SR S*^<ۨ2`}ۮ-Rd9S"ev/jsx[ i@&>!~xϖ[ Gx_cx@.I Fj,I]|$Sl,qgn%:lM@ɧl5؝Ki$@nN@12MMYGj=tBW!,M`{(\m: %0xn!i:Ekwfa&qd J)fa bˍIh|tNt3P! s=^@ =5Pfu}3B{vq RvېҨZri+bcbIG t'=DAgR&MwntQ>4;RJd[%@&3160;s@C`2FyV`O1KMFC U|}XzUOoa9؜f^6*-l 6YBW烎c<,j%O%|Qќ-YU)26r.%F{n0zs,POsl\D@FW灊0}0ߪOW=$A&-F~GGjx;zwFAT2~5=Ⅾ)>\t Sjh}z䙢JHx h-zJ66gƛy\EWv^:ivCX&Zi}.5oM_FWz] CK#[WBm{ bfwB*Dv1;,L - rc4 anXhU(oEW2E: /W@*~x7f Ӕx庙GilFjA0"1pXQ+UW".Ổ븈Ӎh+,0=7L V.&9|\GFt~B;PxCJۄ )dĥVᷴ$Q]+hp`8X% /3efIAwUslIK#Uaw+rV#**҉Y Z=]B {_C(]M q* @c,XOC p"5YnUĶ V(lbd=w ; 6Wmf(P~V ],l*2[ݓ:J@$Pz9J"agI@:FWkh! vD4*BTfVXDĬ_Q$E|&gHu?~ ™6dfUR/d?- 4ϱRВJQ)IENDB`(0` Ҭ(˝ǙηϏWϧ ʛ~~~~~~~~ۺkώ~~~~~~~~~~~ѩ΍o~~~~~~~~~~~~~őֱ̀%~~~~~~~~~~~~~~~$Pܿ~~~|~~~~~~~~~~~~ҩN`ʜ~~~QՕ\~~~~~zjۚaՕ]Ԕ[ؗ_yֽgf׵Î~~~~t>n9 a.o~~~ʊTn9 n9 n9 n9 n9 n9 n9 tA`7jٺdQɚ~~~~~~t>n9 n9 s>u@y~ʊTn9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 P$sN$ۼ~~~~~~~t>n9 n9 n9 n9 JiʊTn9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 xEǤ'~~~~~~~t>n9 n9 n9 n9 n9 ʊTʊTn9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 zTwӭ~~~~~~~~t>n9 n9 n9 n9 n9 ʊTʊTn9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 qIs~~~~~~~~t>n9 n9 n9 n9 n9 ʊTʊTn9 n9 n9 n9 n9 r< ˌU|Ga8n9 n9 n9 n9 n9 n9 n9 n9 qϏ~~~~~~~~~t>n9 n9 n9 n9 n9 ʊTʊTn9 n9 n9 n9 n9 t>~œX-n9 n9 n9 n9 n9 n9 q=˰Ό~~~~~~~~~t>n9 n9 n9 n9 n9 ʊTʊTn9 n9 n9 n9 n9 t>ۼkDn9 n9 n9 n9 n9 n9 ySmĐ~mԔ[ёYߞdt~~~Z'n9 n9 n9 n9 n9 ʊTʊTn9 n9 n9 n9 n9 t@W,n9 n9 n9 n9 n9 s>jUJn9 n9 n9 n9 n9 Mu@p~ʊT|Fn9 n9 n9 ʊTʌUn9 n9 n9 n9 n9 t@ڻo: n9 n9 n9 n9 n9 ͫen9 n9 n9 n9 n9 n9 n9 ɦδnFn9 t@mȚֲǖƕ~wܵ׷ҮԳعǬԴX-hԫvӮɛ~~~~~ȗܿ͢QХGΣ~~~~~~~~ں׵&ƪ Ȗ~~~~~~~ڹȘ~̀Ρ9Ɠ~~~~~Œ͡đΣ״ƔϏС&əŒ~~~~~~~Чm՟0˟ǖďӬ͹ΑY????( @ ̣˟ƕӫΗV ̠~~~~~~˞ϙ"΁ǖ~~~~~~~~őӮΆĐ}~~~~~~~~~ۼ.ֳÄNs>x~~~~ёY}Gu@Wzӳֽ-Ð~~Ln9 |EˌU~~~T#n9 n9 n9 n9 n9 o: X.{߿˟~~~Ln9 n9 n9 g2~~T#n9 n9 n9 n9 n9 n9 n9 n9 |Iظ͊ܿ~~~Ln9 n9 n9 T#~~T#n9 n9 n9 n9 n9 n9 n9 n9 n9 o; ß~·%Ï~~~~Ln9 n9 n9 T#~~T#n9 n9 n9 r?̪qIo: n9 n9 n9 p; ۽"Μ״~~~~~lT"n9 n9 T#~~T#n9 n9 n9 mֶtAn9 n9 n9 ]4͚ նАX}Hv@Kۚa|}ƇPyDT#~~U$n9 n9 n9 y̪n9 n9 n9 n9 Ĩ Xӳ|In9 n9 n9 n9 n9 zC}H{vÅN~Ӭ]4n9 n9 n9 yY/n9 n9 n9 uU͚ٺq<n9 n9 n9 n9 n9 n9 n9 n9 LՕ]~`n9 n9 n9 yln9 n9 n9 {UΘV+n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 g:ʭٺW,n9 yɦn9 n9 n9 c3ϥݿn9 n9 n9 n9 b2ߝdqח^i5q< n9 n9 n9 s?lzɦ͘in9 n9 n9 c0qn9 n9 n9 W,͢ȗЧܽžiAn9 n9 n9 n9 P$ѯЧ~In9 n9 n9 j6qCn9 n9 n9 tѯP$n9 n9 n9 n9 jA˰}Pn9 n9 n9 ~IŒa/n9 n9 n9 άɦzls?n9 n9 n9 q=zTٺӹb1n9 n9 n9 n9 eطךe6n9 n9 n9 ʧyn9 W,ٺδnGn9 n9 n9 n9 n9 n9 n9 n9 n9 n9 n9 O~ΛyQn9 n9 n9 nyn9 n9 n9 `ԳR'n9 n9 n9 n9 n9 n9 n9 n9 q< ܜb~ǗYsn9 n9 n9 \1yn9 n9 n9 ]4ž|ryGn9 n9 n9 n9 n9 {EԔ\~~̜n9 n9 n9 n9 άyn9 n9 n9 ]4]4yGšعwdpϝny~~ӦյU'n9 n9 n9 uAظyn9 n9 n9 ]4]4n9 n9 ]3ɯԯ~~ǔЩAۛbo: n9 n9 n9 o; tNάan9 n9 n9 ]4]4n9 n9 n9 y˜6ƗLo: n9 n9 n9 n9 n9 n9 n9 n9 n9 ]4]4n9 n9 n9 y΢̙ח^{En9 n9 n9 n9 n9 n9 n9 n9 ]4vOn9 n9 n9 yڼ ˥DxLOn9 n9 n9 n9 n9 n9 ]4ʧ|In9 y޵ͤW˞ƔxԖ`KZeoЮbž}.ӫ:̠͡~~~ԭ ʝ~~~~ΣƔ͊߿ӧ@ǔ~~ǖ̠ѩΜ%ӱͣǖŽʛ͚X ??(  حAȗÐ΍*Γֱ~~~~ÏΑμܾŒl8ϐW~ז^Y'i>[Ұ˼ϔ~^+n9 ˌUƇPn9 n9 n9 n9 ^4ͳ,Σ~~m9n9 ƇPƇPn9 \,ֵW,n9 h@*͐}PY'e1ΎWˌUӓ[Œen9 kBѷtAn9 Փf=n9 n9 n9 n9 V$஀ͱa8kBh>n9 Ρzɭn9 u@ܩ|]xEo; }X׾èa/n9 ՗_Ӡrn9 mEè׾}Xo; xExԼܮt?n9 oΣ~n9 kBkBa8Ͳ¦^4n9 n9 n9 n9 \)ܼn9 uAѸkBn9 šұʧͫqKa8uAxƖհ=_/n9 W,׷d;n9 ššn9 ~YϣPՌS"n9 n9 n9 n9 šʧn9 kBڻϟƔҔ^n image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/icons/qutebrowser.xpm0000644000175100017510000001455315102145205022361 0ustar00runnerrunner/* XPM */ static char * qutebrowser_xpm[] = { "32 32 267 2", " c None", ". c #9FD4FD", "+ c #99CBFE", "@ c #90C3FE", "# c #89BFFE", "$ c #81BCFF", "% c #80BBFF", "& c #9BCAFD", "* c #A9DBFB", "= c #88D3FB", "- c #98CBFE", "; c #81BBFF", "> c #7EBAFF", ", c #84BDFF", "' c #8DC2FF", ") c #96C7FE", "! c #A0CCFE", "~ c #A9D1FE", "{ c #CEE5FD", "] c #C7E3FC", "^ c #8AD3FB", "/ c #9DCFFD", "( c #C3DFFD", "_ c #CDE4FD", ": c #A3CEFE", "< c #94C6FE", "[ c #CAE5FC", "} c #7DD0FB", "| c #9ECDFD", "1 c #A1CDFE", "2 c #8BC1FF", "3 c #87BFFF", "4 c #ADD4FE", "5 c #C6E1FD", "6 c #CCE3FC", "7 c #A7DAFB", "8 c #9DCBFE", "9 c #78AFF1", "0 c #6096D4", "a c #4B82C0", "b c #5A84B3", "c c #6589B1", "d c #6F92B9", "e c #90AED0", "f c #C4DBF5", "g c #6286AE", "h c #7D9EC2", "i c #BADFFC", "j c #85BDFE", "k c #78B4F8", "l c #4C83C0", "m c #1E4F87", "n c #0A396E", "o c #345D8D", "p c #CDE4FC", "q c #88A7CA", "r c #1D497C", "s c #799BBF", "t c #8AC1FD", "u c #5E97D7", "v c #14457B", "w c #4F76A0", "x c #A9D5FC", "y c #95C9FD", "z c #4C82C1", "A c #0A3A6F", "B c #C9E3FD", "C c #95CCFC", "D c #629BDB", "E c #0B3A6F", "F c #0C3B6F", "G c #4E749F", "H c #8CACCE", "I c #6185AD", "J c #CBE4FD", "K c #89C0FF", "L c #98CDFA", "M c #27558A", "N c #144175", "O c #9BB8D8", "P c #335D8C", "Q c #AFC9E6", "R c #AFD4FE", "S c #91C7FD", "T c #A0C0DE", "U c #194779", "V c #80A1C5", "W c #C8E1F9", "X c #9CB9D8", "Y c #7799BE", "Z c #6489B0", "` c #7092B9", " . c #6E9DCF", ".. c #79B5F9", "+. c #83BDFE", "@. c #7395BA", "#. c #315C8B", "$. c #7C9EC2", "%. c #C0D9F3", "&. c #7294BA", "*. c #5C94D4", "=. c #91CCFC", "-. c #88CBFA", ";. c #5179A3", ">. c #6E91B7", ",. c #6084AC", "'. c #96B3D4", "). c #275283", "!. c #0C3C71", "~. c #629CDC", "{. c #94C6FD", "]. c #A7D2FC", "^. c #36659A", "/. c #2C5788", "(. c #9DBAD9", "_. c #B4CEEA", ":. c #476E9A", "<. c #7EB9FE", "[. c #8DC3FD", "}. c #8CC2FE", "|. c #2F619B", "1. c #87A6C9", "2. c #7A9BC0", "3. c #CBE2FB", "4. c #C7DFF8", "5. c #6C8FB5", "6. c #113F73", "7. c #0F3D71", "8. c #547AA4", "9. c #9CBAD9", "0. c #B9D3EE", "a. c #A3C0DE", "b. c #31629A", "c. c #659EE0", "d. c #87BFFE", "e. c #C3E0FD", "f. c #4371A4", "g. c #7496BB", "h. c #90AFD1", "i. c #245081", "j. c #416A96", "k. c #B0CBE7", "l. c #CCE4FD", "m. c #7DB8FD", "n. c #1E5088", "o. c #497EBC", "p. c #C9E3FC", "q. c #7193B9", "r. c #C6E0FB", "s. c #A2CDFE", "t. c #97C8FE", "u. c #A7D0FE", "v. c #BDDCFD", "w. c #9EC2E8", "x. c #416996", "y. c #366AA6", "z. c #C0DEFC", "A. c #A2BFDD", "B. c #326299", "C. c #649DDF", "D. c #71ABED", "E. c #3569A4", "F. c #0D3C71", "G. c #6998CD", "H. c #30639D", "I. c #A8D3F8", "J. c #2B5686", "K. c #3A679B", "L. c #ADCAEA", "M. c #85A6C9", "N. c #33639B", "O. c #9CCBFD", "P. c #86C2F7", "Q. c #0E3C71", "R. c #1B4C83", "S. c #5D95D5", "T. c #557BA5", "U. c #85C0F6", "V. c #55A8EF", "W. c #94B3D3", "X. c #1C497C", "Y. c #13437A", "Z. c #487DBB", "`. c #7BB7FB", " + c #76B1F5", ".+ c #4E85C3", "++ c #ACD3FE", "@+ c #2F5989", "#+ c #7597BC", "$+ c #53A7EF", "%+ c #C6E1FC", "&+ c #B6D5F7", "*+ c #5890D0", "=+ c #4076B2", "-+ c #619ADB", ";+ c #7CB7FC", ">+ c #7DB9FE", ",+ c #5087C6", "'+ c #134479", ")+ c #23548D", "!+ c #24558D", "~+ c #8AAACC", "{+ c #A2C1E1", "]+ c #86C1F5", "^+ c #B4D7FE", "/+ c #6CA5E8", "(+ c #22548C", "_+ c #6D94BF", ":+ c #98B6D6", "<+ c #134174", "[+ c #84BDF5", "}+ c #CAE4FC", "|+ c #CBE3FD", "1+ c #8FC3FF", "2+ c #3F72AD", "3+ c #49719C", "4+ c #0C3B70", "5+ c #9CBBDB", "6+ c #79B7F3", "7+ c #BFDCFD", "8+ c #7FBBFF", "9+ c #7E9FC3", "0+ c #77B6F3", "a+ c #A5CEF7", "b+ c #9FCBFE", "c+ c #3267A1", "d+ c #A4CDF7", "e+ c #B9D9FA", "f+ c #C7E1FD", "g+ c #90C3FF", "h+ c #15457C", "i+ c #558CCB", "j+ c #2E5889", "k+ c #7B9CC1", "l+ c #C4DDF6", "m+ c #BBDAFA", "n+ c #CDE5FD", "o+ c #B3D6FE", "p+ c #80BAFF", "q+ c #4E84C3", "r+ c #3E73AF", "s+ c #78B3F7", "t+ c #5991D1", "u+ c #477DBA", "v+ c #4075B2", "w+ c #5783B6", "x+ c #BDD6F0", "y+ c #A1CBF6", "z+ c #90C4FF", "A+ c #BCDBFD", "B+ c #73B0F1", "C+ c #C5E0FB", "D+ c #91C5FF", "E+ c #AED3FE", "F+ c #C9E2FC", "G+ c #76B2F2", "H+ c #8BBFF9", "I+ c #81BBFE", "J+ c #9ECBFE", "K+ c #84B8F3", "L+ c #79B4F4", "M+ c #88BEFA", "N+ c #83BCFE", "O+ c #A4CFFC", "P+ c #A6CDF6", "Q+ c #82B8F2", "R+ c #529BEC", " . + @ # $ % & * = ", " - ; > > , ' ) ! ~ { { { ] ^ ", " / ; > > > > ; ( _ : < { { { { { [ } ", " | 1 2 > > > 2 3 4 5 { { { { { 6 { { { 7 ", " 8 $ < 9 0 a b c d e { { { { f g h { { { { i ", " j k l m n n n n n n o { { p q r n s { { { { { i ", " t u v n n n n n n n n o { { w n n n s { { { { { { x ", " y z A n n n n n n n n n o { { o n n n s { { { { { { B C ", " D E n n n F G H I n n n o { { o n n n s { { { { { J K % ", " L M n n n N O { { s n n n o { { o n n P Q { { { { { R > > S ", " T n n n n H { { { s n n n o { { o U V 6 W X Y Z ` ...> > +. ", " @.n n n #.{ { { { s n n n o { { $.%.W &.U n n n n n v *.> > =.", "-.;.n n n >.{ { { { s n n n ,.{ { { '.).n n n n n n n n !.~.> {.", "].^.n n n q { { { { s n /.(.{ { _.:.n n n n n n n n n n n m <.[.", "}.|.n n n H { { { { 1.2.3.{ 4.5.6.n n n 7.8.9.0.a.b.n n n n c.d.", "e.f.n n n g.{ { { { { { { h.i.n n n n j.k.{ { { l.m.n.n n n o.$ ", "p.q.n n n /.r.s.t.u.v.w.x.n n n n i.h.{ { { { { { u.o.n n n y.$ ", "z.A.n n n n B.C.D.u E.F.n n n 6.5.4.{ 3.2.1.{ { { { G.n n n H.d.", "I.p J.n n n n n n n n n n n K.L.{ { (./.n s { { { { M.n n n N.O.", "P.{ (.Q.n n n n n n n n R.S.> K _ ,.n n n s { { { { 5.n n n T.U.", "V.{ { W.X.n n n n n Y.Z.`. +.+> ++o n n n s { { { { @+n n n #+$+", " %+{ { &+*+Z.=+a -+;+>+,+'+)+> > !+n n n s { { { ~+n n n n {+ ", " ]+{ { ^+> > > > > /+(+n n )+> > )+n n n _+{ { :+<+n n n o [+ ", " }+{ |+1+> > > > l n n n )+> > )+n n n 2+~+3+E n n n 4+5+ ", " 6+{ { 7+8+> > > l n n n )+> > )+n n n n n n n n n F 9+0+ ", " a+{ { b+> > > l n n n c+> > )+n n n n n n n n r O d+ ", " e+{ f+g+> > l n h+i+<.> > )+n n n n n E j+k+l+m+ ", " e+{ n+o+p+q+r+s+> > > > t+u+v+w+2.W.x+{ { e+ ", " y+{ { z+>+> > > > > > > > > A+{ { { { d+ ", " B+C+) > > > > > > > > D+E+{ { { F+G+ ", " H+I+> > > > > > J+{ { { C+K+ ", " L+M+# N+; 8+O+P+Q+R+ "}; ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1762183912.534639 qutebrowser-3.6.1/qutebrowser/img/0000755000175100017510000000000015102145351016704 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/img/broken_qutebrowser_logo.png0000644000175100017510000016200215102145205024353 0ustar00runnerrunnerPNG  IHDRغMgAMA a cHRMz&u0`:pQ<bKGD pHYs B(xtIME rIDATxw|gvSv7=ԄދT^vbM^UszłDH( Uz'$gyX)fg7;g$;yss***\bCth,upJhKVɈ&b^+2GH -hk5KF$ +XԴO(TiuJJBJZy'UC%4& }%:$JuJ(J0HhQB(B 8ٖՙP q"dJ%RA"Q-P8[ք***^.N6qQN':9DwH;msƹ$$a3!A!0$͡:'ߝJME%PXE>)"KmF:%guR''Jm q Q i m#SB>#`q:1jE'[bq"!NNz co#fkm*vOTzTT>`TT9ʘKiIt7 p6Չ1N8{(~***a[85‰\lv NI"W2c@;0POHXƆɮ]**r_;:䌒z;A빁SXxh*mVCbBɸ]8J8$wPi0Һpq D{6F%bϪxS h裦Bq _қR4_K$*mJ@b1/t8Z+zve q!aWp葤

UB**-طm2%F4PN*Kl0dC"6zFzWFP# C:IicTUU36 70z$3ѲzB~!}lR u&H*,tFGRQ >T!V9'WdO $N񌮉 m 3٢ZBA15j)$( 7%/L Q T!V9œ+89V3$U$=#."9/@.ihF kJɥ"Ҿ"-%1/YGZQQUU"uKdN3 Rb :@o[k_{"甠WڜPCCU @g^ԁ)%":o4GxNk[Q̏]!*GVH )`ՉJ!)!΃j_`ʍ~uvdtS#̿3kFtmJ?Ԣlcԗ@ `0y' 6𔚡uPlq^Itn$CI`?ژHWDE˄K; 3}q`ԯUO8m 7t,{V-otZ^#d,iљ+mw3%'ODHدpGcFE6FW`ڎR=tHn*gf߶~ڝc)@8h9$`1Bqg`r_b*JS{ZBJ0 ͉ qDخ63EJ$Do2;0`{L63#%N A,؎JBsD03m*3#F v?ğ >NXOʃ$ǫhӭ)K1S ksc9NjJ˔@.wm+GUJ!Db1GM< ((z9W }>HK]7!#oox BjK<#IzX%?4=w >&hmDj$C h ޏF)q@Jlv(=6%\(Hjd Uڐֆ,7r5ۖL–m|k"PX(OtN`tN`$DB@atsF} v o,~T!#e#Shƴ&T!7Z./;fIt`Jn~6{D((f:s:JgGgfVV<>D@b1]͎O ݫ  Bu:kTژւ*~lZMf.y˛.ސ#5@82S@dFg6`KV tx>R^/6 gV1,зKKѾ1U[ܥ+,HDQuÕ& :γ@! I j?{*K㢀mـFN!RnGE~MfnyYlv+ZfzXLQژPGb?&N?Vǫv>mFr#8=(m۲[AD}+q83]Ƌˡ*jV TI0kw巖 *JBQe#x8x$p:3 Bڑ RL6&Q XˏabЎRu]k qP@$*}33Pi*M DIbQSWu3lbhjjjllg&oO scc\. .Z-bcu#.FXq&)$".֠ˊf;?$/jsB$J*ͣ q aU=fDl9<)۟,*qCum=*P]S:TԣHzk{.:&a^HUri~(*{Vq1z4 1uhJB sH?3?'ozTnisM9T%eU8VVҊjUTC[-ՎÅ0YlFMf KW˴]K$T|x_6&P +̸c)qh*mVژ`@ys4]h pB(.•M*!hkh:JQH`P~EG+:cp"EWK[dJ8aK~Z I=J4<#li2i&–bϫ+5D Oe1\q p>\E]Np$IA=fUgrJiO4A({œv"%K|~U=xy+|48,x no 6']Zf}k6cןk[>J:&"$w/9*mJQӭzPeFK;!~5uvmԪ3;س;lPg!Fkx>n9ݱSp܈^ÏJ6j-#}z۹ *!fx7?GIYZhן:+PVnWM(mW +!N?WvՂ;6cӺը(?9*>g 4uF: JvB)ȻnE MnFg3$'e%ؚ]pւRM;[KP^YxD0xG[S]Nc)|҄O/0DV+j ROƺM$,exEMV3+'ZU 5`(m'XFl\S}3/^?k=[I:Fb6'hr7)mhuBF.0**d+ep:TV?R_{*-P[CS >擁\m'Qp*!~y+꫱!W$Iؼ]jJdw> g1P[Z +CkJȴM$(m?hUBeD^qJGo?~fRiS(Jh{sa8ESmT~DxnKcД_WZsK1Hi;X=;(mJc;Eytf{K~ " r`١G+mJ`$>Z#t V!oIeF<Mpر_yTiBqJi qN$qɎ<~@ 5pl_RpFg9e~VME<ͮU]k [ԀҋRo y!r_maÎU˰q!SF%x4)*2BiE0a"gN%J !-į?1Bi;PW[)*!٢"6YQԽ*,ۛJx:-a~AUFKi;bZ7PEX'#tTZ'% F]8[a /2JwJ !'oUǍ+=W/ʟVHR#i5h5![g`-Z+Pcu8n‚6KH_e]?<҅/XW)*ofxQpPݾ/]8*m;Ԍ lt-cÚX|UUdÛ]'V]M (=}v܄!l'BJ๋hii2 &E'~^BP%xSc7*׉8]N0.ǿpץ!~+#^\ñ`$9Ls = @Iل/+=*a7k9/fJÂJ!#N;Vr9S?'S$eT(?+= *a7ٵTtxbsEK^XeFJ!!m;M8@Gb|rG4FP 3pZ_xs'sQ@ Cn@ڰf~ @l_ g!47JhQn$>$ʍ`F}q+7Z!UV#g~E9+év&&-H#F֮ /U*a7kjV9L=B2ޞف(Ct$h}-/6e`gqNb>dE ZgbʟFުWYբ -Pao&F|}lHP qLЄglYLKxz3-y o5m3ւSr8\%O*cPkG''d7N/ H~zz9/0g*L/ ~DXjw**Û}đ+vFZRB4{:+lf)ד/X/Vc%V I,㜜}TItOU XtUTNMV8/`3Z.SGcoqP ;kkIW <+3fo2e Jjf-OQCY8oM$^ b6zX/W-5ۭY** C Y+ p2#|AB/iF{9|邉UǷҲ_ 8,yshIL)(r&6>pvMNK%x"`.H!fr3})J @g%nCz-_buyLdk:JJxWYuO0(cj, !~;_Sg@kk{0CzJØ ԯǜ7w*1O[PBE9W86|=81'0[QA1^7s氆%TT^TdjPpywH,NL{>mK2l種,bVs<Րe͜nѥe'f-VRiXMG\@ECՀox*,5 BjX@HJƿIy_ńxQ>x=ϖk{s"`đSuY9Xsj[eݜҳ!ݯPеS{ӧ+Cj6H鐌mE5zMB^pwM<-P(&"Bblܘ("Lr.?VU?&U+2a,ys`Č QQS]@hг[ A{`^3xF8'qM:4 ė6 =7?VJvK=_v ӳg =pE`g @#g_As0~`| $)^Q=^jVH!)3PmZȒxXkTWU})$<~g;yd- 2p(AApAPZs"#"v慰DEFmI@uqh Ө6#uA_;#~f;bG-5C{}@uIƳ܄̙y?4C-dоM"2S.Lk2Y.Ta0VsMꕶeLV` ،x26G"{CYzd1Ө0ҴnN qAv ȺJ|:I\;e|EjMbϰ;T!,PU/LookOfcDk %,3H.C*?Хeݦe̝gÃy0Ր9IxAJ qvd\19>MMp/5:l+%JpgB{dFV>7 rW0"/16Ҳ$Ɍ;iݼ  Jh4.Oj.-{YG\P !+1ѭ"G%<ըiPg ?*)*mT[yQ_$W X`u7.?V[7 9EĹE1f=e͜aQ 7 [5a5C` 3PocfŲ qn.ʍJ{+pew钓ҳ1mY/,֙eеQȺJ<؝HJA yp/"?F;Au6R&V+G۲ (e]v9( /꘯=lɛl 'z^xQp΂ήVoņ!t†cuZFbhr-7 ebU?Np1^mHZp糎,0rhXZ7.ۄsi?o}*]#/$CpM)*rw h0oAqQ\+ 10]\yV Kެbs{ a4[jPpSlaՋ`0Qӊ "'p$c%&VQ90:UO?,g ͐uqc'-04꣏0p* ca.X+K+3PZOXyPck|&fqbk,<.IIQ'LcVB 4W! [pa'}z uyyݼ1g]d'rMOݍ^]Sׄ؛}<^q1ڀ#55Lb,N+Y6ebG8嗈Һ10z'̌+PVO(#ԻM $!6 _0;ؙ"t>#%sd X ?((ѫ{/?uMߗ3JttIqۗ/卑K ~v3G!kzK!p]s:_;ƅ$hh,7Bp7K7l݃PAƳz{ƊIsߠ A[P]5-IB-x3^*Sn$~@[Z :|'=ψYx4hk`5'D:\$ sM|MXΆO3?Хgݢ8˸~3O9onS7;&WQӀ+r:Bsp͆,1*"PPLmh0z `?xqe`@xF6nI6EPa)6ߟ&4\( b0<5bEmGa8 J+mvp.[f_&k-Qewp?ҭWr-8QLbEog'rH, p>&>#VX>,N@+ܒ-vToh˯BL]rud? j>6g?%⪂B9ݩfx` JgL'*BHBO&% Ԟdi&/. ?@.t}׮%:+tD'Zc0*ݟ3 VNSsOPpϋHvjx"?W_~=(m+#p %eS/;FCvIٵ#zuKEn=q1&YpS,/cX9:u9ÁJŸe54i"4x$mE {\r5 㣈Ar* BU$I8ڿ+mY!ft['2SHc:ڳh-):'_wg/|#lw?U-WOŒ./D&u1I|AoZ.Q؝nD3/1/O)#bG?x'ygLCrq V;8x6l݃[`뮃.z[ѠG0t4zƯCD=Gg!.($H[Ͽ>dx{ أ!)khJ/͝WP*0fB{ɭO䌢$1h.[ `wc'%bWႌ]9Wc޽ɶ$i:;G [bmXe7ӣ91`(t9A{<:zsBD|o-W!Gb|,>x$%g&@)tsvH{t#+gxU )1fZZauͬJ6AƾhHdvb rߪVIљZ*$snG'[, \(M0fD84S<1c0͛k}BE R dJQSg~y k]ĸQݴ1 MMX_$=~sAQi fv7(X_e~>GhڋXKyuCdQ*}D<:c҆VF%Ȟ֛~), t'!F$&ba붎bc}Eۘ=:ղx/j>qx~f~hD'm"K_![2) ĭA 3}Ƞ7+Y9B‰_> ڝ:DxɁ͏mS ؂@bRy*(9bF#gH: ~L"oԇFL\|6$=B{ӛp[?@McpS0`H \c9tay=B!A-.uF3>Kސ!<5:ɕIBÙzv>6bE1xݲf_js_n`F?2c֧of>޺5O=w^w"#dQI Rrғ#9A7&M̸ *.=0ihӮҦ]$ON 7^ qA8в/5Z0ip<;sR/?w.j͇8#'W?c}vސdKdϮ`ݸ.J"L7 !ŷFE(ċiڗ4wؽG^i &DOL=1Y# `J!h4=B79,]q͝KOZ7;J3{X.ߕ#֠s@RɮViҁl"HҿAۛӯd!^} ^Qxߴ~N5pZ. ?XE i4tck5 MKˋK+$ot:HS<S>`'m{\v-hQC,~0kP̝x{}K8".)6 qnN'qx] ϴUZPiD,)YN`^`Z7WX*~}N1r*IR C9k}/|({j hl^ ARkݨ; i^@h0K1j܅v>mO/D{rWcsHau96QGD.((q+ ^% 51O5!b2RN* x>=3}&-oVө{4@u TOƲB/9/Tg@J "VX#p[eLnKN?{WB q%y҆F23fo.+()hh"bKKL/$6S E묱 x] :]q ٹ`HKuy0fT{:%3kzPbi"\<?vs'~BɲkIM`tR};[hj o-(z%UqNn8?mfK6k#MBf/n)䏼)()c)Dv ȅ>kȴiєtx:l x,c_ӧg$Ї& ,P$}ѧJnc<%q//W`?VY-kP:GЍӘ:idCdX#ewļܹGDدu['Ӧi%/" 1p93AI/#'(m_霌 As3bf\+dˮCa/zu 5DfU"ݖ_™#ShMS'HsDtf;xov+F-V'`::QZWLyDҙ֟~ DŽ 3vZ0pPx3c_X_ .o5 @!?= 09߶K!'*m/ XZHXt Fǚ:qDvȴn oۉ@IIKvЉҧgmf)>31M/D 3B<)Qjv NǤ/o% G&  LkY'=bA+M{]SFvj#^!n"+ WM8Qy":oX"`Fhh4Mı@aD< N~Ex3bF.Rwg@bK[ ~xϘ}Ÿn.O`ڴ   X=bkZ8E'. !/uV$;1 [:#!&XjCE+.pa ztcMFDK3 JDossƎ~ Yj-O&hg X3[ҳ2fgŒ@Lƽ3;7vFs_ȟy=f"~})bhc ;6G6/&h<ҹn Jc4FgҮ}G:t\rX^P֐tOMiXPK'ffcS&Ъ5 L4}dtҳ"i;fIHpY$)UpZJם]F5N>4PD`Yrk.W0h*)đq2dMk_,?ZD)n*FEnԈ+ǕHFGFH>*>$AqNΉD4!oAh5`F|n#Ii ~43Oi4( | H!I=7۬) N|7gtS|&= !#3WRo0Zk} |pt:MO5WLv[y#fzg'^=.ɌWc2M#W(&h!ay#-#1>gMb>d>ޑ|\%xD}o4N/"=k1Xx˼nryw-W'~['&w`ϩ86'lwt̴D**+Gq =uf(&5#<1CH!7F{=.xѿWMmZ,-'I3_$ESz~̫<$LtH:Ktol Z΄@OP-8V֝a hdm\ ]P=nvdRZH$-կ)Os?#Z Xb"`H(!&( 1.>~5>kIv0EG-KÈim*r>1,U76衺 ۤt}å"?#U:SD*T,~E,7m֦S"}8fHDŽOXw-klm3ʇfd_~i06nV6<: $uoba|2v@X!0:rδKbf:f\/Ib_ӧjd1IYŝIaQsaOнF?+xD~ZV9of}zևnUz X8wM V ୦iQX%|b^B<J\0)oS$Lӊ1F=ZU&A 9kr4_ns> BߟV鏋!~DOGFRZb)rݐy ^*a t]zV `;0 !NH²Mp`0zh?F ,]vj~g)pAM+/1z'67cKZ >+iHZBú۠,yiY/t݅Zf;7;jQ.+ fơL0fh_Dh5J̴aHp{չ 7FIiA"lb\GFo}͙~&fҼxTkbŊcʴ۔6mꭔ6Ni0U7vE!frYVQ {C!HڰJ"Iqx橘|ވ.YJ~(AOf8} Upp#o6,0w {3v"˜a[Z%xh\7e<>$.?RvXxqC/"4IE̩6k)o·6*)O/a*c,ԱfgB8!~SzTƒzỮB.)+Ϥ>^y*wQbN%-y305!GISU0;d7d}FB4u6Mdz3 *,Eֳ "=D JbK\0R %6-LvF5y!d^) S!ү&Bƿ]pьWw>*pKLF9^Vz=bu6E,jtRBLi0}3_wMP SF^} x9%<ܐ_|l-$IRFb<ͅԒt=g+O[i:N4 ?ktF,JhpHTdTz\Nj*3rpq.’䊆oUF22B8'86lZf4]JѨHMBBu7~.];(mJGogCT`<}2bČO3 ;J)=xC[8+KNZ<ƼH x(5=:wT0`Ġ޲ߣǁ#%JwY譏˛k-?PHX-fif4[|ڋYB@C}s,;{[9 g;3b)l*ܐ<~Y]rڌ},ˌ;NU|O)[j0gg nr`o$~B>YiTTfWl*m[p;h}؛+MI5U8wf4]&6YB,B[!+O}DMPQUș: nWzLJ[Z%ſ}gPaxs9off|t<- au/s)iB[:0f6`oEAK%'(mJ8">Wܐ_,=4J˩x2f{u1i2V Tڌ'}Gj_R>9DH@suDQ ʷ9 k*͆{p/͏^0^BKެb*Oعu&4 te4!&~qK[@5-,JbHeO ǫ\n&gpa<جKlB. k%z K8}ii,Bk6 UT<]ZĢ/WnpE0mЂf=""v|N =OKf`vvB0;<3$X3ȉ:#VqF+/o[6{K)*S* =Ҕ>~@r`έ:zf]SI,xED(7or7p☘hMP Ǝۯ@j6~mXy5Vo*2DƈQٕ9G*EGb6B{H9p R:'>p2`fB{&~BkP]*Mӹc[d~%҆kv|~6+L!"AZO+DKH}chf&"SBv)׈##7GGᖫ'ẩoM;1g(.P*A +=Vh.%@`TBoF;OCyJDhU }2F 9+k'?X*-hZ:hHzDK݆yg`4bCdyb " 1~.=;=}o6+>7Zex0:׬xzmD.Mÿ ~FE>VڔS8$ (]6( hUt!+K߁w/SEXšUkXWCv)mi8%4r8%F3bpBS.{n8nc+*_!osp=h^ЭS{thv HNC|ё7>|iM}7OŒW㛟N4^0^LڊE EGcdJ;]`-IGf)(NX K%OhOaV1h&76m} +jfXпW\}ɹ?f0twMrBzF\qQ=;vy ;r<)8:qJOcج}GRὕ[ #4 0OK,:*JcqBdǎc_n1_t;2b`nx͏ǒW5O/4YnHz*=.MQxh_P13IB/--?wfGo.5JEcjQ_WRǓamO~ *as h!uF&xaҹqh׶f+._0, HzwK\(m Oe߆.D7 pJϋk%zicEG; Ai3 2 @NKC KetOG8zuFj=gn33볟— Q|&_~M6ӫٖus3Ǣ1y\BBkF̔pm0J{o`Ψ q]7\+&C̸H1^}+luPnjA0Ke~l/%ـCG0n\mSbI#@488)CJhs@XsP%G5*fqs2[.Gb|_6-Xju,3{uCw^*mJ)'~%W(J lҥ=8ֲ5ܲ~^>#s648(rKBL M$0Շd 7tUIU|dPxΫЧ23~Zo|܉Þ8?}*&7"h2a]4FŔknӻ $/8כk͑zv^~ɁjA]m5 ֓$Ĝ%4wbH"!B6#.;^ *w{$.9f` z|9tW[5ŢWEWB3h5=B$iuL"&xU$.] T+mD :׀ h l*"kuKQz1d]0jH;TvnM.vm=;*r 7E5.2:$X@p('[Ȭ3G_:wl;D05G;_aH ZEGWMD֫8$QZ/&xXy(:{:^h7Wkq-󂪊2$&QA!A+kdt$0&Bch \+د tQcEvyj4~mo~=~Z X.OErBP֘jZZ4w 1q0=r`yy7u&,ǩ\i!hYD\\,J* [Po$ qQl ǔ6"[%|l ~3fx 3Pڔlz6 FA#Ұ_E'b7DE EWU)y{0 ^#m ҽ2X6{HjQQ]*>N0Ny[w|ږ*0Kq4EhZ֖gXc+v3}zy6{|qgd=%ǩRۃ ՈH .ǞEٶ.X_{dUuRy3ߕ+wCB\pAa1/^p7s~Z)|^`-I]@v Jp6HB$@hH|:I\{y~a(bq}X{~abkΛw@Õpgxx:&UĽV cRQ^LjeDh1$HeZꂇ zΦ1{Wq-_si7j$'SqQgH7XK} r<5U;'QaEl\@`@иRN:*<:«FIӧbҹ>V^}mSr`zaL9Fy9j0pV{7%41v"mT!a!zS>r_~'jbC8WQR:|fƆ{~߲z¾w#^6}lv] 7|p%ys?k0F CF^} Ow^qn"'X6%TD={w1?$ӫi2:׉HCTZS۷u8wX~&e:f`1¡)5Y%~65JʔKl;S J+0ݯig  hϨZ ݩ[/u> 0ۛͬ]u5)YJ Q]t$$xUu[vfw=8%Wy6WTebxU<ђr^"^^T}ZN;+.j_,W AOАB5OҩKp =8b{ <ͲPZSLQT]R@$A(֐NM1+Uf`4\(ꐀ"њ3*.=r+al^`lDی},~r[1b>8">E|ͯ@Mj[g0Mߌ#o Q+2`6߹c[l~[zV)%yBLTQ%4  8\.vetI`(=f?6{^̩aɒF"ZƀHm;;˜aG1띯uW=+|!R.PR8KTaCQij7W =B".(9#D A \wC?6lGum=S)5= VgiÈk$ 8cl.!& (;{bZj[̹ssZ8Ia]t$_9 mߋ3ԛxӟ/aޟhՙ [qCmJGgŖҳ0pmƨ,-ѣsGMPSď+7+.z *Qn*Lg ϩFXk-C%2u`A&3|yM8<ݔ;#НW<$1~}zv\\H`ӽem'6ÛEH@7{Q` K0+B . _esHq#`VQ@֕{H58TEtNH]#u7unܹc[d~%҆Sfr;_cǾ~o;E zn׾_N$?bهuY x"۝` s0:$'!!.5uFM 8%eXa;&5jt!5+7%sf`acû4|uͼ=Eׁ"q\Wo{K~?5E c1 9ho1,b_d Zj1+"$Q`AvYx=Yq\ u^Dtp?5(C,puӹoª_ghfJzNLƽ$ioq1zuåbR:n0ꈍ%`g˛>USm8-ysfpMGh 1sy0:>[Ji3eٸf~sSu5U]3vlpU[bb2L0"}!Z, 0MB H^0a pb2PbˈG Fg$+ٸ↻1Dyi16 {ܤΜ\_ rrr 7}d\ޯVꉊm)oHϺؔ7go7ޢ^eU qpBI[<D>u0;:|lȋEGb8oJ%0)w>;P$r"y-.{xS0" X/k,@+ :Q&B$!w(:r\J,`u8^$Ebe0F~xmܺ G*DŏDDe3 6dTdŇ\l^Z>-era`! !%MeٴGK6#h9zp/ޟ|kkPYQBc<-ahȘ9Y鏹z]@?KtSq`zGaqWzƭ{CTB",= M&!XZ o]?vF@6IrPһ7ـf~=:M 9V چ}\kzF6Hn1qc@faT_j֠b [ 0ԥgOc'}".THK2b괽DI ԃz4 oe@a"֍~ݲfW)-Y_k|_I^mZ`O3Pla0~ |lҦ<( Ujk!22c(5%~?ͳp9 Cz z2 Mĵ1@DxéS4996 cF 7kn yۖj 3:F2c)mA& ӳ``9OX@{ƃ"b$8{$fX`Z*͢XJ-1Yx{uk4#>mQnVz&3&0g" L8Uԥ$i zdHHmeJ yUP7&GȬo)݉ɑ e=4@;M`G8 f&-RO@Ri pU= l9']z2\jv ;q%IXD<]D CSb2o\\z}Z?AמY 4.ftOpH9[#b @G.?-YU 3_Pu4H(:M Oռt$ZSVWHi0*l^9ky79/DF'v΁$`+` = |V Xx@T?s;6QQ+l)oi^mgPrOư=IMyĸ'%=Vi4”F~Wn_sPP,bfLqN ϫӥg>B V0FxKop k׶x9vk} Rŏ/kMLj[ F22«,)=k&b&:2yKƵS }Nb&:Ƹ@Z.W^2Nb`AxΔ7'ǝJrxbc%S K'GiyW:gm4ddNV !dLL#xKo;K~!zb̪7:8<5bň͂ᩚ¤a4=B<0|ѩA@I1譏˗6wddR{ ;OGwh\>)]i3TTR-/;'ҳ.@@!ƫ# ӯْL?3r|e˛oװFدOZKϞ 9<)T0,E0ʪUgT~F !ǼNc\7{v%iSNkYó&#RЄ**MQL>%tY | 6MɧA ܆ӶkxV^MA*2 D/۪Ҳ_Ѝzl sPgŧaX!TsY>K Fz r +*m.ä8\|OyUTmgL$3J6"0N6/LGEo@G?ͳ e"~C̉4j`}4Fuy 97>Fi@ğ ?8M'r׳6W\x~/@ђ4β:1f5/`͍͂Sz} '$'4^}DIedzg_YNnD` -7g1dd>;mֺ:+>Jp&1/ bo!`B|p`;26lEct9Hi i6C%,EZBbȘ9TrR>4aa' $7=k!#q%zݚ:+>E={), Ή Ќ7YBL8`WT\>Ti3T,<3!#q-Cr8myK "يTXr_!L/ň#Wޑv]VFΈ5>map6y኏53?󣳄%M 1y%hԵbY1ous[#<}z֗'*'ya ,!&"$%8+>(uj}zr$RD}mgd3f` _X uVc啾7"$/ mf㦎Iv#Ҝk,%;Q$@= A;sxnS4 3qտ>ž$@ׅ+خ7g߂3bقnmU%ѵہ^9sF !ub*ӟJX'h9ݶ~n^w ڧF 2t>+>pXTA}ilx g&%$q!fnq&,L2]R Ui]|m&ƖOѥe8|a = ^x.2gH:+ ̈}r$b|ܗF<Ŵn {6C4yV|h&ۓjQy%8@8DKD$zm.Fh5ȾJ?_Ri=J&X°IM1wW̸R@=مw}z^2|PTk!=X!}5hru*3b`“8- gH2@cN:Kd| %V{]u+(0U2ߘyظu/k}oL%\1vKO[:ѐu1 ܱ-b\1'Qnۿgn Dq!=FSޜekq C-]nj>rF:eSB̫hҶ I@fxBSy6`͛>#ĦO> "4$(%|.(#x]L~]@i3TBj |;ՓtY-mv ɸ1Ms'Z2ۄ 3xCL~ ܭSL=)zCJ  )m?hi!16{VPAj{gQim(a~ζҟ;VT9)FÌ.ŒןMW\3oމmKt5֦Q!)Z-1a6',wkMi0x0sOH 1ٛFB<2@lc:^ggVN^c;% 3)!stё~D,ypՓv/ /7u%׶@oKϞ&=L_/eH9uF jr:)A YbSA`Z0wWdEAfҖJҲmrRVi_?sebˑTM<.FIb!-{N=ҩtEi4 qb1#UFi3|/1g]Ь!)pEc11JJνIH$A0.# I !3Ol،|c @QM%&2تVd{9E,At~=o-H;Aۍ y6g[ &b^"u5*^ *''v Z-%@=ۄw:޾`{) ƳLWX1`A:s'4CDzVnK 1#ui!Y!NK:[6["#"wS5x+ qnÝԳ@zߣzHLvQ8\MuwSaȞ( @EP''dFܐNZO˺ G٭OP$[:_~ ڑvY?+m_ qIfHVrg 􅄸H}]M%xp.Ϫڥ=W4m7\)r-ώE o >;>$53n{KomT.XPK\/tm4"ILZxa;|o(8讫J - 1IJ+)ғw:Zhm_TŐ]qw;ѹ[/vDXy@@/tR:O@a>*W7.M0ACtGdDgR:YTBd*X,Ӯ!(B^]/n0֥tQ7i_ۄ확ڴ+n 7z| %l\%I|pt䦎['5ok#H/Wd l@'fNQĪ[6ߴ FxJBZr1׌Lf;v`o˾8YkyJ[e*h0/ޒ{+`@oܶu&͐쨴VU8'6f=s?UF&͹/,y8y'*'pal\ο*ܙ &%8N̛$¬oQPAٵ>%Zfz ?b13k@JP1zzwwNHZɬxŚM7g6ڬst={ 5u7{ /7ie㪜D1MQcպ>F]CE]5߶tX3ΝvPgt5|`(fd^e˝wfY(=~/# E!M%җ`-X Xaf6sqlf.FB"d$ f$P"3F"&!%-@X24*DE*=Gxw` ظ{kan$Zܹlmd>WvDDF&` :(GFgƺ31Mk=|5w(mwQU$IO($JFI( mUW]pWUQwUꂽ ((]l@)$$}Bʔ{ޙuss33=5hٌ Q%氫#`=>*{!?±n3e}N'ǣ/XNfnqpCΚQgj?ɌL/wJ䪪qVtN~4j ǿv tBq`Ha죚N!%>l=~ې.kv`/gC-fXQW_Q^zIm%v9{kX= 3bo&1lm<=-eN;\AkܵG04#Mb}.ihiTn'2jF Byh8ƨ2ɩ{۽;X(0O~~[?|5u:q2̜6^Zg $.V$Yҭ#iFSmؗ`PF&Tf1WpЁ&#`c]W7G*>PDkC*N*罫)Dvn>/hԘC١g->B04'kC$Iãi 4WqR0?G^p~ll/XPrpzGz6R:58eO&ݓ_wˌP3X, [30 {>DR^D='*:'ҏ%3#r& 4SZ/$mU=|,NL IO[`7>Y %ՀtZ9DёJs.3 s~V\vZ04'1.U^f!xslj ˳@x_ifss#+g~'l߼Y [й+:wzAXŒXcoq\.R+c~R|n':a+>BG%eZ$2 2Ȝ 7V5- M5C쾊LIo9nǮQsᶶD2 ]j=3Gż%Z UwZ^Oy&҉-%_j4ֳc014|4î q!( `k :wCN|*" S{ٗ%(:s\رm=mXcGy |\a2!Y{88FħT8!v'hyoe~"k݆fA0)@{8*s$ 9z WKԉY "$CRJ; 2?h:gCdFHI[NHMHe&O"{SLZe/Suc%G`,50c\2kč#"馜]]Sg@Df;" ]"r&VY6/2g“ ǝlnE*x-^F'*NV!Ð~վOzNă[S"+?pj(RvqHu l]-DIQ;GQ\|ǎD)pZ#`FTL,"q q=))GqI3n>mpT+"zIDYqЬ7N F |Gn Oj)@zK eIȣ!Dk /:@ߙ2e? aH?G܅%iiD cCցŽͦlͫ%Yaf^5;s-_'n2'-@|7mX#ՠyѦUW&ѯS"L6-Z$^^jFT8|n0^c&74i1 oBfkW,Tu7˘⭀GșIy.ͪOR PQ!u6F O\?}5g[ԩSais[:BH,󨸤R-2b{ u_CrrcRcRcf Y ÓI |_IC,|I8榫y1sśZ327vS2C]/Yo>RSS蘸dz-6J a9-P{ϴTmCT#,_GkT{+:!Rڪ-NOsT۟ǥQC}o9H(◭`5u{=,F"vf- Ky,#ץ~'jYoa_THc蓕svE9чS[ 1=ܕM1@, D gT|H1 =F}Tkkl"&}H5@S(f- O:'}+GYo/a?xӄزMuc-fbBrD|ջ*VS+N `wS{T쨮Ͻ>Í1bp,*1|.:"x8Q9 ('`3.<Ι#/gӤ%,fX樴8}oYݷg{Ey9*M &|!!F|eị1?/' ŠeX:6ޜ v7Hiԥn*Ƣ]#>Mey} `y5n%;v RڢMb-ƨܫafSѭτdȡT{% ʹ\k@U-v`0#1a2*KX Xs-X Ycz#]'-{uݧ.M~n`ԛ3WvbbUn{ң-1CTm_PlZPz9ɯ{?p֏ˍ$G|'-Ѳ:9e2 -G]Ju8~L/u]jF"41V7Yc*9*e~^`gw)“DNl\ 3NFźtb/@ 4@6Ω3blU9MߝG-(@QWҗ&F"̬9p#(KE|w<"1O>p%6pre*>)Z:U-?b!=u`hrD(l52"Vj/ous`_GoYWޟz0 WRxωiҍ*>%opVO,AT #鹆oؗ-ބpþPeغo=o~FZHKnZxgPmUqtT#*/0>޴sҁ~cλe%DZu3_-m06&$/Cyj**t:*zݽ=o*ϰI2SLA㩤oqCSp Bx5/:Z/$%]u 9JZ0\vݓ9ʵ 6m8x`/֯X?Ժ~pg-O,''Ը˘Ew* b"չ> {9ip=j2p#ʅ9{CM@H5}y6 M-V}5 >>MYTR| >R&avTbs2075(Ɔ1P?߻PMٳubw(Uy݅z$[C@;ƎRB@*+o/bC~<ܨ;I?،ءu~a(-9{1NZWrA>wz:/MWNɉj`q&T/SvNǺ<#?s=eoNQbSӧXx^S*OYZ̳fE=8r4 Mk5FU-1DwTSN%pg@%WյF5HUx~aн cPj>|isˣqQC CO#b(>v;nh=.b bBwD,Q,OG EQ3}MQyfYewyڶg$euA:舾H3W3M ^)M++0Ή1UM@ןia8~?߁IM#b'|U[XJ\kC]ԣ-gM5+'=;}e'֒5JʒӜ 8Α+7\6 ]:z &zҟ}RMY XGkA@$  عjf#3_/a |ƀ=5}i Ge96]o`c/O 򞽌J_KqDF/؊u$'JQY~'pA>GI؍}V?^ L* Fb/0̈~H]r[w?rzu;0+:hk"L5nJ9QV T ƭ ݓ_2s`̌Zu6k 1D!@oз_nTbL/"״~z2 ^jtܱ@[ >Bզf\|@7ul@Dx7"¢R`o~W &}-ł0av9]5էzÕ.8knyޱx%TfM{;\ĒE_*Pm+(bEGZSvڏy=I eg#l>_ / PGpAy=yibNMH[KV*CmLHOa 3ܓi<$(8ÌXv5L(Tflߜ́Cغ-̳/s]+|b^µ֓Zj)*/[n{7m v  ӶخuZ z0IΚ(gswpȉHpXl%;Aa95 "|LoH?Y拡5Ohh)q7]&|8;I<"!W3|l2Xt>K 9>yuzkFE2e;̌4(l%>X{ `ȧvCT Mj3iiy%і[,. y$\)$_ ȭKD,Z9cȈf;/?tXGKZ^-|0*}"|4_}{)ś% |*$g[MN\zxI礞j]YpS,d~KuԊ%YƘrr յOb_r*jް@ݟ^v3}4KĽaf<{Kړz/~ɑt/Ik+b|(:őÎ--GVRߙk3eӪlhːl9_b2hMNԊ& HOnaxO|:s^)`Ͳ/r,`ĄX=@)(6Fľrv/ZYG} LQKI[prx7+=QY[5gE&$iޑAoXNڪncah_ Z,9W΀oXܗ]&bl#NZ=i>EkĊ럢GbڛgL2~Yf|dnox*KfzM֜kN0q9`^uJн5âE㐐q-nuր@& hD֯gG*A%r^:YԴ2:Z>LPOO啧&fz6"wȺ/ W[.}X^9GE 0fT :l͙0?"giחiEy"t7Ln}2a:9i16x+,aVlYQSUK=\ȌKm&aQѱGԺYG>~W+eǮs^'-0ZKuT;g_zd vǓ,.#lkLሜ Z_>.E"Hm4H4,jW$_ Kvg+N [7Bi1wsww:kLgܭУ΅̜bG ;$]HbSdjśxZq,4#V}aݟ7ri&lgD $ Bq ,_P)$_qF"vkfs0C-Lp\XONI K 8՝c"g+ln71.g̴;#U9Ha $x5gw kͷoEZXZڗ-ބUL}w4YǛ?FV|D ؂}{<9%U6a}9ǵtXoZy6 $q@2ln7>_/̜:?nsOXs?Ϻ[v*o`FSj9i|b-uVyp*P|0 {s#?:31??iѭy L@k/e"҅j =@ X6ODF". j;˵X•Z1jNoFy~^x5*d7 zpOl=%wktAa2܉nTGX$j{3nD ɴZ+]Z{Ph(A6"Ge96[ͩ"w=Hñ!o3$~) ̫9hجDu{u m"Z<@%!A CC 3Zkݗг1N*f ڮv\Ae5aVk%{/#sֻYjn ްL\SdF4w8YLei5g܉ukݭ16d@浿P`L<@sE6{kJv'xl6k͙P Yzg`7z|^]m-^U^:k3kΗIF@ZXڗc͙@@5Tp %TS5 .aF0%̢U$U˨-qƜ9'D [s|q3Ҧu̸{#8$cM|aɻj9S^i^MYs&,4! U}X<abH銩 b/K Tbm5V7i!(P  1&Jg<;kǔ$%I'BpT}S+c\t0[2*@ QIKՅDK:!y{23Yƪq-k2+z=RF .A< LF"lZ2Drˣr@Q/'%@#Ͽu-vWebl9jJ(Pp5A0s % B TL(aJL%58ޥK; = :_I6*}yL C# 0SyK5tu+OO;ww:#@$0t аzLc?b7PzKŜ TX#ab8^ Un|x;ZH@@|@̘J z@_W˲|*Wo_{qD"g!JnVs1=<`1;\#toç, /x"k_=8'1Ty/-8}P]:  É=AFp6ELB-:0=ӽs;7?^Om{ʪQ煦qTt Src0Y$0xav)UWDHr<ȝM4  F[N;*y)t?r!:(%وxUm\%b~.?Q=P<#.޳ ?&c1 "~ȚS{$1uC> _w>|qG~A4$oǠl?2-̰>O;R5H\ ֱ-uZĐ4:(IJ%YOM-zVn=ӑ@np.VK8n~fOt(& Re!ZTwcݴJ/$܆>qH&F⋙i|tPB[Atv8&-<"`:.q\{jj4̲z[Dᣯ$W F]x$IbD>(|%Z-  g׶w(x=S0_zwj s߉/NG&ѯ2Sk3dFtǧqb1㵾/Y4Za@(u+s S+ޮ-t3< IuA6>{|pwMky 1<^Wc0{1 C#*⍻"(A #E<&EDHҥC 'a൬Tl5DLDrL7H Õs8ڌeT]ٹ6 =Mrj9 $BtǍg3ۯ֮jyomЖ<@0?,-dgl Z[ʀjMB\&БWhyѾs7#L&3:wC..6*TVV,X #g_߮BߓQQ?}r#JhC6Lp :i};!\}{Эg:-Xbᒫn{wuGٌ>]pan.j {v_ו$/;o}J/[ꟓ}If^@=r:WZ I܊Z83^QgAfL9~gP7"°Kօs6h&c$I•7܁Y~}(L&nͿ;:Kc];ޘrtRYRRLom)ru㱦I钯$e񌟎Jn_*ԩ.I2{!olk-SpDptGttAnvH 1I~cxj\p1ᶫ` A!=r2Ť z)n$n`@(Ɓ2G4zğۗiVp}9GkMq -V [bbqקڡu8ckܬү7{t${W$!Yы\x'|mw뜆=SWYɴ_ DLDkUne%x]X]sΩƍTo L5] q<:Lۅ,/H uN|z+0tEvZ h9LS?O֜'k{Qq%n`}e!K)(+ d6hUW3'OSL]a~-ʄBbqb$7Hp!}g|;`VF]LH@Nѫ3wGnuT82fz֛f#f[@B*J#L-|! Fp'FAb943BdG03jkXL;M]/NOӭW*9Z$p^d>;oNFppN|T5wb9AmkNZI(k& ҉Lg!?7c"[gᬩ0J"`#F$[~ 3ihc(X\P">6=C.ѫ{G8ފ f~dǺiDN(gƛh!Im׍ĥz:6iB633m;(O` 3W43u_($W&CG˰WQYݤu(a Cv6^;=׃Tb@„N[֍*;8 )&:\9_:af͜܆ʰ>! փ<=K"cO F5 Z\}5Ys&.Fl\֡F]y*+NFaETINERR*Ff VA,d"uy[s&V<!5!2`]ɤlq%gg| Hv+xkV<^L4+K8*8|1VQ1h6qHlV qtM@.21q0|"ni^qO2j}-Wȍ OVGC9B&X)\~ +vr l oY0u?φ 4_r _w [קDib[O@Bb$nVmغ djycYHkYP H>bh${S@vg*Iɠ%(FSL˙M[%y ][À~iAn[*l_*vTuZ^|.Bxkp?I WzmR@'2%߮%=c\+< O:1I 4B",kKxϞeAx}qeh3wq%( %7UF(-R LBw͠Pqh;x=X"ox*e6Zf_$qX*f{nA :YFx?D-b|X=}#7}R攆g&Q\n,+&"Zfے,3b3~ q ƸR YVLy`#녎q siN^F20#uqMO*جoJ5.cjzCI"vq:V}:)1d5i"i;!+ExXqY)YZ'l2mR:iQFT(>,OUi5~² '90S7gH/kdHρW^?0Ϊ2lc3hL]4?:ַZu:MfhFG$}u_E6*5%9AV0:oUno9r@>O@}9|xݬtm虤d%+ȟQ93h'Eka44b@*: ;SN&j0_D ԫTuhtIg2VXju?|۵!IeH^I!|{Vt_v6j;dC˳lZǪ$A`pk*'4B[j%ѥ~t|$BWnzhG kz>&ʻC2RMok'dQ!}M],_n3g}3:RIۭ|cQmXW#ow[=+o- -NbLO@ HfMm3"#F:;X-eqqVSK@$I#(d܁H1$xZծSW|q6ISL*bT mѩxg]3^p,ai3d>@7*=ėɘOõGIf %[%[j RkR0gFsm5uh.&i>`oϴ~3BZV#מteqiVY&b>ţ˭Zǣ$Rhyu /j,:~BH^S3@z 8RMlHZ;&qm~:%H'!]3 - D ]PΝ|]uf*QD+5U5Ъ rF*p/(h~I+;}A?BHZ/ZPȳ^VU Ydȡu(v.,M !ѳ'9-t,EhM5x&v#+$ &|uJ(WdmE^0 k%G3 g8o)e= F榅FB`D|=9yl6uKHtpc$%d1dL403 AkZL ־ l=vpeJن*@=B:@fy$AhZTX$FaMGé@P  E'CjY D }h\b oJ#: uw/G|Uf@-4P#$S Jc񄈰bLMwМV 'p2GL0=#4O` #FWI|u,bՁaQMLlYu3Ҹ\` Х d@g /<CD--’P h蕑f^u8a$F "Lc/PuBsZtiߚ1s@$aDs$66# xoZf0$U5#B 4ŨHΌnmBϗg@DvMB?jA`cLM+p:jWHuhN'-C!PM ⴎhɴQ֑ƈXa2+d%Cp>- (t񌋺BbXfa$aP̴HDq:`ᏺ{;y:aKLZ;:=:; c"3JԀԴJyya^ k_5\:$# P6D緑u$RlYeZcXe_-.0^ ZuCVģ ܠuz$Ή:8+L:u0-Egxu7+hA(Ò?!1aXkCMtk6rhAׯFPiFm_w= їL:sa}cuBoQ6Q@$Ft^W*x5/'K 7$-ߘ*H5ͩ3^`K;N1[#@ș^ZP6#䶗KI蝙bbg$a1FoC2Sŵ<Qoۉ0kݓӬ_B?k_Ѕ9? GEDR\b$kO63aL"53E7XB1Դdlߘ!?<Fe:؅FVZǡ&QH;g,uVB9$@X HٺhZ1"%'ZGdF!i 1TKe'WV+}&dU491Hm|9!5bf|cEEZǡGQ EDUM3#Uo>O-~ {0>L}| 08郂"X~X:^cߘnB9H.c-( dz;gf">SĜlxK=y$|}cxMN!#~ ku@F`_9xrnAE|@!0$>H 0F:'+~0v뾺1,h*.g_ I@`%bNo#/gyM[>psĜd3Aa$!#ʹMOV50[ZV3jX=> g,RId Rw9&AN0zbKR4x`eL>hZyA 0q\:`;۸ @ɘv(쌮 Et8NDB}OHOŢԺ߁% lJ'UFM!X&'a3[s6åҜ($>  FXMk$3N_I0A8>V9?\ !-'Q:ĆXE,>A QOQ(,:9=&/p=K>xQ,_Φ_#'ʪi|I5zzehݏ@,r޶!kH32ٻOZwD|.uc֧'$ͪIKy3c1.& ?Zl23T~(15ܒN3;RZTZjsAx_F!|"Q7Kx,|zG. Hz.W(`%?Z)F[n:|u9RD@L*"(0*lQuØ6hvrzm%u#jayH:@eR%EQ\fjoI@;Aޓ3-F& o;{ 64!m%tEXtdate:create2016-04-14T17:13:06+02:00k5%tEXtdate:modify2016-04-14T17:13:06+02:006ItEXtSoftwarewww.inkscape.org<IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/img/file.svg0000644000175100017510000005636715102145205020363 0ustar00runnerrunner image/svg+xml Generic Text text plaintext regular document Jakub Steiner http://jimmac.musichall.cz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/img/folder.svg0000644000175100017510000005452515102145205020711 0ustar00runnerrunner image/svg+xml Folder Icon Jakub Steiner http://jimmac.musichall.cz folder directory ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1762183912.536639 qutebrowser-3.6.1/qutebrowser/javascript/0000755000175100017510000000000015102145351020276 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/caret.js0000644000175100017510000014324315102145205021737 0ustar00runnerrunner/* eslint-disable max-len, max-statements, complexity, default-case */ /* Copyright 2014 The Chromium Authors * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. * https://source.chromium.org/chromium/chromium/src/+/main:LICENSE */ /** * SPDX-FileCopyrightText: Florian Bruhin (The Compiler) * * SPDX-License-Identifier: GPL-3.0-or-later */ /** * Ported chrome-caretbrowsing extension. * https://cs.chromium.org/chromium/src/ui/accessibility/extensions/caretbrowsing/ * * The behavior is based on Mozilla's spec whenever possible: * https://web.archive.org/web/20110818013741/http://www.mozilla.org/access/keyboard/proposal * * The one exception is that Esc is used to escape out of a form control, * rather than their proposed key (which doesn't seem to work in the * latest Firefox anyway). * * Some details about how Chrome selection works, which will help in * understanding the code: * * The Selection object (window.getSelection()) has four components that * completely describe the state of the caret or selection: * * base and anchor: this is the start of the selection, the fixed point. * extent and focus: this is the end of the selection, the part that * moves when you hold down shift and press the left or right arrows. * * When the selection is a cursor, the base, anchor, extent, and focus are * all the same. * * There's only one time when the base and anchor are not the same, or the * extent and focus are not the same, and that's when the selection is in * an ambiguous state - i.e. it's not clear which edge is the focus and which * is the anchor. As an example, if you double-click to select a word, then * the behavior is dependent on your next action. If you press Shift+Right, * the right edge becomes the focus. But if you press Shift+Left, the left * edge becomes the focus. * * When the selection is in an ambiguous state, the base and extent are set * to the position where the mouse clicked, and the anchor and focus are set * to the boundaries of the selection. * * The only way to set the selection and give it direction is to use * the non-standard Selection.setBaseAndExtent method. If you try to use * Selection.addRange(), the anchor will always be on the left and the focus * will always be on the right, making it impossible to manipulate * selections that move from right to left. * * Finally, Chrome will throw an exception if you try to set an invalid * selection - a selection where the left and right edges are not the same, * but it doesn't span any visible characters. A common example is that * there are often many whitespace characters in the DOM that are not * visible on the page; trying to select them will fail. Another example is * any node that's invisible or not displayed. * * While there are probably many possible methods to determine what is * selectable, this code uses the method of determining if there's a valid * bounding box for the range or not - keep moving the cursor forwards until * the range from the previous position and candidate next position has a * valid bounding box. */ "use strict"; window._qutebrowser.caret = (function() { function isElementInViewport(node) { let i; let boundingRect = (node.getClientRects()[0] || node.getBoundingClientRect()); if (boundingRect.width <= 1 && boundingRect.height <= 1) { const rects = node.getClientRects(); for (i = 0; i < rects.length; i++) { if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { boundingRect = rects[i]; } } } if (boundingRect === undefined) { return null; } if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { return null; } if (boundingRect.width <= 1 || boundingRect.height <= 1) { const children = node.children; let visibleChildNode = false; for (i = 0; i < children.length; ++i) { boundingRect = (children[i].getClientRects()[0] || children[i].getBoundingClientRect()); if (boundingRect.width > 1 && boundingRect.height > 1) { visibleChildNode = true; break; } } if (visibleChildNode === false) { return null; } } if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { return null; } const computedStyle = window.getComputedStyle(node, null); if (computedStyle.visibility !== "visible" || computedStyle.display === "none" || node.hasAttribute("disabled") || parseInt(computedStyle.width, 10) === 0 || parseInt(computedStyle.height, 10) === 0) { return null; } return boundingRect.top >= -20; } function positionCaret() { const walker = document.createTreeWalker(document.body, -1); let node; const textNodes = []; let el; while ((node = walker.nextNode())) { if (node.nodeType === 3 && node.nodeValue.trim() !== "") { textNodes.push(node); } } for (let i = 0; i < textNodes.length; i++) { const element = textNodes[i].parentElement; if (isElementInViewport(element)) { el = element; break; } } if (el !== undefined) { /* eslint-disable no-use-before-define */ const start = new Cursor(el, 0, ""); const end = new Cursor(el, 0, ""); const nodesCrossed = []; const result = TraverseUtil.getNextChar( start, end, nodesCrossed, true); if (result === null) { return; } CaretBrowsing.setAndValidateSelection(start, start); /* eslint-enable no-use-before-define */ } } /** * Return whether a node is focusable. This includes nodes whose tabindex * attribute is set to "-1" explicitly - these nodes are not in the tab * order, but they should still be focused if the user navigates to them * using linear or smart DOM navigation. * * Note that when the tabIndex property of an Element is -1, that doesn't * tell us whether the tabIndex attribute is missing or set to "-1" explicitly, * so we have to check the attribute. * * @param {Object} targetNode The node to check if it's focusable. * @return {boolean} True if the node is focusable. */ function isFocusable(targetNode) { if (!targetNode || typeof (targetNode.tabIndex) !== "number") { return false; } if (targetNode.tabIndex >= 0) { return true; } if (targetNode.hasAttribute && targetNode.hasAttribute("tabindex") && targetNode.getAttribute("tabindex") === "-1") { return true; } return false; } const axs = {}; axs.dom = {}; axs.color = {}; axs.utils = {}; axs.dom.parentElement = function(node) { if (!node) { return null; } const composedNode = axs.dom.composedParentNode(node); if (!composedNode) { return null; } switch (composedNode.nodeType) { case Node.ELEMENT_NODE: return composedNode; default: return axs.dom.parentElement(composedNode); } }; axs.dom.shadowHost = function(node) { if ("host" in node) { return node.host; } return null; }; axs.dom.composedParentNode = function(node) { if (!node) { return null; } if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { return axs.dom.shadowHost(node); } const parentNode = node.parentNode; if (!parentNode) { return null; } if (parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { return axs.dom.shadowHost(parentNode); } if (!parentNode.shadowRoot) { return parentNode; } const points = node.getDestinationInsertionPoints(); if (points.length > 0) { return axs.dom.composedParentNode(points[points.length - 1]); } return null; }; axs.color.Color = function(red, green, blue, alpha) { this.red = red; this.green = green; this.blue = blue; this.alpha = alpha; }; axs.color.parseColor = function(colorText) { if (colorText === "transparent") { return new axs.color.Color(0, 0, 0, 0); } let match = colorText.match(/^rgb\((\d+), (\d+), (\d+)\)$/); if (match) { const blue = parseInt(match[3], 10); const green = parseInt(match[2], 10); const red = parseInt(match[1], 10); return new axs.color.Color(red, green, blue, 1); } match = colorText.match(/^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/); if (match) { const red = parseInt(match[1], 10); const green = parseInt(match[2], 10); const blue = parseInt(match[3], 10); const alpha = parseFloat(match[4]); return new axs.color.Color(red, green, blue, alpha); } return null; }; axs.color.flattenColors = function(color1, color2) { const colorAlpha = color1.alpha; return new axs.color.Color( ((1 - colorAlpha) * color2.red) + (colorAlpha * color1.red), ((1 - colorAlpha) * color2.green) + (colorAlpha * color1.green), ((1 - colorAlpha) * color2.blue) + (colorAlpha * color2.blue), color1.alpha + (color2.alpha * (1 - color1.alpha))); }; axs.utils.getParentBgColor = function(_el) { let el = _el; let el2 = el; let iter = null; el = []; for (iter = null; (el2 = axs.dom.parentElement(el2));) { const style = window.getComputedStyle(el2, null); if (style) { const color = axs.color.parseColor(style.backgroundColor); if (color && (style.opacity < 1 && (color.alpha *= style.opacity), color.alpha !== 0 && (el.push(color), color.alpha === 1))) { iter = !0; break; } } } if (!iter) { el.push(new axs.color.Color(255, 255, 255, 1)); } for (el2 = el.pop(); el.length;) { iter = el.pop(); el2 = axs.color.flattenColors(iter, el2); } return el2; }; axs.utils.getFgColor = function(el, el2, color) { let color2 = axs.color.parseColor(el.color); if (!color2) { return null; } if (color2.alpha < 1) { color2 = axs.color.flattenColors(color2, color); } if (el.opacity < 1) { const el3 = axs.utils.getParentBgColor(el2); color2.alpha *= el.opacity; color2 = axs.color.flattenColors(color2, el3); } return color2; }; axs.utils.getBgColor = function(el, elParent) { let color = axs.color.parseColor(el.backgroundColor); if (!color) { return null; } if (el.opacity < 1) { color.alpha *= el.opacity; } if (color.alpha < 1) { const bgColor = axs.utils.getParentBgColor(elParent); if (bgColor === null) { return null; } color = axs.color.flattenColors(color, bgColor); } return color; }; axs.color.colorChannelToString = function(_color) { const color = Math.round(_color); if (color < 15) { return `0${color.toString(16)}`; } return color.toString(16); }; axs.color.colorToString = function(color) { if (color.alpha === 1) { const red = axs.color.colorChannelToString(color.red); const green = axs.color.colorChannelToString(color.green); const blue = axs.color.colorChannelToString(color.blue); return `#${red}${green}${blue}`; } const arr = [color.red, color.green, color.blue, color.alpha].join(); return `rgba(${arr})`; }; /** * A class to represent a cursor location in the document, * like the start position or end position of a selection range. * * Later this may be extended to support "virtual text" for an object, * like the ALT text for an image. * * Note: we cache the text of a particular node at the time we * traverse into it. Later we should add support for dynamically * reloading it. * @param {Node} node The DOM node. * @param {number} index The index of the character within the node. * @param {string} text The cached text contents of the node. * @constructor */ // eslint-disable-next-line func-style const Cursor = function(node, index, text) { this.node = node; this.index = index; this.text = text; }; /** * @return {Cursor} A new cursor pointing to the same location. */ Cursor.prototype.clone = function() { return new Cursor(this.node, this.index, this.text); }; /** * Modify this cursor to point to the location that another cursor points to. * @param {Cursor} otherCursor The cursor to copy from. */ Cursor.prototype.copyFrom = function(otherCursor) { this.node = otherCursor.node; this.index = otherCursor.index; this.text = otherCursor.text; }; /** * Utility functions for stateless DOM traversal. * @constructor */ const TraverseUtil = {}; /** * Gets the text representation of a node. This allows us to substitute * alt text, names, or titles for html elements that provide them. * @param {Node} node A DOM node. * @return {string} A text string representation of the node. */ TraverseUtil.getNodeText = function(node) { if (node.constructor === Text) { return node.data; } return ""; }; /** * Return true if a node should be treated as a leaf node, because * its children are properties of the object that shouldn't be traversed. * * TODO(dmazzoni): replace this with a predicate that detects nodes with * ARIA roles and other objects that have their own description. * For now we just detect a couple of common cases. * * @param {Node} node A DOM node. * @return {boolean} True if the node should be treated as a leaf node. */ TraverseUtil.treatAsLeafNode = function(node) { return node.childNodes.length === 0 || node.nodeName === "SELECT" || node.nodeName === "OBJECT"; }; /** * Return true only if a single character is whitespace. * From https://developer.mozilla.org/en/Whitespace_in_the_DOM, * whitespace is defined as one of the characters * "\t" TAB \u0009 * "\n" LF \u000A * "\r" CR \u000D * " " SPC \u0020. * * @param {string} c A string containing a single character. * @return {boolean} True if the character is whitespace, otherwise false. */ TraverseUtil.isWhitespace = function(ch) { return (ch === " " || ch === "\n" || ch === "\r" || ch === "\t"); }; /** * Use the computed CSS style to figure out if this DOM node is currently * visible. * @param {Node} node A HTML DOM node. * @return {boolean} Whether or not the html node is visible. */ TraverseUtil.isVisible = function(node) { if (!node.style) { return true; } const style = window.getComputedStyle(node, null); return (Boolean(style) && style.display !== "none" && style.visibility !== "hidden"); }; /** * Use the class name to figure out if this DOM node should be traversed. * @param {Node} node A HTML DOM node. * @return {boolean} Whether or not the html node should be traversed. */ TraverseUtil.isSkipped = function(_node) { let node = _node; if (node.constructor === Text) { node = node.parentElement; } if (node.className === "CaretBrowsing_Caret") { return true; } return false; }; /** * Moves the cursor forwards until it has crossed exactly one character. * @param {Cursor} cursor The cursor location where the search should start. * On exit, the cursor will be immediately to the right of the * character returned. * @param {Array} nodesCrossed Any HTML nodes crossed between the * initial and final cursor position will be pushed onto this array. * @return {?string} The character found, or null if the bottom of the * document has been reached. */ TraverseUtil.forwardsChar = function(cursor, nodesCrossed) { for (;;) { let childNode = null; if (!TraverseUtil.treatAsLeafNode(cursor.node)) { for (let i = cursor.index; i < cursor.node.childNodes.length; i++) { const node = cursor.node.childNodes[i]; if (TraverseUtil.isSkipped(node)) { nodesCrossed.push(node); } else if (TraverseUtil.isVisible(node)) { childNode = node; break; } } } if (childNode) { cursor.node = childNode; cursor.index = 0; cursor.text = TraverseUtil.getNodeText(cursor.node); if (cursor.node.constructor !== Text) { nodesCrossed.push(cursor.node); } } else { // Return the next character from this leaf node. if (cursor.index < cursor.text.length) { return cursor.text[cursor.index++]; } // Move to the next sibling, going up the tree as necessary. while (cursor.node !== null) { // Try to move to the next sibling. let siblingNode = null; for (let node = cursor.node.nextSibling; node !== null; node = node.nextSibling) { if (TraverseUtil.isSkipped(node)) { nodesCrossed.push(node); } else if (TraverseUtil.isVisible(node)) { siblingNode = node; break; } } if (siblingNode) { cursor.node = siblingNode; cursor.text = TraverseUtil.getNodeText(siblingNode); cursor.index = 0; if (cursor.node.constructor !== Text) { nodesCrossed.push(cursor.node); } break; } // Otherwise, move to the parent. const parentNode = cursor.node.parentNode; if (parentNode && parentNode.constructor !== HTMLBodyElement) { cursor.node = cursor.node.parentNode; cursor.text = null; cursor.index = 0; } else { return null; } } } } }; /** * Finds the next character, starting from endCursor. Upon exit, startCursor * and endCursor will surround the next character. If skipWhitespace is * true, will skip until a real character is found. Otherwise, it will * attempt to select all of the whitespace between the initial position * of endCursor and the next non-whitespace character. * @param {Cursor} startCursor On exit, points to the position before * the char. * @param {Cursor} endCursor The position to start searching for the next * char. On exit, will point to the position past the char. * @param {Array} nodesCrossed Any HTML nodes crossed between the * initial and final cursor position will be pushed onto this array. * @param {boolean} skipWhitespace If true, will keep scanning until a * non-whitespace character is found. * @return {?string} The next char, or null if the bottom of the * document has been reached. */ TraverseUtil.getNextChar = function( startCursor, endCursor, nodesCrossed, skipWhitespace) { // Save the starting position and get the first character. startCursor.copyFrom(endCursor); let fChar = TraverseUtil.forwardsChar(endCursor, nodesCrossed); if (fChar === null) { return null; } // Keep track of whether the first character was whitespace. const initialWhitespace = TraverseUtil.isWhitespace(fChar); // Keep scanning until we find a non-whitespace or non-skipped character. while ((TraverseUtil.isWhitespace(fChar)) || (TraverseUtil.isSkipped(endCursor.node))) { fChar = TraverseUtil.forwardsChar(endCursor, nodesCrossed); if (fChar === null) { return null; } } if (skipWhitespace || !initialWhitespace) { // If skipWhitepace is true, or if the first character we encountered // was not whitespace, return that non-whitespace character. startCursor.copyFrom(endCursor); startCursor.index--; return fChar; } for (let i = 0; i < nodesCrossed.length; i++) { if (TraverseUtil.isSkipped(nodesCrossed[i])) { // We need to make sure that startCursor and endCursor aren't // surrounding a skippable node. endCursor.index--; startCursor.copyFrom(endCursor); startCursor.index--; return " "; } } // Otherwise, return all of the whitespace before that last character. endCursor.index--; return " "; }; /** * The class handling the Caret Browsing implementation in the page. * Sets up communication with the background page, and then when caret * browsing is enabled, response to various key events to move the caret * or selection within the text content of the document. * @constructor */ const CaretBrowsing = {}; /** * Is caret browsing enabled? * @type {boolean} */ CaretBrowsing.isEnabled = false; /** * Keep it enabled even when flipped off (for the options page)? * @type {boolean} */ CaretBrowsing.forceEnabled = false; /** * What to do when the caret appears? * @type {string} */ CaretBrowsing.onEnable = undefined; /** * What to do when the caret jumps? * @type {string} */ CaretBrowsing.onJump = undefined; /** * Is this window / iframe focused? We won't show the caret if not, * especially so that carets aren't shown in two iframes of the same * tab. * @type {boolean} */ CaretBrowsing.isWindowFocused = false; /** * Is the caret actually visible? This is true only if isEnabled and * isWindowFocused are both true. * @type {boolean} */ CaretBrowsing.isCaretVisible = false; /** * Selection modes. * NOTE: Values need to line up with SelectionState in browsertab.py! * * @type {enum} */ CaretBrowsing.SelectionState = { "NONE": "none", "NORMAL": "normal", "LINE": "line", }; /** * The actual caret element, an absolute-positioned flashing line. * @type {Element} */ CaretBrowsing.caretElement = undefined; /** * The x-position of the caret, in absolute pixels. * @type {number} */ CaretBrowsing.caretX = 0; /** * The y-position of the caret, in absolute pixels. * @type {number} */ CaretBrowsing.caretY = 0; /** * The width of the caret in pixels. * @type {number} */ CaretBrowsing.caretWidth = 0; /** * The height of the caret in pixels. * @type {number} */ CaretBrowsing.caretHeight = 0; /** * The foregroundc color. * @type {string} */ CaretBrowsing.caretForeground = "#000"; /** * The backgroundc color. * @type {string} */ CaretBrowsing.caretBackground = "#fff"; /** * Is the selection collapsed, i.e. are the start and end locations * the same? If so, our blinking caret image is shown; otherwise * the Chrome selection is shown. * @type {boolean} */ CaretBrowsing.isSelectionCollapsed = false; /** * Whether we're running on Windows. * @type {boolean} */ CaretBrowsing.isWindows = null; /** * Whether we should log debug outputs. * @type {boolean} */ CaretBrowsing.isDebug = null; /** * The id returned by window.setInterval for our stopAnimation function, so * we can cancel it when we call stopAnimation again. * @type {number?} */ CaretBrowsing.animationFunctionId = null; /** * Check if a node is a control that normally allows the user to interact * with it using arrow keys. We won't override the arrow keys when such a * control has focus, the user must press Escape to do caret browsing outside * that control. * @param {Node} node A node to check. * @return {boolean} True if this node is a control that the user can * interact with using arrow keys. */ CaretBrowsing.isControlThatNeedsArrowKeys = function(node) { if (!node) { return false; } if (node === document.body || node !== document.activeElement) { return false; } if (node.constructor === HTMLSelectElement) { return true; } if (node.constructor === HTMLInputElement) { switch (node.type) { case "email": case "number": case "password": case "search": case "text": case "tel": case "url": case "": return true; // All of these are text boxes. case "datetime": case "datetime-local": case "date": case "month": case "radio": case "range": case "week": return true; // These are other input elements that use arrows. } } // Handle focusable ARIA controls. if (node.getAttribute && isFocusable(node)) { const role = node.getAttribute("role"); switch (role) { case "combobox": case "grid": case "gridcell": case "listbox": case "menu": case "menubar": case "menuitem": case "menuitemcheckbox": case "menuitemradio": case "option": case "radiogroup": case "scrollbar": case "slider": case "spinbutton": case "tab": case "tablist": case "textbox": case "tree": case "treegrid": case "treeitem": return true; } } return false; }; CaretBrowsing.injectCaretStyles = function() { const style = ` .CaretBrowsing_Caret { position: absolute; z-index: 2147483647; min-height: 1em; min-width: 0.2em; animation: blink 1s step-end infinite; --inherited-color: inherit; background-color: var(--inherited-color, #000); color: var(--inherited-color, #000); mix-blend-mode: difference; filter: invert(85%); } @keyframes blink { 50% { visibility: hidden; } } `; const node = document.createElement("style"); node.innerHTML = style; document.body.appendChild(node); }; /** * If there's no initial selection, set the cursor just before the * first text character in the document. */ CaretBrowsing.setInitialCursor = function() { const selectionRange = window.getSelection().toString().length; if (selectionRange === 0) { positionCaret(); } CaretBrowsing.injectCaretStyles(); CaretBrowsing.toggle(); CaretBrowsing.initiated = true; if (selectionRange > 0) { CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL; } else { CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE; } }; /** * Try to set the window's selection to be between the given start and end * cursors, and return whether or not it was successful. * @param {Cursor} start The start position. * @param {Cursor} end The end position. * @return {boolean} True if the selection was successfully set. */ CaretBrowsing.setAndValidateSelection = function(start, end) { const sel = window.getSelection(); sel.setBaseAndExtent(start.node, start.index, end.node, end.index); if (sel.rangeCount !== 1) { return false; } return (sel.anchorNode === start.node && sel.anchorOffset === start.index && sel.focusNode === end.node && sel.focusOffset === end.index); }; /** * Set focus to a node if it's focusable. If it's an input element, * select the text, otherwise it doesn't appear focused to the user. * Every other control behaves normally if you just call focus() on it. * @param {Node} node The node to focus. * @return {boolean} True if the node was focused. */ CaretBrowsing.setFocusToNode = function(nodeArg) { let node = nodeArg; while (node && node !== document.body) { if (isFocusable(node) && node.constructor !== HTMLIFrameElement) { node.focus(); if (node.constructor === HTMLInputElement && node.select) { node.select(); } return true; } node = node.parentNode; } return false; }; /** * Set the caret element's normal style, i.e. not when animating. */ CaretBrowsing.setCaretElementNormalStyle = function() { const element = CaretBrowsing.caretElement; element.className = "CaretBrowsing_Caret"; if (CaretBrowsing.isSelectionCollapsed) { element.style.opacity = "1.0"; } else { element.style.opacity = "0.0"; } element.style.left = `${CaretBrowsing.caretX}px`; element.style.top = `${CaretBrowsing.caretY}px`; element.style.width = `${CaretBrowsing.caretWidth}px`; element.style.height = `${CaretBrowsing.caretHeight}px`; element.style.color = CaretBrowsing.caretForeground; }; /** * Create the caret element. This assumes that caretX, caretY, * caretWidth, and caretHeight have all been set. The caret is * animated in so the user can find it when it first appears. */ CaretBrowsing.createCaretElement = function() { const element = document.createElement("div"); element.className = "CaretBrowsing_Caret"; document.body.appendChild(element); CaretBrowsing.caretElement = element; CaretBrowsing.setCaretElementNormalStyle(); }; /** * Recreate the caret element, triggering any intro animation. */ CaretBrowsing.recreateCaretElement = function() { if (CaretBrowsing.caretElement) { CaretBrowsing.caretElement.parentElement.removeChild( CaretBrowsing.caretElement); CaretBrowsing.caretElement = null; CaretBrowsing.updateIsCaretVisible(); } }; /** * Get the rectangle for a cursor position. This is tricky because * you can't get the bounding rectangle of an empty range, so this function * computes the rect by trying a range including one character earlier or * later than the cursor position. * @param {Cursor} cursor A single cursor position. * @return {{left: number, top: number, width: number, height: number}} * The bounding rectangle of the cursor. */ CaretBrowsing.getCursorRect = function(cursor) { let node = cursor.node; const index = cursor.index; const rect = { "left": 0, "top": 0, "width": 1, "height": 0, }; if (node.constructor === Text) { let left = index; let right = index; const max = node.data.length; const newRange = document.createRange(); while (left > 0 || right < max) { if (left > 0) { left--; newRange.setStart(node, left); newRange.setEnd(node, index); const rangeRect = newRange.getBoundingClientRect(); if (rangeRect && rangeRect.width && rangeRect.height) { rect.left = rangeRect.right; rect.top = rangeRect.top; rect.height = rangeRect.height; break; } } if (right < max) { right++; newRange.setStart(node, index); newRange.setEnd(node, right); const rangeRect = newRange.getBoundingClientRect(); if (rangeRect && rangeRect.width && rangeRect.height) { rect.left = rangeRect.left; rect.top = rangeRect.top; rect.height = rangeRect.height; break; } } } } else { rect.height = node.offsetHeight; while (node !== null) { rect.left += node.offsetLeft; rect.top += node.offsetTop; node = node.offsetParent; } } rect.left += window.pageXOffset; rect.top += window.pageYOffset; return rect; }; /** * Compute the new location of the caret or selection and update * the element as needed. * @param {boolean} scrollToSelection If true, will also scroll the page * to the caret / selection location. */ CaretBrowsing.updateCaretOrSelection = function(scrollToSelection) { const sel = window.getSelection(); if (sel.rangeCount === 0) { if (CaretBrowsing.caretElement) { CaretBrowsing.isSelectionCollapsed = false; CaretBrowsing.caretElement.style.opacity = "0.0"; } return; } const range = sel.getRangeAt(0); if (!range) { if (CaretBrowsing.caretElement) { CaretBrowsing.isSelectionCollapsed = false; CaretBrowsing.caretElement.style.opacity = "0.0"; } return; } if (CaretBrowsing.isControlThatNeedsArrowKeys( document.activeElement)) { let node = document.activeElement; CaretBrowsing.caretWidth = node.offsetWidth; CaretBrowsing.caretHeight = node.offsetHeight; CaretBrowsing.caretX = 0; CaretBrowsing.caretY = 0; while (node.offsetParent) { CaretBrowsing.caretX += node.offsetLeft; CaretBrowsing.caretY += node.offsetTop; node = node.offsetParent; } CaretBrowsing.isSelectionCollapsed = false; } else if (range.startOffset !== range.endOffset || range.startContainer !== range.endContainer) { const rect = range.getBoundingClientRect(); if (!rect) { return; } CaretBrowsing.caretX = rect.left + window.pageXOffset; CaretBrowsing.caretY = rect.top + window.pageYOffset; CaretBrowsing.caretWidth = rect.width; CaretBrowsing.caretHeight = rect.height; CaretBrowsing.isSelectionCollapsed = false; } else { const rect = CaretBrowsing.getCursorRect( new Cursor(range.startContainer, range.startOffset, TraverseUtil.getNodeText(range.startContainer))); CaretBrowsing.caretX = rect.left; CaretBrowsing.caretY = rect.top; CaretBrowsing.caretWidth = rect.width; CaretBrowsing.caretHeight = rect.height; CaretBrowsing.isSelectionCollapsed = true; } if (CaretBrowsing.caretElement) { const element = CaretBrowsing.caretElement; if (CaretBrowsing.isSelectionCollapsed) { element.style.opacity = "1.0"; element.style.left = `${CaretBrowsing.caretX}px`; element.style.top = `${CaretBrowsing.caretY}px`; element.style.width = `${CaretBrowsing.caretWidth}px`; element.style.height = `${CaretBrowsing.caretHeight}px`; } else { element.style.opacity = "0.0"; } } else { CaretBrowsing.createCaretElement(); } let elem = range.startContainer; if (elem.constructor === Text) { elem = elem.parentElement; } const style = window.getComputedStyle(elem); const bg = axs.utils.getBgColor(style, elem); const fg = axs.utils.getFgColor(style, elem, bg); CaretBrowsing.caretBackground = axs.color.colorToString(bg); CaretBrowsing.caretForeground = axs.color.colorToString(fg); if (scrollToSelection) { // Scroll just to the "focus" position of the selection, // the part the user is manipulating. const rect = CaretBrowsing.getCursorRect( new Cursor(sel.focusNode, sel.focusOffset, TraverseUtil.getNodeText(sel.focusNode))); const yscroll = window.pageYOffset; const pageHeight = window.innerHeight; const caretY = rect.top; const caretHeight = Math.min(rect.height, 30); if (yscroll + pageHeight < caretY + caretHeight) { window.scroll(0, (caretY + caretHeight - pageHeight + 100)); } else if (caretY < yscroll) { window.scroll(0, (caretY - 100)); } } }; CaretBrowsing.reverseSelection = () => { const sel = window.getSelection(); sel.setBaseAndExtent( sel.extentNode, sel.extentOffset, sel.baseNode, sel.baseOffset ); }; CaretBrowsing.selectLine = function() { const sel = window.getSelection(); sel.modify("extend", "right", "lineboundary"); CaretBrowsing.reverseSelection(); sel.modify("extend", "left", "lineboundary"); CaretBrowsing.reverseSelection(); }; CaretBrowsing.updateLineSelection = function(direction, granularity) { if (granularity !== "character" && granularity !== "word") { window. getSelection(). modify("extend", direction, granularity); CaretBrowsing.selectLine(); } }; CaretBrowsing.move = function(direction, granularity, count = 1) { let action = "move"; if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) { action = "extend"; } CaretBrowsing.debug(`(move) ${action} ${count} ${granularity} ${direction}, selection ${CaretBrowsing.selectionState}`); for (let i = 0; i < count; i++) { if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) { CaretBrowsing.updateLineSelection(direction, granularity); } else { window. getSelection(). modify(action, direction, granularity); } } if (CaretBrowsing.isWindows && (direction === "forward" || direction === "right") && granularity === "word") { CaretBrowsing.move("left", "character"); } }; CaretBrowsing.finishMove = function() { window.setTimeout(() => { CaretBrowsing.updateCaretOrSelection(true); }, 0); CaretBrowsing.stopAnimation(); }; CaretBrowsing.moveToBlock = function(paragraph, boundary, count = 1) { let action = "move"; if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) { action = "extend"; } CaretBrowsing.debug(`(moveToBlock) ${action} paragraph ${paragraph}, boundary ${boundary}, count ${count}, selection ${CaretBrowsing.selectionState}`); for (let i = 0; i < count; i++) { window. getSelection(). modify(action, paragraph, "paragraph"); window. getSelection(). modify(action, boundary, "paragraphboundary"); if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) { CaretBrowsing.selectLine(); } } }; CaretBrowsing.toggle = function(value) { CaretBrowsing.debug(`(toggle) enabled ${CaretBrowsing.isEnabled}, force ${CaretBrowsing.forceEnabled}`); if (CaretBrowsing.forceEnabled) { CaretBrowsing.recreateCaretElement(); return; } if (value === undefined) { CaretBrowsing.isEnabled = !CaretBrowsing.isEnabled; } else { CaretBrowsing.isEnabled = value; } CaretBrowsing.updateIsCaretVisible(); }; /** * Event handler, called when the mouse is clicked. Chrome already * sets the selection when the mouse is clicked, all we need to do is * update our cursor. * @param {Event} evt The DOM event. * @return {boolean} True if the default action should be performed. */ CaretBrowsing.onClick = function() { if (!CaretBrowsing.isEnabled) { return true; } window.setTimeout(() => { CaretBrowsing.updateCaretOrSelection(false); }, 0); return true; }; /** * Update whether or not the caret is visible, based on whether caret browsing * is enabled and whether this window / iframe has focus. */ CaretBrowsing.updateIsCaretVisible = function() { CaretBrowsing.debug(`(updateIsCaretVisible) isEnabled ${CaretBrowsing.isEnabled}, isWindowFocused ${CaretBrowsing.isWindowFocused}, isCaretVisible ${CaretBrowsing.isCaretVisible}, caretElement ${CaretBrowsing.caretElement}`); CaretBrowsing.isCaretVisible = (CaretBrowsing.isEnabled && CaretBrowsing.isWindowFocused); if (CaretBrowsing.isCaretVisible && !CaretBrowsing.caretElement) { CaretBrowsing.setInitialCursor(); CaretBrowsing.updateCaretOrSelection(true); } else if (!CaretBrowsing.isCaretVisible && CaretBrowsing.caretElement) { if (CaretBrowsing.caretElement) { CaretBrowsing.isSelectionCollapsed = false; CaretBrowsing.caretElement.parentElement.removeChild( CaretBrowsing.caretElement); CaretBrowsing.caretElement = null; } } }; CaretBrowsing.onWindowFocus = function() { CaretBrowsing.isWindowFocused = true; CaretBrowsing.updateIsCaretVisible(); }; CaretBrowsing.onWindowBlur = function() { CaretBrowsing.isWindowFocused = false; CaretBrowsing.updateIsCaretVisible(); }; CaretBrowsing.startAnimation = function() { if (CaretBrowsing.caretElement) { CaretBrowsing.caretElement.style.animationIterationCount = "infinite"; } }; CaretBrowsing.stopAnimation = function() { if (CaretBrowsing.caretElement) { CaretBrowsing.caretElement.style.animationIterationCount = 0; window.clearTimeout(CaretBrowsing.animationFunctionId); CaretBrowsing.animationFunctionId = window.setTimeout(() => { CaretBrowsing.startAnimation(); }, 1000); } }; CaretBrowsing.debug = (text) => { if (CaretBrowsing.isDebug) { console.debug(`caret: ${text}`); } } CaretBrowsing.init = function() { CaretBrowsing.isWindowFocused = document.hasFocus(); document.addEventListener("click", CaretBrowsing.onClick, false); window.addEventListener("focus", CaretBrowsing.onWindowFocus, false); window.addEventListener("blur", CaretBrowsing.onWindowBlur, false); }; window.setTimeout(() => { if (!window.caretBrowsingLoaded) { window.caretBrowsingLoaded = true; CaretBrowsing.init(); if (document.body && document.body.getAttribute("caretbrowsing") === "on") { CaretBrowsing.forceEnabled = true; CaretBrowsing.isEnabled = true; CaretBrowsing.updateIsCaretVisible(); } } }, 0); const funcs = {}; funcs.setInitialCursor = () => { if (!CaretBrowsing.initiated) { CaretBrowsing.setInitialCursor(); return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE; } if (window.getSelection().toString().length === 0) { positionCaret(); } CaretBrowsing.toggle(); return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE; }; funcs.setFlags = (flags) => { CaretBrowsing.isWindows = flags.includes("windows"); CaretBrowsing.isDebug = flags.includes("debug"); }; funcs.disableCaret = () => { CaretBrowsing.toggle(false); }; funcs.toggle = () => { CaretBrowsing.toggle(); }; funcs.moveRight = (count = 1) => { CaretBrowsing.move("right", "character", count); CaretBrowsing.finishMove(); }; funcs.moveLeft = (count = 1) => { CaretBrowsing.move("left", "character", count); CaretBrowsing.finishMove(); }; funcs.moveDown = (count = 1) => { CaretBrowsing.move("forward", "line", count); CaretBrowsing.finishMove(); }; funcs.moveUp = (count = 1) => { CaretBrowsing.move("backward", "line", count); CaretBrowsing.finishMove(); }; funcs.moveToEndOfWord = (count = 1) => { CaretBrowsing.move("forward", "word", count); CaretBrowsing.finishMove(); }; funcs.moveToNextWord = (count = 1) => { CaretBrowsing.move("forward", "word", count); CaretBrowsing.move("right", "character"); CaretBrowsing.finishMove(); }; funcs.moveToPreviousWord = (count = 1) => { CaretBrowsing.move("backward", "word", count); CaretBrowsing.finishMove(); }; funcs.moveToStartOfLine = () => { CaretBrowsing.move("left", "lineboundary"); CaretBrowsing.finishMove(); }; funcs.moveToEndOfLine = () => { CaretBrowsing.move("right", "lineboundary"); CaretBrowsing.finishMove(); }; funcs.moveToStartOfNextBlock = (count = 1) => { CaretBrowsing.moveToBlock("forward", "backward", count); CaretBrowsing.finishMove(); }; funcs.moveToStartOfPrevBlock = (count = 1) => { CaretBrowsing.moveToBlock("backward", "backward", count); CaretBrowsing.finishMove(); }; funcs.moveToEndOfNextBlock = (count = 1) => { CaretBrowsing.moveToBlock("forward", "forward", count); CaretBrowsing.finishMove(); }; funcs.moveToEndOfPrevBlock = (count = 1) => { CaretBrowsing.moveToBlock("backward", "forward", count); CaretBrowsing.finishMove(); }; funcs.moveToStartOfDocument = () => { CaretBrowsing.move("backward", "documentboundary"); CaretBrowsing.finishMove(); }; funcs.moveToEndOfDocument = () => { CaretBrowsing.move("forward", "documentboundary"); CaretBrowsing.finishMove(); }; funcs.dropSelection = () => { window.getSelection().removeAllRanges(); }; funcs.getSelection = () => window.getSelection().toString(); funcs.toggleSelection = (line) => { if (line) { CaretBrowsing.selectionState = CaretBrowsing.SelectionState.LINE; CaretBrowsing.selectLine(); CaretBrowsing.finishMove(); } else if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NORMAL) { CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL; } else { CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE; } return CaretBrowsing.selectionState; }; funcs.reverseSelection = () => { CaretBrowsing.reverseSelection(); }; return funcs; })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/global_wrapper.js0000644000175100017510000000044415102145205023634 0ustar00runnerrunner(function() { "use strict"; if (!window.hasOwnProperty("_qutebrowser")) { window._qutebrowser = {"initialized": {}}; } if (window._qutebrowser.initialized["{{name}}"]) { return; } {{code}} window._qutebrowser.initialized["{{name}}"] = true; })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/greasemonkey_wrapper.js0000644000175100017510000001724715102145205025076 0ustar00runnerrunner(function() { const _qute_script_id = "__gm_{{ scriptName }}"; function GM_log(text) { console.log(text); } const GM_info = { 'script': {{ scriptInfo }}, 'scriptMetaStr': "{{ scriptMeta }}", 'scriptWillUpdate': false, 'version': "0.0.1", // so scripts don't expect exportFunction 'scriptHandler': 'Tampermonkey', }; function checkKey(key, funcName) { if (typeof key !== "string") { throw new Error(`${funcName} requires the first parameter to be of type string, not '${typeof key}'`); } } function GM_setValue(key, value) { checkKey(key, "GM_setValue"); if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") { throw new Error(`GM_setValue requires the second parameter to be of type string, number or boolean, not '${typeof value}'`); } localStorage.setItem(_qute_script_id + key, value); } function GM_getValue(key, default_) { checkKey(key, "GM_getValue"); return localStorage.getItem(_qute_script_id + key) || default_; } function GM_deleteValue(key) { checkKey(key, "GM_deleteValue"); localStorage.removeItem(_qute_script_id + key); } function GM_listValues() { const keys = []; for (let i = 0; i < localStorage.length; i++) { if (localStorage.key(i).startsWith(_qute_script_id)) { keys.push(localStorage.key(i).slice(_qute_script_id.length)); } } return keys; } function GM_openInTab(url) { window.open(url); } // Almost verbatim copy from Eric function GM_xmlhttpRequest(/* object */ details) { details.method = details.method ? details.method.toUpperCase() : "GET"; if (!details.url) { throw new Error("GM_xmlhttpRequest requires a URL."); } // build XMLHttpRequest object const oXhr = new XMLHttpRequest(); // run it if ("onreadystatechange" in details) { oXhr.onreadystatechange = function() { details.onreadystatechange(oXhr); }; } if ("onload" in details) { oXhr.onload = function() { details.onload(oXhr); }; } if ("onerror" in details) { oXhr.onerror = function () { details.onerror(oXhr); }; } if ("overrideMimeType" in details) { oXhr.overrideMimeType(details.overrideMimeType); } oXhr.open(details.method, details.url, true); if ("headers" in details) { for (const header in details.headers) { oXhr.setRequestHeader(header, details.headers[header]); } } if ("data" in details) { oXhr.send(details.data); } else { oXhr.send(); } } function GM_addStyle(/* String */ styles) { const oStyle = document.createElement("style"); oStyle.setAttribute("type", "text/css"); oStyle.appendChild(document.createTextNode(styles)); const head = document.getElementsByTagName("head")[0]; if (head === undefined) { // no head yet, stick it wherever document.documentElement.appendChild(oStyle); } else { head.appendChild(oStyle); } } // Based on GreaseMonkey: // https://github.com/greasemonkey/greasemonkey/blob/4.11/src/bg/api-provider-source.js#L232-L249 function GM_setClipboard(text) { function onCopy(event) { document.removeEventListener('copy', onCopy, true); event.stopImmediatePropagation(); event.preventDefault(); event.clipboardData.setData('text/plain', text); } document.addEventListener('copy', onCopy, true); document.execCommand('copy'); } // Stub these two so that the gm4 polyfill script doesn't try to // create broken versions as attributes of window. function GM_getResourceText(caption, commandFunc, accessKey) { console.info(`${GM_info.script.name} called unimplemented GM_getResourceText`); } function GM_registerMenuCommand(caption, commandFunc, accessKey) { console.info(`${GM_info.script.name} called unimplemented GM_registerMenuCommand`); } // Mock the greasemonkey 4.0 async API. const GM = {}; GM.info = GM_info; const entries = { 'log': GM_log, 'addStyle': GM_addStyle, 'setClipboard': GM_setClipboard, 'deleteValue': GM_deleteValue, 'getValue': GM_getValue, 'listValues': GM_listValues, 'openInTab': GM_openInTab, 'setValue': GM_setValue, 'xmlHttpRequest': GM_xmlhttpRequest, } for (newKey in entries) { let old = entries[newKey]; if (old && (typeof GM[newKey] == 'undefined')) { GM[newKey] = function(...args) { return new Promise((resolve, reject) => { try { resolve(old(...args)); } catch (e) { reject(e); } }); }; } }; const unsafeWindow = window; {% if use_proxy %} /* * Try to give userscripts an environment that they expect. Which seems * to be that the global window object should look the same as the page's * one and that if a script writes to an attribute of window all other * scripts should be able to access that variable in the global scope. * Use a Proxy to stop scripts from actually changing the global window * (that's what unsafeWindow is for). Use the "with" statement to make * the proxy provide what looks like global scope. * * There are other Proxy functions that we may need to override. set, * get and has are definitely required. */ if (!window._qute_gm_window_proxy) { const qute_gm_window_shadow = {}; // stores local changes to window const qute_gm_windowProxyHandler = { get: function (target, prop) { if (prop in qute_gm_window_shadow) return qute_gm_window_shadow[prop]; if (prop in target) { if (typeof target[prop] === 'function' && typeof target[prop].prototype == 'undefined') // Getting TypeError: Illegal Execution when callers try // to execute eg addEventListener from here because they // were returned unbound return target[prop].bind(target); return target[prop]; } }, set: function(target, prop, val) { return qute_gm_window_shadow[prop] = val; }, has: function(target, key) { return key in qute_gm_window_shadow || key in target; } }; window._qute_gm_window_proxy = new Proxy(unsafeWindow, qute_gm_windowProxyHandler); } const qute_gm_window_proxy = window._qute_gm_window_proxy; with (qute_gm_window_proxy) { // We can't return `this` or `qute_gm_window_proxy` from // `qute_gm_window_proxy.get('window')` because the Proxy implementation // does typechecking on read-only things. So we have to shadow `window` // more conventionally here. const window = qute_gm_window_proxy; // ====== The actual user script source ====== // {{ scriptSource }} // ====== End User Script ====== // }; {% else %} // ====== The actual user script source ====== // {{ scriptSource }} // ====== End User Script ====== // {% endif %} })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/history.js0000644000175100017510000001422215102145205022334 0ustar00runnerrunner// SPDX-FileCopyrightText: Imran Sobir // SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) // // SPDX-License-Identifier: GPL-3.0-or-later "use strict"; window.loadHistory = (function() { // Date of last seen item. let lastItemDate = null; // Each request for new items includes the time of the last item and an // offset. The offset is equal to the number of items from the previous // request that had time=nextTime, and causes the next request to skip // those items to avoid duplicates. let nextTime = null; let nextOffset = 0; // The URL to fetch data from. const DATA_URL = "qute://history/data"; // Various fixed elements const EOF_MESSAGE = document.getElementById("eof"); const LOAD_LINK = document.getElementById("load"); const HIST_CONTAINER = document.getElementById("hist-container"); /** * Finds or creates the session table>tbody to which item with given date * should be added. * * @param {Date} date - the date of the item being added. * @returns {Element} the element to which new rows should be added. */ function getSessionNode(date) { // Find/create table const tableId = ["hist", date.getDate(), date.getMonth(), date.getYear()].join("-"); let table = document.getElementById(tableId); if (table === null) { table = document.createElement("table"); table.id = tableId; // Caption contains human-readable date const caption = document.createElement("caption"); caption.className = "date"; const options = { "weekday": "long", "year": "numeric", "month": "long", "day": "numeric", }; caption.innerHTML = date.toLocaleDateString("en-US", options); table.appendChild(caption); // Add table to page HIST_CONTAINER.appendChild(table); } // Find/create tbody let tbody = table.lastChild; if (tbody.tagName !== "TBODY") { tbody = document.createElement("tbody"); table.appendChild(tbody); } // Create session-separator and new tbody if necessary if (tbody.lastChild !== null && lastItemDate !== null && window.GAP_INTERVAL > 0) { const interval = lastItemDate.getTime() - date.getTime(); if (interval > window.GAP_INTERVAL) { // Add session-separator const sessionSeparator = document.createElement("td"); sessionSeparator.className = "session-separator"; sessionSeparator.colSpan = 2; sessionSeparator.innerHTML = "§"; table.appendChild(document.createElement("tr")); table.lastChild.appendChild(sessionSeparator); // Create new tbody tbody = document.createElement("tbody"); table.appendChild(tbody); } } return tbody; } /** * Given a history item, create and return for it. * * @param {string} itemUrl - The url for this item. * @param {string} itemTitle - The title for this item. * @param {string} itemTime - The formatted time for this item. * @returns {Element} the completed tr. */ function makeHistoryRow(itemUrl, itemTitle, itemTime) { const row = document.createElement("tr"); const title = document.createElement("td"); title.className = "title"; const link = document.createElement("a"); link.href = itemUrl; link.innerHTML = itemTitle; // Properly escaped in qutescheme.py const host = document.createElement("span"); host.className = "hostname"; host.innerHTML = link.hostname; title.appendChild(link); title.appendChild(host); const time = document.createElement("td"); time.className = "time"; time.innerHTML = itemTime; row.appendChild(title); row.appendChild(time); return row; } /** * Get JSON from given URL. * * @param {string} url - the url to fetch data from. * @param {function} callback - the function to callback with data. * @returns {void} */ function getJSON(url, callback) { const xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.responseType = "json"; xhr.onload = () => { const status = xhr.status; callback(status, xhr.response); }; xhr.send(); } /** * Receive history data from qute://history/data. * * @param {Number} status - The status of the query. * @param {Array} history - History data. * @returns {void} */ function receiveHistory(status, history) { if (history === null) { return; } if (history.length === 0) { // Reached end of history window.onscroll = null; EOF_MESSAGE.style.display = "block"; LOAD_LINK.style.display = "none"; return; } nextTime = history[history.length - 1].time; nextOffset = 0; for (let i = 0, len = history.length; i < len; i++) { const item = history[i]; // python's time.time returns seconds, but js Date expects ms const currentItemDate = new Date(item.time * 1000); getSessionNode(currentItemDate).appendChild(makeHistoryRow( item.url, item.title, currentItemDate.toLocaleTimeString() )); lastItemDate = currentItemDate; if (item.time === nextTime) { nextOffset++; } } } /** * Load new history. * @return {void} */ function loadHistory() { let url = DATA_URL.concat("?offset=", nextOffset.toString()); if (nextTime === null) { getJSON(url, receiveHistory); } else { url = url.concat("&start_time=", nextTime.toString()); getJSON(url, receiveHistory); } } return loadHistory; })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/pac_utils.js0000644000175100017510000001746115102145205022626 0ustar00runnerrunner/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * https://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1998 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Akhil Arora * Tomi Leppikangas * Darin Fisher * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ /* Script for Proxy Auto Config in the new world order. - Gagan Saksena 04/24/00 */ function dnsDomainIs(host, domain) { return (host.length >= domain.length && host.substring(host.length - domain.length) == domain); } function dnsDomainLevels(host) { return host.split('.').length-1; } function convert_addr(ipchars) { var bytes = ipchars.split('.'); var result = ((bytes[0] & 0xff) << 24) | ((bytes[1] & 0xff) << 16) | ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff); return result; } function isInNet(ipaddr, pattern, maskstr) { var test = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ .exec(ipaddr); if (test == null) { ipaddr = dnsResolve(ipaddr); if (ipaddr == null) return false; } else if (test[1] > 255 || test[2] > 255 || test[3] > 255 || test[4] > 255) { return false; // not an IP address } var host = convert_addr(ipaddr); var pat = convert_addr(pattern); var mask = convert_addr(maskstr); return ((host & mask) == (pat & mask)); } function isPlainHostName(host) { return (host.search('\\.') == -1); } function isResolvable(host) { var ip = dnsResolve(host); return (ip != null); } function localHostOrDomainIs(host, hostdom) { return (host == hostdom) || (hostdom.lastIndexOf(host + '.', 0) == 0); } function shExpMatch(url, pattern) { pattern = pattern.replace(/\./g, '\\.'); pattern = pattern.replace(/\*/g, '.*'); pattern = pattern.replace(/\?/g, '.'); var newRe = new RegExp('^'+pattern+'$'); return newRe.test(url); } var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6}; var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11}; function weekdayRange() { function getDay(weekday) { if (weekday in wdays) { return wdays[weekday]; } return -1; } var date = new Date(); var argc = arguments.length; var wday; if (argc < 1) return false; if (arguments[argc - 1] == 'GMT') { argc--; wday = date.getUTCDay(); } else { wday = date.getDay(); } var wd1 = getDay(arguments[0]); var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1; return (wd1 == -1 || wd2 == -1) ? false : (wd1 <= wday && wday <= wd2); } function dateRange() { function getMonth(name) { if (name in months) { return months[name]; } return -1; } var date = new Date(); var argc = arguments.length; if (argc < 1) { return false; } var isGMT = (arguments[argc - 1] == 'GMT'); if (isGMT) { argc--; } // function will work even without explicit handling of this case if (argc == 1) { var tmp = parseInt(arguments[0]); if (isNaN(tmp)) { return ((isGMT ? date.getUTCMonth() : date.getMonth()) == getMonth(arguments[0])); } else if (tmp < 32) { return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp); } else { return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) == tmp); } } var year = date.getFullYear(); var date1, date2; date1 = new Date(year, 0, 1, 0, 0, 0); date2 = new Date(year, 11, 31, 23, 59, 59); var adjustMonth = false; for (var i = 0; i < (argc >> 1); i++) { var tmp = parseInt(arguments[i]); if (isNaN(tmp)) { var mon = getMonth(arguments[i]); date1.setMonth(mon); } else if (tmp < 32) { adjustMonth = (argc <= 2); date1.setDate(tmp); } else { date1.setFullYear(tmp); } } for (var i = (argc >> 1); i < argc; i++) { var tmp = parseInt(arguments[i]); if (isNaN(tmp)) { var mon = getMonth(arguments[i]); date2.setMonth(mon); } else if (tmp < 32) { date2.setDate(tmp); } else { date2.setFullYear(tmp); } } if (adjustMonth) { date1.setMonth(date.getMonth()); date2.setMonth(date.getMonth()); } if (isGMT) { var tmp = date; tmp.setFullYear(date.getUTCFullYear()); tmp.setMonth(date.getUTCMonth()); tmp.setDate(date.getUTCDate()); tmp.setHours(date.getUTCHours()); tmp.setMinutes(date.getUTCMinutes()); tmp.setSeconds(date.getUTCSeconds()); date = tmp; } return ((date1 <= date) && (date <= date2)); } function timeRange() { var argc = arguments.length; var date = new Date(); var isGMT= false; if (argc < 1) { return false; } if (arguments[argc - 1] == 'GMT') { isGMT = true; argc--; } var hour = isGMT ? date.getUTCHours() : date.getHours(); var date1, date2; date1 = new Date(); date2 = new Date(); if (argc == 1) { return (hour == arguments[0]); } else if (argc == 2) { return ((arguments[0] <= hour) && (hour <= arguments[1])); } else { switch (argc) { case 6: date1.setSeconds(arguments[2]); date2.setSeconds(arguments[5]); case 4: var middle = argc >> 1; date1.setHours(arguments[0]); date1.setMinutes(arguments[1]); date2.setHours(arguments[middle]); date2.setMinutes(arguments[middle + 1]); if (middle == 2) { date2.setSeconds(59); } break; default: throw 'timeRange: bad number of arguments'; } } if (isGMT) { date.setFullYear(date.getUTCFullYear()); date.setMonth(date.getUTCMonth()); date.setDate(date.getUTCDate()); date.setHours(date.getUTCHours()); date.setMinutes(date.getUTCMinutes()); date.setSeconds(date.getUTCSeconds()); } return ((date1 <= date) && (date <= date2)); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/pdfjs_polyfills.js0000644000175100017510000000172315102145205024040 0ustar00runnerrunner/* eslint-disable strict */ /* (this file gets used as a snippet) */ /* SPDX-FileCopyrightText: Florian Bruhin (The Compiler) SPDX-License-Identifier: GPL-3.0-or-later */ (function() { // Chromium 119 / QtWebEngine 6.8 // https://caniuse.com/mdn-javascript_builtins_promise_withresolvers if (typeof Promise.withResolvers === "undefined") { Promise.withResolvers = function() { let resolve, reject const promise = new Promise((res, rej) => { resolve = res reject = rej }) return { promise, resolve, reject } } } // Chromium 126 / QtWebEngine 6.9 // https://caniuse.com/mdn-api_url_parse_static if (typeof URL.parse === "undefined") { URL.parse = function(url, base) { try { return new URL(url, base); } catch (ex) { return null; } } } })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/position_caret.js0000644000175100017510000000656615102145205023671 0ustar00runnerrunner// SPDX-FileCopyrightText: Artur Shaik // SPDX-FileCopyrightText: Florian Bruhin (The Compiler) // // SPDX-License-Identifier: GPL-3.0-or-later /** * Snippet to position caret at top of the page when caret mode is enabled. * Some code was borrowed from: * * https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js * https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js */ "use strict"; (function() { // FIXME:qtwebengine integrate this with other window._qutebrowser code? function isElementInViewport(node) { // eslint-disable-line complexity let i; let boundingRect = (node.getClientRects()[0] || node.getBoundingClientRect()); if (boundingRect.width <= 1 && boundingRect.height <= 1) { const rects = node.getClientRects(); for (i = 0; i < rects.length; i++) { if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { boundingRect = rects[i]; } } } if (boundingRect === undefined) { return null; } if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { return null; } if (boundingRect.width <= 1 || boundingRect.height <= 1) { const children = node.children; let visibleChildNode = false; for (i = 0; i < children.length; ++i) { boundingRect = (children[i].getClientRects()[0] || children[i].getBoundingClientRect()); if (boundingRect.width > 1 && boundingRect.height > 1) { visibleChildNode = true; break; } } if (visibleChildNode === false) { return null; } } if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { return null; } const computedStyle = window.getComputedStyle(node, null); if (computedStyle.visibility !== "visible" || computedStyle.display === "none" || node.hasAttribute("disabled") || parseInt(computedStyle.width, 10) === 0 || parseInt(computedStyle.height, 10) === 0) { return null; } return boundingRect.top >= -20; } function positionCaret() { const walker = document.createTreeWalker(document.body, 4, null); let node; const textNodes = []; let el; while ((node = walker.nextNode())) { if (node.nodeType === 3 && node.data.trim() !== "") { textNodes.push(node); } } for (let i = 0; i < textNodes.length; i++) { const element = textNodes[i].parentElement; if (isElementInViewport(element.parentElement)) { el = element; break; } } if (el !== undefined) { const range = document.createRange(); range.setStart(el, 0); range.setEnd(el, 0); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } } positionCaret(); })(); ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1762183912.537639 qutebrowser-3.6.1/qutebrowser/javascript/quirks/0000755000175100017510000000000015102145351021614 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/quirks/array_at.user.js0000644000175100017510000000374615102145205024741 0ustar00runnerrunner// Based on: https://github.com/tc39/proposal-relative-indexing-method#polyfill /* Copyright (c) 2020 Tab Atkins Jr. 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. */ /* eslint-disable no-invalid-this */ "use strict"; (function() { function at(idx) { // ToInteger() abstract op let n = Math.trunc(idx) || 0; // Allow negative indexing from the end if (n < 0) { n += this.length; } // OOB access is guaranteed to return undefined if (n < 0 || n >= this.length) { return undefined; } // Otherwise, this is just normal property access return this[n]; } const TypedArray = Reflect.getPrototypeOf(Int8Array); for (const type of [Array, String, TypedArray]) { Object.defineProperty( type.prototype, "at", { "value": at, "writable": true, "enumerable": false, "configurable": true, } ); } })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/quirks/discord.user.js0000644000175100017510000000045515102145205024560 0ustar00runnerrunner// ==UserScript== // @include https://discord.com/* // ==/UserScript== // Workaround for Discord's silly bot detection (or whatever it is logging // people out with vertical tabs). "use strict"; Object.defineProperty(window, "outerWidth", { get() { return window.innerWidth; }, }); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/quirks/googledocs.user.js0000644000175100017510000000053715102145205025257 0ustar00runnerrunner// ==UserScript== // @include https://docs.google.com/* // ==/UserScript== // Workaround for typing dead keys on Google Docs // See https://bugreports.qt.io/browse/QTBUG-69652 "use strict"; Object.defineProperty(navigator, "userAgent", { get() { return "Mozilla/5.0 (X11; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0"; }, }); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/quirks/string_replaceall.user.js0000644000175100017510000000155215102145205026622 0ustar00runnerrunner/* eslint-disable no-extend-native,no-implicit-globals */ "use strict"; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // Based on: https://vanillajstoolkit.com/polyfills/stringreplaceall/ /** * String.prototype.replaceAll() polyfill * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ * @author Chris Ferdinandi * @license MIT */ if (!String.prototype.replaceAll) { String.prototype.replaceAll = function(str, newStr) { // If a regex pattern if (Object.prototype.toString.call(str) === "[object RegExp]") { return this.replace(str, newStr); } // If a string return this.replace(new RegExp(escapeRegExp(str), "g"), newStr); }; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/quirks/whatsapp_web.user.js0000644000175100017510000000075015102145205025613 0ustar00runnerrunner// ==UserScript== // @include https://web.whatsapp.com/ // ==/UserScript== // Quirk for WhatsApp Web, based on: // https://github.com/jiahaog/nativefier/issues/719#issuecomment-443809630 "use strict"; if (document.querySelector("a[href='https://support.google.com/chrome/answer/95414']")) { navigator.serviceWorker.getRegistration().then((registration) => { if (registration) { registration.unregister(); } document.location.reload(); }); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/scroll.js0000644000175100017510000000323615102145205022134 0ustar00runnerrunner// SPDX-FileCopyrightText: Florian Bruhin (The Compiler) // // SPDX-License-Identifier: GPL-3.0-or-later "use strict"; window._qutebrowser.scroll = (function() { const funcs = {}; funcs.to_perc = (x, y) => { let x_px = window.scrollX; let y_px = window.scrollY; const width = Math.max( document.body.scrollWidth, document.body.offsetWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth ); const height = Math.max( document.body.scrollHeight, document.body.offsetHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight ); if (x !== undefined) { x_px = (width - window.innerWidth) / 100 * x; } if (y !== undefined) { y_px = (height - window.innerHeight) / 100 * y; } /* console.log(JSON.stringify({ "x": x, "window.scrollX": window.scrollX, "window.innerWidth": window.innerWidth, "elem.scrollWidth": document.documentElement.scrollWidth, "x_px": x_px, "y": y, "window.scrollY": window.scrollY, "window.innerHeight": window.innerHeight, "elem.scrollHeight": document.documentElement.scrollHeight, "y_px": y_px, })); */ window.scroll(x_px, y_px); }; funcs.delta_page = (x, y) => { const dx = window.innerWidth * x; const dy = window.innerHeight * y; window.scrollBy(dx, dy); }; return funcs; })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/stylesheet.js0000644000175100017510000001163515102145205023031 0ustar00runnerrunner// SPDX-FileCopyrightText: Ulrik de Muelenaere // SPDX-FileCopyrightText: Florian Bruhin (The Compiler) // // SPDX-License-Identifier: GPL-3.0-or-later "use strict"; window._qutebrowser.stylesheet = (function() { if (window._qutebrowser.stylesheet) { return window._qutebrowser.stylesheet; } const funcs = {}; const xhtml_ns = "http://www.w3.org/1999/xhtml"; const svg_ns = "http://www.w3.org/2000/svg"; let root_elem; let style_elem; let css_content = ""; let root_observer; let initialized = false; // Watch for rewrites of the root element and changes to its children, // then move the stylesheet to the end. Partially inspired by Stylus: // https://github.com/openstyles/stylus/blob/1.1.4.2/content/apply.js#L235-L355 function watch_root() { if (!document.documentElement) { root_observer.observe(document, {"childList": true}); return; } if (root_elem !== document.documentElement) { root_elem = document.documentElement; root_observer.disconnect(); root_observer.observe(document, {"childList": true}); root_observer.observe(root_elem, {"childList": true}); } if (style_elem !== root_elem.lastChild) { root_elem.appendChild(style_elem); } } function create_style() { let ns = xhtml_ns; if (document.documentElement && document.documentElement.namespaceURI === svg_ns) { ns = svg_ns; } style_elem = document.createElementNS(ns, "style"); style_elem.textContent = css_content; root_observer = new MutationObserver(watch_root); watch_root(); } // We should only inject the stylesheet if the document already has style // information associated with it. Otherwise we wait until the browser // rewrites it to an XHTML document showing the document tree. As a // starting point for exploring the relevant code in Chromium, see // https://github.com/qt/qtwebengine-chromium/blob/cfe8c60/chromium/third_party/WebKit/Source/core/xml/parser/XMLDocumentParser.cpp#L1539-L1540 function check_style(node) { const stylesheet = node.nodeType === Node.PROCESSING_INSTRUCTION_NODE && node.target === "xml-stylesheet" && node.parentNode === document; const known_ns = node.nodeType === Node.ELEMENT_NODE && (node.namespaceURI === xhtml_ns || node.namespaceURI === svg_ns); return stylesheet || known_ns; } function init() { initialized = true; // Chromium will not rewrite a document inside a frame, so add the // stylesheet even if the document is unstyled. if (window !== window.top) { create_style(); return; } const iter = document.createNodeIterator(document, NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_ELEMENT); let node; while ((node = iter.nextNode())) { if (check_style(node)) { create_style(); return; } } const style_observer = new MutationObserver((mutations) => { for (const mutation of mutations) { const nodes = mutation.addedNodes; for (let i = 0; i < nodes.length; ++i) { if (check_style(nodes[i])) { create_style(); style_observer.disconnect(); return; } } } }); style_observer.observe(document, {"childList": true, "subtree": true}); } funcs.set_css = function(css) { if (!initialized) { init(); } if (style_elem) { style_elem.textContent = css; // The browser seems to rewrite the document in same-origin frames // without notifying the mutation observer. Ensure that the // stylesheet is in the current document. watch_root(); } else { css_content = css; } // Propagate the new CSS to all child frames. for (let i = 0; i < window.frames.length; ++i) { const frame = window.frames[i]; try { if (frame._qutebrowser && frame._qutebrowser.stylesheet) { frame._qutebrowser.stylesheet.set_css(css); } } catch (exc) { if (exc instanceof DOMException && exc.name === "SecurityError") { // FIXME:qtwebengine This does not work for cross-origin frames. console.log(`Failed to style frame: ${exc.message}`); } else { throw exc; } } } }; return funcs; })(); ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/javascript/webelem.js0000644000175100017510000003156015102145205022257 0ustar00runnerrunner// SPDX-FileCopyrightText: Florian Bruhin (The Compiler) // // SPDX-License-Identifier: GPL-3.0-or-later /** * The connection for web elements between Python and Javascript works like * this: * * - Python calls into Javascript and invokes a function to find elements (one * of the find_* functions). * - Javascript gets the requested element, and calls serialize_elem on it. * - serialize_elem saves the javascript element object in "elements", gets some * attributes from the element, and assigns an ID (index into 'elements') to * it. * - Python gets this information and constructs a Python wrapper object with * the information it got right away, and the ID. * - When Python wants to modify an element, it calls javascript again with the * element ID. * - Javascript gets the element from the elements array, and modifies it. */ "use strict"; window._qutebrowser.webelem = (function() { const funcs = {}; const elements = []; function get_frame_offset(frame) { if (frame === null) { // Dummy object with zero offset return { "top": 0, "right": 0, "bottom": 0, "left": 0, "height": 0, "width": 0, }; } return frame.frameElement.getBoundingClientRect(); } // Add an offset rect to a base rect, for use with frames function add_offset_rect(base, offset) { return { "top": base.top + offset.top, "left": base.left + offset.left, "bottom": base.bottom + offset.top, "right": base.right + offset.left, "height": base.height, "width": base.width, }; } function serialize_elem(elem, frame = null) { if (!elem) { return null; } const id = elements.length; elements[id] = elem; const caret_position = elem.selectionStart; // isContentEditable occasionally returns undefined. const is_content_editable = elem.isContentEditable || false; const out = { "id": id, "rects": [], // Gets filled up later "caret_position": caret_position, "is_content_editable": is_content_editable, }; // Deal with various fun things which can happen in form elements // https://github.com/qutebrowser/qutebrowser/issues/2569 // https://github.com/qutebrowser/qutebrowser/issues/2877 // https://stackoverflow.com/q/22942689/2085149 if (typeof elem.tagName === "string") { out.tag_name = elem.tagName; } else if (typeof elem.nodeName === "string") { out.tag_name = elem.nodeName; } else { out.tag_name = ""; } if (typeof elem.className === "string") { out.class_name = elem.className; } else { // e.g. SVG elements out.class_name = ""; } if (typeof elem.value === "string" || typeof elem.value === "number") { out.value = elem.value; } else { out.value = ""; } if (typeof elem.outerHTML === "string") { out.outer_xml = elem.outerHTML; } else { out.outer_xml = ""; } if (typeof elem.textContent === "string") { out.text = elem.textContent; } else if (typeof elem.text === "string") { out.text = elem.text; } // else: don't add the text at all const attributes = {}; for (let i = 0; i < elem.attributes.length; ++i) { const attr = elem.attributes[i]; attributes[attr.name] = attr.value; } out.attributes = attributes; const client_rects = elem.getClientRects(); const frame_offset_rect = get_frame_offset(frame); for (let k = 0; k < client_rects.length; ++k) { const rect = client_rects[k]; out.rects.push( add_offset_rect(rect, frame_offset_rect) ); } // console.log(JSON.stringify(out)); return out; } function is_hidden_css(elem) { // Check if the element is hidden via CSS const win = elem.ownerDocument.defaultView; const style = win.getComputedStyle(elem, null); const invisible = style.getPropertyValue("visibility") !== "visible"; const none_display = style.getPropertyValue("display") === "none"; const zero_opacity = style.getPropertyValue("opacity") === "0"; const is_framework = ( // ACE editor elem.classList.contains("ace_text-input") || // bootstrap CSS elem.classList.contains("custom-control-input") ); return (invisible || none_display || (zero_opacity && !is_framework)); } function is_visible(elem, frame = null) { // Adopted from vimperator: // https://github.com/vimperator/vimperator-labs/blob/vimperator-3.14.0/common/content/hints.js#L259-L285 // FIXME:qtwebengine we might need something more sophisticated like // the cVim implementation here? // https://github.com/1995eaton/chromium-vim/blob/1.2.85/content_scripts/dom.js#L74-L134 if (is_hidden_css(elem)) { return false; } const offset_rect = get_frame_offset(frame); let rect = add_offset_rect(elem.getBoundingClientRect(), offset_rect); if (!rect || rect.top > window.innerHeight || rect.bottom < 0 || rect.left > window.innerWidth || rect.right < 0) { return false; } rect = elem.getClientRects()[0]; return Boolean(rect); } // Returns true if the iframe is accessible without // cross domain errors, else false. function iframe_same_domain(frame) { try { frame.document; // eslint-disable-line no-unused-expressions return true; } catch (exc) { if (exc instanceof DOMException && exc.name === "SecurityError") { // FIXME:qtwebengine This does not work for cross-origin frames. return false; } throw exc; } } // Recursively finds elements from DOM that have a shadowRoot // and returns the shadow roots in a list function find_shadow_roots(container = document) { const roots = []; for (const elem of container.querySelectorAll("*")) { if (elem.shadowRoot) { roots.push(elem.shadowRoot, ...find_shadow_roots(elem.shadowRoot)); } } return roots; } funcs.find_css = (selector, only_visible) => { // Find all places where we need to look for elements: const containers = [[document, null]]; // Same-domain iframes for (const frame of Array.from(window.frames)) { if (iframe_same_domain(frame)) { containers.push([frame.document, frame]); } } // Open shadow roots for (const root of find_shadow_roots()) { containers.push([root, null]); } // Then find elements in all of them const elems = []; for (const [container, frame] of containers) { try { for (const elem of container.querySelectorAll(selector)) { elems.push([elem, frame]); } } catch (ex) { return {"success": false, "error": ex.toString()}; } } // Finally, filter by visibility const out = []; for (const [elem, frame] of elems) { if (!only_visible || is_visible(elem, frame)) { out.push(serialize_elem(elem, frame)); } } return {"success": true, "result": out}; }; // Runs a function in a frame until the result is not null, then return // If no frame succeeds, return null function run_frames(func) { for (let i = 0; i < window.frames.length; ++i) { const frame = window.frames[i]; if (iframe_same_domain(frame)) { const result = func(frame); if (result) { return result; } } } return null; } funcs.find_id = (id) => { const elem = document.getElementById(id); if (elem) { return serialize_elem(elem); } const serialized_elem = run_frames((frame) => { const element = frame.window.document.getElementById(id); return serialize_elem(element, frame); }); if (serialized_elem) { return serialized_elem; } return null; }; // Check if elem is an iframe, and if so, return the result of func on it. // If no iframes match, return null function call_if_frame(elem, func) { // Check if elem is a frame, and if so, call func on the window if ("contentWindow" in elem) { const frame = elem.contentWindow; if (iframe_same_domain(frame) && "frameElement" in elem.contentWindow) { return func(frame); } } return null; } funcs.find_focused = () => { const elem = document.activeElement; if (!elem || elem === document.body) { // "When there is no selection, the active element is the page's // or null." return null; } // Check if we got an iframe, and if so, recurse inside of it const frame_elem = call_if_frame(elem, (frame) => serialize_elem(frame.document.activeElement, frame)); if (frame_elem !== null) { return frame_elem; } return serialize_elem(elem); }; funcs.find_at_pos = (x, y) => { const elem = document.elementFromPoint(x, y); if (!elem) { return null; } // Check if we got an iframe, and if so, recurse inside of it const frame_elem = call_if_frame(elem, (frame) => { // Subtract offsets due to being in an iframe const frame_offset_rect = frame.frameElement.getBoundingClientRect(); return serialize_elem(frame.document. elementFromPoint(x - frame_offset_rect.left, y - frame_offset_rect.top), frame); }); if (frame_elem !== null) { return frame_elem; } return serialize_elem(elem); }; // Function for returning a selection or focus to python (so we can click // it). If nothing is selected but there is something focused, returns // "focused" funcs.find_selected_focused_link = () => { const elem = window.getSelection().anchorNode; if (elem) { return serialize_elem(elem.parentNode); } const serialized_frame_elem = run_frames((frame) => { const node = frame.window.getSelection().anchorNode; if (node) { return serialize_elem(node.parentNode, frame); } return null; }); if (serialized_frame_elem) { return serialized_frame_elem; } return funcs.find_focused() && "focused"; }; funcs.set_value = (id, value) => { elements[id].value = value; }; funcs.insert_text = (id, text) => { const elem = elements[id]; elem.focus(); document.execCommand("insertText", false, text); }; funcs.dispatch_event = (id, event, bubbles = false, cancelable = false, composed = false) => { const elem = elements[id]; elem.dispatchEvent( new Event(event, {"bubbles": bubbles, "cancelable": cancelable, "composed": composed})); }; funcs.set_attribute = (id, name, value) => { elements[id].setAttribute(name, value); }; funcs.remove_blank_target = (id) => { let elem = elements[id]; while (elem !== null) { const tag = elem.tagName.toLowerCase(); if (tag === "a" || tag === "area") { if (elem.getAttribute("target") === "_blank") { elem.setAttribute("target", "_top"); } break; } elem = elem.parentElement; } }; funcs.click = (id) => { const elem = elements[id]; elem.click(); }; funcs.focus = (id) => { const elem = elements[id]; elem.focus(); }; funcs.move_cursor_to_end = (id) => { const elem = elements[id]; elem.selectionStart = elem.value.length; elem.selectionEnd = elem.value.length; }; funcs.delete = (id) => { const elem = elements[id]; elem.remove(); }; return funcs; })(); ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5386388 qutebrowser-3.6.1/qutebrowser/keyinput/0000755000175100017510000000000015102145351020000 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/keyinput/__init__.py0000644000175100017510000000027115102145205022107 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Modules related to keyboard input and mode handling.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/keyinput/basekeyparser.py0000644000175100017510000003323715102145205023220 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Base class for vim-like key sequence parser.""" import string import types import dataclasses import traceback from typing import Optional from collections.abc import Mapping, MutableMapping, Sequence from qutebrowser.qt.core import QObject, pyqtSignal from qutebrowser.qt.gui import QKeySequence, QKeyEvent from qutebrowser.config import config from qutebrowser.utils import log, usertypes, utils, message from qutebrowser.keyinput import keyutils @dataclasses.dataclass(frozen=True) class MatchResult: """The result of matching a keybinding.""" match_type: QKeySequence.SequenceMatch command: Optional[str] sequence: keyutils.KeySequence def __post_init__(self) -> None: if self.match_type == QKeySequence.SequenceMatch.ExactMatch: assert self.command is not None else: assert self.command is None class BindingTrie: """Helper class for key parser. Represents a set of bindings. Every BindingTree will either contain children or a command (for leaf nodes). The only exception is the root BindingNode, if there are no bindings at all. From the outside, this class works similar to a mapping of keyutils.KeySequence to str. Doing trie[sequence] = 'command' adds a binding, and so does calling .update() with a mapping. Additionally, a "matches" method can be used to do partial matching. However, some mapping methods are not (yet) implemented: - __getitem__ (use matches() instead) - __len__ - __iter__ - __delitem__ Attributes: children: A mapping from KeyInfo to children BindingTries. command: Command associated with this trie node. """ __slots__ = 'children', 'command' def __init__(self) -> None: self.children: MutableMapping[keyutils.KeyInfo, BindingTrie] = {} self.command: Optional[str] = None def __setitem__(self, sequence: keyutils.KeySequence, command: str) -> None: node = self for key in sequence: if key not in node.children: node.children[key] = BindingTrie() node = node.children[key] node.command = command def __contains__(self, sequence: keyutils.KeySequence) -> bool: return self.matches(sequence).match_type == QKeySequence.SequenceMatch.ExactMatch def __repr__(self) -> str: return utils.get_repr(self, children=self.children, command=self.command) def __str__(self) -> str: return '\n'.join(self.string_lines(blank=True)) def string_lines(self, indent: int = 0, blank: bool = False) -> Sequence[str]: """Get a list of strings for a pretty-printed version of this trie.""" lines = [] if self.command is not None: lines.append('{}=> {}'.format(' ' * indent, self.command)) for key, child in sorted(self.children.items()): lines.append('{}{}:'.format(' ' * indent, key)) lines.extend(child.string_lines(indent=indent+1)) if blank: lines.append('') return lines def update(self, mapping: Mapping[keyutils.KeySequence, str]) -> None: """Add data from the given mapping to the trie.""" for key in mapping: self[key] = mapping[key] def matches(self, sequence: keyutils.KeySequence) -> MatchResult: """Try to match a given keystring with any bound keychain. Args: sequence: The key sequence to match. Return: A MatchResult object. """ node = self for key in sequence: try: node = node.children[key] except KeyError: return MatchResult(match_type=QKeySequence.SequenceMatch.NoMatch, command=None, sequence=sequence) if node.command is not None: return MatchResult(match_type=QKeySequence.SequenceMatch.ExactMatch, command=node.command, sequence=sequence) elif node.children: return MatchResult(match_type=QKeySequence.SequenceMatch.PartialMatch, command=None, sequence=sequence) else: # This can only happen when there are no bindings at all. return MatchResult(match_type=QKeySequence.SequenceMatch.NoMatch, command=None, sequence=sequence) class BaseKeyParser(QObject): """Parser for vim-like key sequences and shortcuts. Not intended to be instantiated directly. Subclasses have to override execute() to do whatever they want to. Attributes: mode_name: The name of the mode in the config. bindings: Bound key bindings _mode: The usertypes.KeyMode associated with this keyparser. _win_id: The window ID this keyparser is associated with. _sequence: The currently entered key sequence _do_log: Whether to log keypresses or not. passthrough: Whether unbound keys should be passed through with this handler. _supports_count: Whether count is supported. Signals: keystring_updated: Emitted when the keystring is updated. arg: New keystring. request_leave: Emitted to request leaving a mode. arg 0: Mode to leave. arg 1: Reason for leaving. arg 2: Ignore the request if we're not in that mode """ keystring_updated = pyqtSignal(str) request_leave = pyqtSignal(usertypes.KeyMode, str, bool) def __init__(self, *, mode: usertypes.KeyMode, win_id: int, parent: QObject = None, do_log: bool = True, passthrough: bool = False, supports_count: bool = True) -> None: super().__init__(parent) self._win_id = win_id self._sequence = keyutils.KeySequence() self._count = '' self._mode = mode self._do_log = do_log self.passthrough = passthrough self._supports_count = supports_count self.bindings = BindingTrie() self._read_config() config.instance.changed.connect(self._on_config_changed) def __repr__(self) -> str: return utils.get_repr(self, mode=self._mode, win_id=self._win_id, do_log=self._do_log, passthrough=self.passthrough, supports_count=self._supports_count) def _debug_log(self, msg: str) -> None: """Log a message to the debug log if logging is active. Args: message: The message to log. """ if self._do_log: prefix = '{} for mode {}: '.format(self.__class__.__name__, self._mode.name) log.keyboard.debug(prefix + msg) def _match_key(self, sequence: keyutils.KeySequence) -> MatchResult: """Try to match a given keystring with any bound keychain. Args: sequence: The command string to find. Return: A tuple (matchtype, binding). matchtype: Match.definitive, Match.partial or Match.none. binding: - None with Match.partial/Match.none. - The found binding with Match.definitive. """ assert sequence return self.bindings.matches(sequence) def _match_without_modifiers( self, sequence: keyutils.KeySequence) -> MatchResult: """Try to match a key with optional modifiers stripped.""" self._debug_log("Trying match without modifiers") sequence = sequence.strip_modifiers() return self._match_key(sequence) def _match_key_mapping( self, sequence: keyutils.KeySequence) -> MatchResult: """Try to match a key in bindings.key_mappings.""" self._debug_log("Trying match with key_mappings") mapped = sequence.with_mappings( types.MappingProxyType(config.cache['bindings.key_mappings'])) if sequence != mapped: self._debug_log("Mapped {} -> {}".format( sequence, mapped)) return self._match_key(mapped) return MatchResult(match_type=QKeySequence.SequenceMatch.NoMatch, command=None, sequence=sequence) def _match_count(self, sequence: keyutils.KeySequence, dry_run: bool) -> bool: """Try to match a key as count.""" if not config.val.input.match_counts: return False txt = str(sequence[-1]) # To account for sequences changed above. if (txt in string.digits and self._supports_count and not (not self._count and txt == '0')): self._debug_log("Trying match as count") assert len(txt) == 1, txt if not dry_run: self._count += txt self.keystring_updated.emit(self._count + str(self._sequence)) return True return False def handle(self, e: QKeyEvent, *, dry_run: bool = False) -> QKeySequence.SequenceMatch: """Handle a new keypress. Separate the keypress into count/command, then check if it matches any possible command, and either run the command, ignore it, or display an error. Args: e: the KeyPressEvent from Qt. dry_run: Don't actually execute anything, only check whether there would be a match. Return: A QKeySequence match. """ try: info = keyutils.KeyInfo.from_event(e) except keyutils.InvalidKeyError as ex: # See https://github.com/qutebrowser/qutebrowser/issues/7047 log.keyboard.debug(f"Got invalid key: {ex}") self.clear_keystring() return QKeySequence.SequenceMatch.NoMatch self._debug_log(f"Got key: {info!r} (dry_run {dry_run})") if info.is_modifier_key(): self._debug_log("Ignoring, only modifier") return QKeySequence.SequenceMatch.NoMatch try: sequence = self._sequence.append_event(e) except keyutils.KeyParseError as ex: self._debug_log("{} Aborting keychain.".format(ex)) self.clear_keystring() return QKeySequence.SequenceMatch.NoMatch result = self._match_key(sequence) del sequence # Enforce code below to use the modified result.sequence if result.match_type == QKeySequence.SequenceMatch.NoMatch: result = self._match_without_modifiers(result.sequence) if result.match_type == QKeySequence.SequenceMatch.NoMatch: result = self._match_key_mapping(result.sequence) if result.match_type == QKeySequence.SequenceMatch.NoMatch: was_count = self._match_count(result.sequence, dry_run) if was_count: return QKeySequence.SequenceMatch.ExactMatch if dry_run: return result.match_type self._sequence = result.sequence self._handle_result(info, result) return result.match_type def _handle_result(self, info: keyutils.KeyInfo, result: MatchResult) -> None: """Handle a final MatchResult from handle().""" if result.match_type == QKeySequence.SequenceMatch.ExactMatch: assert result.command is not None self._debug_log("Definitive match for '{}'.".format( result.sequence)) try: count = int(self._count) if self._count else None except ValueError as err: message.error(f"Failed to parse count: {err}", stack=traceback.format_exc()) self.clear_keystring() return self.clear_keystring() self.execute(result.command, count) elif result.match_type == QKeySequence.SequenceMatch.PartialMatch: self._debug_log("No match for '{}' (added {})".format( result.sequence, info)) self.keystring_updated.emit(self._count + str(result.sequence)) elif result.match_type == QKeySequence.SequenceMatch.NoMatch: self._debug_log("Giving up with '{}', no matches".format( result.sequence)) self.clear_keystring() else: raise utils.Unreachable("Invalid match value {!r}".format( result.match_type)) @config.change_filter('bindings') def _on_config_changed(self) -> None: self._read_config() def _read_config(self) -> None: """Read the configuration.""" self.bindings = BindingTrie() config_bindings = config.key_instance.get_bindings_for(self._mode.name) for key, cmd in config_bindings.items(): assert cmd self.bindings[key] = cmd def execute(self, cmdstr: str, count: int = None) -> None: """Handle a completed keychain. Args: cmdstr: The command to execute as a string. count: The count if given. """ raise NotImplementedError def clear_keystring(self) -> None: """Clear the currently entered key sequence.""" if self._sequence: self._debug_log("Clearing keystring (was: {}).".format( self._sequence)) self._sequence = keyutils.KeySequence() self._count = '' self.keystring_updated.emit('') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/keyinput/eventfilter.py0000644000175100017510000001014115102145205022674 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Global Qt event filter which dispatches key events.""" from typing import cast, Optional from qutebrowser.qt.core import pyqtSlot, QObject, QEvent, qVersion from qutebrowser.qt.gui import QKeyEvent, QWindow from qutebrowser.keyinput import modeman from qutebrowser.misc import quitter, objects from qutebrowser.utils import objreg, debug, log, qtutils class EventFilter(QObject): """Global Qt event filter. Attributes: _activated: Whether the EventFilter is currently active. _handlers: A {QEvent.Type: callable} dict with the handlers for an event. """ def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._activated = True self._handlers = { QEvent.Type.KeyPress: self._handle_key_event, QEvent.Type.KeyRelease: self._handle_key_event, QEvent.Type.ShortcutOverride: self._handle_key_event, } self._log_qt_events = "log-qt-events" in objects.debug_flags def install(self) -> None: objects.qapp.installEventFilter(self) @pyqtSlot() def shutdown(self) -> None: objects.qapp.removeEventFilter(self) def _handle_key_event(self, event: QKeyEvent) -> bool: """Handle a key press/release event. Args: event: The QEvent which is about to be delivered. Return: True if the event should be filtered, False if it's passed through. """ active_window = objects.qapp.activeWindow() if active_window not in objreg.window_registry.values(): # Some other window (print dialog, etc.) is focused so we pass the # event through. return False try: man = modeman.instance('current') return man.handle_event(event) except objreg.RegistryUnavailableError: # No window available yet, or not a MainWindow return False def eventFilter(self, obj: Optional[QObject], event: Optional[QEvent]) -> bool: """Handle an event. Args: obj: The object which will get the event. event: The QEvent which is about to be delivered. Return: True if the event should be filtered, False if it's passed through. """ assert event is not None ev_type = event.type() if self._log_qt_events: try: source = repr(obj) except AttributeError: # might not be fully initialized yet source = type(obj).__name__ ev_type_str = debug.qenum_key(QEvent, ev_type) log.misc.debug(f"{source} got event: {ev_type_str}") if ( ev_type == QEvent.Type.DragEnter and qtutils.is_wayland() and qVersion() == "6.5.2" ): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-115757 # Fixed in Qt 6.5.3 # Can't do this via self._handlers since handling it for QWindow # seems to be too late. log.mouse.warning("Ignoring drag event to prevent Qt crash") event.ignore() return True if not isinstance(obj, QWindow): # We already handled this same event at some point earlier, so # we're not interested in it anymore. return False if ev_type not in self._handlers: return False if not self._activated: return False handler = self._handlers[ev_type] try: return handler(cast(QKeyEvent, event)) except: # If there is an exception in here and we leave the eventfilter # activated, we'll get an infinite loop and a stack overflow. self._activated = False raise def init() -> None: """Initialize the global EventFilter instance.""" event_filter = EventFilter(parent=objects.qapp) event_filter.install() quitter.instance.shutting_down.connect(event_filter.shutdown) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/keyinput/keyutils.py0000644000175100017510000006272215102145205022232 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Our own QKeySequence-like class and related utilities. Note that Qt's type safety (or rather, lack thereof) is somewhat scary when it comes to keys/modifiers. Many places (such as QKeyEvent::key()) don't actually return a Qt::Key, they return an int. To make things worse, when talking about a "key", sometimes Qt means a Qt::Key member. However, sometimes it means a Qt::Key member ORed with a Qt.KeyboardModifier... Because of that, _assert_plain_key() and _assert_plain_modifier() make sure we handle what we actually think we do. """ import itertools import dataclasses from typing import Optional, Union, overload, cast from collections.abc import Iterator, Iterable, Mapping from qutebrowser.qt import machinery from qutebrowser.qt.core import Qt, QEvent from qutebrowser.qt.gui import QKeySequence, QKeyEvent if machinery.IS_QT6: from qutebrowser.qt.core import QKeyCombination else: QKeyCombination: None = None # QKeyCombination was added in Qt 6 from qutebrowser.utils import utils, qtutils, debug class InvalidKeyError(Exception): """Raised when a key can't be represented by PyQt. WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html Should be fixed in PyQt 6.3.1 (or 6.4.0?). """ # Map Qt::Key values to their Qt::KeyboardModifier value. _MODIFIER_MAP = { Qt.Key.Key_Shift: Qt.KeyboardModifier.ShiftModifier, Qt.Key.Key_Control: Qt.KeyboardModifier.ControlModifier, Qt.Key.Key_Alt: Qt.KeyboardModifier.AltModifier, Qt.Key.Key_Meta: Qt.KeyboardModifier.MetaModifier, Qt.Key.Key_AltGr: Qt.KeyboardModifier.GroupSwitchModifier, Qt.Key.Key_Mode_switch: Qt.KeyboardModifier.GroupSwitchModifier, } try: _NIL_KEY: Union[Qt.Key, int] = Qt.Key(0) except ValueError: # WORKAROUND for # https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html _NIL_KEY = 0 if machinery.IS_QT6: _KeyInfoType = QKeyCombination _ModifierType = Qt.KeyboardModifier else: _KeyInfoType = int _ModifierType = Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] _SPECIAL_NAMES = { # Some keys handled in a weird way by QKeySequence::toString. # See https://bugreports.qt.io/browse/QTBUG-40030 # Most are unlikely to be ever needed, but you never know ;) # For dead/combining keys, we return the corresponding non-combining # key, as that's easier to add to the config. Qt.Key.Key_Super_L: 'Super L', Qt.Key.Key_Super_R: 'Super R', Qt.Key.Key_Hyper_L: 'Hyper L', Qt.Key.Key_Hyper_R: 'Hyper R', Qt.Key.Key_Direction_L: 'Direction L', Qt.Key.Key_Direction_R: 'Direction R', Qt.Key.Key_Shift: 'Shift', Qt.Key.Key_Control: 'Control', Qt.Key.Key_Meta: 'Meta', Qt.Key.Key_Alt: 'Alt', Qt.Key.Key_AltGr: 'AltGr', Qt.Key.Key_Multi_key: 'Multi key', Qt.Key.Key_SingleCandidate: 'Single Candidate', Qt.Key.Key_Mode_switch: 'Mode switch', Qt.Key.Key_Dead_Grave: '`', Qt.Key.Key_Dead_Acute: '´', Qt.Key.Key_Dead_Circumflex: '^', Qt.Key.Key_Dead_Tilde: '~', Qt.Key.Key_Dead_Macron: '¯', Qt.Key.Key_Dead_Breve: '˘', Qt.Key.Key_Dead_Abovedot: '˙', Qt.Key.Key_Dead_Diaeresis: '¨', Qt.Key.Key_Dead_Abovering: '˚', Qt.Key.Key_Dead_Doubleacute: '˝', Qt.Key.Key_Dead_Caron: 'ˇ', Qt.Key.Key_Dead_Cedilla: '¸', Qt.Key.Key_Dead_Ogonek: '˛', Qt.Key.Key_Dead_Iota: 'Iota', Qt.Key.Key_Dead_Voiced_Sound: 'Voiced Sound', Qt.Key.Key_Dead_Semivoiced_Sound: 'Semivoiced Sound', Qt.Key.Key_Dead_Belowdot: 'Belowdot', Qt.Key.Key_Dead_Hook: 'Hook', Qt.Key.Key_Dead_Horn: 'Horn', Qt.Key.Key_Dead_Stroke: '\u0335', # '̵' Qt.Key.Key_Dead_Abovecomma: '\u0313', # '̓' Qt.Key.Key_Dead_Abovereversedcomma: '\u0314', # '̔' Qt.Key.Key_Dead_Doublegrave: '\u030f', # '̏' Qt.Key.Key_Dead_Belowring: '\u0325', # '̥' Qt.Key.Key_Dead_Belowmacron: '\u0331', # '̱' Qt.Key.Key_Dead_Belowcircumflex: '\u032d', # '̭' Qt.Key.Key_Dead_Belowtilde: '\u0330', # '̰' Qt.Key.Key_Dead_Belowbreve: '\u032e', # '̮' Qt.Key.Key_Dead_Belowdiaeresis: '\u0324', # '̤' Qt.Key.Key_Dead_Invertedbreve: '\u0311', # '̑' Qt.Key.Key_Dead_Belowcomma: '\u0326', # '̦' Qt.Key.Key_Dead_Currency: '¤', Qt.Key.Key_Dead_a: 'a', Qt.Key.Key_Dead_A: 'A', Qt.Key.Key_Dead_e: 'e', Qt.Key.Key_Dead_E: 'E', Qt.Key.Key_Dead_i: 'i', Qt.Key.Key_Dead_I: 'I', Qt.Key.Key_Dead_o: 'o', Qt.Key.Key_Dead_O: 'O', Qt.Key.Key_Dead_u: 'u', Qt.Key.Key_Dead_U: 'U', Qt.Key.Key_Dead_Small_Schwa: 'ə', Qt.Key.Key_Dead_Capital_Schwa: 'Ə', Qt.Key.Key_Dead_Greek: 'Greek', Qt.Key.Key_Dead_Lowline: '\u0332', # '̲' Qt.Key.Key_Dead_Aboveverticalline: '\u030d', # '̍' Qt.Key.Key_Dead_Belowverticalline: '\u0329', Qt.Key.Key_Dead_Longsolidusoverlay: '\u0338', # '̸' Qt.Key.Key_MediaLast: 'Media Last', Qt.Key.Key_unknown: 'Unknown', # For some keys, we just want a different name Qt.Key.Key_Escape: 'Escape', _NIL_KEY: 'nil', } def _assert_plain_key(key: Qt.Key) -> None: """Make sure this is a key without KeyboardModifier mixed in.""" key_int = qtutils.extract_enum_val(key) mask = qtutils.extract_enum_val(Qt.KeyboardModifier.KeyboardModifierMask) assert not key_int & mask, hex(key_int) def _assert_plain_modifier(key: _ModifierType) -> None: """Make sure this is a modifier without a key mixed in.""" key_int = qtutils.extract_enum_val(key) mask = qtutils.extract_enum_val(Qt.KeyboardModifier.KeyboardModifierMask) assert not key_int & ~mask, hex(key_int) def _is_printable(key: Qt.Key) -> bool: _assert_plain_key(key) return key <= 0xff and key not in [Qt.Key.Key_Space, _NIL_KEY] def _is_surrogate(key: Qt.Key) -> bool: """Check if a codepoint is a UTF-16 surrogate. UTF-16 surrogates are a reserved range of Unicode from 0xd800 to 0xd8ff, used to encode Unicode codepoints above the BMP (Base Multilingual Plane). """ _assert_plain_key(key) return 0xd800 <= key <= 0xdfff def _remap_unicode(key: Qt.Key, text: str) -> Qt.Key: """Work around QtKeyEvent's bad values for high codepoints. QKeyEvent handles higher unicode codepoints poorly. It uses UTF-16 to handle key events, and for higher codepoints that require UTF-16 surrogates (e.g. emoji and some CJK characters), it sets the keycode to just the upper half of the surrogate, which renders it useless, and breaks UTF-8 encoding, causing crashes. So we detect this case, and reassign the key code to be the full Unicode codepoint, which we can recover from the text() property, which has the full character. This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72776. """ _assert_plain_key(key) if _is_surrogate(key): if len(text) != 1: raise KeyParseError(text, "Expected 1 character for surrogate, " "but got {}!".format(len(text))) return Qt.Key(ord(text[0])) return key def _check_valid_utf8(s: str, data: Union[Qt.Key, _ModifierType]) -> None: """Make sure the given string is valid UTF-8. Makes sure there are no chars where Qt did fall back to weird UTF-16 surrogates. """ try: s.encode('utf-8') except UnicodeEncodeError as e: # pragma: no cover i = qtutils.extract_enum_val(data) raise ValueError(f"Invalid encoding in 0x{i:x} -> {s}: {e}") def _key_to_string(key: Qt.Key) -> str: """Convert a Qt::Key member to a meaningful name. Args: key: A Qt::Key member. Return: A name of the key as a string. """ _assert_plain_key(key) if key in _SPECIAL_NAMES: return _SPECIAL_NAMES[key] result = QKeySequence(key).toString() _check_valid_utf8(result, key) return result def _modifiers_to_string(modifiers: _ModifierType) -> str: """Convert the given Qt::KeyboardModifier to a string. Handles Qt.KeyboardModifier.GroupSwitchModifier because Qt doesn't handle that as a modifier. """ _assert_plain_modifier(modifiers) altgr = Qt.KeyboardModifier.GroupSwitchModifier if modifiers & altgr: modifiers = _unset_modifier_bits(modifiers, altgr) result = 'AltGr+' else: result = '' result += QKeySequence(qtutils.extract_enum_val(modifiers)).toString() _check_valid_utf8(result, modifiers) return result class KeyParseError(Exception): """Raised by _parse_single_key/parse_keystring on parse errors.""" def __init__(self, keystr: Optional[str], error: str) -> None: if keystr is None: msg = "Could not parse keystring: {}".format(error) else: msg = "Could not parse {!r}: {}".format(keystr, error) super().__init__(msg) def _parse_keystring(keystr: str) -> Iterator[str]: key = '' special = False for c in keystr: if c == '>': if special: yield _parse_special_key(key) key = '' special = False else: yield '>' assert not key, key elif c == '<': special = True elif special: key += c else: yield _parse_single_key(c) if special: yield '<' for c in key: yield _parse_single_key(c) def _parse_special_key(keystr: str) -> str: """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. Args: keystr: The key combination as a string. Return: The normalized keystring. """ keystr = keystr.lower() replacements = ( ('control', 'ctrl'), ('windows', 'meta'), ('mod4', 'meta'), ('command', 'meta'), ('cmd', 'meta'), ('super', 'meta'), ('mod1', 'alt'), ('less', '<'), ('greater', '>'), ) for (orig, repl) in replacements: keystr = keystr.replace(orig, repl) for mod in ['ctrl', 'meta', 'alt', 'shift', 'num']: keystr = keystr.replace(mod + '-', mod + '+') return keystr def _parse_single_key(keystr: str) -> str: """Get a keystring for QKeySequence for a single key.""" return 'Shift+' + keystr if keystr.isupper() else keystr def _unset_modifier_bits( modifiers: _ModifierType, mask: _ModifierType ) -> _ModifierType: """Unset all bits in modifiers which are given in mask. Equivalent to modifiers & ~mask, but with a WORKAROUND with PyQt 6, for a bug in Python 3.11.4 where that isn't possible with an enum.Flag...: https://github.com/python/cpython/issues/105497 """ if machinery.IS_QT5: return Qt.KeyboardModifiers(modifiers & ~mask) # can lose type if it's 0 else: return Qt.KeyboardModifier(modifiers.value & ~mask.value) @dataclasses.dataclass(frozen=True, order=True) class KeyInfo: """A key with optional modifiers. Attributes: key: A Qt::Key member. modifiers: A Qt::KeyboardModifier enum value. """ key: Qt.Key modifiers: _ModifierType = Qt.KeyboardModifier.NoModifier def __post_init__(self) -> None: """Run some validation on the key/modifier values.""" # This changed with Qt 6, and e.g. to_qt() relies on this. if machinery.IS_QT5: modifier_classes = (Qt.KeyboardModifier, Qt.KeyboardModifiers) elif machinery.IS_QT6: modifier_classes = Qt.KeyboardModifier else: raise utils.Unreachable() assert isinstance(self.key, Qt.Key), self.key assert isinstance(self.modifiers, modifier_classes), self.modifiers _assert_plain_key(self.key) _assert_plain_modifier(self.modifiers) def __repr__(self) -> str: return utils.get_repr( self, key=debug.qenum_key(Qt, self.key, klass=Qt.Key), modifiers=debug.qflags_key(Qt, self.modifiers, klass=Qt.KeyboardModifier), text=str(self), ) @classmethod def from_event(cls, e: QKeyEvent) -> 'KeyInfo': """Get a KeyInfo object from a QKeyEvent. This makes sure that key/modifiers are never mixed and also remaps UTF-16 surrogates to work around QTBUG-72776. """ try: key = Qt.Key(e.key()) except ValueError as ex: raise InvalidKeyError(str(ex)) key = _remap_unicode(key, e.text()) modifiers = e.modifiers() return cls(key, modifiers) @classmethod def from_qt(cls, combination: _KeyInfoType) -> 'KeyInfo': """Construct a KeyInfo from a Qt5-style int or Qt6-style QKeyCombination.""" if machinery.IS_QT5: assert isinstance(combination, int) key = Qt.Key( int(combination) & ~Qt.KeyboardModifier.KeyboardModifierMask) modifiers = Qt.KeyboardModifier( int(combination) & Qt.KeyboardModifier.KeyboardModifierMask) return cls(key, modifiers) else: # QKeyCombination is now guaranteed to be available here assert isinstance(combination, QKeyCombination) try: key = combination.key() except ValueError as e: raise InvalidKeyError(str(e)) return cls( key=key, modifiers=combination.keyboardModifiers(), ) def __str__(self) -> str: """Convert this KeyInfo to a meaningful name. Return: A name of the key (combination) as a string. """ key_string = _key_to_string(self.key) modifiers = self.modifiers if self.key in _MODIFIER_MAP: # Don't return e.g. modifiers = _unset_modifier_bits(modifiers, _MODIFIER_MAP[self.key]) elif _is_printable(self.key): # "normal" binding if not key_string: # pragma: no cover raise ValueError("Got empty string for key 0x{:x}!" .format(self.key)) assert len(key_string) == 1, key_string if self.modifiers == Qt.KeyboardModifier.ShiftModifier: assert not self.is_special() return key_string.upper() elif self.modifiers == Qt.KeyboardModifier.NoModifier: assert not self.is_special() return key_string.lower() else: # Use special binding syntax, but instead of key_string = key_string.lower() modifiers = Qt.KeyboardModifier(modifiers) # "special" binding assert self.is_special() modifier_string = _modifiers_to_string(modifiers) return '<{}{}>'.format(modifier_string, key_string) def text(self) -> str: """Get the text which would be displayed when pressing this key.""" control = { Qt.Key.Key_Space: ' ', Qt.Key.Key_Tab: '\t', Qt.Key.Key_Backspace: '\b', Qt.Key.Key_Return: '\r', Qt.Key.Key_Enter: '\r', Qt.Key.Key_Escape: '\x1b', } if self.key in control: return control[self.key] elif not _is_printable(self.key): return '' text = QKeySequence(self.key).toString() if not self.modifiers & Qt.KeyboardModifier.ShiftModifier: text = text.lower() return text def to_event(self, typ: QEvent.Type = QEvent.Type.KeyPress) -> QKeyEvent: """Get a QKeyEvent from this KeyInfo.""" return QKeyEvent(typ, self.key, self.modifiers, self.text()) def to_qt(self) -> _KeyInfoType: """Get something suitable for a QKeySequence.""" if machinery.IS_QT5: return int(self.key) | int(self.modifiers) else: return QKeyCombination(self.modifiers, self.key) def with_stripped_modifiers(self, modifiers: Qt.KeyboardModifier) -> "KeyInfo": mods = _unset_modifier_bits(self.modifiers, modifiers) return KeyInfo(key=self.key, modifiers=mods) def is_special(self) -> bool: """Check whether this key requires special key syntax.""" return not ( _is_printable(self.key) and self.modifiers in [ Qt.KeyboardModifier.ShiftModifier, Qt.KeyboardModifier.NoModifier, ] ) def is_modifier_key(self) -> bool: """Test whether the given key is a modifier. This only considers keys which are part of Qt::KeyboardModifier, i.e. which would interrupt a key chain like "yY" when handled. """ return self.key in _MODIFIER_MAP class KeySequence: """A sequence of key presses. This internally uses chained QKeySequence objects and exposes a nicer interface over it. NOTE: While private members of this class are in theory mutable, they must not be mutated in order to ensure consistent hashing. Attributes: _sequences: A list of QKeySequence Class attributes: _MAX_LEN: The maximum amount of keys in a QKeySequence. """ _MAX_LEN = 4 def __init__(self, *keys: KeyInfo) -> None: self._sequences: list[QKeySequence] = [] for sub in utils.chunk(keys, self._MAX_LEN): try: args = [info.to_qt() for info in sub] except InvalidKeyError as e: raise KeyParseError(keystr=None, error=f"Got invalid key: {e}") sequence = QKeySequence(*args) self._sequences.append(sequence) if keys: assert self self._validate() def __str__(self) -> str: parts = [] for info in self: parts.append(str(info)) return ''.join(parts) def __iter__(self) -> Iterator[KeyInfo]: """Iterate over KeyInfo objects.""" # FIXME:mypy Stubs seem to be unaware that iterating a QKeySequence produces # _KeyInfoType sequences = cast(list[Iterable[_KeyInfoType]], self._sequences) for combination in itertools.chain.from_iterable(sequences): yield KeyInfo.from_qt(combination) def __repr__(self) -> str: return utils.get_repr(self, keys=str(self)) def __lt__(self, other: 'KeySequence') -> bool: return self._sequences < other._sequences def __gt__(self, other: 'KeySequence') -> bool: return self._sequences > other._sequences def __le__(self, other: 'KeySequence') -> bool: return self._sequences <= other._sequences def __ge__(self, other: 'KeySequence') -> bool: return self._sequences >= other._sequences def __eq__(self, other: object) -> bool: if not isinstance(other, KeySequence): return NotImplemented return self._sequences == other._sequences def __ne__(self, other: object) -> bool: if not isinstance(other, KeySequence): return NotImplemented return self._sequences != other._sequences def __hash__(self) -> int: return hash(tuple(self._sequences)) def __len__(self) -> int: return sum(len(seq) for seq in self._sequences) def __bool__(self) -> bool: return bool(self._sequences) @overload def __getitem__(self, item: int) -> KeyInfo: ... @overload def __getitem__(self, item: slice) -> 'KeySequence': ... def __getitem__(self, item: Union[int, slice]) -> Union[KeyInfo, 'KeySequence']: infos = list(self) if isinstance(item, slice): return self.__class__(*infos[item]) else: return infos[item] def _validate(self, keystr: str = None) -> None: try: for info in self: if info.key < Qt.Key.Key_Space or info.key >= Qt.Key.Key_unknown: raise KeyParseError(keystr, "Got invalid key!") except InvalidKeyError as e: raise KeyParseError(keystr, f"Got invalid key: {e}") for seq in self._sequences: if not seq: raise KeyParseError(keystr, "Got invalid key!") def matches(self, other: 'KeySequence') -> QKeySequence.SequenceMatch: """Check whether the given KeySequence matches with this one. We store multiple QKeySequences with <= 4 keys each, so we need to match those pair-wise, and account for an unequal amount of sequences as well. """ # pylint: disable=protected-access if len(self._sequences) > len(other._sequences): # If we entered more sequences than there are in the config, # there's no way there can be a match. return QKeySequence.SequenceMatch.NoMatch for entered, configured in zip(self._sequences, other._sequences): # If we get NoMatch/PartialMatch in a sequence, we can abort there. match = entered.matches(configured) if match != QKeySequence.SequenceMatch.ExactMatch: return match # We checked all common sequences and they had an ExactMatch. # # If there's still more sequences configured than entered, that's a # PartialMatch, as more keypresses can still follow and new sequences # will appear which we didn't check above. # # If there's the same amount of sequences configured and entered, # that's an EqualMatch. if len(self._sequences) == len(other._sequences): return QKeySequence.SequenceMatch.ExactMatch elif len(self._sequences) < len(other._sequences): return QKeySequence.SequenceMatch.PartialMatch else: raise utils.Unreachable("self={!r} other={!r}".format(self, other)) def append_event(self, ev: QKeyEvent) -> 'KeySequence': """Create a new KeySequence object with the given QKeyEvent added.""" try: key = Qt.Key(ev.key()) except ValueError as e: raise KeyParseError(None, f"Got invalid key: {e}") _assert_plain_key(key) _assert_plain_modifier(ev.modifiers()) key = _remap_unicode(key, ev.text()) modifiers: _ModifierType = ev.modifiers() if key == _NIL_KEY: raise KeyParseError(None, "Got nil key!") # We always remove Qt.KeyboardModifier.GroupSwitchModifier because QKeySequence has no # way to mention that in a binding anyways... modifiers = _unset_modifier_bits(modifiers, Qt.KeyboardModifier.GroupSwitchModifier) # We change Qt.Key.Key_Backtab to Key_Tab here because nobody would # configure "Shift-Backtab" in their config. if modifiers & Qt.KeyboardModifier.ShiftModifier and key == Qt.Key.Key_Backtab: key = Qt.Key.Key_Tab # We don't care about a shift modifier with symbols (Shift-: should # match a : binding even though we typed it with a shift on an # US-keyboard) # # However, we *do* care about Shift being involved if we got an # upper-case letter, as Shift-A should match a Shift-A binding, but not # an "a" binding. # # In addition, Shift also *is* relevant when other modifiers are # involved. Shift-Ctrl-X should not be equivalent to Ctrl-X. shift_modifier = Qt.KeyboardModifier.ShiftModifier if (modifiers == shift_modifier and _is_printable(key) and not ev.text().isupper()): modifiers = Qt.KeyboardModifier.NoModifier # On macOS, swap Ctrl and Meta back # # We don't use Qt.ApplicationAttribute.AA_MacDontSwapCtrlAndMeta because that also affects # Qt/QtWebEngine's own shortcuts. However, we do want "Ctrl" and "Meta" # (or "Cmd") in a key binding name to actually represent what's on the # keyboard. if utils.is_mac: if modifiers & Qt.KeyboardModifier.ControlModifier and modifiers & Qt.KeyboardModifier.MetaModifier: pass elif modifiers & Qt.KeyboardModifier.ControlModifier: modifiers = _unset_modifier_bits(modifiers, Qt.KeyboardModifier.ControlModifier) modifiers |= Qt.KeyboardModifier.MetaModifier elif modifiers & Qt.KeyboardModifier.MetaModifier: modifiers = _unset_modifier_bits(modifiers, Qt.KeyboardModifier.MetaModifier) modifiers |= Qt.KeyboardModifier.ControlModifier infos = list(self) infos.append(KeyInfo(key, modifiers)) return self.__class__(*infos) def strip_modifiers(self) -> 'KeySequence': """Strip optional modifiers from keys.""" modifiers = Qt.KeyboardModifier.KeypadModifier infos = [info.with_stripped_modifiers(modifiers) for info in self] return self.__class__(*infos) def with_mappings( self, mappings: Mapping['KeySequence', 'KeySequence'] ) -> 'KeySequence': """Get a new KeySequence with the given mappings applied.""" infos: list[KeyInfo] = [] for info in self: key_seq = KeySequence(info) if key_seq in mappings: infos += mappings[key_seq] else: infos.append(info) return self.__class__(*infos) @classmethod def parse(cls, keystr: str) -> 'KeySequence': """Parse a keystring like or xyz and return a KeySequence.""" new = cls() strings = list(_parse_keystring(keystr)) for sub in utils.chunk(strings, cls._MAX_LEN): sequence = QKeySequence(', '.join(sub)) new._sequences.append(sequence) if keystr: assert new, keystr new._validate(keystr) return new ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/keyinput/macros.py0000644000175100017510000000772115102145205021643 0ustar00runnerrunner# SPDX-FileCopyrightText: Jan Verbeek (blyxxyz) # SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Keyboard macro system.""" from typing import cast, Optional from qutebrowser.commands import runners from qutebrowser.api import cmdutils from qutebrowser.keyinput import modeman from qutebrowser.utils import message, objreg, usertypes _CommandType = tuple[str, int] # command, type macro_recorder = cast('MacroRecorder', None) class MacroRecorder: """An object for recording and running keyboard macros. Attributes: _macros: A list of commands for each macro register. _recording_macro: The register to which a macro is being recorded. _macro_count: The count passed to run_macro_command for each window. Stored for use by run_macro, which may be called from keyinput/modeparsers.py after a key input. _last_register: The macro which did run last. """ def __init__(self) -> None: self._macros: dict[str, list[_CommandType]] = {} self._recording_macro: Optional[str] = None self._macro_count: dict[int, int] = {} self._last_register: Optional[str] = None @cmdutils.register(instance='macro-recorder') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) def macro_record(self, win_id: int, register: str = None) -> None: """Start or stop recording a macro. Args: register: Which register to store the macro in. """ if self._recording_macro is None: if register is None: mode_manager = modeman.instance(win_id) mode_manager.enter(usertypes.KeyMode.record_macro, 'record_macro') else: self.record_macro(register) else: message.info("Macro '{}' recorded.".format(self._recording_macro)) self._recording_macro = None def record_macro(self, register: str) -> None: """Start recording a macro.""" message.info("Recording macro '{}'...".format(register)) self._macros[register] = [] self._recording_macro = register @cmdutils.register(instance='macro-recorder') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('count', value=cmdutils.Value.count) def macro_run(self, win_id: int, count: int = 1, register: str = None) -> None: """Run a recorded macro. Args: count: How many times to run the macro. register: Which macro to run. """ self._macro_count[win_id] = count if register is None: mode_manager = modeman.instance(win_id) mode_manager.enter(usertypes.KeyMode.run_macro, 'run_macro') else: self.run_macro(win_id, register) def run_macro(self, win_id: int, register: str) -> None: """Run a recorded macro.""" if register == '@': if self._last_register is None: raise cmdutils.CommandError("No previous macro") register = self._last_register self._last_register = register if register not in self._macros: raise cmdutils.CommandError( "No macro recorded in '{}'!".format(register)) commandrunner = runners.CommandRunner(win_id) for _ in range(self._macro_count[win_id]): for cmd in self._macros[register]: commandrunner.run_safely(*cmd) def record_command(self, text: str, count: int) -> None: """Record a command if a macro is being recorded.""" if self._recording_macro is not None: self._macros[self._recording_macro].append((text, count)) def init() -> None: """Initialize the MacroRecorder.""" global macro_recorder macro_recorder = MacroRecorder() objreg.register('macro-recorder', macro_recorder, command_only=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/keyinput/modeman.py0000644000175100017510000004202615102145205021774 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Mode manager (per window) which handles the current keyboard mode.""" import functools import dataclasses from typing import Union, cast from collections.abc import Mapping, MutableMapping, Callable from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt, QObject, QEvent from qutebrowser.qt.gui import QKeyEvent, QKeySequence from qutebrowser.commands import runners from qutebrowser.keyinput import modeparsers, basekeyparser from qutebrowser.config import config from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes, log, objreg, utils, qtutils from qutebrowser.browser import hints from qutebrowser.misc import objects INPUT_MODES = [usertypes.KeyMode.insert, usertypes.KeyMode.passthrough] PROMPT_MODES = [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno] # FIXME:mypy TypedDict? ParserDictType = MutableMapping[usertypes.KeyMode, basekeyparser.BaseKeyParser] @dataclasses.dataclass(frozen=True) class KeyEvent: """A small wrapper over a QKeyEvent storing its data. This is needed because Qt apparently mutates existing events with new data. It doesn't store the modifiers because they can be different for a key press/release. Attributes: key: Usually a Qt.Key member, but could be other ints (QKeyEvent::key). text: A string (QKeyEvent::text). """ # int instead of Qt.Key: # WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html key: int text: str @classmethod def from_event(cls, event: QKeyEvent) -> 'KeyEvent': """Initialize a KeyEvent from a QKeyEvent.""" return cls(event.key(), event.text()) class NotInModeError(Exception): """Exception raised when we want to leave a mode we're not in.""" class UnavailableError(Exception): """Exception raised when trying to access modeman before initialization. Thrown by instance() if modeman has not been initialized yet. """ def init(win_id: int, parent: QObject) -> 'ModeManager': """Initialize the mode manager and the keyparsers for the given win_id.""" commandrunner = runners.CommandRunner(win_id) modeman = ModeManager(win_id, parent) objreg.register('mode-manager', modeman, scope='window', window=win_id) hintmanager = hints.HintManager(win_id, parent=parent) objreg.register('hintmanager', hintmanager, scope='window', window=win_id, command_only=True) modeman.hintmanager = hintmanager log_sensitive_keys = 'log-sensitive-keys' in objects.debug_flags keyparsers: ParserDictType = { usertypes.KeyMode.normal: modeparsers.NormalKeyParser( win_id=win_id, commandrunner=commandrunner, parent=modeman), usertypes.KeyMode.hint: modeparsers.HintKeyParser( win_id=win_id, commandrunner=commandrunner, hintmanager=hintmanager, parent=modeman), usertypes.KeyMode.insert: modeparsers.CommandKeyParser( mode=usertypes.KeyMode.insert, win_id=win_id, commandrunner=commandrunner, parent=modeman, passthrough=True, do_log=log_sensitive_keys, supports_count=False), usertypes.KeyMode.passthrough: modeparsers.CommandKeyParser( mode=usertypes.KeyMode.passthrough, win_id=win_id, commandrunner=commandrunner, parent=modeman, passthrough=True, do_log=log_sensitive_keys, supports_count=False), usertypes.KeyMode.command: modeparsers.CommandKeyParser( mode=usertypes.KeyMode.command, win_id=win_id, commandrunner=commandrunner, parent=modeman, passthrough=True, do_log=log_sensitive_keys, supports_count=False), usertypes.KeyMode.prompt: modeparsers.CommandKeyParser( mode=usertypes.KeyMode.prompt, win_id=win_id, commandrunner=commandrunner, parent=modeman, passthrough=True, do_log=log_sensitive_keys, supports_count=False), usertypes.KeyMode.yesno: modeparsers.CommandKeyParser( mode=usertypes.KeyMode.yesno, win_id=win_id, commandrunner=commandrunner, parent=modeman, supports_count=False), usertypes.KeyMode.caret: modeparsers.CommandKeyParser( mode=usertypes.KeyMode.caret, win_id=win_id, commandrunner=commandrunner, parent=modeman, passthrough=True), usertypes.KeyMode.set_mark: modeparsers.RegisterKeyParser( mode=usertypes.KeyMode.set_mark, win_id=win_id, commandrunner=commandrunner, parent=modeman), usertypes.KeyMode.jump_mark: modeparsers.RegisterKeyParser( mode=usertypes.KeyMode.jump_mark, win_id=win_id, commandrunner=commandrunner, parent=modeman), usertypes.KeyMode.record_macro: modeparsers.RegisterKeyParser( mode=usertypes.KeyMode.record_macro, win_id=win_id, commandrunner=commandrunner, parent=modeman), usertypes.KeyMode.run_macro: modeparsers.RegisterKeyParser( mode=usertypes.KeyMode.run_macro, win_id=win_id, commandrunner=commandrunner, parent=modeman), } for mode, parser in keyparsers.items(): modeman.register(mode, parser) return modeman def instance(win_id: Union[int, str]) -> 'ModeManager': """Get a modemanager object. Raises UnavailableError if there is no instance available yet. """ mode_manager = objreg.get('mode-manager', scope='window', window=win_id, default=None) if mode_manager is not None: return mode_manager else: raise UnavailableError("ModeManager is not initialized yet.") def enter(win_id: int, mode: usertypes.KeyMode, reason: str = None, only_if_normal: bool = False) -> None: """Enter the mode 'mode'.""" instance(win_id).enter(mode, reason, only_if_normal) def leave(win_id: int, mode: usertypes.KeyMode, reason: str = None, *, maybe: bool = False) -> None: """Leave the mode 'mode'.""" instance(win_id).leave(mode, reason, maybe=maybe) class ModeManager(QObject): """Manager for keyboard modes. Attributes: mode: The mode we're currently in. hintmanager: The HintManager associated with this window. _win_id: The window ID of this ModeManager _prev_mode: Mode before a prompt popped up parsers: A dictionary of modes and their keyparsers. _forward_unbound_keys: If we should forward unbound keys. _releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was passed through, so the release event should as well. Signals: entered: Emitted when a mode is entered. arg1: The mode which has been entered. arg2: The window ID of this mode manager. left: Emitted when a mode is left. arg1: The mode which has been left. arg2: The new current mode. arg3: The window ID of this mode manager. keystring_updated: Emitted when the keystring was updated in any mode. arg 1: The mode in which the keystring has been updated. arg 2: The new key string. """ entered = pyqtSignal(usertypes.KeyMode, int) left = pyqtSignal(usertypes.KeyMode, usertypes.KeyMode, int) keystring_updated = pyqtSignal(usertypes.KeyMode, str) def __init__(self, win_id: int, parent: QObject = None) -> None: super().__init__(parent) self._win_id = win_id self.parsers: ParserDictType = {} self._prev_mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal self._releaseevents_to_pass: set[KeyEvent] = set() # Set after __init__ self.hintmanager = cast(hints.HintManager, None) def __repr__(self) -> str: return utils.get_repr(self, mode=self.mode) def _handle_keypress(self, event: QKeyEvent, *, dry_run: bool = False) -> bool: """Handle filtering of KeyPress events. Args: event: The KeyPress to examine. dry_run: Don't actually handle the key, only filter it. Return: True if event should be filtered, False otherwise. """ curmode = self.mode parser = self.parsers[curmode] if curmode != usertypes.KeyMode.insert: log.modes.debug("got keypress in mode {} - delegating to " "{}".format(curmode, utils.qualname(parser))) match = parser.handle(event, dry_run=dry_run) if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing ignored_modifiers = [ cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.NoModifier), cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier), ] else: ignored_modifiers = [ Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.ShiftModifier, ] has_modifier = event.modifiers() not in ignored_modifiers is_non_alnum = has_modifier or not event.text().strip() forward_unbound_keys = config.cache['input.forward_unbound_keys'] if match != QKeySequence.SequenceMatch.NoMatch: filter_this = True elif (parser.passthrough or forward_unbound_keys == 'all' or (forward_unbound_keys == 'auto' and is_non_alnum)): filter_this = False else: filter_this = True if not filter_this and not dry_run: self._releaseevents_to_pass.add(KeyEvent.from_event(event)) if curmode != usertypes.KeyMode.insert: focus_widget = objects.qapp.focusWidget() log.modes.debug("match: {}, forward_unbound_keys: {}, " "passthrough: {}, is_non_alnum: {}, dry_run: {} " "--> filter: {} (focused: {})".format( match, forward_unbound_keys, parser.passthrough, is_non_alnum, dry_run, filter_this, qtutils.qobj_repr(focus_widget))) return filter_this def _handle_keyrelease(self, event: QKeyEvent) -> bool: """Handle filtering of KeyRelease events. Args: event: The KeyPress to examine. Return: True if event should be filtered, False otherwise. """ # handle like matching KeyPress keyevent = KeyEvent.from_event(event) if keyevent in self._releaseevents_to_pass: self._releaseevents_to_pass.remove(keyevent) filter_this = False else: filter_this = True if self.mode != usertypes.KeyMode.insert: log.modes.debug("filter: {}".format(filter_this)) return filter_this def register(self, mode: usertypes.KeyMode, parser: basekeyparser.BaseKeyParser) -> None: """Register a new mode.""" assert parser is not None self.parsers[mode] = parser parser.request_leave.connect(self.leave) parser.keystring_updated.connect( functools.partial(self.keystring_updated.emit, mode)) def enter(self, mode: usertypes.KeyMode, reason: str = None, only_if_normal: bool = False) -> None: """Enter a new mode. Args: mode: The mode to enter as a KeyMode member. reason: Why the mode was entered. only_if_normal: Only enter the new mode if we're in normal mode. """ if mode == usertypes.KeyMode.normal: self.leave(self.mode, reason='enter normal: {}'.format(reason)) return log.modes.debug("Entering mode {}{}".format( mode, '' if reason is None else ' (reason: {})'.format(reason))) if mode not in self.parsers: raise ValueError("No keyparser for mode {}".format(mode)) if self.mode == mode or (self.mode in PROMPT_MODES and mode in PROMPT_MODES): log.modes.debug("Ignoring request as we're in mode {} " "already.".format(self.mode)) return if self.mode != usertypes.KeyMode.normal: if only_if_normal: log.modes.debug("Ignoring request as we're in mode {} " "and only_if_normal is set..".format( self.mode)) return log.modes.debug("Overriding mode {}.".format(self.mode)) self.left.emit(self.mode, mode, self._win_id) if mode in PROMPT_MODES and self.mode in INPUT_MODES: self._prev_mode = self.mode else: self._prev_mode = usertypes.KeyMode.normal self.mode = mode self.entered.emit(mode, self._win_id) @cmdutils.register(instance='mode-manager', scope='window') def mode_enter(self, mode: str) -> None: """Enter a key mode. Args: mode: The mode to enter. See `:help bindings.commands` for the available modes, but note that hint/command/yesno/prompt mode can't be entered manually. """ try: m = usertypes.KeyMode[mode] except KeyError: raise cmdutils.CommandError("Mode {} does not exist!".format(mode)) if m in [usertypes.KeyMode.hint, usertypes.KeyMode.command, usertypes.KeyMode.yesno, usertypes.KeyMode.prompt, usertypes.KeyMode.register]: raise cmdutils.CommandError( "Mode {} can't be entered manually!".format(mode)) self.enter(m, 'command') @pyqtSlot(usertypes.KeyMode, str, bool) def leave(self, mode: usertypes.KeyMode, reason: str = None, maybe: bool = False) -> None: """Leave a key mode. Args: mode: The mode to leave as a usertypes.KeyMode member. reason: Why the mode was left. maybe: If set, ignore the request if we're not in that mode. """ if self.mode != mode: if maybe: log.modes.debug("Ignoring leave request for {} (reason {}) as " "we're in mode {}".format( mode, reason, self.mode)) return else: raise NotInModeError("Not in mode {}!".format(mode)) log.modes.debug("Leaving mode {}{}".format( mode, '' if reason is None else ' (reason: {})'.format(reason))) # leaving a mode implies clearing keychain, see # https://github.com/qutebrowser/qutebrowser/issues/1805 self.clear_keychain() self.mode = usertypes.KeyMode.normal self.left.emit(mode, self.mode, self._win_id) if mode in PROMPT_MODES: self.enter(self._prev_mode, reason='restore mode before {}'.format(mode.name)) @cmdutils.register(instance='mode-manager', not_modes=[usertypes.KeyMode.normal], scope='window') def mode_leave(self) -> None: """Leave the mode we're currently in.""" if self.mode == usertypes.KeyMode.normal: raise ValueError("Can't leave normal mode!") self.leave(self.mode, 'leave current') def handle_event(self, event: QEvent) -> bool: """Filter all events based on the currently set mode. Also calls the real keypress handler. Args: event: The KeyPress to examine. Return: True if event should be filtered, False otherwise. """ handlers: Mapping[QEvent.Type, Callable[[QKeyEvent], bool]] = { QEvent.Type.KeyPress: self._handle_keypress, QEvent.Type.KeyRelease: self._handle_keyrelease, QEvent.Type.ShortcutOverride: functools.partial(self._handle_keypress, dry_run=True), } handler = handlers[event.type()] return handler(cast(QKeyEvent, event)) @cmdutils.register(instance='mode-manager', scope='window') def clear_keychain(self) -> None: """Clear the currently entered key chain.""" self.parsers[self.mode].clear_keystring() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/keyinput/modeparsers.py0000644000175100017510000002743015102145205022702 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """KeyChainParser for "hint" and "normal" modes. Module attributes: STARTCHARS: Possible chars for starting a commandline input. """ import traceback import enum from typing import TYPE_CHECKING from collections.abc import Sequence from qutebrowser.qt.core import pyqtSlot, Qt, QObject from qutebrowser.qt.gui import QKeySequence, QKeyEvent from qutebrowser.browser import hints from qutebrowser.commands import cmdexc from qutebrowser.config import config from qutebrowser.keyinput import basekeyparser, keyutils, macros from qutebrowser.utils import usertypes, log, message, objreg, utils if TYPE_CHECKING: from qutebrowser.commands import runners STARTCHARS = ":/?" class LastPress(enum.Enum): """Whether the last keypress filtered a text or was part of a keystring.""" none = enum.auto() filtertext = enum.auto() keystring = enum.auto() class CommandKeyParser(basekeyparser.BaseKeyParser): """KeyChainParser for command bindings. Attributes: _commandrunner: CommandRunner instance. """ def __init__(self, *, mode: usertypes.KeyMode, win_id: int, commandrunner: 'runners.CommandRunner', parent: QObject = None, do_log: bool = True, passthrough: bool = False, supports_count: bool = True) -> None: super().__init__(mode=mode, win_id=win_id, parent=parent, do_log=do_log, passthrough=passthrough, supports_count=supports_count) self._commandrunner = commandrunner def execute(self, cmdstr: str, count: int = None) -> None: try: self._commandrunner.run(cmdstr, count) except cmdexc.Error as e: message.error(str(e), stack=traceback.format_exc()) class NormalKeyParser(CommandKeyParser): """KeyParser for normal mode with added STARTCHARS detection and more. Attributes: _partial_timer: Timer to clear partial keypresses. """ _sequence: keyutils.KeySequence def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', parent: QObject = None) -> None: super().__init__(mode=usertypes.KeyMode.normal, win_id=win_id, commandrunner=commandrunner, parent=parent) self._partial_timer = usertypes.Timer(self, 'partial-match') self._partial_timer.setSingleShot(True) self._partial_timer.timeout.connect(self._clear_partial_match) self._inhibited = False self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited') self._inhibited_timer.setSingleShot(True) self._inhibited_timer.timeout.connect(self._clear_inhibited) def __repr__(self) -> str: return utils.get_repr(self) def handle(self, e: QKeyEvent, *, dry_run: bool = False) -> QKeySequence.SequenceMatch: """Override to abort if the key is a startchar.""" txt = e.text().strip() if self._inhibited: self._debug_log("Ignoring key '{}', because the normal mode is " "currently inhibited.".format(txt)) return QKeySequence.SequenceMatch.NoMatch match = super().handle(e, dry_run=dry_run) if match == QKeySequence.SequenceMatch.PartialMatch and not dry_run: timeout = config.val.input.partial_timeout if timeout != 0: self._partial_timer.setInterval(timeout) self._partial_timer.start() return match def set_inhibited_timeout(self, timeout: int) -> None: """Ignore keypresses for the given duration.""" if timeout != 0: self._debug_log("Inhibiting the normal mode for {}ms.".format( timeout)) self._inhibited = True self._inhibited_timer.setInterval(timeout) self._inhibited_timer.start() @pyqtSlot() def _clear_partial_match(self) -> None: """Clear a partial keystring after a timeout.""" self._debug_log("Clearing partial keystring {}".format( self._sequence)) self._sequence = keyutils.KeySequence() self.keystring_updated.emit(str(self._sequence)) @pyqtSlot() def _clear_inhibited(self) -> None: """Reset inhibition state after a timeout.""" self._debug_log("Releasing inhibition state of normal mode.") self._inhibited = False class HintKeyParser(basekeyparser.BaseKeyParser): """KeyChainParser for hints. Attributes: _filtertext: The text to filter with. _hintmanager: The HintManager to use. _last_press: The nature of the last keypress, a LastPress member. """ _sequence: keyutils.KeySequence def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', hintmanager: hints.HintManager, parent: QObject = None) -> None: super().__init__(mode=usertypes.KeyMode.hint, win_id=win_id, parent=parent, supports_count=False) self._command_parser = CommandKeyParser(mode=usertypes.KeyMode.hint, win_id=win_id, commandrunner=commandrunner, parent=self, supports_count=False) self._hintmanager = hintmanager self._filtertext = '' self._last_press = LastPress.none self.keystring_updated.connect(self._hintmanager.handle_partial_key) def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch: """Handle keys for string filtering.""" log.keyboard.debug("Got filter key 0x{:x} text {}".format( e.key(), e.text())) if e.key() == Qt.Key.Key_Backspace: log.keyboard.debug("Got backspace, mode {}, filtertext '{}', " "sequence '{}'".format(self._last_press, self._filtertext, self._sequence)) if self._last_press != LastPress.keystring and self._filtertext: self._filtertext = self._filtertext[:-1] self._hintmanager.filter_hints(self._filtertext) return QKeySequence.SequenceMatch.ExactMatch elif self._last_press == LastPress.keystring and self._sequence: self._sequence = self._sequence[:-1] self.keystring_updated.emit(str(self._sequence)) if not self._sequence and self._filtertext: # Switch back to hint filtering mode (this can happen only # in numeric mode after the number has been deleted). self._hintmanager.filter_hints(self._filtertext) self._last_press = LastPress.filtertext return QKeySequence.SequenceMatch.ExactMatch else: return QKeySequence.SequenceMatch.NoMatch elif self._hintmanager.current_mode() != 'number': return QKeySequence.SequenceMatch.NoMatch elif not e.text(): return QKeySequence.SequenceMatch.NoMatch else: self._filtertext += e.text() self._hintmanager.filter_hints(self._filtertext) self._last_press = LastPress.filtertext return QKeySequence.SequenceMatch.ExactMatch def handle(self, e: QKeyEvent, *, dry_run: bool = False) -> QKeySequence.SequenceMatch: """Handle a new keypress and call the respective handlers.""" if dry_run: return super().handle(e, dry_run=True) assert not dry_run if (self._command_parser.handle(e, dry_run=True) != QKeySequence.SequenceMatch.NoMatch): log.keyboard.debug("Handling key via command parser") self.clear_keystring() return self._command_parser.handle(e) match = super().handle(e) if match == QKeySequence.SequenceMatch.PartialMatch: self._last_press = LastPress.keystring elif match == QKeySequence.SequenceMatch.ExactMatch: self._last_press = LastPress.none elif match == QKeySequence.SequenceMatch.NoMatch: # We couldn't find a keychain so we check if it's a special key. return self._handle_filter_key(e) else: raise ValueError("Got invalid match type {}!".format(match)) return match def update_bindings(self, strings: Sequence[str], preserve_filter: bool = False) -> None: """Update bindings when the hint strings changed. Args: strings: A list of hint strings. preserve_filter: Whether to keep the current value of `self._filtertext`. """ self._read_config() self.bindings.update({keyutils.KeySequence.parse(s): s for s in strings}) if not preserve_filter: self._filtertext = '' def execute(self, cmdstr: str, count: int = None) -> None: assert count is None self._hintmanager.handle_partial_key(cmdstr) class RegisterKeyParser(CommandKeyParser): """KeyParser for modes that record a register key. Attributes: _register_mode: One of KeyMode.set_mark, KeyMode.jump_mark, KeyMode.record_macro and KeyMode.run_macro. """ def __init__(self, *, win_id: int, mode: usertypes.KeyMode, commandrunner: 'runners.CommandRunner', parent: QObject = None) -> None: super().__init__(mode=usertypes.KeyMode.register, win_id=win_id, commandrunner=commandrunner, parent=parent, supports_count=False) self._register_mode = mode def handle(self, e: QKeyEvent, *, dry_run: bool = False) -> QKeySequence.SequenceMatch: """Override to always match the next key and use the register.""" match = super().handle(e, dry_run=dry_run) if match != QKeySequence.SequenceMatch.NoMatch or dry_run: return match try: info = keyutils.KeyInfo.from_event(e) except keyutils.InvalidKeyError as ex: # See https://github.com/qutebrowser/qutebrowser/issues/7047 log.keyboard.debug(f"Got invalid key: {ex}") return QKeySequence.SequenceMatch.NoMatch if info.is_special(): # this is not a proper register key, let it pass and keep going return QKeySequence.SequenceMatch.NoMatch key = e.text() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) try: if self._register_mode == usertypes.KeyMode.set_mark: tabbed_browser.set_mark(key) elif self._register_mode == usertypes.KeyMode.jump_mark: tabbed_browser.jump_mark(key) elif self._register_mode == usertypes.KeyMode.record_macro: macros.macro_recorder.record_macro(key) elif self._register_mode == usertypes.KeyMode.run_macro: macros.macro_recorder.run_macro(self._win_id, key) else: raise ValueError("{} is not a valid register mode".format( self._register_mode)) except cmdexc.Error as err: message.error(str(err), stack=traceback.format_exc()) self.request_leave.emit( self._register_mode, "valid register key", True) return QKeySequence.SequenceMatch.ExactMatch ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5396388 qutebrowser-3.6.1/qutebrowser/mainwindow/0000755000175100017510000000000015102145351020304 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/__init__.py0000644000175100017510000000025015102145205022410 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Widgets needed for the main window.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/mainwindow.py0000644000175100017510000006672715102145205023052 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The main window of qutebrowser.""" import binascii import base64 import itertools import functools from typing import Optional, cast from collections.abc import MutableSequence from qutebrowser.qt import machinery from qutebrowser.qt.core import (pyqtBoundSignal, pyqtSlot, QRect, QPoint, QTimer, Qt, QCoreApplication, QEventLoop, QByteArray) from qutebrowser.qt.widgets import QWidget, QVBoxLayout, QSizePolicy from qutebrowser.qt.gui import QPalette from qutebrowser.commands import runners from qutebrowser.api import cmdutils from qutebrowser.config import config, configfiles, stylesheet, websettings from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, jinja, debug) from qutebrowser.mainwindow import messageview, prompt from qutebrowser.completion import completionwidget, completer from qutebrowser.keyinput import modeman from qutebrowser.browser import downloadview, hints, downloads from qutebrowser.misc import crashsignal, keyhintwidget, sessions, objects from qutebrowser.qt import sip win_id_gen = itertools.count(0) def get_window(*, via_ipc: bool, target: str, no_raise: bool = False) -> "MainWindow": """Helper function for app.py to get a window id. Args: via_ipc: Whether the request was made via IPC. target: Where/how to open the window (via setting, command-line or override). no_raise: suppress target window raising Return: The MainWindow that was used to open URL """ if not via_ipc: # Initial main window return objreg.get("main-window", scope="window", window=0) window = None # Try to find the existing tab target if opening in a tab if target not in {'window', 'private-window'}: window = get_target_window() window.should_raise = target not in {'tab-silent', 'tab-bg-silent'} and not no_raise is_private = target == 'private-window' # Otherwise, or if no window was found, create a new one if window is None: window = MainWindow(private=is_private) window.should_raise = not no_raise return window def raise_window(window, alert=True): """Raise the given MainWindow object.""" window.setWindowState(window.windowState() & ~Qt.WindowState.WindowMinimized) window.setWindowState(window.windowState() | Qt.WindowState.WindowActive) window.raise_() # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-69568 QCoreApplication.processEvents( QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents | QEventLoop.ProcessEventsFlag.ExcludeSocketNotifiers) if sip.isdeleted(window): # Could be deleted by the events run above return window.activateWindow() if alert: objects.qapp.alert(window) def get_target_window(): """Get the target window for new tabs, or None if none exist.""" getters = { 'last-focused': objreg.last_focused_window, 'first-opened': objreg.first_opened_window, 'last-opened': objreg.last_opened_window, 'last-visible': objreg.last_visible_window, } getter = getters[config.val.new_instance_open_target_window] try: return getter() except objreg.NoWindow: return None _OverlayInfoType = tuple[QWidget, pyqtBoundSignal, bool, str] class MainWindow(QWidget): """The main window of qutebrowser. Adds all needed components to a vbox, initializes sub-widgets and connects signals. Attributes: status: The StatusBar widget. tabbed_browser: The TabbedBrowser widget. state_before_fullscreen: window state before activation of fullscreen. should_raise: Whether the window should be raised/activated when maybe_raise() gets called. _downloadview: The DownloadView widget. _download_model: The DownloadModel instance. _vbox: The main QVBoxLayout. _commandrunner: The main CommandRunner instance. _overlays: Widgets shown as overlay for the current webpage. _private: Whether the window is in private browsing mode. """ # Application wide stylesheets STYLESHEET = """ HintLabel { background-color: {{ conf.colors.hints.bg }}; color: {{ conf.colors.hints.fg }}; font: {{ conf.fonts.hints }}; border: {{ conf.hints.border }}; border-radius: {{ conf.hints.radius }}px; padding-top: {{ conf.hints.padding['top'] }}px; padding-left: {{ conf.hints.padding['left'] }}px; padding-right: {{ conf.hints.padding['right'] }}px; padding-bottom: {{ conf.hints.padding['bottom'] }}px; } QToolTip { {% if conf.fonts.tooltip %} font: {{ conf.fonts.tooltip }}; {% endif %} {% if conf.colors.tooltip.bg %} background-color: {{ conf.colors.tooltip.bg }}; {% endif %} {% if conf.colors.tooltip.fg %} color: {{ conf.colors.tooltip.fg }}; {% endif %} } QMenu { {% if conf.fonts.contextmenu %} font: {{ conf.fonts.contextmenu }}; {% endif %} {% if conf.colors.contextmenu.menu.bg %} background-color: {{ conf.colors.contextmenu.menu.bg }}; {% endif %} {% if conf.colors.contextmenu.menu.fg %} color: {{ conf.colors.contextmenu.menu.fg }}; {% endif %} } QMenu::item:selected { {% if conf.colors.contextmenu.selected.bg %} background-color: {{ conf.colors.contextmenu.selected.bg }}; {% endif %} {% if conf.colors.contextmenu.selected.fg %} color: {{ conf.colors.contextmenu.selected.fg }}; {% endif %} } QMenu::item:disabled { {% if conf.colors.contextmenu.disabled.bg %} background-color: {{ conf.colors.contextmenu.disabled.bg }}; {% endif %} {% if conf.colors.contextmenu.disabled.fg %} color: {{ conf.colors.contextmenu.disabled.fg }}; {% endif %} } """ def __init__(self, *, private: bool, geometry: Optional[QByteArray] = None, parent: Optional[QWidget] = None) -> None: """Create a new main window. Args: geometry: The geometry to load, as a bytes-object (or None). private: Whether the window is in private browsing mode. parent: The parent the window should get. """ super().__init__(parent) # Late import to avoid a circular dependency # - browsertab -> hints -> webelem -> mainwindow -> bar -> browsertab from qutebrowser.mainwindow import tabbedbrowser from qutebrowser.mainwindow.statusbar import bar self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) if config.val.window.transparent: self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.palette().setColor(QPalette.ColorRole.Window, Qt.GlobalColor.transparent) self._overlays: MutableSequence[_OverlayInfoType] = [] self.win_id = next(win_id_gen) self.registry = objreg.ObjectRegistry() objreg.window_registry[self.win_id] = self objreg.register('main-window', self, scope='window', window=self.win_id) tab_registry = objreg.ObjectRegistry() objreg.register('tab-registry', tab_registry, scope='window', window=self.win_id) self.setWindowTitle('qutebrowser') self._vbox = QVBoxLayout(self) self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) self._init_downloadmanager() self._downloadview = downloadview.DownloadView( model=self._download_model) self.is_private = config.val.content.private_browsing or private self.tabbed_browser: tabbedbrowser.TabbedBrowser = tabbedbrowser.TabbedBrowser( win_id=self.win_id, private=self.is_private, parent=self) objreg.register('tabbed-browser', self.tabbed_browser, scope='window', window=self.win_id) self._init_command_dispatcher() # We need to set an explicit parent for StatusBar because it does some # show/hide magic immediately which would mean it'd show up as a # window. self.status = bar.StatusBar(win_id=self.win_id, private=self.is_private, parent=self) self._add_widgets() self._downloadview.show() self._init_completion() log.init.debug("Initializing modes...") modeman.init(win_id=self.win_id, parent=self) self._commandrunner = runners.CommandRunner( self.win_id, partial_match=True, find_similar=True) self._keyhint = keyhintwidget.KeyHintView(self.win_id, self) self._add_overlay(self._keyhint, self._keyhint.update_geometry) self._prompt_container = prompt.PromptContainer(self.win_id, self) self._add_overlay(self._prompt_container, self._prompt_container.update_geometry, centered=True, padding=10) objreg.register('prompt-container', self._prompt_container, scope='window', window=self.win_id, command_only=True) self._prompt_container.hide() self._messageview = messageview.MessageView(parent=self) self._add_overlay(self._messageview, self._messageview.update_geometry) self._init_geometry(geometry) self._connect_signals() # When we're here the statusbar might not even really exist yet, so # resizing will fail. Therefore, we use singleShot QTimers to make sure # we defer this until everything else is initialized. QTimer.singleShot(0, self._connect_overlay_signals) config.instance.changed.connect(self._on_config_changed) objects.qapp.new_window.emit(self) self._set_decoration(config.val.window.hide_decoration) self.state_before_fullscreen = self.windowState() self.should_raise: bool = False stylesheet.set_register(self) def _init_geometry(self, geometry): """Initialize the window geometry or load it from disk.""" if geometry is not None: self._load_geometry(geometry) elif self.win_id == 0: self._load_state_geometry() else: self._set_default_geometry() log.init.debug("Initial main window geometry: {}".format( self.geometry())) def _add_overlay(self, widget, signal, *, centered=False, padding=0): self._overlays.append((widget, signal, centered, padding)) def _update_overlay_geometries(self): """Update the size/position of all overlays.""" for w, _signal, centered, padding in self._overlays: self._update_overlay_geometry(w, centered, padding) def _update_overlay_geometry(self, widget, centered, padding): """Reposition/resize the given overlay.""" if not widget.isVisible(): return if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Policy.Expanding: width = self.width() - 2 * padding if widget.hasHeightForWidth(): height = widget.heightForWidth(width) else: height = widget.sizeHint().height() left = padding else: size_hint = widget.sizeHint() width = min(size_hint.width(), self.width() - 2 * padding) height = size_hint.height() left = (self.width() - width) // 2 if centered else 0 height_padding = 20 status_position = config.val.statusbar.position if status_position == 'bottom': if self.status.isVisible(): status_height = self.status.height() bottom = self.status.geometry().top() else: status_height = 0 bottom = self.height() top = self.height() - status_height - height top = qtutils.check_overflow(top, 'int', fatal=False) topleft = QPoint(left, max(height_padding, top)) bottomright = QPoint(left + width, bottom) elif status_position == 'top': if self.status.isVisible(): status_height = self.status.height() top = self.status.geometry().bottom() else: status_height = 0 top = 0 topleft = QPoint(left, top) bottom = status_height + height bottom = qtutils.check_overflow(bottom, 'int', fatal=False) bottomright = QPoint(left + width, min(self.height() - height_padding, bottom)) else: raise ValueError("Invalid position {}!".format(status_position)) rect = QRect(topleft, bottomright) log.misc.debug('new geometry for {!r}: {}'.format(widget, rect)) if rect.isValid(): widget.setGeometry(rect) def _init_downloadmanager(self): log.init.debug("Initializing downloads...") qtnetwork_download_manager = objreg.get('qtnetwork-download-manager') try: webengine_download_manager = objreg.get( 'webengine-download-manager') except KeyError: webengine_download_manager = None self._download_model = downloads.DownloadModel( qtnetwork_download_manager, webengine_download_manager) objreg.register('download-model', self._download_model, scope='window', window=self.win_id, command_only=True) def _init_completion(self): self._completion = completionwidget.CompletionView(cmd=self.status.cmd, win_id=self.win_id, parent=self) completer_obj = completer.Completer(cmd=self.status.cmd, win_id=self.win_id, parent=self._completion) self._completion.selection_changed.connect( completer_obj.on_selection_changed) objreg.register('completion', self._completion, scope='window', window=self.win_id, command_only=True) self._add_overlay(self._completion, self._completion.update_geometry) def _init_command_dispatcher(self): # Lazy import to avoid circular imports from qutebrowser.browser import commands self._command_dispatcher = commands.CommandDispatcher( self.win_id, self.tabbed_browser) objreg.register('command-dispatcher', self._command_dispatcher, command_only=True, scope='window', window=self.win_id) widget = self.tabbed_browser.widget widget.destroyed.connect( functools.partial(objreg.delete, 'command-dispatcher', scope='window', window=self.win_id)) def __repr__(self): return utils.get_repr(self) @pyqtSlot(str) def _on_config_changed(self, option): """Resize the completion if related config options changed.""" if option == 'statusbar.padding': self._update_overlay_geometries() elif option == 'downloads.position': self._add_widgets() elif option == 'statusbar.position': self._add_widgets() self._update_overlay_geometries() elif option == 'window.hide_decoration': self._set_decoration(config.val.window.hide_decoration) def _add_widgets(self): """Add or re-add all widgets to the VBox.""" self._vbox.removeWidget(self.tabbed_browser.widget) self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self.status) widgets: list[QWidget] = [self.tabbed_browser.widget] downloads_position = config.val.downloads.position if downloads_position == 'top': widgets.insert(0, self._downloadview) elif downloads_position == 'bottom': widgets.append(self._downloadview) else: raise ValueError("Invalid position {}!".format(downloads_position)) status_position = config.val.statusbar.position if status_position == 'top': widgets.insert(0, self.status) elif status_position == 'bottom': widgets.append(self.status) else: raise ValueError("Invalid position {}!".format(status_position)) for widget in widgets: self._vbox.addWidget(widget) def _load_state_geometry(self): """Load the geometry from the state file.""" try: data = configfiles.state['geometry']['mainwindow'] geom = base64.b64decode(data, validate=True) except KeyError: # First start self._set_default_geometry() except binascii.Error: log.init.exception("Error while reading geometry") self._set_default_geometry() else: self._load_geometry(geom) def _save_geometry(self): """Save the window geometry to the state config.""" data = self.saveGeometry().data() geom = base64.b64encode(data).decode('ASCII') configfiles.state['geometry']['mainwindow'] = geom def _load_geometry(self, geom): """Load geometry from a bytes object. If loading fails, loads default geometry. """ log.init.debug("Loading mainwindow from {!r}".format(geom)) ok = self.restoreGeometry(geom) if not ok: log.init.warning("Error while loading geometry.") self._set_default_geometry() def _connect_overlay_signals(self): """Connect the resize signal and resize everything once.""" for widget, signal, centered, padding in self._overlays: signal.connect( functools.partial(self._update_overlay_geometry, widget, centered, padding)) self._update_overlay_geometry(widget, centered, padding) def _set_default_geometry(self): """Set some sensible default geometry.""" self.setGeometry(QRect(50, 50, 800, 600)) def _connect_signals(self): """Connect all mainwindow signals.""" mode_manager = modeman.instance(self.win_id) # misc self._prompt_container.release_focus.connect( self.tabbed_browser.on_release_focus) self.tabbed_browser.close_window.connect(self.close) mode_manager.entered.connect(hints.on_mode_entered) # status bar mode_manager.hintmanager.set_text.connect(self.status.set_text) mode_manager.entered.connect(self.status.on_mode_entered) mode_manager.left.connect(self.status.on_mode_left) mode_manager.left.connect(self.status.cmd.on_mode_left) mode_manager.left.connect(message.global_bridge.mode_left) # commands mode_manager.keystring_updated.connect( self.status.keystring.on_keystring_updated) self.status.cmd.got_cmd[str].connect(self._commandrunner.run_safely) self.status.cmd.got_cmd[str, int].connect(self._commandrunner.run_safely) self.status.cmd.returnPressed.connect(self.tabbed_browser.on_cmd_return_pressed) self.status.cmd.got_search.connect(self._command_dispatcher.search) # key hint popup mode_manager.keystring_updated.connect(self._keyhint.update_keyhint) # messages message.global_bridge.show_message.connect( self._messageview.show_message) message.global_bridge.flush() message.global_bridge.clear_messages.connect( self._messageview.clear_messages) # statusbar self.tabbed_browser.current_tab_changed.connect( self.status.on_tab_changed) self.tabbed_browser.cur_progress.connect( self.status.prog.on_load_progress) self.tabbed_browser.cur_load_started.connect( self.status.prog.on_load_started) self.tabbed_browser.cur_scroll_perc_changed.connect( self.status.percentage.set_perc) self.tabbed_browser.widget.tab_index_changed.connect( self.status.tabindex.on_tab_index_changed) self.tabbed_browser.cur_url_changed.connect( self.status.url.set_url) self.tabbed_browser.cur_url_changed.connect(functools.partial( self.status.backforward.on_tab_cur_url_changed, tabs=self.tabbed_browser)) self.tabbed_browser.cur_link_hovered.connect( self.status.url.set_hover_url) self.tabbed_browser.cur_load_status_changed.connect( self.status.url.on_load_status_changed) self.tabbed_browser.cur_search_match_changed.connect( self.status.search_match.set_match) self.tabbed_browser.cur_caret_selection_toggled.connect( self.status.on_caret_selection_toggled) self.tabbed_browser.cur_fullscreen_requested.connect( self._on_fullscreen_requested) self.tabbed_browser.cur_fullscreen_requested.connect( self.status.maybe_hide) # downloadview self.tabbed_browser.cur_fullscreen_requested.connect( self._downloadview.on_fullscreen_requested) # command input / completion mode_manager.entered.connect( self.tabbed_browser.on_mode_entered) mode_manager.left.connect( self.tabbed_browser.on_mode_left) self.status.cmd.clear_completion_selection.connect( self._completion.on_clear_completion_selection) self.status.cmd.hide_completion.connect( self._completion.hide) self.status.release_focus.connect(self.tabbed_browser.on_release_focus) def _set_decoration(self, hidden): """Set the visibility of the window decoration via Qt.""" if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing window_flags = cast(Qt.WindowFlags, Qt.WindowType.Window) else: window_flags = Qt.WindowType.Window refresh_window = self.isVisible() if hidden: modifiers = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.NoDropShadowWindowHint window_flags |= modifiers self.setWindowFlags(window_flags) if utils.is_mac and hidden and not qtutils.version_check('6.3', compiled=False): # WORKAROUND for https://codereview.qt-project.org/c/qt/qtbase/+/371279 from ctypes import c_void_p # pylint: disable=import-error from objc import objc_object from AppKit import NSWindowStyleMaskResizable win = objc_object(c_void_p=c_void_p(int(self.winId()))).window() win.setStyleMask_(win.styleMask() | NSWindowStyleMaskResizable) if refresh_window: self.show() @pyqtSlot(bool) def _on_fullscreen_requested(self, on): if not config.val.content.fullscreen.window: if on: self.state_before_fullscreen = self.windowState() self.setWindowState(Qt.WindowState.WindowFullScreen | self.state_before_fullscreen) elif self.isFullScreen(): self.setWindowState(self.state_before_fullscreen) log.misc.debug('on: {}, state before fullscreen: {}'.format( on, debug.qflags_key(Qt, self.state_before_fullscreen))) @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() def close(self): """Close the current window. // Extend close() so we can register it as a command. """ super().close() def resizeEvent(self, e): """Extend resizewindow's resizeEvent to adjust completion. Args: e: The QResizeEvent """ super().resizeEvent(e) self._update_overlay_geometries() self._downloadview.updateGeometry() self.tabbed_browser.widget.tab_bar().refresh() def showEvent(self, e): """Extend showEvent to register us as the last-visible-main-window. Args: e: The QShowEvent """ super().showEvent(e) objreg.register('last-visible-main-window', self, update=True) def _confirm_quit(self): """Confirm that this window should be closed. Return: True if closing is okay, False if a closeEvent should be ignored. """ tab_count = self.tabbed_browser.widget.count() window_count = len(objreg.window_registry) download_count = self._download_model.running_downloads() quit_texts = [] # Ask if multiple-tabs are open if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1: quit_texts.append("{} tabs are open.".format(tab_count)) # Ask if downloads running if ('downloads' in config.val.confirm_quit and download_count > 0 and window_count <= 1): quit_texts.append("{} {} running.".format( download_count, "download is" if download_count == 1 else "downloads are")) # Process all quit messages that user must confirm if quit_texts or 'always' in config.val.confirm_quit: msg = jinja.environment.from_string("""

    {% for text in quit_texts %}
  • {{text}}
  • {% endfor %}
""".strip()).render(quit_texts=quit_texts) confirmed = message.ask('Really quit?', msg, mode=usertypes.PromptMode.yesno, default=True) # Stop asking if the user cancels if not confirmed: log.destroy.debug("Cancelling closing of window {}".format( self.win_id)) return False return True def maybe_raise(self) -> None: """Raise the window if self.should_raise is set.""" if self.should_raise: raise_window(self) self.should_raise = False def closeEvent(self, e): """Override closeEvent to display a confirmation if needed.""" if crashsignal.crash_handler.is_crashing: e.accept() return if not self._confirm_quit(): e.ignore() return e.accept() for key in ['last-visible-main-window', 'last-focused-main-window']: try: win = objreg.get(key) if self is win: objreg.delete(key) except KeyError: pass sessions.session_manager.save_last_window_session() self._save_geometry() # Wipe private data if we close the last private window, but there are # still other windows if ( self.is_private and len(objreg.window_registry) > 1 and len([window for window in objreg.window_registry.values() if window.is_private]) == 1 ): log.destroy.debug("Wiping private data before closing last " "private window") websettings.clear_private_data() log.destroy.debug("Closing window {}".format(self.win_id)) self.tabbed_browser.shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/messageview.py0000644000175100017510000001340115102145205023172 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Showing messages above the statusbar.""" from typing import Optional from collections.abc import MutableSequence from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt from qutebrowser.qt.widgets import QWidget, QVBoxLayout, QLabel, QSizePolicy from qutebrowser.config import config, stylesheet from qutebrowser.utils import usertypes, message class Message(QLabel): """A single error/warning/info message.""" def __init__( self, level: usertypes.MessageLevel, text: str, replace: Optional[str], text_format: Qt.TextFormat, parent: QWidget = None, ) -> None: super().__init__(text, parent) self.replace = replace self.level = level self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) self.setWordWrap(True) self.setTextFormat(text_format) qss = """ padding-top: 2px; padding-bottom: 2px; """ if level == usertypes.MessageLevel.error: qss += """ background-color: {{ conf.colors.messages.error.bg }}; color: {{ conf.colors.messages.error.fg }}; font: {{ conf.fonts.messages.error }}; border-bottom: 1px solid {{ conf.colors.messages.error.border }}; """ elif level == usertypes.MessageLevel.warning: qss += """ background-color: {{ conf.colors.messages.warning.bg }}; color: {{ conf.colors.messages.warning.fg }}; font: {{ conf.fonts.messages.warning }}; border-bottom: 1px solid {{ conf.colors.messages.warning.border }}; """ elif level == usertypes.MessageLevel.info: qss += """ background-color: {{ conf.colors.messages.info.bg }}; color: {{ conf.colors.messages.info.fg }}; font: {{ conf.fonts.messages.info }}; border-bottom: 1px solid {{ conf.colors.messages.info.border }} """ else: # pragma: no cover raise ValueError("Invalid level {!r}".format(level)) stylesheet.set_register(self, qss, update=False) @staticmethod def _text_format(info: message.MessageInfo) -> Qt.TextFormat: """The Qt.TextFormat to use based on the given MessageInfo.""" return Qt.TextFormat.RichText if info.rich else Qt.TextFormat.PlainText @classmethod def from_info(cls, info: message.MessageInfo, parent: QWidget = None) -> "Message": return cls( level=info.level, text=info.text, replace=info.replace, text_format=cls._text_format(info), parent=parent, ) def update_from_info(self, info: message.MessageInfo) -> None: """Update the text from the given info. Both the message this gets called on and the given MessageInfo need to have the same level. """ assert self.level == info.level, (self, info) self.setTextFormat(self._text_format(info)) self.setText(info.text) class MessageView(QWidget): """Widget which stacks error/warning/info messages.""" update_geometry = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self._messages: MutableSequence[Message] = [] self._vbox = QVBoxLayout(self) self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self._clear_timer = usertypes.Timer() self._clear_timer.timeout.connect(self.clear_messages) config.instance.changed.connect(self._set_clear_timer_interval) self._last_info = None @config.change_filter('messages.timeout') def _set_clear_timer_interval(self): """Configure self._clear_timer according to the config.""" interval = config.val.messages.timeout if interval > 0: interval *= min(5, len(self._messages)) self._clear_timer.setInterval(interval) def _remove_message(self, widget): """Fully remove and destroy widget from this object.""" self._vbox.removeWidget(widget) widget.hide() widget.deleteLater() @pyqtSlot() def clear_messages(self): """Hide and delete all messages.""" for widget in self._messages: self._remove_message(widget) self._messages = [] self._last_info = None self.hide() self._clear_timer.stop() @pyqtSlot(message.MessageInfo) def show_message(self, info: message.MessageInfo) -> None: """Show the given message with the given MessageLevel.""" if info == self._last_info: return if info.replace is not None: existing = [msg for msg in self._messages if msg.replace == info.replace] if existing: assert len(existing) == 1, existing existing[0].update_from_info(info) self.update_geometry.emit() return widget = Message.from_info(info) self._vbox.addWidget(widget) widget.show() self._messages.append(widget) self._last_info = info self.show() self.update_geometry.emit() if config.val.messages.timeout != 0: self._set_clear_timer_interval() self._clear_timer.start() def mousePressEvent(self, e): """Clear messages when they are clicked on.""" if e.button() in [Qt.MouseButton.LeftButton, Qt.MouseButton.MiddleButton, Qt.MouseButton.RightButton]: self.clear_messages() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/prompt.py0000644000175100017510000011227115102145205022201 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Showing prompts above the statusbar.""" import os.path import html import collections import functools import dataclasses from typing import Optional, cast from collections.abc import MutableSequence from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, QItemSelectionModel, QObject, QEventLoop, QUrl) from qutebrowser.qt.widgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QTreeView, QSizePolicy, QSpacerItem, QFileIconProvider) from qutebrowser.qt.gui import (QFileSystemModel, QIcon) from qutebrowser.browser import downloads from qutebrowser.config import config, configtypes, configexc, stylesheet from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message from qutebrowser.keyinput import modeman from qutebrowser.api import cmdutils from qutebrowser.utils import urlmatch, urlutils prompt_queue = cast('PromptQueue', None) @dataclasses.dataclass class AuthInfo: """Authentication info returned by a prompt.""" user: str password: str class Error(Exception): """Base class for errors in this module.""" class UnsupportedOperationError(Error): """Raised when the prompt class doesn't support the requested operation.""" class PromptQueue(QObject): """Global manager and queue for upcoming prompts. The way in which multiple questions are handled deserves some explanation. If a question is blocking, we *need* to ask it immediately, and can't wait for previous questions to finish. We could theoretically ask a blocking question inside of another blocking one, so in ask_question we simply save the current question on the stack, let the user answer the *most recent* question, and then restore the previous state. With a non-blocking question, things are a bit easier. We simply add it to self._queue if we're still busy handling another question, since it can be answered at any time. In either case, as soon as we finished handling a question, we call _pop_later() which schedules a _pop to ask the next question in _queue. We schedule it rather than doing it immediately because then the order of how things happen is clear, e.g. on_mode_left can't happen after we already set up the *new* question. Attributes: _shutting_down: Whether we're currently shutting down the prompter and should ignore future questions to avoid segfaults. _loops: A list of local EventLoops to spin in when blocking. _queue: A deque of waiting questions. _question: The current Question object if we're handling a question. Signals: show_prompts: Emitted with a Question object when prompts should be shown. """ show_prompts = pyqtSignal(usertypes.Question) def __init__(self, parent=None): super().__init__(parent) self._question = None self._shutting_down = False self._loops: MutableSequence[qtutils.EventLoop] = [] self._queue: collections.deque[usertypes.Question] = collections.deque() message.global_bridge.mode_left.connect(self._on_mode_left) def __repr__(self): return utils.get_repr(self, loops=len(self._loops), queue=len(self._queue), question=self._question) def _pop_later(self): """Helper to call self._pop as soon as everything else is done.""" QTimer.singleShot(0, self._pop) def _pop(self): """Pop a question from the queue and ask it, if there are any.""" log.prompt.debug("Popping from queue {}".format(self._queue)) if self._queue: question = self._queue.popleft() if not question.is_aborted: # the question could already be aborted, e.g. by a cancelled # download. See # https://github.com/qutebrowser/qutebrowser/issues/415 and # https://github.com/qutebrowser/qutebrowser/issues/1249 self.ask_question(question, blocking=False) def shutdown(self): """Cancel all blocking questions. Quits and removes all running event loops. """ log.prompt.debug(f"Shutting down with loops {self._loops}") self._shutting_down = True for loop in self._loops: loop.quit() loop.deleteLater() @pyqtSlot(usertypes.Question, bool) def ask_question(self, question, blocking): """Display a prompt for a given question. Args: question: The Question object to ask. blocking: If True, this function blocks and returns the result. Return: The answer of the user when blocking=True. None if blocking=False. """ log.prompt.debug("Asking question {}, blocking {}, loops {}, queue " "{}".format(question, blocking, self._loops, self._queue)) if self._shutting_down: # If we're currently shutting down we have to ignore this question # to avoid segfaults - see # https://github.com/qutebrowser/qutebrowser/issues/95 log.prompt.debug("Ignoring question because we're shutting down.") question.abort() return None if self._question is not None and not blocking: # We got an async question, but we're already busy with one, so we # just queue it up for later. log.prompt.debug("Adding {} to queue.".format(question)) self._queue.append(question) return None if blocking: # If we're blocking we save the old question on the stack, so we # can restore it after exec, if exec gets called multiple times. log.prompt.debug("New question is blocking, saving {}".format( self._question)) old_question = self._question if old_question is not None: old_question.interrupted = True self._question = question self.show_prompts.emit(question) if blocking: loop = qtutils.EventLoop() self._loops.append(loop) loop.destroyed.connect(lambda: self._loops.remove(loop)) question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) log.prompt.debug("Starting loop.exec() for {}".format(question)) flags = QEventLoop.ProcessEventsFlag.ExcludeSocketNotifiers loop.exec(flags) log.prompt.debug("Ending loop.exec() for {}".format(question)) log.prompt.debug("Restoring old question {}".format(old_question)) self._question = old_question self.show_prompts.emit(old_question) if old_question is None: # Nothing left to restore, so we can go back to popping async # questions. if self._queue: self._pop_later() return question.answer else: question.completed.connect(self._pop_later) return None @pyqtSlot(usertypes.KeyMode) def _on_mode_left(self, mode): """Abort question when a prompt mode was left.""" if mode not in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: return if self._question is None: return log.prompt.debug("Left mode {}, hiding {}".format( mode, self._question)) self.show_prompts.emit(None) if self._question.answer is None and not self._question.is_aborted: log.prompt.debug("Cancelling {} because {} was left".format( self._question, mode)) self._question.cancel() self._question = None class PromptContainer(QWidget): """Container for prompts to be shown above the statusbar. This is a per-window object, however each window shows the same prompt. Attributes: _layout: The layout used to show prompts in. _win_id: The window ID this object is associated with. Signals: update_geometry: Emitted when the geometry should be updated. """ STYLESHEET = """ QWidget#PromptContainer { {% if conf.statusbar.position == 'top' %} border-bottom-left-radius: {{ conf.prompt.radius }}px; border-bottom-right-radius: {{ conf.prompt.radius }}px; {% else %} border-top-left-radius: {{ conf.prompt.radius }}px; border-top-right-radius: {{ conf.prompt.radius }}px; {% endif %} } QWidget { font: {{ conf.fonts.prompts }}; color: {{ conf.colors.prompts.fg }}; background-color: {{ conf.colors.prompts.bg }}; } QLineEdit { border: {{ conf.colors.prompts.border }}; } QTreeView { selection-color: {{ conf.colors.prompts.selected.fg }}; selection-background-color: {{ conf.colors.prompts.selected.bg }}; border: {{ conf.colors.prompts.border }}; } QTreeView::branch { background-color: {{ conf.colors.prompts.bg }}; } QTreeView::item:selected, QTreeView::item:selected:hover, QTreeView::branch:selected { color: {{ conf.colors.prompts.selected.fg }}; background-color: {{ conf.colors.prompts.selected.bg }}; } """ update_geometry = pyqtSignal() release_focus = pyqtSignal() def __init__(self, win_id, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) self._layout.setContentsMargins(10, 10, 10, 10) self._win_id = win_id self._prompt: Optional[_BasePrompt] = None self.setObjectName('PromptContainer') self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) stylesheet.set_register(self) message.global_bridge.prompt_done.connect(self._on_prompt_done) prompt_queue.show_prompts.connect(self._on_show_prompts) message.global_bridge.mode_left.connect(self._on_global_mode_left) def __repr__(self): return utils.get_repr(self, win_id=self._win_id) @pyqtSlot(usertypes.Question) def _on_show_prompts(self, question): """Show a prompt for the given question. Args: question: A Question object or None. """ item = qtutils.add_optional(self._layout.takeAt(0)) if item is None: widget = None else: widget = item.widget() assert widget is not None log.prompt.debug(f"Deleting old prompt {widget!r}") widget.deleteLater() if question is None: log.prompt.debug("No prompts left, hiding prompt container.") self._prompt = None self.release_focus.emit() self.hide() return elif widget is not None: # We have more prompts to show, just hide the old one. # This needs to happen *after* we possibly hid the entire prompt container, # so that keyboard focus can be reassigned properly via release_focus. widget.hide() classes = { usertypes.PromptMode.yesno: YesNoPrompt, usertypes.PromptMode.text: LineEditPrompt, usertypes.PromptMode.user_pwd: AuthenticationPrompt, usertypes.PromptMode.download: DownloadFilenamePrompt, usertypes.PromptMode.alert: AlertPrompt, } klass = classes[question.mode] prompt = klass(question) log.prompt.debug("Displaying prompt {}".format(prompt)) self._prompt = prompt # If this question was interrupted, we already connected the signal if not question.interrupted: question.aborted.connect( functools.partial(self._on_aborted, prompt.KEY_MODE)) modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked') self.setSizePolicy(prompt.sizePolicy()) self._layout.addWidget(prompt) prompt.show() self.show() prompt.setFocus() self.update_geometry.emit() @pyqtSlot() def _on_aborted(self, key_mode): """Leave KEY_MODE whenever a prompt is aborted.""" try: modeman.leave(self._win_id, key_mode, 'aborted', maybe=True) except (objreg.RegistryUnavailableError, RuntimeError): # window was deleted: ignore log.prompt.debug(f"Ignoring leaving {key_mode} as window was deleted") @pyqtSlot(usertypes.KeyMode) def _on_prompt_done(self, key_mode): """Leave the prompt mode in this window if a question was answered.""" modeman.leave(self._win_id, key_mode, ':prompt-accept', maybe=True) @pyqtSlot(usertypes.KeyMode) def _on_global_mode_left(self, mode): """Leave prompt/yesno mode in this window if it was left elsewhere. This ensures no matter where a prompt was answered, we leave the prompt mode and dispose of the prompt object in every window. """ if mode not in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: return modeman.leave(self._win_id, mode, 'left in other window', maybe=True) item = self._layout.takeAt(0) if item is not None: widget = item.widget() assert widget is not None log.prompt.debug("Deleting prompt {}".format(widget)) widget.hide() widget.deleteLater() @cmdutils.register(instance='prompt-container', scope='window', modes=[usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]) def prompt_accept(self, value=None, *, save=False): """Accept the current prompt. // This executes the next action depending on the question mode, e.g. asks for the password or leaves the mode. Args: value: If given, uses this value instead of the entered one. For boolean prompts, "yes"/"no" are accepted as value. save: Save the value to the config. """ assert self._prompt is not None question = self._prompt.question try: done = self._prompt.accept(value, save=save) except Error as e: raise cmdutils.CommandError(str(e)) if done: message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE) question.done() @cmdutils.register(instance='prompt-container', scope='window', modes=[usertypes.KeyMode.prompt], maxsplit=0) def prompt_open_download(self, cmdline: str = None, pdfjs: bool = False) -> None: """Immediately open a download. If no specific command is given, this will use the system's default application to open the file. Args: cmdline: The command which should be used to open the file. A `{}` is expanded to the temporary file name. If no `{}` is present, the filename is automatically appended to the cmdline. pdfjs: Open the download via PDF.js. """ assert self._prompt is not None try: self._prompt.download_open(cmdline, pdfjs=pdfjs) except UnsupportedOperationError: pass @cmdutils.register(instance='prompt-container', scope='window', modes=[usertypes.KeyMode.prompt]) @cmdutils.argument('which', choices=['next', 'prev']) def prompt_item_focus(self, which): """Shift the focus of the prompt file completion menu to another item. Args: which: 'next', 'prev' """ assert self._prompt is not None try: self._prompt.item_focus(which) except UnsupportedOperationError: pass @cmdutils.register( instance='prompt-container', scope='window', modes=[usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]) def prompt_yank(self, sel=False): """Yank URL to clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. """ assert self._prompt is not None question = self._prompt.question if question.url is None: message.error('No URL found.') return if sel and utils.supports_selection(): target = 'primary selection' else: sel = False target = 'clipboard' url_str = urlutils.get_url_yank_text(QUrl(question.url), pretty=False) utils.set_clipboard(url_str, sel) message.info("Yanked to {}: {}".format(target, url_str)) @cmdutils.register( instance='prompt-container', scope='window', modes=[usertypes.KeyMode.prompt]) def prompt_fileselect_external(self): """Choose a location using a configured external picker. This spawns the external fileselector configured via `fileselect.folder.command`. """ assert self._prompt is not None if not isinstance(self._prompt, FilenamePrompt): raise cmdutils.CommandError( "Can only launch external fileselect for FilenamePrompt, " f"not {self._prompt.__class__.__name__}" ) # XXX to avoid current cyclic import from qutebrowser.browser import shared folders = shared.choose_file(shared.FileSelectionMode.folder) if not folders: message.info("No folder chosen.") return # choose_file already checks that this is max one folder assert len(folders) == 1 self.prompt_accept(folders[0]) class LineEdit(QLineEdit): """A line edit used in prompts.""" def __init__(self, parent=None): super().__init__(parent) self.setStyleSheet(""" QLineEdit { background-color: transparent; } """) self.setAttribute(Qt.WidgetAttribute.WA_MacShowFocusRect, False) def keyPressEvent(self, e): """Override keyPressEvent to paste primary selection on Shift + Ins.""" if e.key() == Qt.Key.Key_Insert and e.modifiers() == Qt.KeyboardModifier.ShiftModifier: try: text = utils.get_clipboard(selection=True, fallback=True) except utils.ClipboardError: # pragma: no cover e.ignore() else: e.accept() self.insert(text) return super().keyPressEvent(e) def __repr__(self): return utils.get_repr(self) class _BasePrompt(QWidget): """Base class for all prompts.""" KEY_MODE = usertypes.KeyMode.prompt def __init__(self, question, parent=None): super().__init__(parent) self.question = question self._vbox = QVBoxLayout(self) self._vbox.setSpacing(15) self._key_grid = None def __repr__(self): return utils.get_repr(self, question=self.question, constructor=True) def _init_texts(self, question): assert question.title is not None, question title = '{}'.format( html.escape(question.title)) title_label = QLabel(title, self) self._vbox.addWidget(title_label) if question.text is not None: # Not doing any HTML escaping here as the text can be formatted text_label = QLabel(question.text) text_label.setWordWrap(True) text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self._vbox.addWidget(text_label) def _init_key_label(self): assert self._key_grid is None, self._key_grid self._key_grid = QGridLayout() self._key_grid.setVerticalSpacing(0) all_bindings = config.key_instance.get_reverse_bindings_for( self.KEY_MODE.name) labels = [] has_bindings = False for cmd, text in self._allowed_commands(): bindings = all_bindings.get(cmd, []) if bindings: has_bindings = True binding = None preferred = ['', ''] for pref in preferred: if pref in bindings: binding = pref if binding is None: binding = bindings[0] key_label = QLabel('{}'.format(html.escape(binding))) else: key_label = QLabel(f'unbound ({html.escape(cmd)})') text_label = QLabel(text) labels.append((key_label, text_label)) for i, (key_label, text_label) in enumerate(labels): self._key_grid.addWidget(key_label, i, 0) self._key_grid.addWidget(text_label, i, 1) spacer = QSpacerItem(0, 0, QSizePolicy.Policy.Expanding) self._key_grid.addItem(spacer, 0, 2) self._vbox.addLayout(self._key_grid) if not has_bindings: label = QLabel( "Note: You seem to have unbound all keys for this prompt " f"({self.KEY_MODE.name} key mode)." "
Run qutebrowser :CMD with a command from above to " "close this prompt, then fix this in your config.") self._vbox.addWidget(label) def _check_save_support(self, save): if save: raise UnsupportedOperationError("Saving answers is only possible " "with yes/no prompts.") def accept(self, value=None, save=False): raise NotImplementedError def download_open(self, cmdline, pdfjs): """Open the download directly if this is a download prompt.""" utils.unused(cmdline) utils.unused(pdfjs) raise UnsupportedOperationError def item_focus(self, _which): """Switch to next file item if this is a filename prompt..""" raise UnsupportedOperationError def _allowed_commands(self): """Get the commands we could run as response to this message.""" raise NotImplementedError class LineEditPrompt(_BasePrompt): """A prompt for a single text value.""" def __init__(self, question, parent=None): super().__init__(question, parent) self._lineedit = LineEdit(self) self._init_texts(question) self._vbox.addWidget(self._lineedit) if question.default: self._lineedit.setText(question.default) self._lineedit.selectAll() self.setFocusProxy(self._lineedit) self._init_key_label() def accept(self, value=None, save=False): self._check_save_support(save) text = value if value is not None else self._lineedit.text() self.question.answer = text return True def _allowed_commands(self): return [('prompt-accept', 'Accept'), ('mode-leave', 'Abort')] class NullIconProvider(QFileIconProvider): """Returns empty icon for everything.""" def __init__(self): super().__init__() self.null_icon = QIcon() def icon(self, _t): return self.null_icon def type(self, _info): return 'unknown' class FilenamePrompt(_BasePrompt): """A prompt for a filename.""" # Note: This *must* be a class variable! If it's not, for unknown reasons, # we get a segfault in Qt/PyQt in QFileInfoGatherer::getInfo() if we have # nested download prompts (i.e. trigger a download while a download prompt # is open already). _null_icon_provider = NullIconProvider() def __init__(self, question, parent=None): super().__init__(question, parent) self._init_texts(question) self._init_key_label() self._lineedit = LineEdit(self) if question.default: self._lineedit.setText(question.default) self._lineedit.textEdited.connect(self._set_fileview_root) self._vbox.addWidget(self._lineedit) self.setFocusProxy(self._lineedit) self._init_fileview() self._set_fileview_root(question.default) if config.val.prompt.filebrowser: self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) self._to_complete = '' self._root_index = QModelIndex() def _directories_hide_show_model(self): """Get rid of non-matching directories.""" num_rows = self._file_model.rowCount(self._root_index) for row in range(num_rows): index = self._file_model.index(row, 0, self._root_index) filename = index.data() hidden = self._to_complete not in filename and filename != '..' self._file_view.setRowHidden(index.row(), index.parent(), hidden) @pyqtSlot(str) def _set_fileview_root(self, path, *, tabbed=False): """Set the root path for the file display.""" separators = os.sep if os.altsep is not None: separators += os.altsep dirname = os.path.dirname(path) basename = os.path.basename(path) if not tabbed: self._to_complete = '' try: if not path: pass elif path in separators and os.path.isdir(path): # Input "/" -> don't strip anything pass elif path[-1] in separators and os.path.isdir(path): # Input like /foo/bar/ -> show /foo/bar/ contents path = path.rstrip(separators) elif os.path.isdir(dirname) and not tabbed: # Input like /foo/ba -> show /foo contents path = dirname self._to_complete = basename else: return except OSError: log.prompt.exception("Failed to get directory information") return self._root_index = self._file_model.setRootPath(path) self._file_view.setRootIndex(self._root_index) self._directories_hide_show_model() @pyqtSlot(QModelIndex) def _insert_path(self, index, *, clicked=True): """Handle an element selection. Args: index: The QModelIndex of the selected element. clicked: Whether the element was clicked. """ if index == QModelIndex(): path = os.path.join(self._file_model.rootPath(), self._to_complete) else: path = os.path.normpath(self._file_model.filePath(index)) if clicked: path += os.sep else: # On Windows, when we have C:\foo and tab over .., we get C:\ path = path.rstrip(os.sep) log.prompt.debug('Inserting path {}'.format(path)) self._lineedit.setText(path) self._lineedit.setFocus() self._set_fileview_root(path, tabbed=True) if clicked: # Avoid having a ..-subtree highlighted self._file_view.setCurrentIndex(QModelIndex()) def _init_fileview(self): self._file_view = QTreeView(self) self._file_model = QFileSystemModel(self) # avoid icon and mime type lookups, they are slow in Qt6 self._file_model.setIconProvider(self._null_icon_provider) self._file_view.setModel(self._file_model) self._file_view.clicked.connect(self._insert_path) if config.val.prompt.filebrowser: self._vbox.addWidget(self._file_view) else: self._file_view.hide() # Only show name self._file_view.setHeaderHidden(True) for col in range(1, 4): self._file_view.setColumnHidden(col, True) # Nothing selected initially self._file_view.setCurrentIndex(QModelIndex()) self._file_model.directoryLoaded.connect(self.on_directory_loaded) @pyqtSlot() def on_directory_loaded(self): """Sort the model after a directory gets loaded. The model needs to be sorted so we get the correct first/last index. NOTE: This needs to be a proper @pystSlot() function, and not a lambda. Otherwise, PyQt seems to fail to disconnect it immediately after the object gets destroyed, and we get segfaults when deleting the directory in unit tests. """ self._file_model.sort(0) def accept(self, value=None, save=False): self._check_save_support(save) text = value if value is not None else self._lineedit.text() text = downloads.transform_path(text) if text is None: message.error("Invalid filename") return False self.question.answer = text return True def item_focus(self, which): # This duplicates some completion code, but I don't see a nicer way... assert which in ['prev', 'next'], which selmodel = self._file_view.selectionModel() assert selmodel is not None parent = self._file_view.rootIndex() first_index = self._file_model.index(0, 0, parent) row = self._file_model.rowCount(parent) - 1 last_index = self._file_model.index(row, 0, parent) if not first_index.isValid(): # No entries return assert last_index.isValid() idx = selmodel.currentIndex() if not idx.isValid(): # No item selected yet idx = last_index if which == 'prev' else first_index elif which == 'prev': idx = self._file_view.indexAbove(idx) else: assert which == 'next', which idx = self._file_view.indexBelow(idx) # wrap around if we arrived at beginning/end if not idx.isValid(): idx = last_index if which == 'prev' else first_index idx = self._do_completion(idx, which) selmodel.setCurrentIndex( idx, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows) self._insert_path(idx, clicked=False) def _do_completion(self, idx, which): while idx.isValid() and self._file_view.isIndexHidden(idx): if which == 'prev': idx = self._file_view.indexAbove(idx) else: assert which == 'next', which idx = self._file_view.indexBelow(idx) return idx def _allowed_commands(self): return [('prompt-accept', 'Accept'), ('mode-leave', 'Abort')] class DownloadFilenamePrompt(FilenamePrompt): """A prompt for a filename for downloads.""" def __init__(self, question, parent=None): super().__init__(question, parent) self._file_model.setFilter( QDir.Filter.AllDirs | QDir.Filter.Drives | QDir.Filter.NoDotAndDotDot) def accept(self, value=None, save=False): done = super().accept(value, save) answer = self.question.answer if answer is not None: self.question.answer = downloads.FileDownloadTarget(answer) return done def download_open(self, cmdline, pdfjs): if pdfjs: target: 'downloads._DownloadTarget' = downloads.PDFJSDownloadTarget() else: target = downloads.OpenFileDownloadTarget(cmdline) self.question.answer = target self.question.done() message.global_bridge.prompt_done.emit(self.KEY_MODE) def _allowed_commands(self): cmds = [ ('prompt-accept', 'Accept'), ('mode-leave', 'Abort'), ('rl-filename-rubout', "Go to parent directory"), ('prompt-open-download', "Open download"), ('prompt-open-download --pdfjs', "Open download via PDF.js"), ('prompt-yank', "Yank URL"), ('prompt-fileselect-external', "Launch external file selector"), ] return cmds class AuthenticationPrompt(_BasePrompt): """A prompt for username/password.""" def __init__(self, question, parent=None): super().__init__(question, parent) self._init_texts(question) user_label = QLabel("Username:", self) self._user_lineedit = LineEdit(self) password_label = QLabel("Password:", self) self._password_lineedit = LineEdit(self) self._password_lineedit.setEchoMode(QLineEdit.EchoMode.Password) grid = QGridLayout() grid.addWidget(user_label, 1, 0) grid.addWidget(self._user_lineedit, 1, 1) grid.addWidget(password_label, 2, 0) grid.addWidget(self._password_lineedit, 2, 1) self._vbox.addLayout(grid) self._init_key_label() assert not question.default, question.default self.setFocusProxy(self._user_lineedit) def accept(self, value=None, save=False): self._check_save_support(save) if value is not None: if ':' not in value: raise Error("Value needs to be in the format " "username:password, but {} was given".format( value)) username, password = value.split(':', maxsplit=1) self.question.answer = AuthInfo(username, password) return True elif self._user_lineedit.hasFocus(): # Earlier, tab was bound to :prompt-accept, so to still support # that we simply switch the focus when tab was pressed. self._password_lineedit.setFocus() return False else: self.question.answer = AuthInfo(self._user_lineedit.text(), self._password_lineedit.text()) return True def item_focus(self, which): """Support switching between fields with tab.""" assert which in ['prev', 'next'], which if which == 'next' and self._user_lineedit.hasFocus(): self._password_lineedit.setFocus() elif which == 'prev' and self._password_lineedit.hasFocus(): self._user_lineedit.setFocus() def _allowed_commands(self): return [('prompt-accept', "Accept"), ('mode-leave', "Abort")] class YesNoPrompt(_BasePrompt): """A prompt with yes/no answers.""" KEY_MODE = usertypes.KeyMode.yesno def __init__(self, question, parent=None): super().__init__(question, parent) self._init_texts(question) self._init_key_label() def _check_save_support(self, save): if save and self.question.option is None: raise Error("No setting available to save the answer for this " "question.") def accept(self, value=None, save=False): self._check_save_support(save) if value is None: if self.question.default is None: raise Error("No default value was set for this question!") self.question.answer = self.question.default elif value == 'yes': self.question.answer = True elif value == 'no': self.question.answer = False else: raise Error("Invalid value {} - expected yes/no!".format(value)) if save: value = self.question.answer opt = config.instance.get_opt(self.question.option) if isinstance(opt.typ, configtypes.Bool): pass elif isinstance(opt.typ, configtypes.AsBool): value = opt.typ.from_bool(value) else: raise AssertionError( f"Cannot save prompt answer ({opt.name}). Expected 'Bool' or 'AsBool' " f"type option, got: value={value} type={type(opt.typ)}" ) pattern = urlmatch.UrlPattern(self.question.url) try: config.instance.set_obj(opt.name, value, pattern=pattern, save_yaml=True) except configexc.Error as e: raise Error(str(e)) return True def _allowed_commands(self): cmds = [] cmds.append(('prompt-accept yes', "Yes")) if self.question.option is not None: cmds.append(('prompt-accept --save yes', "Always")) cmds.append(('prompt-accept no', "No")) if self.question.option is not None: cmds.append(('prompt-accept --save no', "Never")) if self.question.default is not None: assert self.question.default in [True, False] default = 'yes' if self.question.default else 'no' cmds.append(('prompt-accept', "Use default ({})".format(default))) cmds.append(('mode-leave', "Abort")) cmds.append(('prompt-yank', "Yank URL")) return cmds class AlertPrompt(_BasePrompt): """A prompt without any answer possibility.""" def __init__(self, question, parent=None): super().__init__(question, parent) self._init_texts(question) self._init_key_label() def accept(self, value=None, save=False): self._check_save_support(save) if value is not None: raise Error("No value is permitted with alert prompts!") # Simply mark prompt as done without setting self.question.answer return True def _allowed_commands(self): return [('prompt-accept', "Hide")] def init(): """Initialize global prompt objects.""" global prompt_queue prompt_queue = PromptQueue() message.global_bridge.ask_question.connect( # type: ignore[call-arg] prompt_queue.ask_question, Qt.ConnectionType.DirectConnection) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5416389 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/0000755000175100017510000000000015102145351022314 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/__init__.py0000644000175100017510000000024615102145205024425 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Widgets needed for the statusbar.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/backforward.py0000644000175100017510000000212715102145205025153 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Navigation (back/forward) indicator displayed in the statusbar.""" from qutebrowser.mainwindow.statusbar import textbase class Backforward(textbase.TextBase): """Shows navigation indicator (if you can go backward and/or forward).""" def __init__(self, parent=None): super().__init__(parent) self.enabled = False def on_tab_cur_url_changed(self, tabs): """Called on URL changes.""" tab = tabs.widget.currentWidget() if tab is None: # pragma: no cover self.setText('') self.hide() return self.on_tab_changed(tab) def on_tab_changed(self, tab): """Update the text based on the given tab.""" text = '' if tab.history.can_go_back(): text += '<' if tab.history.can_go_forward(): text += '>' if text: text = '[' + text + ']' self.setText(text) self.setVisible(bool(text) and self.enabled) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/bar.py0000644000175100017510000004155715102145205023444 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The main statusbar widget.""" import enum import dataclasses from qutebrowser.qt.core import pyqtSignal, pyqtProperty, pyqtSlot, Qt, QSize, QTimer from qutebrowser.qt.widgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy from qutebrowser.browser import browsertab from qutebrowser.config import config, stylesheet from qutebrowser.keyinput import modeman from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.mainwindow.statusbar import (backforward, command, progress, keystring, percentage, url, tabindex, textbase, clock, searchmatch) @dataclasses.dataclass class ColorFlags: """Flags which change the appearance of the statusbar. Attributes: prompt: If we're currently in prompt-mode. insert: If we're currently in insert mode. command: If we're currently in command mode. mode: The current caret mode (CaretMode.off/.on/.selection). private: Whether this window is in private browsing mode. passthrough: If we're currently in passthrough-mode. """ class CaretMode(enum.Enum): """The current caret "sub-mode" we're in.""" off = enum.auto() on = enum.auto() selection = enum.auto() prompt: bool = False insert: bool = False command: bool = False caret: CaretMode = CaretMode.off private: bool = False passthrough: bool = False def to_stringlist(self): """Get a string list of set flags used in the stylesheet. This also combines flags in ways they're used in the sheet. """ strings = [] if self.prompt: strings.append('prompt') if self.insert: strings.append('insert') if self.command: strings.append('command') if self.private: strings.append('private') if self.passthrough: strings.append('passthrough') if self.private and self.command: strings.append('private-command') if self.caret == self.CaretMode.on: strings.append('caret') elif self.caret == self.CaretMode.selection: strings.append('caret-selection') else: assert self.caret == self.CaretMode.off return strings def _generate_stylesheet(): flags = [ ('private', 'statusbar.private'), ('caret', 'statusbar.caret'), ('caret-selection', 'statusbar.caret.selection'), ('prompt', 'prompts'), ('insert', 'statusbar.insert'), ('command', 'statusbar.command'), ('passthrough', 'statusbar.passthrough'), ('private-command', 'statusbar.command.private'), ] qss = """ QWidget#StatusBar, QWidget#StatusBar QLabel, QWidget#StatusBar QLineEdit { font: {{ conf.fonts.statusbar }}; color: {{ conf.colors.statusbar.normal.fg }}; } QWidget#StatusBar { background-color: {{ conf.colors.statusbar.normal.bg }}; } """ for flag, option in flags: qss += """ QWidget#StatusBar[color_flags~="%s"], QWidget#StatusBar[color_flags~="%s"] QLabel, QWidget#StatusBar[color_flags~="%s"] QLineEdit { color: {{ conf.colors.%s }}; } QWidget#StatusBar[color_flags~="%s"] { background-color: {{ conf.colors.%s }}; } """ % (flag, flag, flag, # noqa: S001 option + '.fg', flag, option + '.bg') return qss class StatusBar(QWidget): """The statusbar at the bottom of the mainwindow. Attributes: txt: The Text widget in the statusbar. keystring: The KeyString widget in the statusbar. percentage: The Percentage widget in the statusbar. url: The UrlText widget in the statusbar. prog: The Progress widget in the statusbar. cmd: The Command widget in the statusbar. search_match: The SearchMatch widget in the statusbar. _hbox: The main QHBoxLayout. _stack: The QStackedLayout with cmd/txt widgets. _win_id: The window ID the statusbar is associated with. Signals: resized: Emitted when the statusbar has resized, so the completion widget can adjust its size to it. arg: The new size. moved: Emitted when the statusbar has moved, so the completion widget can move to the right position. arg: The new position. release_focus: Emitted just before the statusbar is hidden. """ resized = pyqtSignal('QRect') moved = pyqtSignal('QPoint') release_focus = pyqtSignal() STYLESHEET = _generate_stylesheet() def __init__(self, *, win_id, private, parent=None): super().__init__(parent) self.setObjectName(self.__class__.__name__) self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground) stylesheet.set_register(self) self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed) self._win_id = win_id self._color_flags = ColorFlags() self._color_flags.private = private self._hbox = QHBoxLayout(self) self._set_hbox_padding() self._hbox.setSpacing(5) self._stack = QStackedLayout() self._hbox.addLayout(self._stack) self._stack.setContentsMargins(0, 0, 0, 0) self.cmd = command.Command(private=private, win_id=win_id) self._stack.addWidget(self.cmd) objreg.register('status-command', self.cmd, scope='window', window=win_id) self.txt = textbase.TextBase() self._stack.addWidget(self.txt) self.cmd.show_cmd.connect(self._show_cmd_widget) self.cmd.hide_cmd.connect(self._hide_cmd_widget) self._hide_cmd_widget() self.search_match = searchmatch.SearchMatch() self.url = url.UrlText() self.percentage = percentage.Percentage() self.backforward = backforward.Backforward() self.tabindex = tabindex.TabIndex() self.keystring = keystring.KeyString() self.prog = progress.Progress(self) self.clock = clock.Clock() self._text_widgets = [] self._draw_widgets() config.instance.changed.connect(self._on_config_changed) QTimer.singleShot(0, self.maybe_hide) def __repr__(self): return utils.get_repr(self) def _get_widget_from_config(self, key): """Return the widget that fits with config string key.""" if key == 'url': return self.url elif key == 'scroll': return self.percentage elif key == 'scroll_raw': return self.percentage elif key == 'history': return self.backforward elif key == 'tabs': return self.tabindex elif key == 'keypress': return self.keystring elif key == 'progress': return self.prog elif key == 'search_match': return self.search_match elif key.startswith('text:'): new_text_widget = textbase.TextBase() self._text_widgets.append(new_text_widget) return new_text_widget elif key.startswith('clock:') or key == 'clock': return self.clock else: raise utils.Unreachable(key) @pyqtSlot(str) def _on_config_changed(self, option): if option == 'statusbar.show': self.maybe_hide() elif option == 'statusbar.padding': self._set_hbox_padding() elif option == 'statusbar.widgets': self._draw_widgets() def _draw_widgets(self): """Draw statusbar widgets.""" self._clear_widgets() tab = self._current_tab() # Read the list and set widgets accordingly for segment in config.val.statusbar.widgets: widget = self._get_widget_from_config(segment) self._hbox.addWidget(widget) if segment == 'scroll_raw': widget.set_raw() elif segment in ('history', 'progress'): widget.enabled = True if tab: widget.on_tab_changed(tab) # Do not call .show() for these widgets. They are not always shown, and # dynamically show/hide themselves in their on_tab_changed() methods. continue elif segment.startswith('text:'): widget.setText(segment.split(':', maxsplit=1)[1]) elif segment.startswith('clock:') or segment == 'clock': split_segment = segment.split(':', maxsplit=1) if len(split_segment) == 2 and split_segment[1]: widget.format = split_segment[1] else: widget.format = '%X' widget.show() def _clear_widgets(self): """Clear widgets before redrawing them.""" # Start with widgets hidden and show them when needed for widget in [self.url, self.percentage, self.backforward, self.tabindex, self.keystring, self.prog, self.clock, *self._text_widgets]: assert isinstance(widget, QWidget) if widget in [self.prog, self.backforward]: widget.enabled = False # type: ignore[attr-defined] widget.hide() self._hbox.removeWidget(widget) self._text_widgets.clear() @pyqtSlot() def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" strategy = config.val.statusbar.show tab = self._current_tab() if tab is not None and tab.data.fullscreen: self.hide() elif strategy == 'never': self.hide() elif strategy == 'in-mode': try: mode_manager = modeman.instance(self._win_id) except modeman.UnavailableError: self.hide() else: if mode_manager.mode == usertypes.KeyMode.normal: self.hide() else: self.show() elif strategy == 'always': self.show() else: raise utils.Unreachable def _set_hbox_padding(self): padding = config.val.statusbar.padding self._hbox.setContentsMargins(padding.left, 0, padding.right, 0) @pyqtProperty('QStringList') # type: ignore[type-var] def color_flags(self): """Getter for self.color_flags, so it can be used as Qt property.""" return self._color_flags.to_stringlist() def _current_tab(self): """Get the currently displayed tab.""" window = objreg.get('tabbed-browser', scope='window', window=self._win_id) return window.widget.currentWidget() def set_mode_active(self, mode, val): """Setter for self.{insert,command,caret}_active. Re-set the stylesheet after setting the value, so everything gets updated by Qt properly. """ if mode == usertypes.KeyMode.insert: log.statusbar.debug("Setting insert flag to {}".format(val)) self._color_flags.insert = val if mode == usertypes.KeyMode.passthrough: log.statusbar.debug("Setting passthrough flag to {}".format(val)) self._color_flags.passthrough = val if mode == usertypes.KeyMode.command: log.statusbar.debug("Setting command flag to {}".format(val)) self._color_flags.command = val elif mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: log.statusbar.debug("Setting prompt flag to {}".format(val)) self._color_flags.prompt = val elif mode == usertypes.KeyMode.caret: if not val: # Turning on is handled in on_current_caret_selection_toggled log.statusbar.debug("Setting caret mode off") self._color_flags.caret = ColorFlags.CaretMode.off stylesheet.set_register(self, update=False) def _set_mode_text(self, mode): """Set the mode text.""" if mode == 'passthrough': key_instance = config.key_instance all_bindings = key_instance.get_reverse_bindings_for('passthrough') bindings = all_bindings.get('mode-leave') if bindings: suffix = ' ({} to leave)'.format(' or '.join(bindings)) else: suffix = '' else: suffix = '' text = "-- {} MODE --{}".format(mode.upper(), suffix) self.txt.setText(text) def _show_cmd_widget(self): """Show command widget instead of temporary text.""" self._stack.setCurrentWidget(self.cmd) self.show() def _hide_cmd_widget(self): """Show temporary text instead of command widget.""" log.statusbar.debug("Hiding cmd widget") self.release_focus.emit() self._stack.setCurrentWidget(self.txt) self.maybe_hide() @pyqtSlot(str) def set_text(self, text): """Set a normal (persistent) text in the status bar.""" log.message.debug(text) self.txt.setText(text) @pyqtSlot(usertypes.KeyMode) def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" if config.val.statusbar.show == 'in-mode' and mode != usertypes.KeyMode.command: # Showing in command mode is handled via _show_cmd_widget() self.show() mode_manager = modeman.instance(self._win_id) if mode_manager.parsers[mode].passthrough: self._set_mode_text(mode.name) if mode in [usertypes.KeyMode.insert, usertypes.KeyMode.command, usertypes.KeyMode.caret, usertypes.KeyMode.prompt, usertypes.KeyMode.yesno, usertypes.KeyMode.passthrough]: self.set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): """Clear marked mode.""" if config.val.statusbar.show == 'in-mode' and old_mode != usertypes.KeyMode.command: # Hiding in command mode is handled via _hide_cmd_widget() self.hide() mode_manager = modeman.instance(self._win_id) if mode_manager.parsers[old_mode].passthrough: if mode_manager.parsers[new_mode].passthrough: self._set_mode_text(new_mode.name) else: self.txt.setText('') if old_mode in [usertypes.KeyMode.insert, usertypes.KeyMode.command, usertypes.KeyMode.caret, usertypes.KeyMode.prompt, usertypes.KeyMode.yesno, usertypes.KeyMode.passthrough]: self.set_mode_active(old_mode, False) @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): """Notify sub-widgets when the tab has been changed.""" self.url.on_tab_changed(tab) self.prog.on_tab_changed(tab) self.percentage.on_tab_changed(tab) self.backforward.on_tab_changed(tab) self.maybe_hide() assert tab.is_private == self._color_flags.private @pyqtSlot(browsertab.SelectionState) def on_caret_selection_toggled(self, selection_state): """Update the statusbar when entering/leaving caret selection mode.""" log.statusbar.debug("Setting caret selection {}" .format(selection_state)) if selection_state is browsertab.SelectionState.normal: self._set_mode_text("caret selection") self._color_flags.caret = ColorFlags.CaretMode.selection elif selection_state is browsertab.SelectionState.line: self._set_mode_text("caret line selection") self._color_flags.caret = ColorFlags.CaretMode.selection else: self._set_mode_text("caret") self._color_flags.caret = ColorFlags.CaretMode.on stylesheet.set_register(self, update=False) def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. Args: e: The QResizeEvent. """ super().resizeEvent(e) self.resized.emit(self.geometry()) def moveEvent(self, e): """Extend moveEvent of QWidget to emit a moved signal afterwards. Args: e: The QMoveEvent. """ super().moveEvent(e) self.moved.emit(e.pos()) def minimumSizeHint(self): """Set the minimum height to the text height plus some padding.""" padding = config.cache['statusbar.padding'] width = super().minimumSizeHint().width() height = self.fontMetrics().height() + padding.top + padding.bottom return QSize(width, height) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/clock.py0000644000175100017510000000225615102145205023764 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Clock displayed in the statusbar.""" from datetime import datetime from qutebrowser.qt.core import Qt from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.utils import usertypes class Clock(textbase.TextBase): """Shows current time and date in the statusbar.""" UPDATE_DELAY = 500 # ms def __init__(self, parent=None): super().__init__(parent, elidemode=Qt.TextElideMode.ElideNone) self.format = "" self.timer = usertypes.Timer(self) self.timer.timeout.connect(self._show_time) def _show_time(self): """Set text to current time, using self.format as format-string.""" self.setText(datetime.now().strftime(self.format)) def hideEvent(self, event): """Stop timer when widget is hidden.""" self.timer.stop() super().hideEvent(event) def showEvent(self, event): """Override showEvent to show time and start self.timer for updating.""" self.timer.start(Clock.UPDATE_DELAY) self._show_time() super().showEvent(event) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/command.py0000644000175100017510000002523315102145205024307 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The commandline in the statusbar.""" from typing import Optional, cast from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSignal, pyqtSlot, Qt, QSize from qutebrowser.qt.gui import QKeyEvent from qutebrowser.qt.widgets import QSizePolicy, QWidget from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.api import cmdutils from qutebrowser.misc import cmdhistory, editor from qutebrowser.misc import miscwidgets as misc from qutebrowser.utils import usertypes, log, objreg, message, utils from qutebrowser.config import config class Command(misc.CommandLineEdit): """The commandline part of the statusbar. Attributes: _win_id: The window ID this widget is associated with. Signals: got_cmd: Emitted when a command is triggered by the user. arg: The command string and also potentially the count. got_search: Emitted when a search should happen. clear_completion_selection: Emitted before the completion widget is hidden. hide_completion: Emitted when the completion widget should be hidden. update_completion: Emitted when the completion should be shown/updated. show_cmd: Emitted when command input should be shown. hide_cmd: Emitted when command input can be hidden. """ got_cmd = pyqtSignal([str], [str, int]) got_search = pyqtSignal(str, bool) # text, reverse clear_completion_selection = pyqtSignal() hide_completion = pyqtSignal() update_completion = pyqtSignal() show_cmd = pyqtSignal() hide_cmd = pyqtSignal() def __init__(self, *, win_id: int, private: bool, parent: QWidget = None) -> None: super().__init__(parent) self._win_id = win_id if not private: command_history = objreg.get('command-history') self.history.history = command_history.data self.history.changed.connect(command_history.changed) self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Ignored) self.cursorPositionChanged.connect(self.update_completion) self.textChanged.connect(self.update_completion) self.textChanged.connect(self.updateGeometry) self.textChanged.connect(self._incremental_search) self.setStyleSheet( """ QLineEdit { border: 0px; padding-left: 1px; background-color: transparent; } """ ) self.setAttribute(Qt.WidgetAttribute.WA_MacShowFocusRect, False) def _handle_search(self) -> bool: """Check if the currently entered text is a search, and if so, run it. Return: True if a search was executed, False otherwise. """ if self.prefix() == '/': self.got_search.emit(self.text()[1:], False) return True elif self.prefix() == '?': self.got_search.emit(self.text()[1:], True) return True else: return False def prefix(self) -> str: """Get the currently entered command prefix.""" text = self.text() if not text: return '' elif text[0] in modeparsers.STARTCHARS: return text[0] else: return '' def cmd_set_text(self, text: str) -> None: """Preset the statusbar to some text. Args: text: The text to set as string. """ self.setText(text) log.modes.debug("Setting command text, focusing {!r}".format(self)) modeman.enter(self._win_id, usertypes.KeyMode.command, 'cmd focus') self.setFocus() self.show_cmd.emit() @cmdutils.register(instance='status-command', name='cmd-set-text', scope='window', maxsplit=0, deprecated_name='set-cmd-text') @cmdutils.argument('count', value=cmdutils.Value.count) def cmd_set_text_command(self, text: str, count: int = None, space: bool = False, append: bool = False, run_on_count: bool = False) -> None: """Preset the statusbar to some text. // Wrapper for cmd_set_text to check the arguments and allow multiple strings which will get joined. Args: text: The commandline to set. count: The count if given. space: If given, a space is added to the end. append: If given, the text is appended to the current text. run_on_count: If given with a count, the command is run with the given count rather than setting the command text. """ if space: text += ' ' if append: if not self.text(): raise cmdutils.CommandError("No current text!") text = self.text() + text if not text or text[0] not in modeparsers.STARTCHARS: raise cmdutils.CommandError( "Invalid command text '{}'.".format(text)) if run_on_count and count is not None: self.got_cmd[str, int].emit(text, count) else: self.cmd_set_text(text) @cmdutils.register(instance='status-command', modes=[usertypes.KeyMode.command], scope='window') def command_history_prev(self) -> None: """Go back in the commandline history.""" try: if not self.history.is_browsing(): item = self.history.start(self.text().strip()) else: item = self.history.previtem() except (cmdhistory.HistoryEmptyError, cmdhistory.HistoryEndReachedError): return if item: self.cmd_set_text(item) @cmdutils.register(instance='status-command', modes=[usertypes.KeyMode.command], scope='window') def command_history_next(self) -> None: """Go forward in the commandline history.""" if not self.history.is_browsing(): return try: item = self.history.nextitem() except cmdhistory.HistoryEndReachedError: return if item: self.cmd_set_text(item) @cmdutils.register(instance='status-command', modes=[usertypes.KeyMode.command], scope='window') def command_accept(self, rapid: bool = False) -> None: """Execute the command currently in the commandline. Args: rapid: Run the command without closing or clearing the command bar. """ was_search = self._handle_search() text = self.text() if not (self.prefix() == ':' and text[1:].startswith(' ')): self.history.append(text) if not rapid: modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept') if not was_search: self.got_cmd[str].emit(text[1:]) @cmdutils.register(instance='status-command', scope='window', deprecated_name='edit-command') def cmd_edit(self, run: bool = False) -> None: """Open an editor to modify the current command. Args: run: Run the command if the editor exits successfully. """ ed = editor.ExternalEditor(parent=self) def callback(text: str) -> None: """Set the commandline to the edited text.""" if not text or text[0] not in modeparsers.STARTCHARS: message.error('command must start with one of {}' .format(modeparsers.STARTCHARS)) return self.cmd_set_text(text) if run: self.command_accept() ed.file_updated.connect(callback) ed.edit(self.text()) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode: usertypes.KeyMode) -> None: """Clear up when command mode was left. - Clear the statusbar text if it's explicitly unfocused. - Clear completion selection - Hide completion Args: mode: The mode which was left. """ if mode == usertypes.KeyMode.command: self.setText('') self.history.stop() self.hide_cmd.emit() self.clear_completion_selection.emit() self.hide_completion.emit() def setText(self, text: Optional[str]) -> None: """Extend setText to set prefix and make sure the prompt is ok.""" if not text: pass elif text[0] in modeparsers.STARTCHARS: super().set_prompt(text[0]) else: raise utils.Unreachable("setText got called with invalid text " "'{}'!".format(text)) # FIXME:mypy PyQt6 stubs issue if machinery.IS_QT6: text = cast(str, text) super().setText(text) def keyPressEvent(self, e: Optional[QKeyEvent]) -> None: """Override keyPressEvent to ignore Return key presses, and add Shift-Ins. If this widget is focused, we are in passthrough key mode, and Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished without command_accept to be called. """ assert e is not None if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing shift = cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier) else: shift = Qt.KeyboardModifier.ShiftModifier text = self.text() if text in modeparsers.STARTCHARS and e.key() == Qt.Key.Key_Backspace: e.accept() modeman.leave(self._win_id, usertypes.KeyMode.command, 'prefix deleted') elif e.key() == Qt.Key.Key_Return: e.ignore() elif e.key() == Qt.Key.Key_Insert and e.modifiers() == shift: try: text = utils.get_clipboard(selection=True, fallback=True) except utils.ClipboardError: e.ignore() else: e.accept() self.insert(text) else: super().keyPressEvent(e) def sizeHint(self) -> QSize: """Dynamically calculate the needed size.""" height = super().sizeHint().height() text = self.text() if not text: text = 'x' width = self.fontMetrics().boundingRect(text).width() return QSize(width, height) @pyqtSlot() def _incremental_search(self) -> None: if not config.val.search.incremental: return self._handle_search() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/keystring.py0000644000175100017510000000101515102145205024700 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Keychain string displayed in the statusbar.""" from qutebrowser.qt.core import pyqtSlot from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.utils import usertypes class KeyString(textbase.TextBase): """Keychain string displayed in the statusbar.""" @pyqtSlot(usertypes.KeyMode, str) def on_keystring_updated(self, _mode, keystr): self.setText(keystr) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/percentage.py0000644000175100017510000000301415102145205024777 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Scroll percentage displayed in the statusbar.""" from qutebrowser.qt.core import pyqtSlot, Qt from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.misc import throttle from qutebrowser.utils import utils class Percentage(textbase.TextBase): """Reading percentage displayed in the statusbar.""" def __init__(self, parent=None): """Constructor. Set percentage to 0%.""" super().__init__(parent, elidemode=Qt.TextElideMode.ElideNone) self._strings = self._calc_strings() self._set_text = throttle.Throttle(self.setText, 100, parent=self) self.set_perc(0, 0) def set_raw(self): self._strings = self._calc_strings(raw=True) def _calc_strings(self, raw=False): """Pre-calculate strings for the statusbar.""" fmt = '[{:02}]' if raw else '[{:02}%]' strings = {i: fmt.format(i) for i in range(1, 100)} strings.update({0: '[top]', 100: '[bot]'}) return strings @pyqtSlot(int, int) def set_perc(self, x, y): """Setter to be used as a Qt slot. Args: x: The x percentage (int), currently ignored. y: The y percentage (int) """ utils.unused(x) self._set_text(self._strings.get(y, '[???]')) def on_tab_changed(self, tab): """Update scroll position when tab changed.""" self.set_perc(*tab.scroller.pos_perc()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/progress.py0000644000175100017510000000435615102145205024540 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The progress bar in the statusbar.""" from qutebrowser.qt.core import pyqtSlot, QSize from qutebrowser.qt.widgets import QProgressBar, QSizePolicy from qutebrowser.config import stylesheet from qutebrowser.utils import utils, usertypes class Progress(QProgressBar): """The progress bar part of the status bar.""" STYLESHEET = """ QProgressBar { border-radius: 0px; border: 2px solid transparent; background-color: transparent; font: {{ conf.fonts.statusbar }}; } QProgressBar::chunk { background-color: {{ conf.colors.statusbar.progress.bg }}; } """ def __init__(self, parent=None): super().__init__(parent) stylesheet.set_register(self) self.enabled = False self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.setTextVisible(False) self.hide() def __repr__(self): return utils.get_repr(self, value=self.value()) @pyqtSlot() def on_load_started(self): """Clear old error and show progress, used as slot to loadStarted.""" self.setValue(0) self.setVisible(self.enabled) @pyqtSlot(int) def on_load_progress(self, value): """Hide the statusbar when loading finished. We use this instead of loadFinished because we sometimes get loadStarted and loadProgress(100) without loadFinished from Qt. WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223 """ self.setValue(value) if value == 100: self.hide() def on_tab_changed(self, tab): """Set the correct value when the current tab changed.""" self.setValue(tab.progress()) if self.enabled and tab.load_status() == usertypes.LoadStatus.loading: self.show() else: self.hide() def sizeHint(self): """Set the height to the text height.""" width = super().sizeHint().width() height = self.fontMetrics().height() return QSize(width, height) def minimumSizeHint(self): return self.sizeHint() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/searchmatch.py0000644000175100017510000000203215102145205025143 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The search match indicator in the statusbar.""" from qutebrowser.qt.core import pyqtSlot from qutebrowser.browser import browsertab from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.utils import log class SearchMatch(textbase.TextBase): """The part of the statusbar that displays the search match counter.""" @pyqtSlot(browsertab.SearchMatch) def set_match(self, search_match: browsertab.SearchMatch) -> None: """Set the match counts in the statusbar. Passing SearchMatch(0, 0) hides the match counter. Args: search_match: The currently active search match. """ if search_match.is_null(): self.setText('') log.statusbar.debug('Clearing search match text.') else: self.setText(f'Match [{search_match}]') log.statusbar.debug(f'Setting search match text to {search_match}') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/tabindex.py0000644000175100017510000000106015102145205024457 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """TabIndex displayed in the statusbar.""" from qutebrowser.qt.core import pyqtSlot from qutebrowser.mainwindow.statusbar import textbase class TabIndex(textbase.TextBase): """Shows current tab index and number of tabs in the statusbar.""" @pyqtSlot(int, int) def on_tab_index_changed(self, current, count): """Update tab index when tab changed.""" self.setText('[{}/{}]'.format(current + 1, count)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/textbase.py0000644000175100017510000000460515102145205024510 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Base text widgets for statusbar.""" from qutebrowser.qt.core import Qt from qutebrowser.qt.widgets import QLabel, QSizePolicy from qutebrowser.qt.gui import QPainter from qutebrowser.utils import qtutils, utils class TextBase(QLabel): """A text in the statusbar. Unlike QLabel, the text will get elided. Eliding is loosely based on https://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html Attributes: _elidemode: Where to elide the text. _elided_text: The current elided text. """ def __init__(self, parent=None, elidemode=Qt.TextElideMode.ElideRight): super().__init__(parent) self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum) self._elidemode = elidemode self._elided_text = '' def __repr__(self): return utils.get_repr(self, text=self.text()) def _update_elided_text(self, width): """Update the elided text when necessary. Args: width: The maximal width the text should take. """ if self.text(): self._elided_text = self.fontMetrics().elidedText( self.text(), self._elidemode, width, Qt.TextFlag.TextShowMnemonic) else: self._elided_text = '' def setText(self, txt): """Extend QLabel::setText to update the elided text afterwards. Args: txt: The text to set (string). """ super().setText(txt) if self._elidemode != Qt.TextElideMode.ElideNone: self._update_elided_text(self.geometry().width()) def resizeEvent(self, e): """Extend QLabel::resizeEvent to update the elided text afterwards.""" super().resizeEvent(e) size = e.size() qtutils.ensure_valid(size) self._update_elided_text(size.width()) def paintEvent(self, e): """Override QLabel::paintEvent to draw elided text.""" if self._elidemode == Qt.TextElideMode.ElideNone: super().paintEvent(e) else: e.accept() painter = QPainter(self) geom = self.geometry() qtutils.ensure_valid(geom) painter.drawText(0, 0, geom.width(), geom.height(), int(self.alignment()), self._elided_text) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/statusbar/url.py0000644000175100017510000001242615102145205023473 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """URL displayed in the statusbar.""" import enum from qutebrowser.qt.core import pyqtSlot, pyqtProperty, QUrl from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.config import stylesheet from qutebrowser.utils import usertypes, urlutils class UrlType(enum.Enum): """The type/color of the URL being shown. Note this has entries for success/error/warn from widgets.webview:LoadStatus. """ success = enum.auto() success_https = enum.auto() error = enum.auto() warn = enum.auto() hover = enum.auto() normal = enum.auto() class UrlText(textbase.TextBase): """URL displayed in the statusbar. Attributes: _normal_url: The normal URL to be displayed as a UrlType instance. _normal_url_type: The type of the normal URL as a UrlType instance. _hover_url: The URL we're currently hovering over. _ssl_errors: Whether SSL errors occurred while loading. _urltype: The URL type to show currently (normal/ok/error/warn/hover). Accessed via the urltype property. """ STYLESHEET = """ QLabel#UrlText[urltype="normal"] { color: {{ conf.colors.statusbar.url.fg }}; } QLabel#UrlText[urltype="success"] { color: {{ conf.colors.statusbar.url.success.http.fg }}; } QLabel#UrlText[urltype="success_https"] { color: {{ conf.colors.statusbar.url.success.https.fg }}; } QLabel#UrlText[urltype="error"] { color: {{ conf.colors.statusbar.url.error.fg }}; } QLabel#UrlText[urltype="warn"] { color: {{ conf.colors.statusbar.url.warn.fg }}; } QLabel#UrlText[urltype="hover"] { color: {{ conf.colors.statusbar.url.hover.fg }}; } """ def __init__(self, parent=None): super().__init__(parent) self._urltype = None self.setObjectName(self.__class__.__name__) stylesheet.set_register(self) self._hover_url = None self._normal_url = None self._normal_url_type = UrlType.normal @pyqtProperty(str) # type: ignore[type-var] def urltype(self): """Getter for self.urltype, so it can be used as Qt property. Return: The urltype as a string (!) """ if self._urltype is None: return "" else: return self._urltype.name def _update_url(self): """Update the displayed URL if the url or the hover url changed.""" old_urltype = self._urltype if self._hover_url is not None: self.setText(self._hover_url) self._urltype = UrlType.hover elif self._normal_url is not None: self.setText(self._normal_url) self._urltype = self._normal_url_type else: self.setText('') self._urltype = UrlType.normal if old_urltype != self._urltype: # We can avoid doing an unpolish here because the new style will # always override the old one. style = self.style() assert style is not None style.polish(self) @pyqtSlot(usertypes.LoadStatus) def on_load_status_changed(self, status): """Slot for load_status_changed. Sets URL color accordingly. Args: status: The usertypes.LoadStatus. """ assert isinstance(status, usertypes.LoadStatus), status if status in [usertypes.LoadStatus.success, usertypes.LoadStatus.success_https, usertypes.LoadStatus.error, usertypes.LoadStatus.warn]: self._normal_url_type = UrlType[status.name] else: self._normal_url_type = UrlType.normal self._update_url() @pyqtSlot(QUrl) def set_url(self, url): """Setter to be used as a Qt slot. Args: url: The URL to set as QUrl, or None. """ if url is None: self._normal_url = None elif not url.isValid(): self._normal_url = "Invalid URL!" else: self._normal_url = urlutils.safe_display_string(url) self._normal_url_type = UrlType.normal self._update_url() @pyqtSlot(str) def set_hover_url(self, link): """Setter to be used as a Qt slot. Saves old shown URL in self._old_url and restores it later if a link is "un-hovered" when it gets called with empty parameters. Args: link: The link which was hovered (string) """ if link: qurl = QUrl(link) if qurl.isValid(): self._hover_url = urlutils.safe_display_string(qurl) else: self._hover_url = '(invalid URL!) {}'.format(link) else: self._hover_url = None self._update_url() def on_tab_changed(self, tab): """Update URL if the tab changed.""" self._hover_url = None if tab.url().isValid(): self._normal_url = urlutils.safe_display_string(tab.url()) else: self._normal_url = '' self.on_load_status_changed(tab.load_status()) self._update_url() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/tabbedbrowser.py0000644000175100017510000012641115102145205023506 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The main tabbed browser widget.""" import os import signal import collections import functools import weakref import datetime import dataclasses from typing import ( Any, Optional) from collections.abc import Mapping, MutableMapping, MutableSequence from qutebrowser.qt.widgets import QSizePolicy, QWidget, QApplication from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QTimer, QUrl, QPoint from qutebrowser.config import config from qutebrowser.keyinput import modeman from qutebrowser.mainwindow import tabwidget, mainwindow from qutebrowser.browser import signalfilter, browsertab, history from qutebrowser.utils import (log, usertypes, utils, qtutils, urlutils, message, jinja, version) from qutebrowser.misc import quitter, objects @dataclasses.dataclass class _UndoEntry: """Information needed for :undo.""" url: QUrl history: bytes index: int pinned: bool created_at: datetime.datetime = dataclasses.field( default_factory=datetime.datetime.now) UndoStackType = MutableSequence[MutableSequence[_UndoEntry]] class TabDeque: """Class which manages the 'last visited' tab stack. Instead of handling deletions by clearing old entries, they are handled by checking if they exist on access. This allows us to save an iteration on every tab delete. Currently, we assume we will switch to the tab returned by any of the getter functions. This is done because the on_switch functions will be called upon switch, and we don't want to duplicate entries in the stack for a single switch. """ def __init__(self) -> None: size = config.val.tabs.focus_stack_size if size < 0: size = None self._stack: collections.deque[weakref.ReferenceType[browsertab.AbstractTab]] = ( collections.deque(maxlen=size)) # Items that have been removed from the primary stack. self._stack_deleted: list[weakref.ReferenceType[browsertab.AbstractTab]] = [] self._ignore_next = False self._keep_deleted_next = False def on_switch(self, old_tab: browsertab.AbstractTab) -> None: """Record tab switch events.""" if self._ignore_next: self._ignore_next = False self._keep_deleted_next = False return tab = weakref.ref(old_tab) if self._stack_deleted and not self._keep_deleted_next: self._stack_deleted = [] self._keep_deleted_next = False self._stack.append(tab) def prev(self, cur_tab: browsertab.AbstractTab) -> browsertab.AbstractTab: """Get the 'previous' tab in the stack. Throws IndexError on failure. """ tab: Optional[browsertab.AbstractTab] = None while tab is None or tab.pending_removal or tab is cur_tab: tab = self._stack.pop()() self._stack_deleted.append(weakref.ref(cur_tab)) self._ignore_next = True return tab def next( self, cur_tab: browsertab.AbstractTab, *, keep_overflow: bool = True, ) -> browsertab.AbstractTab: """Get the 'next' tab in the stack. Throws IndexError on failure. """ tab: Optional[browsertab.AbstractTab] = None while tab is None or tab.pending_removal or tab is cur_tab: tab = self._stack_deleted.pop()() # On next tab-switch, current tab will be added to stack as normal. # However, we shouldn't wipe the overflow stack as normal. if keep_overflow: self._keep_deleted_next = True return tab def last(self, cur_tab: browsertab.AbstractTab) -> browsertab.AbstractTab: """Get the last tab. Throws IndexError on failure. """ try: return self.next(cur_tab, keep_overflow=False) except IndexError: return self.prev(cur_tab) def update_size(self) -> None: """Update the maxsize of this TabDeque.""" newsize = config.val.tabs.focus_stack_size if newsize < 0: newsize = None # We can't resize a collections.deque so just recreate it >:( self._stack = collections.deque(self._stack, maxlen=newsize) class TabDeletedError(Exception): """Exception raised when _tab_index is called for a deleted tab.""" class TabbedBrowser(QWidget): """A TabWidget with QWebViews inside. Provides methods to manage tabs, convenience methods to interact with the current tab (cur_*) and filters signals to re-emit them when they occurred in the currently visible tab. For all tab-specific signals (cur_*) emitted by a tab, this happens: - the signal gets filtered with _filter_signals and self.cur_* gets emitted if the signal occurred in the current tab. Attributes: search_text/search_options: Search parameters which are shared between all tabs. _win_id: The window ID this tabbedbrowser is associated with. _filter: A SignalFilter instance. _now_focused: The tab which is focused now. _tab_insert_idx_left: Where to insert a new tab with tabs.new_tab_position set to 'prev'. _tab_insert_idx_right: Same as above, for 'next'. undo_stack: List of lists of _UndoEntry objects of closed tabs. is_shutting_down: Whether we're currently shutting down. _local_marks: Jump markers local to each page _global_marks: Jump markers used across all pages default_window_icon: The qutebrowser window icon is_private: Whether private browsing is on for this window. Signals: cur_progress: Progress of the current tab changed (load_progress). cur_load_started: Current tab started loading (load_started) cur_load_finished: Current tab finished loading (load_finished) cur_url_changed: Current URL changed. cur_link_hovered: Link hovered in current tab (link_hovered) cur_scroll_perc_changed: Scroll percentage of current tab changed. arg 1: x-position in %. arg 2: y-position in %. cur_load_status_changed: Loading status of current tab changed. cur_search_match_changed: The active search match changed. close_window: The last tab was closed, close this window. resized: Emitted when the browser window has resized, so the completion widget can adjust its size to it. arg: The new size. current_tab_changed: The current tab changed to the emitted tab. new_tab: Emits the new WebView and its index when a new tab is opened. shutting_down: This TabbedBrowser will be deleted soon. """ cur_progress = pyqtSignal(int) cur_load_started = pyqtSignal() cur_load_finished = pyqtSignal(bool) cur_url_changed = pyqtSignal(QUrl) cur_link_hovered = pyqtSignal(str) cur_scroll_perc_changed = pyqtSignal(int, int) cur_load_status_changed = pyqtSignal(usertypes.LoadStatus) cur_search_match_changed = pyqtSignal(browsertab.SearchMatch) cur_fullscreen_requested = pyqtSignal(bool) cur_caret_selection_toggled = pyqtSignal(browsertab.SelectionState) close_window = pyqtSignal() resized = pyqtSignal('QRect') current_tab_changed = pyqtSignal(browsertab.AbstractTab) new_tab = pyqtSignal(browsertab.AbstractTab, int) shutting_down = pyqtSignal() def __init__(self, *, win_id, private, parent=None): if private: assert not qtutils.is_single_process() super().__init__(parent) self.widget = tabwidget.TabWidget(win_id, parent=self) self._win_id = win_id self._tab_insert_idx_left = 0 self._tab_insert_idx_right = -1 self.is_shutting_down = False self.widget.tabCloseRequested.connect(self.on_tab_close_requested) self.widget.new_tab_requested.connect( self.tabopen) # type: ignore[arg-type,unused-ignore] self.widget.currentChanged.connect(self._on_current_changed) self.cur_fullscreen_requested.connect(self.widget.tab_bar().maybe_hide) self.widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) if ( objects.backend == usertypes.Backend.QtWebEngine and version.qtwebengine_versions().webengine < utils.VersionNumber(5, 15, 5) ): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223 self.cur_load_finished.connect(self._leave_modes_on_load) else: self.cur_load_started.connect(self._leave_modes_on_load) # handle mode_override self.current_tab_changed.connect(lambda tab: self._mode_override(tab.url())) self.cur_url_changed.connect(self._mode_override) # This init is never used, it is immediately thrown away in the next # line. self.undo_stack: UndoStackType = collections.deque() self._update_stack_size() self._filter = signalfilter.SignalFilter(win_id, self) self._now_focused = None self.search_text = None self.search_options: Mapping[str, Any] = {} self._local_marks: MutableMapping[QUrl, MutableMapping[str, QPoint]] = {} self._global_marks: MutableMapping[str, tuple[QPoint, QUrl]] = {} self.default_window_icon = self._window().windowIcon() self.is_private = private self.tab_deque = TabDeque() config.instance.changed.connect(self._on_config_changed) quitter.instance.shutting_down.connect(self.shutdown) def _update_stack_size(self): newsize = config.instance.get('tabs.undo_stack_size') if newsize < 0: newsize = None # We can't resize a collections.deque so just recreate it >:( self.undo_stack = collections.deque(self.undo_stack, maxlen=newsize) def __repr__(self): return utils.get_repr(self, count=self.widget.count()) @pyqtSlot(str) def _on_config_changed(self, option): if option == 'tabs.favicons.show': self._update_favicons() elif option == 'window.title_format': self._update_window_title() elif option == 'tabs.undo_stack_size': self._update_stack_size() elif option in ['tabs.title.format', 'tabs.title.format_pinned']: self.widget.update_tab_titles() elif option == "tabs.focus_stack_size": self.tab_deque.update_size() def _tab_index(self, tab): """Get the index of a given tab. Raises TabDeletedError if the tab doesn't exist anymore. """ try: idx = self.widget.indexOf(tab) except RuntimeError as e: log.webview.debug("Got invalid tab ({})!".format(e)) raise TabDeletedError(e) if idx == -1: log.webview.debug("Got invalid tab (index is -1)!") raise TabDeletedError("index is -1!") return idx def widgets(self): """Get a list of open tab widgets. We don't implement this as generator so we can delete tabs while iterating over the list. """ widgets = [] for i in range(self.widget.count()): widget = qtutils.add_optional(self.widget.widget(i)) if widget is None: log.webview.debug("Got None-widget in tabbedbrowser!") else: widgets.append(widget) return widgets def _update_window_title(self, field=None): """Change the window title to match the current tab. Args: idx: The tab index to update. field: A field name which was updated. If given, the title is only set if the given field is in the template. """ title_format = config.cache['window.title_format'] if field is not None and ('{' + field + '}') not in title_format: return idx = self.widget.currentIndex() if idx == -1: # (e.g. last tab removed) log.webview.debug("Not updating window title because index is -1") return fields = self.widget.get_tab_fields(idx) fields['id'] = self._win_id title = title_format.format(**fields) # prevent hanging WMs and similar issues with giant URLs title = utils.elide(title, 1024) self._window().setWindowTitle(title) def _connect_tab_signals(self, tab): """Set up the needed signals for tab.""" # filtered signals tab.link_hovered.connect( self._filter.create(self.cur_link_hovered, tab)) tab.load_progress.connect( self._filter.create(self.cur_progress, tab)) tab.load_finished.connect( self._filter.create(self.cur_load_finished, tab)) tab.load_started.connect( self._filter.create(self.cur_load_started, tab)) tab.scroller.perc_changed.connect( self._filter.create(self.cur_scroll_perc_changed, tab)) tab.url_changed.connect( self._filter.create(self.cur_url_changed, tab)) tab.load_status_changed.connect( self._filter.create(self.cur_load_status_changed, tab)) tab.fullscreen_requested.connect( self._filter.create(self.cur_fullscreen_requested, tab)) tab.caret.selection_toggled.connect( self._filter.create(self.cur_caret_selection_toggled, tab)) tab.search.match_changed.connect( self._filter.create(self.cur_search_match_changed, tab)) # misc tab.scroller.perc_changed.connect(self._on_scroll_pos_changed) tab.scroller.before_jump_requested.connect(lambda: self.set_mark("'")) tab.url_changed.connect( functools.partial(self._on_url_changed, tab)) tab.title_changed.connect( functools.partial(self._on_title_changed, tab)) tab.icon_changed.connect( functools.partial(self._on_icon_changed, tab)) tab.pinned_changed.connect( functools.partial(self._on_pinned_changed, tab)) tab.load_progress.connect( functools.partial(self._on_load_progress, tab)) tab.load_finished.connect( functools.partial(self._on_load_finished, tab)) tab.load_started.connect( functools.partial(self._on_load_started, tab)) tab.load_status_changed.connect( functools.partial(self._on_load_status_changed, tab)) tab.window_close_requested.connect( functools.partial(self._on_window_close_requested, tab)) tab.renderer_process_terminated.connect( functools.partial(self._on_renderer_process_terminated, tab)) tab.audio.muted_changed.connect( functools.partial(self._on_audio_changed, tab)) tab.audio.recently_audible_changed.connect( functools.partial(self._on_audio_changed, tab)) tab.new_tab_requested.connect(self.tabopen) if not self.is_private: tab.history_item_triggered.connect( history.web_history.add_from_tab) def _current_tab(self) -> browsertab.AbstractTab: """Get the current browser tab. Note: The assert ensures the current tab is never None. """ tab = self.widget.currentWidget() assert isinstance(tab, browsertab.AbstractTab), tab return tab def _window(self) -> QWidget: """Get the current window widget. Note: This asserts if there is no window. """ window = self.widget.window() assert window is not None return window def _tab_by_idx(self, idx: int) -> Optional[browsertab.AbstractTab]: """Get a browser tab by index. If no tab was found at the given index, None is returned. """ tab = self.widget.widget(idx) if tab is not None: assert isinstance(tab, browsertab.AbstractTab), tab return tab def current_url(self): """Get the URL of the current tab. Intended to be used from command handlers. Return: The current URL as QUrl. """ idx = self.widget.currentIndex() return self.widget.tab_url(idx) def shutdown(self): """Try to shut down all tabs cleanly.""" self.is_shutting_down = True # Reverse tabs so we don't have to recalculate tab titles over and over # Removing first causes [2..-1] to be recomputed # Removing the last causes nothing to be recomputed for idx, tab in enumerate(reversed(self.widgets())): self._remove_tab(tab, new_undo=idx == 0) self.shutting_down.emit() def tab_close_prompt_if_pinned( self, tab, force, yes_action, text="Are you sure you want to close a pinned tab?"): """Helper method for tab_close. If tab is pinned, prompt. If not, run yes_action. If tab is destroyed, abort question. """ if tab.data.pinned and not force: message.confirm_async( title='Pinned Tab', text=text, yes_action=yes_action, default=False, abort_on=[tab.destroyed]) else: yes_action() def close_tab(self, tab, *, add_undo=True, new_undo=True, transfer=False): """Close a tab. Args: tab: The QWebView to be closed. add_undo: Whether the tab close can be undone. new_undo: Whether the undo entry should be a new item in the stack. transfer: Whether the tab is closing because it is moving to a new window. """ if config.val.tabs.tabs_are_windows or transfer: last_close = 'close' else: last_close = config.val.tabs.last_close count = self.widget.count() if last_close == 'ignore' and count == 1: return self._remove_tab(tab, add_undo=add_undo, new_undo=new_undo) if count == 1: # We just closed the last tab above. if last_close == 'close': self.close_window.emit() elif last_close == 'blank': self.load_url(QUrl('about:blank'), newtab=True) elif last_close == 'startpage': for url in config.val.url.start_pages: self.load_url(url, newtab=True) elif last_close == 'default-page': self.load_url(config.val.url.default_page, newtab=True) def _remove_tab(self, tab, *, add_undo=True, new_undo=True, crashed=False): """Remove a tab from the tab list and delete it properly. Args: tab: The QWebView to be closed. add_undo: Whether the tab close can be undone. new_undo: Whether the undo entry should be a new item in the stack. crashed: Whether we're closing a tab with crashed renderer process. """ idx = self.widget.indexOf(tab) if idx == -1: if crashed: return raise TabDeletedError("tab {} is not contained in " "TabbedWidget!".format(tab)) if tab is self._now_focused: self._now_focused = None tab.pending_removal = True if tab.url().isEmpty(): # There are some good reasons why a URL could be empty # (target="_blank" with a download, see [1]), so we silently ignore # this. # [1] https://github.com/qutebrowser/qutebrowser/issues/163 pass elif not tab.url().isValid(): # We display a warning for URLs which are not empty but invalid - # but we don't return here because we want the tab to close either # way. urlutils.invalid_url_error(tab.url(), "saving tab") elif add_undo: try: history_data = tab.history.private_api.serialize() except browsertab.WebTabError: pass # special URL else: entry = _UndoEntry(url=tab.url(), history=history_data, index=idx, pinned=tab.data.pinned) if new_undo or not self.undo_stack: self.undo_stack.append([entry]) else: self.undo_stack[-1].append(entry) tab.private_api.shutdown() self.widget.removeTab(idx) tab.deleteLater() def undo(self, depth=1): """Undo removing of a tab or tabs.""" # Remove unused tab which may be created after the last tab is closed last_close = config.val.tabs.last_close use_current_tab = False last_close_replaces = last_close in [ 'blank', 'startpage', 'default-page' ] only_one_tab_open = self.widget.count() == 1 if only_one_tab_open and last_close_replaces: tab = self._tab_by_idx(0) assert tab is not None no_history = len(tab.history) == 1 urls = { 'blank': QUrl('about:blank'), 'startpage': config.val.url.start_pages[0], 'default-page': config.val.url.default_page, } first_tab_url = tab.url() last_close_urlstr = urls[last_close].toString().rstrip('/') first_tab_urlstr = first_tab_url.toString().rstrip('/') last_close_url_used = first_tab_urlstr == last_close_urlstr use_current_tab = no_history and last_close_url_used entries = self.undo_stack[-depth] del self.undo_stack[-depth] for entry in reversed(entries): if use_current_tab: newtab = self._tab_by_idx(0) assert newtab is not None use_current_tab = False else: newtab = self.tabopen(background=False, idx=entry.index) newtab.history.private_api.deserialize(entry.history) newtab.set_pinned(entry.pinned) newtab.setFocus() @pyqtSlot('QUrl', bool) def load_url(self, url, newtab): """Open a URL, used as a slot. Args: url: The URL to open as QUrl. newtab: True to open URL in a new tab, False otherwise. """ qtutils.ensure_valid(url) if newtab or self.widget.currentWidget() is None: self.tabopen(url, background=False) else: self._current_tab().load_url(url) @pyqtSlot(int) def on_tab_close_requested(self, idx): """Close a tab via an index.""" tab = self._tab_by_idx(idx) if tab is None: log.webview.debug( "Got invalid tab {} for index {}!".format(tab, idx)) return self.tab_close_prompt_if_pinned( tab, False, lambda: self.close_tab(tab)) @pyqtSlot(browsertab.AbstractTab) def _on_window_close_requested(self, widget): """Close a tab with a widget given.""" try: self.close_tab(widget) except TabDeletedError: log.webview.debug("Requested to close {!r} which does not " "exist!".format(widget)) @pyqtSlot('QUrl') @pyqtSlot('QUrl', bool) @pyqtSlot('QUrl', bool, bool) def tabopen( self, url: QUrl = None, background: bool = None, related: bool = True, idx: int = None, ) -> browsertab.AbstractTab: """Open a new tab with a given URL. Inner logic for open-tab and open-tab-bg. Also connect all the signals we need to _filter_signals. Args: url: The URL to open as QUrl or None for an empty tab. background: Whether to open the tab in the background. if None, the `tabs.background` setting decides. related: Whether the tab was opened from another existing tab. If this is set, the new position might be different. With the default settings we handle it like Chromium does: - Tabs from clicked links etc. are to the right of the current (related=True). - Explicitly opened tabs are at the very right (related=False) idx: The index where the new tab should be opened. Return: The opened WebView instance. """ if url is not None: qtutils.ensure_valid(url) log.webview.debug("Creating new tab with URL {}, background {}, " "related {}, idx {}".format( url, background, related, idx)) prev_focus = QApplication.focusWidget() if config.val.tabs.tabs_are_windows and self.widget.count() > 0: window = mainwindow.MainWindow(private=self.is_private) tab = window.tabbed_browser.tabopen( url=url, background=background, related=related) window.show() return tab tab = browsertab.create(win_id=self._win_id, private=self.is_private, parent=self.widget) self._connect_tab_signals(tab) if idx is None: idx = self._get_new_tab_idx(related) self.widget.insertTab(idx, tab, "") if url is not None: tab.load_url(url) if background is None: background = config.val.tabs.background if background: # Make sure the background tab has the correct initial size. # With a foreground tab, it's going to be resized correctly by the # layout anyways. current_widget = self._current_tab() tab.resize(current_widget.size()) self.widget.tab_index_changed.emit(self.widget.currentIndex(), self.widget.count()) # Refocus webview in case we lost it by spawning a bg tab current_widget.setFocus() else: self.widget.setCurrentWidget(tab) mode = modeman.instance(self._win_id).mode if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: # If we were in a command prompt, restore old focus # The above commands need to be run to switch tabs if prev_focus is not None: prev_focus.setFocus() tab.show() self.new_tab.emit(tab, idx) return tab def _get_new_tab_idx(self, related): """Get the index of a tab to insert. Args: related: Whether the tab was opened from another tab (as a "child") Return: The index of the new tab. """ if related: pos = config.val.tabs.new_position.related else: pos = config.val.tabs.new_position.unrelated if pos == 'prev': if config.val.tabs.new_position.stacking: idx = self._tab_insert_idx_left # On first sight, we'd think we have to decrement # self._tab_insert_idx_left here, as we want the next tab to be # *before* the one we just opened. However, since we opened a # tab *before* the currently focused tab, indices will shift by # 1 automatically. else: idx = self.widget.currentIndex() elif pos == 'next': if config.val.tabs.new_position.stacking: idx = self._tab_insert_idx_right else: idx = self.widget.currentIndex() + 1 self._tab_insert_idx_right += 1 elif pos == 'first': idx = 0 elif pos == 'last': idx = -1 else: raise ValueError("Invalid tabs.new_position '{}'.".format(pos)) log.webview.debug("tabs.new_position {} -> opening new tab at {}, " "next left: {} / right: {}".format( pos, idx, self._tab_insert_idx_left, self._tab_insert_idx_right)) return idx def _update_favicons(self): """Update favicons when config was changed.""" for tab in self.widgets(): self.widget.update_tab_favicon(tab) @pyqtSlot() def _on_load_started(self, tab): """Clear icon and update title when a tab started loading. Args: tab: The tab where the signal belongs to. """ if tab.data.keep_icon: tab.data.keep_icon = False elif (config.cache['tabs.tabs_are_windows'] and tab.data.should_show_icon()): self._window().setWindowIcon(self.default_window_icon) @pyqtSlot() def _on_load_status_changed(self, tab): """Update tab/window titles if the load status changed.""" try: idx = self._tab_index(tab) except TabDeletedError: # We can get signals for tabs we already deleted... return self.widget.update_tab_title(idx) if idx == self.widget.currentIndex(): self._update_window_title() @pyqtSlot() def _leave_modes_on_load(self): """Leave insert/hint mode when loading started.""" try: url = self.current_url() if not url.isValid(): url = None except qtutils.QtValueError: url = None if config.instance.get('input.insert_mode.leave_on_load', url=url): modeman.leave(self._win_id, usertypes.KeyMode.insert, 'load started', maybe=True) else: log.modes.debug("Ignoring leave_on_load request due to setting.") if config.cache['hints.leave_on_load']: modeman.leave(self._win_id, usertypes.KeyMode.hint, 'load started', maybe=True) else: log.modes.debug("Ignoring leave_on_load request due to setting.") @pyqtSlot(browsertab.AbstractTab, str) def _on_title_changed(self, tab, text): """Set the title of a tab. Slot for the title_changed signal of any tab. Args: tab: The WebView where the title was changed. text: The text to set. """ if not text: log.webview.debug("Ignoring title change to '{}'.".format(text)) return try: idx = self._tab_index(tab) except TabDeletedError: # We can get signals for tabs we already deleted... return log.webview.debug("Changing title for idx {} to '{}'".format( idx, text)) self.widget.set_page_title(idx, text) if idx == self.widget.currentIndex(): self._update_window_title() @pyqtSlot(browsertab.AbstractTab, QUrl) def _on_url_changed(self, tab, url): """Set the new URL as title if there's no title yet. Args: tab: The WebView where the title was changed. url: The new URL. """ try: idx = self._tab_index(tab) except TabDeletedError: # We can get signals for tabs we already deleted... return if not self.widget.page_title(idx): self.widget.set_page_title(idx, url.toDisplayString()) def _mode_override(self, url: QUrl) -> None: """Override mode if url matches pattern. Args: url: The QUrl to match for """ if not url.isValid(): return mode = config.instance.get('input.mode_override', url=url) if mode: log.modes.debug(f"Mode change to {mode} triggered for url {url}") modeman.enter( self._win_id, usertypes.KeyMode[mode], reason='mode_override', ) @pyqtSlot(browsertab.AbstractTab) def _on_icon_changed(self, tab): """Set the icon of a tab. Slot for the iconChanged signal of any tab. Args: tab: The WebView where the title was changed. """ try: self._tab_index(tab) except TabDeletedError: # We can get signals for tabs we already deleted... return self.widget.update_tab_favicon(tab) @pyqtSlot(usertypes.KeyMode) def on_mode_entered(self, mode): """Save input mode when tabs.mode_on_change = restore.""" if (config.val.tabs.mode_on_change == 'restore' and mode in modeman.INPUT_MODES): tab = self.widget.currentWidget() if tab is not None: assert isinstance(tab, browsertab.AbstractTab), tab tab.data.input_mode = mode @pyqtSlot() def on_release_focus(self): """Give keyboard focus to the current tab when requested by statusbar/prompt. This gets emitted by the statusbar and prompt container before they call .hide() on themselves, with the idea that we can explicitly reassign the focus, instead of Qt implicitly calling its QWidget::focusNextPrevChild() method, finding a new widget to give keyboard focus to. """ widget = qtutils.add_optional(self.widget.currentWidget()) if widget is None: return log.modes.debug(f"Focus released, focusing {widget!r}") widget.setFocus() @pyqtSlot() def on_mode_left(self): """Save input mode for restoring if needed.""" if config.val.tabs.mode_on_change != 'restore': return widget = qtutils.add_optional(self.widget.currentWidget()) if widget is None: return assert isinstance(widget, browsertab.AbstractTab), widget widget.data.input_mode = usertypes.KeyMode.normal @pyqtSlot(int) def _on_current_changed(self, idx): """Add prev tab to stack and leave hinting mode when focus changed.""" mode_on_change = config.val.tabs.mode_on_change if idx == -1 or self.is_shutting_down: # closing the last tab (before quitting) or shutting down return tab = self._tab_by_idx(idx) if tab is None: log.webview.debug( "on_current_changed got called with invalid index {}" .format(idx)) return log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() modes_to_leave = [usertypes.KeyMode.hint, usertypes.KeyMode.caret] mm_instance = modeman.instance(self._win_id) current_mode = mm_instance.mode log.modes.debug("Mode before tab change: {} (mode_on_change = {})" .format(current_mode.name, mode_on_change)) if mode_on_change == 'normal': modes_to_leave += modeman.INPUT_MODES for mode in modes_to_leave: modeman.leave(self._win_id, mode, 'tab changed', maybe=True) if (mode_on_change == 'restore' and current_mode not in modeman.PROMPT_MODES): modeman.enter(self._win_id, tab.data.input_mode, 'restore') if self._now_focused is not None: self.tab_deque.on_switch(self._now_focused) log.modes.debug("Mode after tab change: {} (mode_on_change = {})" .format(current_mode.name, mode_on_change)) self._now_focused = tab self.current_tab_changed.emit(tab) self.cur_search_match_changed.emit(tab.search.match) QTimer.singleShot(0, self._update_window_title) self._tab_insert_idx_left = self.widget.currentIndex() self._tab_insert_idx_right = self.widget.currentIndex() + 1 @pyqtSlot() def on_cmd_return_pressed(self): """Set focus when the commandline closes.""" log.modes.debug("Commandline closed, focusing {!r}".format(self)) def _on_load_progress(self, tab, perc): """Adjust tab indicator on load progress.""" try: idx = self._tab_index(tab) except TabDeletedError: # We can get signals for tabs we already deleted... return start = config.cache['colors.tabs.indicator.start'] stop = config.cache['colors.tabs.indicator.stop'] system = config.cache['colors.tabs.indicator.system'] color = qtutils.interpolate_color(start, stop, perc, system) self.widget.set_tab_indicator_color(idx, color) self.widget.update_tab_title(idx) if idx == self.widget.currentIndex(): self._update_window_title() def _on_load_finished(self, tab, ok): """Adjust tab indicator when loading finished.""" try: idx = self._tab_index(tab) except TabDeletedError: # We can get signals for tabs we already deleted... return if ok: start = config.cache['colors.tabs.indicator.start'] stop = config.cache['colors.tabs.indicator.stop'] system = config.cache['colors.tabs.indicator.system'] color = qtutils.interpolate_color(start, stop, 100, system) else: color = config.cache['colors.tabs.indicator.error'] self.widget.set_tab_indicator_color(idx, color) if idx == self.widget.currentIndex(): tab.private_api.handle_auto_insert_mode(ok) @pyqtSlot() def _on_scroll_pos_changed(self): """Update tab and window title when scroll position changed.""" idx = self.widget.currentIndex() if idx == -1: # (e.g. last tab removed) log.webview.debug("Not updating scroll position because index is " "-1") return self._update_window_title('scroll_pos') self.widget.update_tab_title(idx, 'scroll_pos') def _on_pinned_changed(self, tab): """Update the tab's pinned status.""" idx = self.widget.indexOf(tab) self.widget.update_tab_favicon(tab) self.widget.update_tab_title(idx) def _on_audio_changed(self, tab, _muted): """Update audio field in tab when mute or recentlyAudible changed.""" try: idx = self._tab_index(tab) except TabDeletedError: # We can get signals for tabs we already deleted... return self.widget.update_tab_title(idx, 'audio') if idx == self.widget.currentIndex(): self._update_window_title('audio') def _on_renderer_process_terminated(self, tab, status, code): """Show an error when a renderer process terminated.""" if status == browsertab.TerminationStatus.normal: return messages = { browsertab.TerminationStatus.abnormal: "Renderer process exited", browsertab.TerminationStatus.crashed: "Renderer process crashed", browsertab.TerminationStatus.killed: "Renderer process was killed", browsertab.TerminationStatus.unknown: "Renderer process did not start", } sig = None try: if os.WIFSIGNALED(code): sig = signal.Signals(os.WTERMSIG(code)) except (AttributeError, ValueError): pass if sig is not None: msg = messages[status] + f" (status {code}: {sig.name})" else: msg = messages[status] + f" (status {code})" # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715 versions = version.qtwebengine_versions() if ( status == browsertab.TerminationStatus.unknown and code == 1002 and versions.webengine == utils.VersionNumber(5, 15, 3) ): log.webview.error(msg) log.webview.error('') log.webview.error( 'NOTE: If you see this and "Network service crashed, restarting ' 'service.", please see:') log.webview.error('https://github.com/qutebrowser/qutebrowser/issues/6235') log.webview.error( 'You can set the "qt.workarounds.locale" setting in qutebrowser to ' 'work around the issue.') log.webview.error( 'A proper fix is likely available in QtWebEngine soon (which is why ' 'the workaround is disabled by default).') log.webview.error('') return def show_error_page(html): tab.set_html(html) log.webview.error(msg) url_string = tab.url(requested=True).toDisplayString() error_page = jinja.render( 'error.html', title="Error loading {}".format(url_string), url=url_string, error=msg) QTimer.singleShot(100, lambda: show_error_page(error_page)) def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. Args: e: The QResizeEvent """ super().resizeEvent(e) self.resized.emit(self.geometry()) def wheelEvent(self, e): """Override wheelEvent of QWidget to forward it to the focused tab. Args: e: The QWheelEvent """ if self._now_focused is not None: self._now_focused.wheelEvent(e) else: e.ignore() def set_mark(self, key): """Set a mark at the current scroll position in the current tab. Args: key: mark identifier; capital indicates a global mark """ # strip the fragment as it may interfere with scrolling try: url = self.current_url().adjusted(QUrl.UrlFormattingOption.RemoveFragment) except qtutils.QtValueError: # show an error only if the mark is not automatically set if key != "'": message.error("Failed to set mark: url invalid") return point = self._current_tab().scroller.pos_px() if key.isupper(): self._global_marks[key] = point, url else: if url not in self._local_marks: self._local_marks[url] = {} self._local_marks[url][key] = point def jump_mark(self, key): """Jump to the mark named by `key`. Args: key: mark identifier; capital indicates a global mark """ try: # consider urls that differ only in fragment to be identical urlkey = self.current_url().adjusted(QUrl.UrlFormattingOption.RemoveFragment) except qtutils.QtValueError: urlkey = None tab = self._current_tab() if key.isupper(): if key in self._global_marks: point, url = self._global_marks[key] def callback(ok): """Scroll once loading finished.""" if ok: self.cur_load_finished.disconnect(callback) tab.scroller.to_point(point) self.load_url(url, newtab=False) self.cur_load_finished.connect(callback) else: message.error("Mark {} is not set".format(key)) elif urlkey is None: message.error("Current URL is invalid!") elif urlkey in self._local_marks and key in self._local_marks[urlkey]: point = self._local_marks[urlkey][key] # save the pre-jump position in the special ' mark # this has to happen after we read the mark, otherwise jump_mark # "'" would just jump to the current position every time tab.scroller.before_jump_requested.emit() tab.scroller.to_point(point) else: message.error("Mark {} is not set".format(key)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/tabwidget.py0000644000175100017510000011232015102145205022625 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The tab widget used for TabbedBrowser from browser.py.""" import functools import contextlib import dataclasses from typing import Optional, Any from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, QTimer, QUrl) from qutebrowser.qt.widgets import (QTabWidget, QTabBar, QSizePolicy, QProxyStyle, QStyle, QStylePainter, QStyleOptionTab, QCommonStyle) from qutebrowser.qt.gui import QIcon, QPalette, QColor from qutebrowser.utils import qtutils, objreg, utils, usertypes, log from qutebrowser.config import config, stylesheet from qutebrowser.misc import objects, debugcachestats from qutebrowser.browser import browsertab class TabWidget(QTabWidget): """The tab widget used for TabbedBrowser. Signals: tab_index_changed: Emitted when the current tab was changed. arg 0: The index of the tab which is now focused. arg 1: The total count of tabs. new_tab_requested: Emitted when a new tab is requested. """ tab_index_changed = pyqtSignal(int, int) new_tab_requested = pyqtSignal('QUrl', bool, bool) # Strings for controlling the mute/audible text MUTE_STRING = '[M] ' AUDIBLE_STRING = '[A] ' def __init__(self, win_id, parent=None): super().__init__(parent) bar = TabBar(win_id, self) self.setStyle(TabBarStyle()) self.setTabBar(bar) bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabMoved.connect(functools.partial( QTimer.singleShot, 0, self.update_tab_titles)) bar.currentChanged.connect(self._on_current_changed) bar.new_tab_requested.connect(self._on_new_tab_requested) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.setDocumentMode(True) self.setUsesScrollButtons(True) bar.setDrawBase(False) self._init_config() config.instance.changed.connect(self._init_config) @config.change_filter('tabs') def _init_config(self): """Initialize attributes based on the config.""" self.setMovable(True) self.setTabsClosable(False) position = config.val.tabs.position selection_behavior = config.val.tabs.select_on_remove self.setTabPosition(position) self.setElideMode(config.val.tabs.title.elide) tabbar = self.tab_bar() tabbar.vertical = position in [ QTabWidget.TabPosition.West, QTabWidget.TabPosition.East] tabbar.setSelectionBehaviorOnRemove(selection_behavior) tabbar.refresh() def tab_bar(self) -> "TabBar": """Get the TabBar for this TabWidget.""" bar = self.tabBar() assert isinstance(bar, TabBar), bar return bar def _tab_by_idx(self, idx: int) -> Optional[browsertab.AbstractTab]: """Get the tab at the given index.""" tab = self.widget(idx) if tab is not None: assert isinstance(tab, browsertab.AbstractTab), tab return tab def set_tab_indicator_color(self, idx, color): """Set the tab indicator color. Args: idx: The tab index. color: A QColor. """ bar = self.tab_bar() bar.set_tab_data(idx, 'indicator-color', color) bar.update(bar.tabRect(idx)) def tab_indicator_color(self, idx): """Get the tab indicator color for the given index.""" return self.tab_bar().tab_indicator_color(idx) def set_page_title(self, idx, title): """Set the tab title user data.""" tabbar = self.tab_bar() if config.cache['tabs.tooltips']: # always show only plain title in tooltips tabbar.setTabToolTip(idx, title) tabbar.set_tab_data(idx, 'page-title', title) self.update_tab_title(idx) def page_title(self, idx): """Get the tab title user data.""" return self.tab_bar().page_title(idx) def update_tab_title(self, idx, field=None): """Update the tab text for the given tab. Args: idx: The tab index to update. field: A field name which was updated. If given, the title is only set if the given field is in the template. """ assert idx != -1 tab = self._tab_by_idx(idx) assert tab is not None if tab.data.pinned: fmt = config.cache['tabs.title.format_pinned'] else: fmt = config.cache['tabs.title.format'] if (field is not None and (fmt is None or ('{' + field + '}') not in fmt)): return def right_align(num): return str(num).rjust(len(str(self.count()))) def left_align(num): return str(num).ljust(len(str(self.count()))) bar = self.tab_bar() cur_idx = bar.currentIndex() if idx == cur_idx: rel_idx = left_align(idx + 1) + " " else: rel_idx = " " + right_align(abs(idx - cur_idx)) fields = self.get_tab_fields(idx) fields['current_title'] = fields['current_title'].replace('&', '&&') fields['index'] = idx + 1 fields['aligned_index'] = right_align(idx + 1) fields['relative_index'] = rel_idx title = '' if fmt is None else fmt.format(**fields) # Only change the tab title if it changes, setting the tab title causes # a size recalculation which is slow. if bar.tabText(idx) != title: bar.setTabText(idx, title) def get_tab_fields(self, idx): """Get the tab field data.""" tab = self._tab_by_idx(idx) assert tab is not None page_title = self.page_title(idx) fields: dict[str, Any] = {} fields['id'] = tab.tab_id fields['current_title'] = page_title fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() fields['backend'] = objects.backend.name fields['private'] = ' [Private Mode] ' if tab.is_private else '' try: if tab.audio.is_muted(): fields['audio'] = TabWidget.MUTE_STRING elif tab.audio.is_recently_audible(): fields['audio'] = TabWidget.AUDIBLE_STRING else: fields['audio'] = '' except browsertab.WebTabError: # Muting is only implemented with QtWebEngine fields['audio'] = '' if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) else: fields['perc'] = '' try: url = self.tab_url(idx) except qtutils.QtValueError: fields['host'] = '' fields['current_url'] = '' fields['protocol'] = '' else: fields['host'] = url.host() fields['current_url'] = url.toDisplayString() fields['protocol'] = url.scheme() y = tab.scroller.pos_perc()[1] if y <= 0: scroll_pos = 'top' elif y >= 100: scroll_pos = 'bot' else: scroll_pos = '{:2}%'.format(y) fields['scroll_pos'] = scroll_pos return fields @contextlib.contextmanager def _toggle_visibility(self): """Toggle visibility while running. Every single call to setTabText calls the size hinting functions for every single tab, which are slow. Since we know we are updating all the tab's titles, we can delay this processing by making the tab non-visible. To avoid flickering, disable repaint updates while we work. """ bar = self.tab_bar() toggle = (self.count() > 10 and not bar.drag_in_progress and bar.isVisible()) if toggle: bar.setUpdatesEnabled(False) bar.setVisible(False) yield if toggle: bar.setVisible(True) bar.setUpdatesEnabled(True) def update_tab_titles(self): """Update all texts.""" with self._toggle_visibility(): for idx in range(self.count()): self.update_tab_title(idx) def tabInserted(self, idx): """Update titles when a tab was inserted.""" super().tabInserted(idx) self.update_tab_titles() def tabRemoved(self, idx): """Update titles when a tab was removed.""" super().tabRemoved(idx) self.update_tab_titles() def addTab(self, page, icon_or_text, text_or_empty=None): """Override addTab to use our own text setting logic. Unfortunately QTabWidget::addTab has these two overloads: - QWidget * page, const QIcon & icon, const QString & label - QWidget * page, const QString & label This means we'll get different arguments based on the chosen overload. Args: page: The QWidget to add. icon_or_text: Either the QIcon to add or the label. text_or_empty: Either the label or None. Return: The index of the newly added tab. """ if text_or_empty is None: text = icon_or_text new_idx = super().addTab(page, '') else: icon = icon_or_text text = text_or_empty new_idx = super().addTab(page, icon, '') self.set_page_title(new_idx, text) return new_idx def insertTab(self, idx, page, icon_or_text, text_or_empty=None): """Override insertTab to use our own text setting logic. Unfortunately QTabWidget::insertTab has these two overloads: - int index, QWidget * page, const QIcon & icon, const QString & label - int index, QWidget * page, const QString & label This means we'll get different arguments based on the chosen overload. Args: idx: Where to insert the widget. page: The QWidget to add. icon_or_text: Either the QIcon to add or the label. text_or_empty: Either the label or None. Return: The index of the newly added tab. """ if text_or_empty is None: text = icon_or_text new_idx = super().insertTab(idx, page, '') else: icon = icon_or_text text = text_or_empty new_idx = super().insertTab(idx, page, icon, '') self.set_page_title(new_idx, text) return new_idx @pyqtSlot(int) def _on_current_changed(self, index): """Emit the tab_index_changed signal if the current tab changed.""" self.tab_bar().on_current_changed() self.update_tab_titles() self.tab_index_changed.emit(index, self.count()) @pyqtSlot() def _on_new_tab_requested(self): """Open a new tab.""" self.new_tab_requested.emit(config.val.url.default_page, False, False) def tab_url(self, idx): """Get the URL of the tab at the given index. Return: The tab URL as QUrl. """ tab = self._tab_by_idx(idx) url = QUrl() if tab is None else tab.url() # It's possible for url to be invalid, but the caller will handle that. qtutils.ensure_valid(url) return url def update_tab_favicon(self, tab: browsertab.AbstractTab) -> None: """Update favicon of the given tab.""" idx = self.indexOf(tab) icon = tab.icon() if tab.data.should_show_icon() else QIcon() self.setTabIcon(idx, icon) if config.val.tabs.tabs_are_windows: window = self.window() assert window is not None window.setWindowIcon(tab.icon()) def setTabIcon(self, idx: int, icon: QIcon) -> None: """Always show tab icons for pinned tabs in some circumstances.""" tab = self._tab_by_idx(idx) if (icon.isNull() and config.cache['tabs.favicons.show'] != 'never' and config.cache['tabs.pinned.shrink'] and not self.tab_bar().vertical and tab is not None and tab.data.pinned): style = self.style() assert style is not None icon = style.standardIcon(QStyle.StandardPixmap.SP_FileIcon) super().setTabIcon(idx, icon) class TabBar(QTabBar): """Custom tab bar with our own style. FIXME: Dragging tabs doesn't look as nice as it does in QTabBar. However, fixing this would be a lot of effort, so we'll postpone it until we're reimplementing drag&drop for other reasons. https://github.com/qutebrowser/qutebrowser/issues/126 Attributes: vertical: When the tab bar is currently vertical. win_id: The window ID this TabBar belongs to. Signals: new_tab_requested: Emitted when a new tab is requested. """ STYLESHEET = """ TabBar { font: {{ conf.fonts.tabs.unselected }}; background-color: {{ conf.colors.tabs.bar.bg }}; } TabBar::tab:selected { font: {{ conf.fonts.tabs.selected }}; } """ new_tab_requested = pyqtSignal() def __init__(self, win_id, parent=None): super().__init__(parent) self._win_id = win_id self._our_style = TabBarStyle() self.setStyle(self._our_style) self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.vertical = False self._auto_hide_timer = usertypes.Timer() self._auto_hide_timer.setSingleShot(True) self._auto_hide_timer.timeout.connect(self.maybe_hide) self._on_show_switching_delay_changed() self.setAutoFillBackground(True) # FIXME:mypy Is it a mypy bug that we need to specify bool here? # Otherwise, we get "Cannot determine type of "drag_in_progress" in # TabWidget._toggle_visibility below. self.drag_in_progress: bool = False stylesheet.set_register(self) self.ensurePolished() config.instance.changed.connect(self._on_config_changed) self._set_icon_size() QTimer.singleShot(0, self.maybe_hide) self._minimum_tab_size_hint_helper = functools.lru_cache(maxsize=2**9)( self._minimum_tab_size_hint_helper_uncached ) debugcachestats.register(name=f'tab width cache (win_id={win_id})')( self._minimum_tab_size_hint_helper ) self._minimum_tab_height = functools.lru_cache(maxsize=1)( self._minimum_tab_height_uncached ) def __repr__(self): return utils.get_repr(self, count=self.count()) def _tab_widget(self): """Get the TabWidget we're in.""" parent = self.parent() assert isinstance(parent, TabWidget), parent return parent def _current_tab(self): """Get the current tab object.""" return self._tab_widget().currentWidget() @pyqtSlot(str) def _on_config_changed(self, option: str) -> None: if option.startswith('fonts.tabs.'): self.ensurePolished() self._set_icon_size() elif option == 'tabs.favicons.scale': self._set_icon_size() elif option == 'tabs.show_switching_delay': self._on_show_switching_delay_changed() elif option == 'tabs.show': self.maybe_hide() if option.startswith('colors.tabs.'): self.update() # Clear tab size caches when appropriate if option in ["tabs.indicator.padding", "tabs.padding", "tabs.indicator.width", "tabs.min_width", "tabs.pinned.shrink", "fonts.tabs.selected", "fonts.tabs.unselected"]: self._minimum_tab_size_hint_helper.cache_clear() self._minimum_tab_height.cache_clear() def _on_show_switching_delay_changed(self): """Set timer interval when tabs.show_switching_delay got changed.""" self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay) def on_current_changed(self): """Show tab bar when current tab got changed.""" self.maybe_hide() # for fullscreen tabs if config.val.tabs.show == 'switching': self.show() self._auto_hide_timer.start() @pyqtSlot() def maybe_hide(self): """Hide the tab bar if needed.""" show = config.val.tabs.show tab = self._current_tab() if (show in ['never', 'switching'] or (show == 'multiple' and self.count() == 1) or (tab and tab.data.fullscreen)): self.hide() else: self.show() def set_tab_data(self, idx, key, value): """Set tab data as a dictionary.""" if not 0 <= idx < self.count(): raise IndexError("Tab index ({}) out of range ({})!".format( idx, self.count())) data = self.tabData(idx) if data is None: data = {} data[key] = value self.setTabData(idx, data) def tab_data(self, idx, key): """Get tab data for a given key.""" if not 0 <= idx < self.count(): raise IndexError("Tab index ({}) out of range ({})!".format( idx, self.count())) data = self.tabData(idx) if data is None: data = {} return data[key] def tab_indicator_color(self, idx): """Get the tab indicator color for the given index.""" try: return self.tab_data(idx, 'indicator-color') except KeyError: return QColor() def page_title(self, idx): """Get the tab title user data. Args: idx: The tab index to get the title for. """ try: return self.tab_data(idx, 'page-title') except KeyError: return '' def refresh(self): """Properly repaint the tab bar and relayout tabs.""" # This is a horrible hack, but we need to do this so the underlying Qt # code sets layoutDirty so it actually relayouts the tabs. self.setIconSize(self.iconSize()) def _set_icon_size(self): """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 size = int(size * config.val.tabs.favicons.scale) self.setIconSize(QSize(size, size)) def mouseReleaseEvent(self, e): """Override mouseReleaseEvent to know when drags stop.""" self.drag_in_progress = False super().mouseReleaseEvent(e) def mousePressEvent(self, e): """Override mousePressEvent to close tabs if configured. Also keep track of if we are currently in a drag.""" self.drag_in_progress = True button = config.val.tabs.close_mouse_button if (e.button() == Qt.MouseButton.RightButton and button == 'right' or e.button() == Qt.MouseButton.MiddleButton and button == 'middle'): e.accept() idx = self.tabAt(e.pos()) if idx == -1: action = config.val.tabs.close_mouse_button_on_bar if action == 'ignore': return elif action == 'new-tab': self.new_tab_requested.emit() return elif action == 'close-current': idx = self.currentIndex() elif action == 'close-last': idx = self.count() - 1 self.tabCloseRequested.emit(idx) return super().mousePressEvent(e) def minimumTabSizeHint(self, index: int, ellipsis: bool = True) -> QSize: """Set the minimum tab size to indicator/icon/... text. Args: index: The index of the tab to get a size hint for. ellipsis: Whether to use ellipsis to calculate width instead of the tab's text. Forced to False for pinned tabs. Return: A QSize of the smallest tab size we can make. """ icon = self.tabIcon(index) if icon.isNull(): icon_width = 0 else: icon_width = min( icon.actualSize(self.iconSize()).width(), self.iconSize().width()) + TabBarStyle.ICON_PADDING pinned = self._tab_pinned(index) if not self.vertical and pinned and config.val.tabs.pinned.shrink: # Never consider ellipsis an option for horizontal pinned tabs ellipsis = False return self._minimum_tab_size_hint_helper(self.tabText(index), icon_width, ellipsis, pinned) def _minimum_tab_size_hint_helper_uncached(self, tab_text: str, icon_width: int, ellipsis: bool, pinned: bool) -> QSize: """Helper function to cache tab results. Config values accessed in here should be added to _on_config_changed to ensure cache is flushed when needed. """ text = '\u2026' if ellipsis else tab_text # Don't ever shorten if text is shorter than the ellipsis def _text_to_width(text): # Calculate text width taking into account qt mnemonics return self.fontMetrics().size(Qt.TextFlag.TextShowMnemonic, text).width() text_width = min(_text_to_width(text), _text_to_width(tab_text)) padding = config.cache['tabs.padding'] indicator_width = config.cache['tabs.indicator.width'] indicator_padding = config.cache['tabs.indicator.padding'] padding_h = padding.left + padding.right # Only add padding if indicator exists if indicator_width != 0: padding_h += indicator_padding.left + indicator_padding.right height = self._minimum_tab_height() width = (text_width + icon_width + padding_h + indicator_width) min_width = config.cache['tabs.min_width'] if (not self.vertical and min_width > 0 and not pinned or not config.cache['tabs.pinned.shrink']): width = max(min_width, width) return QSize(width, height) def _minimum_tab_height_uncached(self): padding = config.cache['tabs.padding'] return self.fontMetrics().height() + padding.top + padding.bottom def _tab_pinned(self, index: int) -> bool: """Return True if tab is pinned.""" if not 0 <= index < self.count(): raise IndexError("Tab index ({}) out of range ({})!".format( index, self.count())) widget = self._tab_widget().widget(index) if widget is None: # This could happen when Qt calls tabSizeHint while initializing # tabs. return False return widget.data.pinned def tabSizeHint(self, index: int) -> QSize: """Override tabSizeHint to customize qb's tab size. https://wiki.python.org/moin/PyQt/Customising%20tab%20bars Args: index: The index of the tab. Return: A QSize. """ if self.count() == 0: # This happens on startup on macOS. # We return it directly rather than setting `size' because we don't # want to ensure it's valid in this special case. return QSize() height = self._minimum_tab_height() if self.vertical: confwidth = str(config.cache['tabs.width']) if confwidth.endswith('%'): main_window = objreg.get('main-window', scope='window', window=self._win_id) perc = int(confwidth.rstrip('%')) width = main_window.width() * perc // 100 else: width = int(confwidth) size = QSize(width, height) else: if config.cache['tabs.pinned.shrink'] and self._tab_pinned(index): # Give pinned tabs the minimum size they need to display their # titles, let Qt handle scaling it down if we get too small. width = self.minimumTabSizeHint(index, ellipsis=False).width() else: # Request as much space as possible so we fill the tabbar, let # Qt shrink us down. If for some reason (tests, bugs) # self.width() gives 0, use a sane min of 10 px width = max(self.width(), 10) max_width = config.cache['tabs.max_width'] if max_width > 0: width = min(max_width, width) size = QSize(width, height) qtutils.ensure_valid(size) return size def initStyleOption(self, opt, idx): """Override QTabBar.initStyleOption(). Used to calculate styling clues from a widget for the GUI layer. """ super().initStyleOption(opt, idx) # Re-do the text elision that the base QTabBar does, but using a text # rectangle computed by out TabBarStyle. With Qt6 the base class ends # up using QCommonStyle directly for that which has a different opinion # of how vertical tabs should work. text_rect = self._our_style.subElementRect( QStyle.SubElement.SE_TabBarTabText, opt, self, ) opt.text = self.fontMetrics().elidedText( self.tabText(idx), self.elideMode(), text_rect.width(), Qt.TextFlag.TextShowMnemonic, ) def paintEvent(self, event): """Override paintEvent to draw the tabs like we want to.""" p = QStylePainter(self) selected = self.currentIndex() for idx in range(self.count()): if not event.region().intersects(self.tabRect(idx)): # Don't repaint if we are outside the requested region continue tab = QStyleOptionTab() self.initStyleOption(tab, idx) setting = 'colors.tabs' if self._tab_pinned(idx): setting += '.pinned' if idx == selected: setting += '.selected' setting += '.odd' if (idx + 1) % 2 else '.even' tab.palette.setColor(QPalette.ColorRole.Window, config.cache[setting + '.bg']) tab.palette.setColor(QPalette.ColorRole.WindowText, config.cache[setting + '.fg']) indicator_color = self.tab_indicator_color(idx) tab.palette.setColor(QPalette.ColorRole.Base, indicator_color) p.drawControl(QStyle.ControlElement.CE_TabBarTab, tab) def tabInserted(self, idx): """Update visibility when a tab was inserted.""" super().tabInserted(idx) self.maybe_hide() def tabRemoved(self, idx): """Update visibility when a tab was removed.""" super().tabRemoved(idx) self.maybe_hide() def wheelEvent(self, e): """Override wheelEvent to make the action configurable. Args: e: The QWheelEvent """ if config.val.tabs.mousewheel_switching: if utils.is_mac: # WORKAROUND for this not being customizable until Qt 6: # https://codereview.qt-project.org/c/qt/qtbase/+/327746 index = self.currentIndex() if index == -1: return dx = e.angleDelta().x() dy = e.angleDelta().y() delta = dx if abs(dx) > abs(dy) else dy offset = -1 if delta > 0 else 1 index += offset if 0 <= index < self.count(): self.setCurrentIndex(index) else: super().wheelEvent(e) else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) tabbed_browser.wheelEvent(e) @dataclasses.dataclass class Layouts: """Layout information for tab. Used by TabBarStyle._tab_layout(). """ text: QRect icon: QRect indicator: QRect class TabBarStyle(QProxyStyle): """Qt style used by TabBar to fix some issues with the default one. This fixes the following things: - Remove the focus rectangle Ubuntu draws on tabs. - Force text to be left-aligned even though Qt has "centered" hardcoded. """ ICON_PADDING = 4 def __init__(self, style=None): # "useless" override as WORKAROUND for # https://www.riverbankcomputing.com/pipermail/pyqt/2023-September/045510.html super().__init__(style) def _base_style(self) -> QStyle: """Get the base style.""" style = self.baseStyle() assert style is not None return style def _draw_indicator(self, layouts, opt, p): """Draw the tab indicator. Args: layouts: The layouts from _tab_layout. opt: QStyleOption from drawControl. p: QPainter from drawControl. """ color = opt.palette.base().color() rect = layouts.indicator if color.isValid() and rect.isValid(): p.fillRect(rect, color) def _draw_icon(self, layouts, opt, p): """Draw the tab icon. Args: layouts: The layouts from _tab_layout. opt: QStyleOption p: QPainter """ qtutils.ensure_valid(layouts.icon) icon_mode = (QIcon.Mode.Normal if opt.state & QStyle.StateFlag.State_Enabled else QIcon.Mode.Disabled) icon_state = (QIcon.State.On if opt.state & QStyle.StateFlag.State_Selected else QIcon.State.Off) icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state) self._base_style().drawItemPixmap(p, layouts.icon, Qt.AlignmentFlag.AlignCenter, icon) def drawControl(self, element, opt, p, widget=None): """Override drawControl to draw odd tabs in a different color. Draws the given element with the provided painter with the style options specified by option. Args: element: ControlElement opt: QStyleOption p: QPainter widget: QWidget """ if element not in [QStyle.ControlElement.CE_TabBarTab, QStyle.ControlElement.CE_TabBarTabShape, QStyle.ControlElement.CE_TabBarTabLabel]: # Let the real style draw it. self._base_style().drawControl(element, opt, p, widget) return layouts = self._tab_layout(opt) if layouts is None: log.misc.warning("Could not get layouts for tab!") return if element == QStyle.ControlElement.CE_TabBarTab: # We override this so we can control TabBarTabShape/TabBarTabLabel. self.drawControl(QStyle.ControlElement.CE_TabBarTabShape, opt, p, widget) self.drawControl(QStyle.ControlElement.CE_TabBarTabLabel, opt, p, widget) elif element == QStyle.ControlElement.CE_TabBarTabShape: p.fillRect(opt.rect, opt.palette.window()) self._draw_indicator(layouts, opt, p) # We use QCommonStyle rather than self.baseStyle() here because we don't want # any sophisticated drawing. QCommonStyle.drawControl(self, QStyle.ControlElement.CE_TabBarTabShape, opt, p, widget) elif element == QStyle.ControlElement.CE_TabBarTabLabel: if not opt.icon.isNull() and layouts.icon.isValid(): self._draw_icon(layouts, opt, p) alignment = (config.cache['tabs.title.alignment'] | Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextHideMnemonic) self._base_style().drawItemText( p, layouts.text, int(alignment), opt.palette, bool(opt.state & QStyle.StateFlag.State_Enabled), opt.text, QPalette.ColorRole.WindowText ) else: raise ValueError("Invalid element {!r}".format(element)) def pixelMetric(self, metric, option=None, widget=None): """Override pixelMetric to not shift the selected tab. Args: metric: PixelMetric option: const QStyleOption * widget: const QWidget * Return: An int. """ if metric in [QStyle.PixelMetric.PM_TabBarTabShiftHorizontal, QStyle.PixelMetric.PM_TabBarTabShiftVertical, QStyle.PixelMetric.PM_TabBarTabHSpace, QStyle.PixelMetric.PM_TabBarTabVSpace, QStyle.PixelMetric.PM_TabBarScrollButtonWidth]: return 0 else: return self._base_style().pixelMetric(metric, option, widget) def subElementRect(self, sr, opt, widget=None): """Override subElementRect to use our own _tab_layout implementation. Args: sr: SubElement opt: QStyleOption widget: QWidget Return: A QRect. """ if sr == QStyle.SubElement.SE_TabBarTabText: layouts = self._tab_layout(opt) if layouts is None: log.misc.warning("Could not get layouts for tab!") return QRect() return layouts.text elif sr in [QStyle.SubElement.SE_TabWidgetTabBar, QStyle.SubElement.SE_TabBarScrollLeftButton]: # Handling SE_TabBarScrollLeftButton so the left scroll button is # aligned properly. Otherwise, empty space will be shown after the # last tab even though the button width is set to 0 # # Need to use QCommonStyle here because we also use it to render # element in drawControl(); otherwise, we may get bit by # style differences... return QCommonStyle.subElementRect(self, sr, opt, widget) else: return self._base_style().subElementRect(sr, opt, widget) def _tab_layout(self, opt): """Compute the text/icon rect from the opt rect. This is based on Qt's QCommonStylePrivate::tabLayout (qtbase/src/widgets/styles/qcommonstyle.cpp) as we can't use the private implementation. Args: opt: QStyleOptionTab Return: A Layout object with two QRects. """ padding = config.cache['tabs.padding'] indicator_padding = config.cache['tabs.indicator.padding'] text_rect = QRect(opt.rect) if not text_rect.isValid(): # This happens sometimes according to crash reports, but no idea # why... return None text_rect.adjust(padding.left, padding.top, -padding.right, -padding.bottom) indicator_width = config.cache['tabs.indicator.width'] if indicator_width == 0: indicator_rect = QRect() else: indicator_rect = QRect(opt.rect) qtutils.ensure_valid(indicator_rect) indicator_rect.adjust(padding.left + indicator_padding.left, padding.top + indicator_padding.top, 0, -(padding.bottom + indicator_padding.bottom)) indicator_rect.setWidth(indicator_width) text_rect.adjust(indicator_width + indicator_padding.left + indicator_padding.right, 0, 0, 0) icon_rect = self._get_icon_rect(opt, text_rect) if icon_rect.isValid(): text_rect.adjust( icon_rect.width() + TabBarStyle.ICON_PADDING, 0, 0, 0) text_rect = self._base_style().visualRect(opt.direction, opt.rect, text_rect) return Layouts(text=text_rect, icon=icon_rect, indicator=indicator_rect) def _get_icon_rect(self, opt, text_rect): """Get a QRect for the icon to draw. Args: opt: QStyleOptionTab text_rect: The QRect for the text. Return: A QRect. """ icon_size = opt.iconSize if not icon_size.isValid(): icon_extent = self.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize) icon_size = QSize(icon_extent, icon_extent) icon_mode = (QIcon.Mode.Normal if opt.state & QStyle.StateFlag.State_Enabled else QIcon.Mode.Disabled) icon_state = (QIcon.State.On if opt.state & QStyle.StateFlag.State_Selected else QIcon.State.Off) # reserve space for favicon when tab bar is vertical (issue #1968) position = config.cache['tabs.position'] if (position in [QTabWidget.TabPosition.East, QTabWidget.TabPosition.West] and config.cache['tabs.favicons.show'] != 'never'): tab_icon_size = icon_size else: actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) tab_icon_size = QSize( min(actual_size.width(), icon_size.width()), min(actual_size.height(), icon_size.height())) icon_top = text_rect.center().y() + 1 - tab_icon_size.height() // 2 icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size) icon_rect = self._base_style().visualRect(opt.direction, opt.rect, icon_rect) return icon_rect ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/mainwindow/windowundo.py0000644000175100017510000000427615102145205023062 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Code for :undo --window.""" import collections import dataclasses from typing import cast, TYPE_CHECKING from collections.abc import MutableSequence from qutebrowser.qt.core import QObject, QByteArray from qutebrowser.config import config from qutebrowser.mainwindow import mainwindow from qutebrowser.misc import objects if TYPE_CHECKING: from qutebrowser.mainwindow import tabbedbrowser instance = cast('WindowUndoManager', None) @dataclasses.dataclass class _WindowUndoEntry: """Information needed for :undo -w.""" geometry: QByteArray tab_stack: 'tabbedbrowser.UndoStackType' class WindowUndoManager(QObject): """Manager which saves/restores windows.""" def __init__(self, parent=None): super().__init__(parent) self._undos: MutableSequence[_WindowUndoEntry] = collections.deque() objects.qapp.window_closing.connect(self._on_window_closing) config.instance.changed.connect(self._on_config_changed) @config.change_filter('tabs.undo_stack_size') def _on_config_changed(self): self._update_undo_stack_size() def _on_window_closing(self, window): if window.tabbed_browser.is_private: return self._undos.append(_WindowUndoEntry( geometry=window.saveGeometry(), tab_stack=window.tabbed_browser.undo_stack, )) def _update_undo_stack_size(self): newsize = config.instance.get('tabs.undo_stack_size') if newsize < 0: newsize = None self._undos = collections.deque(self._undos, maxlen=newsize) def undo_last_window_close(self): """Restore the last window to be closed. It will have the same tab and undo stack as when it was closed. """ entry = self._undos.pop() window = mainwindow.MainWindow( private=False, geometry=entry.geometry, ) window.tabbed_browser.undo_stack = entry.tab_stack window.tabbed_browser.undo() window.show() def init(): global instance instance = WindowUndoManager(parent=objects.qapp) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5476387 qutebrowser-3.6.1/qutebrowser/misc/0000755000175100017510000000000015102145351017063 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/__init__.py0000644000175100017510000000022315102145205021167 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Misc. modules.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/autoupdate.py0000644000175100017510000000420615102145205021610 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Classes related to auto-updating and getting the latest version.""" import json from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QUrl from qutebrowser.misc import httpclient class PyPIVersionClient(QObject): """A client for the PyPI API using HTTPClient. It gets the latest version of qutebrowser from PyPI. Attributes: _client: The HTTPClient used. Class attributes: API_URL: The base API URL. Signals: success: Emitted when getting the version info succeeded. arg: The newest version. error: Emitted when getting the version info failed. arg: The error message, as string. """ API_URL = 'https://pypi.org/pypi/{}/json' success = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, parent=None, client=None): super().__init__(parent) if client is None: self._client = httpclient.HTTPClient(self) else: self._client = client self._client.error.connect(self.error) self._client.success.connect(self.on_client_success) def get_version(self, package='qutebrowser'): """Get the newest version of a given package. Emits success/error when done. Args: package: The name of the package to check. """ url = QUrl(self.API_URL.format(package)) self._client.get(url) @pyqtSlot(str) def on_client_success(self, data): """Process the data and finish when the client finished. Args: data: A string with the received data. """ try: json_data = json.loads(data) except ValueError as e: self.error.emit("Invalid JSON received in reply: {}!".format(e)) return try: self.success.emit(json_data['info']['version']) except KeyError as e: self.error.emit("Malformed data received in reply " "({!r} not found)!".format(e)) return ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/backendproblem.py0000644000175100017510000004455115102145205022414 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Dialogs shown when there was a problem with a backend choice.""" import os import sys import functools import html import enum import shutil import os.path import argparse import dataclasses from typing import Any, Optional from collections.abc import Sequence from qutebrowser.qt import machinery from qutebrowser.qt.core import Qt from qutebrowser.qt.widgets import (QDialog, QPushButton, QHBoxLayout, QVBoxLayout, QLabel, QMessageBox, QWidget) from qutebrowser.qt.network import QSslSocket from qutebrowser.config import config, configfiles from qutebrowser.utils import (usertypes, version, qtutils, log, utils, standarddir) from qutebrowser.misc import objects, msgbox, savemanager, quitter class _Result(enum.IntEnum): """The result code returned by the backend problem dialog.""" quit = QDialog.DialogCode.Accepted + 1 restart = QDialog.DialogCode.Accepted + 2 restart_webkit = QDialog.DialogCode.Accepted + 3 restart_webengine = QDialog.DialogCode.Accepted + 4 @dataclasses.dataclass class _Button: """A button passed to BackendProblemDialog.""" text: str setting: str value: Any default: bool = False def _other_backend(backend: usertypes.Backend) -> tuple[usertypes.Backend, str]: """Get the other backend enum/setting for a given backend.""" other_backend = { usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine, usertypes.Backend.QtWebEngine: usertypes.Backend.QtWebKit, }[backend] other_setting = other_backend.name.lower()[2:] return (other_backend, other_setting) def _error_text( because: str, text: str, backend: usertypes.Backend, suggest_other_backend: bool = False, ) -> str: """Get an error text for the given information.""" text = (f"Failed to start with the {backend.name} backend!" f"

qutebrowser tried to start with the {backend.name} backend but " f"failed because {because}.

{text}") if suggest_other_backend: other_backend, other_setting = _other_backend(backend) if other_backend == usertypes.Backend.QtWebKit: warning = ("Note that QtWebKit hasn't been updated since " "July 2017 (including security updates).") suffix = " (not recommended)" else: warning = "" suffix = "" text += (f"

Forcing the {other_backend.name} backend{suffix}

" f"

This forces usage of the {other_backend.name} backend by " f"setting the backend = '{other_setting}' option " f"(if you have a config.py file, you'll need to set " f"this manually). {warning}

") text += f"

{machinery.INFO.to_html()}

" return text class _Dialog(QDialog): """A dialog which gets shown if there are issues with the backend.""" def __init__(self, *, because: str, text: str, backend: usertypes.Backend, suggest_other_backend: bool = True, buttons: Sequence[_Button] = None, parent: QWidget = None) -> None: super().__init__(parent) vbox = QVBoxLayout(self) text = _error_text(because, text, backend, suggest_other_backend=suggest_other_backend) label = QLabel(text) label.setWordWrap(True) label.setTextFormat(Qt.TextFormat.RichText) vbox.addWidget(label) hbox = QHBoxLayout() buttons = [] if buttons is None else buttons quit_button = QPushButton("Quit") quit_button.clicked.connect(lambda: self.done(_Result.quit)) hbox.addWidget(quit_button) if suggest_other_backend: other_backend, other_setting = _other_backend(backend) backend_text = "Force {} backend".format(other_backend.name) if other_backend == usertypes.Backend.QtWebKit: backend_text += ' (not recommended)' backend_button = QPushButton(backend_text) backend_button.clicked.connect(functools.partial( self._change_setting, 'backend', other_setting)) hbox.addWidget(backend_button) for button in buttons: btn = QPushButton(button.text) btn.setDefault(button.default) btn.clicked.connect(functools.partial( self._change_setting, button.setting, button.value)) hbox.addWidget(btn) vbox.addLayout(hbox) def _change_setting(self, setting: str, value: str) -> None: """Change the given setting and restart.""" config.instance.set_obj(setting, value, save_yaml=True) if setting == 'backend' and value == 'webkit': self.done(_Result.restart_webkit) elif setting == 'backend' and value == 'webengine': self.done(_Result.restart_webengine) else: self.done(_Result.restart) @dataclasses.dataclass class _BackendImports: """Whether backend modules could be imported.""" webkit_error: Optional[str] = None webengine_error: Optional[str] = None class _BackendProblemChecker: """Check for various backend-specific issues.""" def __init__(self, *, no_err_windows: bool, save_manager: savemanager.SaveManager) -> None: self._save_manager = save_manager self._no_err_windows = no_err_windows def _show_dialog(self, *args: Any, **kwargs: Any) -> None: """Show a dialog for a backend problem.""" if self._no_err_windows: text = _error_text(*args, **kwargs) log.init.error(text) sys.exit(usertypes.Exit.err_init) dialog = _Dialog(*args, **kwargs) status = dialog.exec() self._save_manager.save_all(is_exit=True) if status in [_Result.quit, QDialog.DialogCode.Rejected]: pass elif status == _Result.restart_webkit: quitter.instance.restart(override_args={'backend': 'webkit'}) elif status == _Result.restart_webengine: quitter.instance.restart(override_args={'backend': 'webengine'}) elif status == _Result.restart: quitter.instance.restart() else: raise utils.Unreachable(status) sys.exit(usertypes.Exit.err_init) def _try_import_backends(self) -> _BackendImports: """Check whether backends can be imported and return BackendImports.""" # pylint: disable=unused-import results = _BackendImports() try: from qutebrowser.qt import webkit, webkitwidgets except (ImportError, ValueError) as e: results.webkit_error = str(e) assert results.webkit_error else: if not qtutils.is_new_qtwebkit(): results.webkit_error = "Unsupported legacy QtWebKit found" try: from qutebrowser.qt import webenginecore, webenginewidgets except (ImportError, ValueError) as e: results.webengine_error = str(e) assert results.webengine_error return results def _handle_ssl_support(self, fatal: bool = False) -> None: """Check for full SSL availability. If "fatal" is given, show an error and exit. """ if QSslSocket.supportsSsl(): return text = ("Could not initialize QtNetwork SSL support. This only " "affects downloads and :adblock-update.") if fatal: errbox = msgbox.msgbox(parent=None, title="SSL error", text="Could not initialize SSL support.", icon=QMessageBox.Icon.Critical, plain_text=False) errbox.exec() sys.exit(usertypes.Exit.err_init) # Doing this here because it's not relevant with QtWebKit where fatal=True if machinery.IS_QT6: text += ("\nHint: If installed via mkvenv.py on a system without " "OpenSSL 3.x (e.g. Ubuntu 20.04), you can use --pyqt-version 6.4 " "to get an older Qt still compatible with OpenSSL 1.1 (at the " "expense of running an older QtWebEngine/Chromium)") assert not fatal log.init.warning(text) def _check_backend_modules(self) -> None: """Check for the modules needed for QtWebKit/QtWebEngine.""" imports = self._try_import_backends() if not imports.webkit_error and not imports.webengine_error: return elif imports.webkit_error and imports.webengine_error: text = ("

qutebrowser needs QtWebKit or QtWebEngine, but " "neither could be imported!

" "

The errors encountered were:

    " "
  • QtWebKit: {webkit_error}" "
  • QtWebEngine: {webengine_error}" "

{info}

".format( webkit_error=html.escape(imports.webkit_error), webengine_error=html.escape(imports.webengine_error), info=machinery.INFO.to_html(), )) errbox = msgbox.msgbox(parent=None, title="No backend library found!", text=text, icon=QMessageBox.Icon.Critical, plain_text=False) errbox.exec() sys.exit(usertypes.Exit.err_init) elif objects.backend == usertypes.Backend.QtWebKit: if not imports.webkit_error: return self._show_dialog( backend=usertypes.Backend.QtWebKit, because="QtWebKit could not be imported", text="

The error encountered was:
{}

".format( html.escape(imports.webkit_error)) ) elif objects.backend == usertypes.Backend.QtWebEngine: if not imports.webengine_error: return self._show_dialog( backend=usertypes.Backend.QtWebEngine, because="QtWebEngine could not be imported", text="

The error encountered was:
{}

".format( html.escape(imports.webengine_error)) ) raise utils.Unreachable def _handle_serviceworker_nuking(self) -> None: """Nuke the service workers directory if the Qt version changed. WORKAROUND for: https://bugreports.qt.io/browse/QTBUG-72532 https://bugreports.qt.io/browse/QTBUG-82105 https://bugreports.qt.io/browse/QTBUG-93744 """ if configfiles.state.qt_version_changed: reason = 'Qt version changed' elif configfiles.state.qtwe_version_changed: reason = 'QtWebEngine version changed' elif config.val.qt.workarounds.remove_service_workers: reason = 'Explicitly enabled' else: return service_worker_dir = os.path.join( standarddir.data(), 'webengine', 'Service Worker') bak_dir = service_worker_dir + '-bak' if not os.path.exists(service_worker_dir): return log.init.info( f"Removing service workers at {service_worker_dir} (reason: {reason})") # Keep one backup around - we're not 100% sure what persistent data # could be in there, but this folder can grow to ~300 MB. if os.path.exists(bak_dir): shutil.rmtree(bak_dir) shutil.move(service_worker_dir, bak_dir) def _confirm_chromium_version_changes(self) -> None: """Ask if there are Chromium downgrades or a Qt 5 -> 6 upgrade.""" versions = version.qtwebengine_versions(avoid_init=True) change = configfiles.state.chromium_version_changed info = f"

{machinery.INFO.to_html()}" if machinery.INFO.reason == machinery.SelectionReason.auto: info += ( "

" "You can use --qt-wrapper or set QUTE_QT_WRAPPER " "in your environment to override this." ) webengine_data_dir = os.path.join(standarddir.data(), "webengine") if change == configfiles.VersionChange.major: icon = QMessageBox.Icon.Information text = ( "Chromium/QtWebEngine upgrade detected:
" f"You are upgrading to QtWebEngine {versions.webengine} but " "used Qt 5 for the last qutebrowser launch.

" "Data managed by Chromium will be upgraded. This is a one-way " "operation: If you open qutebrowser with Qt 5 again later, any " "Chromium data will be invalid and discarded.

" "This affects page data such as cookies, but not data managed by " "qutebrowser, such as your configuration or :open history.
" f"The affected data is in {webengine_data_dir}." ) + info elif change == configfiles.VersionChange.downgrade: icon = QMessageBox.Icon.Warning text = ( "Chromium/QtWebEngine downgrade detected:
" f"You are downgrading to QtWebEngine {versions.webengine}." "

" "Data managed by Chromium will be discarded if you continue." "

" "This affects page data such as cookies, but not data managed by " "qutebrowser, such as your configuration or :open history.
" f"The affected data is in {webengine_data_dir}." ) + info else: return box = msgbox.msgbox( parent=None, title="QtWebEngine version change", text=text, icon=icon, plain_text=False, buttons=QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Abort, ) response = box.exec() if response != QMessageBox.StandardButton.Ok: sys.exit(usertypes.Exit.err_init) def _check_webengine_version(self) -> None: versions = version.qtwebengine_versions(avoid_init=True) if versions.webengine < utils.VersionNumber(5, 15, 2): text = ( "QtWebEngine >= 5.15.2 is required for qutebrowser, but " f"{versions.webengine} is installed.") errbox = msgbox.msgbox(parent=None, title="QtWebEngine too old", text=text, icon=QMessageBox.Icon.Critical, plain_text=False) errbox.exec() sys.exit(usertypes.Exit.err_init) def _check_software_rendering(self) -> None: """Avoid crashing software rendering settings. WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103372 Fixed with QtWebEngine 6.3.1. """ self._assert_backend(usertypes.Backend.QtWebEngine) versions = version.qtwebengine_versions(avoid_init=True) if versions.webengine != utils.VersionNumber(6, 3): return if os.environ.get('QT_QUICK_BACKEND') != 'software': return text = ("You can instead force software rendering on the Chromium level (sets " "qt.force_software_rendering to chromium instead of " "qt-quick).") button = _Button("Force Chromium software rendering", 'qt.force_software_rendering', 'chromium') self._show_dialog( backend=usertypes.Backend.QtWebEngine, suggest_other_backend=False, because="a Qt 6.3.0 bug causes instant crashes with Qt Quick software rendering", text=text, buttons=[button], ) raise utils.Unreachable def _force_wayland_hardware_acceleration(self) -> None: """Set environment variable so hardware acceleration works on Wayland. Set EGL_PLATFORM=wayland to force ANGLE to obtain EGL display connection for wayland platform. Otherwise, the display connection for EGL_DEFAULT_DISPLAY may belong to a platform which Nvidia's EGL driver doesn't support. In case of unsupported platform, EGL may fallback to Mesa software renderer (LLVMPipe) disabling hardware acceleration in Chromium. Equivalent to: https://codereview.qt-project.org/c/qt/qtwebengine/+/663568 """ if objects.qapp.platformName() != 'wayland': return versions = version.qtwebengine_versions(avoid_init=True) if versions.webengine >= utils.VersionNumber(6, 10): # Qt workaround is active return egl_platform_var = "EGL_PLATFORM" egl_platform = os.environ.get(egl_platform_var) if not egl_platform: os.environ[egl_platform_var] = "wayland" elif egl_platform != "wayland": log.init.warning( f"{egl_platform_var} environment variable is set to {egl_platform!r}. " "This may break hardware rendering on Wayland." ) def _assert_backend(self, backend: usertypes.Backend) -> None: assert objects.backend == backend, objects.backend def check(self) -> None: """Run all checks.""" self._check_backend_modules() if objects.backend == usertypes.Backend.QtWebEngine: self._check_webengine_version() self._handle_ssl_support() self._handle_serviceworker_nuking() self._check_software_rendering() self._force_wayland_hardware_acceleration() self._confirm_chromium_version_changes() else: self._assert_backend(usertypes.Backend.QtWebKit) self._handle_ssl_support(fatal=True) def init(*, args: argparse.Namespace, save_manager: savemanager.SaveManager) -> None: """Run all checks.""" checker = _BackendProblemChecker(no_err_windows=args.no_err_windows, save_manager=save_manager) checker.check() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/binparsing.py0000644000175100017510000000204015102145205021563 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities for parsing binary files. Used by elf.py as well as pakjoy.py. """ import struct from typing import Any, IO class ParseError(Exception): """Raised when the file can't be parsed.""" def unpack(fmt: str, fobj: IO[bytes]) -> tuple[Any, ...]: """Unpack the given struct format from the given file.""" size = struct.calcsize(fmt) data = safe_read(fobj, size) try: return struct.unpack(fmt, data) except struct.error as e: raise ParseError(e) def safe_read(fobj: IO[bytes], size: int) -> bytes: """Read from a file, handling possible exceptions.""" try: return fobj.read(size) except (OSError, OverflowError) as e: raise ParseError(e) def safe_seek(fobj: IO[bytes], pos: int) -> None: """Seek in a file, handling possible exceptions.""" try: fobj.seek(pos) except (OSError, OverflowError) as e: raise ParseError(e) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/checkpyver.py0000644000175100017510000000333015102145205021575 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Check if qutebrowser is run with the correct python version. This should import and run fine with both python2 and python3. """ import sys try: # Python3 from tkinter import Tk, messagebox except ImportError: # pragma: no cover try: # Python2 from Tkinter import Tk # type: ignore[import-not-found, no-redef] import tkMessageBox as messagebox # type: ignore[import-not-found, no-redef] # noqa: N813 except ImportError: # Some Python without Tk Tk = None # type: ignore[misc, assignment] messagebox = None # type: ignore[assignment] # First we check the version of Python. This code should run fine with python2 # and python3. We don't have Qt available here yet, so we just print an error # to stderr. def check_python_version(): """Check if correct python version is run.""" if sys.hexversion < 0x03090000: # We don't use .format() and print_function here just in case someone # still has < 2.6 installed. version_str = '.'.join(map(str, sys.version_info[:3])) text = ("At least Python 3.9 is required to run qutebrowser, but " + "it's running with " + version_str + ".\n") show_errors = '--no-err-windows' not in sys.argv if Tk and show_errors: # type: ignore[truthy-function] # pragma: no cover root = Tk() root.withdraw() messagebox.showerror("qutebrowser: Fatal error!", text) else: sys.stderr.write(text) sys.stderr.flush() sys.exit(1) if __name__ == '__main__': check_python_version() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/cmdhistory.py0000644000175100017510000000720015102145205021617 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Command history for the status bar.""" from collections.abc import MutableSequence from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QObject from qutebrowser.utils import usertypes, log, standarddir, objreg from qutebrowser.misc import lineparser class HistoryEmptyError(Exception): """Raised when the history is empty.""" class HistoryEndReachedError(Exception): """Raised when the end of the history is reached.""" class History(QObject): """Command history. Attributes: history: A list of executed commands, with newer commands at the end. _tmphist: Temporary history for history browsing (as NeighborList) Signals: changed: Emitted when an entry was added to the history. """ changed = pyqtSignal() def __init__(self, *, history=None, parent=None): """Constructor. Args: history: The initial history to set. """ super().__init__(parent) self._tmphist = None if history is None: self.history: MutableSequence[str] = [] else: self.history = history def __getitem__(self, idx): return self.history[idx] def is_browsing(self): """Check _tmphist to see if we're browsing.""" return self._tmphist is not None def start(self, text): """Start browsing to the history. Called when the user presses the up/down key and wasn't browsing the history already. Args: text: The preset text. """ log.misc.debug("Preset text: '{}'".format(text)) if text: items: MutableSequence[str] = [ e for e in self.history if e.startswith(text)] else: items = self.history if not items: raise HistoryEmptyError self._tmphist = usertypes.NeighborList(items) return self._tmphist.lastitem() @pyqtSlot() def stop(self): """Stop browsing the history.""" self._tmphist = None def previtem(self): """Get the previous item in the temp history. start() needs to be called before calling this. """ if not self.is_browsing(): raise ValueError("Currently not browsing history") assert self._tmphist is not None try: return self._tmphist.previtem() except IndexError: raise HistoryEndReachedError def nextitem(self): """Get the next item in the temp history. start() needs to be called before calling this. """ if not self.is_browsing(): raise ValueError("Currently not browsing history") assert self._tmphist is not None try: return self._tmphist.nextitem() except IndexError: raise HistoryEndReachedError def append(self, text): """Append a new item to the history. Args: text: The text to append. """ if not self.history or text != self.history[-1]: self.history.append(text) self.changed.emit() def init(): """Initialize the LimitLineParser storing the history.""" save_manager = objreg.get('save-manager') command_history = lineparser.LimitLineParser( standarddir.data(), 'cmd-history', limit='completion.cmd_history_max_items') objreg.register('command-history', command_history) save_manager.add_saveable('command-history', command_history.save, command_history.changed) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/consolewidget.py0000644000175100017510000001464115102145205022307 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Debugging console.""" import sys import code from typing import Optional from collections.abc import MutableSequence from qutebrowser.qt.core import pyqtSignal, pyqtSlot, Qt from qutebrowser.qt.widgets import QTextEdit, QWidget, QVBoxLayout, QApplication from qutebrowser.qt.gui import QTextCursor from qutebrowser.config import stylesheet from qutebrowser.misc import cmdhistory, miscwidgets from qutebrowser.utils import utils, objreg console_widget: Optional["ConsoleWidget"] = None class ConsoleLineEdit(miscwidgets.CommandLineEdit): """A QLineEdit which executes entered code and provides a history. Attributes: _history: The command history of executed commands. Signals: execute: Emitted when a commandline should be executed. """ execute = pyqtSignal(str) def __init__(self, _namespace, parent): """Constructor. Args: _namespace: The local namespace of the interpreter. """ super().__init__(parent=parent) self._history = cmdhistory.History(parent=self) self.returnPressed.connect(self.on_return_pressed) @pyqtSlot() def on_return_pressed(self): """Execute the line of code which was entered.""" self._history.stop() text = self.text() if text: self._history.append(text) self.execute.emit(text) self.setText('') def history_prev(self): """Go back in the history.""" try: if not self._history.is_browsing(): item = self._history.start(self.text().strip()) else: item = self._history.previtem() except (cmdhistory.HistoryEmptyError, cmdhistory.HistoryEndReachedError): return self.setText(item) def history_next(self): """Go forward in the history.""" if not self._history.is_browsing(): return try: item = self._history.nextitem() except cmdhistory.HistoryEndReachedError: return self.setText(item) def keyPressEvent(self, e): """Override keyPressEvent to handle special keypresses.""" if e.key() == Qt.Key.Key_Up: self.history_prev() e.accept() elif e.key() == Qt.Key.Key_Down: self.history_next() e.accept() elif e.modifiers() & Qt.KeyboardModifier.ControlModifier and e.key() == Qt.Key.Key_C: self.setText('') e.accept() else: super().keyPressEvent(e) class ConsoleTextEdit(QTextEdit): """Custom QTextEdit for console output.""" def __init__(self, parent=None): super().__init__(parent) self.setAcceptRichText(False) self.setReadOnly(True) self.setFocusPolicy(Qt.FocusPolicy.ClickFocus) def __repr__(self): return utils.get_repr(self) def append_text(self, text): """Append new text and scroll output to bottom. We can't use Qt's way to append stuff because that inserts weird newlines. """ self.moveCursor(QTextCursor.MoveOperation.End) self.insertPlainText(text) scrollbar = self.verticalScrollBar() assert scrollbar is not None scrollbar.setValue(scrollbar.maximum()) class ConsoleWidget(QWidget): """A widget with an interactive Python console. Attributes: _lineedit: The line edit in the console. _output: The output widget in the console. _vbox: The layout which contains everything. _more: A flag which is set when more input is expected. _buffer: The buffer for multi-line commands. _interpreter: The InteractiveInterpreter to execute code with. """ STYLESHEET = """ ConsoleWidget > ConsoleTextEdit, ConsoleWidget > ConsoleLineEdit { font: {{ conf.fonts.debug_console }}; } """ def __init__(self, parent=None): super().__init__(parent) if not hasattr(sys, 'ps1'): sys.ps1 = '>>> ' if not hasattr(sys, 'ps2'): sys.ps2 = '... ' namespace = { '__name__': '__console__', '__doc__': None, 'q_app': QApplication.instance(), # We use parent as self here because the user "feels" the whole # console, not just the line edit. 'self': parent, 'objreg': objreg, } self._more = False self._buffer: MutableSequence[str] = [] self._lineedit = ConsoleLineEdit(namespace, self) self._lineedit.execute.connect(self.push) self._output = ConsoleTextEdit() self.write(self._curprompt()) self._vbox = QVBoxLayout() self._vbox.setSpacing(0) self._vbox.addWidget(self._output) self._vbox.addWidget(self._lineedit) stylesheet.set_register(self) self.setLayout(self._vbox) self._lineedit.setFocus() self._interpreter = code.InteractiveInterpreter(namespace) def __repr__(self): return utils.get_repr(self, visible=self.isVisible()) def write(self, line): """Write a line of text (without added newline) to the output.""" self._output.append_text(line) @pyqtSlot(str) def push(self, line): """Push a line to the interpreter.""" self._buffer.append(line) source = '\n'.join(self._buffer) self.write(line + '\n') # We do two special things with the context managers here: # - We replace stdout/stderr to capture output. Even if we could # override InteractiveInterpreter's write method, most things are # printed elsewhere (e.g. by exec). Other Python GUI shells do the # same. # - We disable our exception hook, so exceptions from the console get # printed and don't open a crashdialog. with utils.fake_io(self.write), utils.disabled_excepthook(): self._more = self._interpreter.runsource(source, '') self.write(self._curprompt()) if not self._more: self._buffer = [] def _curprompt(self): """Get the prompt which is visible currently.""" return sys.ps2 if self._more else sys.ps1 def init(): """Initialize a global console.""" global console_widget console_widget = ConsoleWidget() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/crashdialog.py0000644000175100017510000006133515102145205021723 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The dialog which gets shown when qutebrowser crashes.""" import re import os import sys import html import getpass import fnmatch import traceback import datetime import enum from qutebrowser.qt.core import pyqtSlot, Qt, QSize from qutebrowser.qt.widgets import (QDialog, QLabel, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QCheckBox, QDialogButtonBox, QMessageBox) import qutebrowser from qutebrowser.utils import version, log, utils from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient, pastebin, objects) from qutebrowser.config import config, configfiles from qutebrowser.browser import history class Result(enum.IntEnum): """The result code returned by the crash dialog.""" restore = QDialog.DialogCode.Accepted + 1 no_restore = QDialog.DialogCode.Accepted + 2 def parse_fatal_stacktrace(text): """Get useful information from a fatal faulthandler stacktrace. Args: text: The text to parse. Return: A tuple with the first element being the error type, and the second element being the first stacktrace frame. """ lines = [ r'(?PFatal Python error|Windows fatal exception): (?P.*)', r' *', r'(Current )?[Tt]hread .* \(most recent call first\): *', r' (File ".*", line \d+ in (?P.*)|)', ] m = re.search('\n'.join(lines), text) if m is None: # We got some invalid text. return ('', '') else: msg = m.group('msg') typ = m.group('type') func = m.group('func') or '' if typ == 'Windows fatal exception': msg = 'Windows ' + msg return msg, func def _get_environment_vars(): """Gather environment variables for the crash info.""" masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG', 'XDG_*', 'QUTE_*', 'PATH', 'XMODIFIERS', 'XIM_*', 'QTWEBENGINE_*') info = [] for key, value in os.environ.items(): for m in masks: if fnmatch.fnmatch(key, m): info.append('{} = {}'.format(key, value)) return '\n'.join(sorted(info)) class _CrashDialog(QDialog): """Dialog which gets shown after there was a crash. Attributes: These are just here to have a static reference to avoid GCing. _vbox: The main QVBoxLayout _lbl: The QLabel with the static text _debug_log: The QTextEdit with the crash information _btn_box: The QDialogButtonBox containing the buttons. _url: Pastebin URL QLabel. _crash_info: A list of tuples with title and crash information. _paste_client: A PastebinClient instance to use. _pypi_client: A PyPIVersionClient instance to use. _paste_text: The text to pastebin. """ def __init__(self, debug, parent=None): """Constructor for CrashDialog. Args: debug: Whether --debug was given. """ super().__init__(parent) # We don't set WA_DeleteOnClose here as on an exception, we'll get # closed anyways, and it only could have unintended side-effects. self._crash_info: list[tuple[str, str]] = [] self._btn_box = None self._paste_text = None self.setWindowTitle("Whoops!") self.resize(QSize(640, 600)) self._vbox = QVBoxLayout(self) http_client = httpclient.HTTPClient() self._paste_client = pastebin.PastebinClient(http_client, self) self._pypi_client = autoupdate.PyPIVersionClient(self) self._paste_client.success.connect(self.on_paste_success) self._paste_client.error.connect(self.show_error) self._init_text() self._init_contact_input() info = QLabel("What were you doing when this crash/bug happened?") self._vbox.addWidget(info) self._info = QTextEdit() self._info.setTabChangesFocus(True) self._info.setAcceptRichText(False) self._info.setPlaceholderText("- Opened http://www.example.com/\n" "- Switched tabs\n" "- etc...") self._vbox.addWidget(self._info, 5) self._vbox.addSpacing(15) self._debug_log = QTextEdit() self._debug_log.setTabChangesFocus(True) self._debug_log.setAcceptRichText(False) self._debug_log.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) self._debug_log.hide() self._fold = miscwidgets.DetailFold("Show log", self) self._fold.toggled.connect(self._debug_log.setVisible) if debug: self._fold.toggle() self._vbox.addWidget(self._fold) self._vbox.addWidget(self._debug_log, 10) self._vbox.addSpacing(15) self._init_checkboxes() self._init_info_text() self._init_buttons() def keyPressEvent(self, e): """Prevent closing :report dialogs when pressing .""" if config.val.input.escape_quits_reporter or e.key() != Qt.Key.Key_Escape: super().keyPressEvent(e) def __repr__(self): return utils.get_repr(self) def _init_contact_input(self): """Initialize the widget asking for contact info.""" contact = QLabel("I'd like to be able to follow up with you, to keep " "you posted on the status of this crash and get more " "information if I need it - how can I contact you?") contact.setWordWrap(True) self._vbox.addWidget(contact) self._contact = QTextEdit() self._contact.setTabChangesFocus(True) self._contact.setAcceptRichText(False) try: try: info = configfiles.state['general']['contact-info'] except KeyError: self._contact.setPlaceholderText("Mail or IRC nickname") else: self._contact.setPlainText(info) except Exception: log.misc.exception("Failed to get contact information!") self._contact.setPlaceholderText("Mail or IRC nickname") self._vbox.addWidget(self._contact, 2) def _init_text(self): """Initialize the main text to be displayed on an exception. Should be extended by subclasses to set the actual text. """ self._lbl = QLabel() self._lbl.setWordWrap(True) self._lbl.setOpenExternalLinks(True) self._lbl.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse) self._vbox.addWidget(self._lbl) def _init_checkboxes(self): """Initialize the checkboxes.""" def _init_buttons(self): """Initialize the buttons.""" self._btn_box = QDialogButtonBox() self._vbox.addWidget(self._btn_box) self._btn_report = QPushButton("Report") self._btn_report.setDefault(True) self._btn_report.clicked.connect(self.on_report_clicked) self._btn_box.addButton(self._btn_report, QDialogButtonBox.ButtonRole.AcceptRole) self._btn_cancel = QPushButton("Don't report") self._btn_cancel.setAutoDefault(False) self._btn_cancel.clicked.connect(self.finish) self._btn_box.addButton(self._btn_cancel, QDialogButtonBox.ButtonRole.RejectRole) def _init_info_text(self): """Add an info text encouraging the user to report crashes.""" info_label = QLabel("
There is currently a big backlog of crash " "reports. Thus, it might take a while until your " "report is seen.
A new tool allowing for more " "automation will fix this, but is not ready yet " "at this point.") info_label.setWordWrap(True) self._vbox.addWidget(info_label) def _gather_crash_info(self): """Gather crash information to display. Args: pages: A list of lists of the open pages (URLs as strings) cmdhist: A list with the command history (as strings) exc: An exception tuple (type, value, traceback) """ try: launch_time = objects.qapp.launch_time.ctime() crash_time = datetime.datetime.now().ctime() text = 'Launch: {}\nCrash: {}'.format(launch_time, crash_time) self._crash_info.append(('Timestamps', text)) except Exception: self._crash_info.append(("Launch time", traceback.format_exc())) try: self._crash_info.append(("Version info", version.version_info())) except Exception: self._crash_info.append(("Version info", traceback.format_exc())) try: self._crash_info.append(("Config", config.instance.dump_userconfig())) except Exception: self._crash_info.append(("Config", traceback.format_exc())) try: self._crash_info.append(("Environment", _get_environment_vars())) except Exception: self._crash_info.append(("Environment", traceback.format_exc())) def _set_crash_info(self): """Set/update the crash info.""" self._crash_info = [] self._gather_crash_info() chunks = [] for (header, body) in self._crash_info: if body is not None: h = '==== {} ===='.format(header) chunks.append('\n'.join([h, body])) text = '\n\n'.join(chunks) self._debug_log.setText(text) def _get_error_type(self): """Get the type of the error we're reporting.""" raise NotImplementedError def _get_paste_title_desc(self): """Get a short description of the paste.""" return '' def _get_paste_title(self): """Get a title for the paste.""" desc = self._get_paste_title_desc() title = "qute {} {}".format(qutebrowser.__version__, self._get_error_type()) if desc: title += ' {}'.format(desc) return title def _save_contact_info(self): """Save the contact info to disk.""" try: info = self._contact.toPlainText() configfiles.state['general']['contact-info'] = info except Exception: log.misc.exception("Failed to save contact information!") def report(self, *, info=None, contact=None): """Paste the crash info into the pastebin. If info/contact are given as arguments, they override the values entered in the dialog. """ lines = [] lines.append("========== Report ==========") lines.append(info or self._info.toPlainText()) lines.append("========== Contact ==========") lines.append(contact or self._contact.toPlainText()) lines.append("========== Debug log ==========") lines.append(self._debug_log.toPlainText()) self._paste_text = '\n\n'.join(lines) try: user = getpass.getuser() except Exception: log.misc.exception("Error while getting user") user = 'unknown' try: # parent: https://p.cmpl.cc/90286958 self._paste_client.paste(user, self._get_paste_title(), self._paste_text, parent='90286958') except Exception as e: log.misc.exception("Error while paste-binning") exc_text = '{}: {}'.format(e.__class__.__name__, e) self.show_error(exc_text) @pyqtSlot() def on_report_clicked(self): """Report and close dialog if report button was clicked.""" self._btn_report.setEnabled(False) self._btn_cancel.setEnabled(False) self._btn_report.setText("Reporting...") self.report() @pyqtSlot() def on_paste_success(self): """Get the newest version from PyPI when the paste is done.""" self._pypi_client.success.connect(self.on_version_success) self._pypi_client.error.connect(self.on_version_error) self._pypi_client.get_version() @pyqtSlot(str) def show_error(self, text): """Show a paste error dialog. Args: text: The paste text to show. """ error_dlg = ReportErrorDialog(text, self._paste_text, self) error_dlg.finished.connect(self.finish) error_dlg.show() @pyqtSlot(str) def on_version_success(self, newest): """Called when the version was obtained from self._pypi_client. Args: newest: The newest version as a string. """ new_version = utils.VersionNumber.parse(newest) cur_version = utils.VersionNumber.parse(qutebrowser.__version__) lines = ['The report has been sent successfully. Thanks!'] if new_version > cur_version: lines.append("Note: The newest available version is v{}, " "but you're currently running v{} - please " "update!".format(newest, qutebrowser.__version__)) text = '

'.join(lines) msgbox.information(self, "Report successfully sent!", text, on_finished=self.finish, plain_text=False) @pyqtSlot(str) def on_version_error(self, msg): """Called when the version was not obtained from self._pypi_client. Args: msg: The error message to show. """ lines = ['The report has been sent successfully. Thanks!'] lines.append("There was an error while getting the newest version: " "{}. Please check for a new version on " "qutebrowser.org " "by yourself.".format(msg)) text = '

'.join(lines) msgbox.information(self, "Report successfully sent!", text, on_finished=self.finish, plain_text=False) @pyqtSlot() def finish(self): """Save contact info and close the dialog.""" self._save_contact_info() self.accept() class ExceptionCrashDialog(_CrashDialog): """Dialog which gets shown on an exception. Attributes: _pages: A list of lists of the open pages (URLs as strings) _cmdhist: A list with the command history (as strings) _exc: An exception tuple (type, value, traceback) _qobjects: A list of all QObjects as string. """ def __init__(self, debug, pages, cmdhist, exc, qobjects, parent=None): super().__init__(debug, parent) self._pages = pages self._cmdhist = cmdhist self._exc = exc self._qobjects = qobjects self.setModal(True) self._set_crash_info() def _init_text(self): super()._init_text() text = "Argh! qutebrowser crashed unexpectedly." self._lbl.setText(text) def _init_checkboxes(self): """Add checkboxes to the dialog.""" super()._init_checkboxes() self._chk_restore = QCheckBox("Restore open pages") self._chk_restore.setChecked(True) self._vbox.addWidget(self._chk_restore) self._chk_log = QCheckBox("Include a debug log in the report") self._chk_log.setChecked(True) try: if config.val.content.private_browsing: self._chk_log.setChecked(False) except Exception: log.misc.exception("Error while checking private browsing mode") self._chk_log.toggled.connect(self._set_crash_info) self._vbox.addWidget(self._chk_log) info_label = QLabel("This makes it a lot easier to diagnose the " "crash.
Note that the log might contain " "sensitive information such as which pages you " "visited or keyboard input.
You can show " "and edit the log above.") info_label.setWordWrap(True) self._vbox.addWidget(info_label) def _get_error_type(self): return 'exc' def _get_paste_title_desc(self): desc = traceback.format_exception_only(self._exc[0], self._exc[1]) return desc[0].rstrip() def _gather_crash_info(self): self._crash_info += [ ("Exception", ''.join(traceback.format_exception(*self._exc))), ] super()._gather_crash_info() if self._chk_log.isChecked(): self._crash_info += [ ("Commandline args", ' '.join(sys.argv[1:])), ("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)), ("Command history", '\n'.join(self._cmdhist)), ("Objects", self._qobjects), ] try: text = "Log output was disabled." if log.ram_handler is not None: text = log.ram_handler.dump_log() self._crash_info.append(("Debug log", text)) except Exception: self._crash_info.append(("Debug log", traceback.format_exc())) @pyqtSlot() def finish(self): self._save_contact_info() if self._chk_restore.isChecked(): self.done(Result.restore) else: self.done(Result.no_restore) class FatalCrashDialog(_CrashDialog): """Dialog which gets shown when a fatal error occurred. Attributes: _log: The log text to display. _type: The type of error which occurred. _func: The function (top of the stack) in which the error occurred. _chk_history: A checkbox for the user to decide if page history should be sent. """ def __init__(self, debug, text, parent=None): super().__init__(debug, parent) self._log = text self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self._set_crash_info() self._type, self._func = parse_fatal_stacktrace(self._log) def _get_error_type(self): if self._type in ['Segmentation fault', 'Windows access violation']: return 'segv' else: return self._type def _get_paste_title_desc(self): return self._func def _init_text(self): super()._init_text() text = ("qutebrowser was restarted after a fatal crash.
" "
Note: Crash reports for fatal crashes sometimes don't " "contain the information necessary to fix an issue. Please " "follow the steps in " "stacktrace.asciidoc to submit a stacktrace.
") self._lbl.setText(text) def _init_checkboxes(self): """Add checkboxes to the dialog.""" super()._init_checkboxes() self._chk_history = QCheckBox("Include a history of the last " "accessed pages in the report.") self._chk_history.setChecked(True) try: if config.val.content.private_browsing: self._chk_history.setChecked(False) except Exception: log.misc.exception("Error while checking private browsing mode") self._chk_history.toggled.connect(self._set_crash_info) self._vbox.addWidget(self._chk_history) def _gather_crash_info(self): self._crash_info.append(("Fault log", self._log)) super()._gather_crash_info() if self._chk_history.isChecked(): try: if history.web_history is None: history_data = '' # type: ignore[unreachable] else: history_data = '\n'.join(str(e) for e in history.web_history.get_recent()) except Exception: history_data = traceback.format_exc() self._crash_info.append(("History", history_data)) @pyqtSlot() def on_report_clicked(self): """Prevent empty reports.""" if (not self._info.toPlainText().strip() and not self._contact.toPlainText().strip() and self._get_error_type() == 'segv' and self._func == 'qt_mainloop'): msgbox.msgbox(parent=self, title='Empty crash info', text="Empty reports for fatal crashes are useless " "and mean I'll spend time deleting reports I could " "spend on developing qutebrowser instead.\n\nPlease " "help making qutebrowser better by providing more " "information, or don't report this.", icon=QMessageBox.Icon.Critical) else: super().on_report_clicked() class ReportDialog(_CrashDialog): """Dialog which gets shown when the user wants to report an issue by hand. Attributes: _pages: A list of the open pages (URLs as strings) _cmdhist: A list with the command history (as strings) _qobjects: A list of all QObjects as string. """ def __init__(self, pages, cmdhist, qobjects, parent=None): super().__init__(False, parent) self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self._pages = pages self._cmdhist = cmdhist self._qobjects = qobjects self._set_crash_info() def _init_text(self): super()._init_text() text = "Please describe the bug you encountered below." self._lbl.setText(text) def _init_info_text(self): """We don't want an info text as the user wanted to report.""" def _get_error_type(self): return 'report' def _gather_crash_info(self): super()._gather_crash_info() self._crash_info += [ ("Commandline args", ' '.join(sys.argv[1:])), ("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)), ("Command history", '\n'.join(self._cmdhist)), ("Objects", self._qobjects), ] try: text = "Log output was disabled." if log.ram_handler is not None: text = log.ram_handler.dump_log() self._crash_info.append(("Debug log", text)) except Exception: self._crash_info.append(("Debug log", traceback.format_exc())) class ReportErrorDialog(QDialog): """An error dialog shown on unsuccessful reports.""" def __init__(self, exc_text, text, parent=None): super().__init__(parent) vbox = QVBoxLayout(self) label = QLabel("There was an error while reporting the crash:" "
{}

" "Please copy the text below and send a mail to " "" "crash@qutebrowser.org - Thanks!".format( html.escape(exc_text))) vbox.addWidget(label) txt = QTextEdit() txt.setReadOnly(True) txt.setTabChangesFocus(True) txt.setAcceptRichText(False) txt.setText(text) txt.selectAll() vbox.addWidget(txt) hbox = QHBoxLayout() hbox.addStretch() btn = QPushButton("Close") btn.clicked.connect(self.close) hbox.addWidget(btn) vbox.addLayout(hbox) def dump_exception_info(exc, pages, cmdhist, qobjects): """Dump exception info to stderr. Args: exc: An exception tuple (type, value, traceback) pages: A list of lists of the open pages (URLs as strings) cmdhist: A list with the command history (as strings) qobjects: A list of all QObjects as string. """ print(file=sys.stderr) print("\n\n===== Handling exception with --no-err-windows... =====\n\n", file=sys.stderr) print("\n---- Exceptions ----", file=sys.stderr) print(''.join(traceback.format_exception(*exc)), file=sys.stderr) print("\n---- Version info ----", file=sys.stderr) try: print(version.version_info(), file=sys.stderr) except Exception: traceback.print_exc() print("\n---- Config ----", file=sys.stderr) try: print(config.instance.dump_userconfig(), file=sys.stderr) except Exception: traceback.print_exc() print("\n---- Commandline args ----", file=sys.stderr) print(' '.join(sys.argv[1:]), file=sys.stderr) print("\n---- Open pages ----", file=sys.stderr) print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr) print("\n---- Command history ----", file=sys.stderr) print('\n'.join(cmdhist), file=sys.stderr) print("\n---- Objects ----", file=sys.stderr) print(qobjects, file=sys.stderr) print("\n---- Environment ----", file=sys.stderr) try: print(_get_environment_vars(), file=sys.stderr) except Exception: traceback.print_exc() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/crashsignal.py0000644000175100017510000004171515102145205021741 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Handlers for crashes and OS signals.""" import os import os.path import sys import bdb import pdb # noqa: T002 import types import signal import argparse import functools import threading import faulthandler import dataclasses from typing import TYPE_CHECKING, Optional, cast from collections.abc import Callable, MutableMapping from qutebrowser.qt.core import (pyqtSlot, qInstallMessageHandler, QObject, QSocketNotifier, QTimer, QUrl) from qutebrowser.qt.widgets import QApplication from qutebrowser.api import cmdutils from qutebrowser.config import configfiles, configexc from qutebrowser.misc import earlyinit, crashdialog, ipc, objects from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils, message from qutebrowser.qt import sip if TYPE_CHECKING: from qutebrowser.misc import quitter @dataclasses.dataclass class ExceptionInfo: """Information stored when there was an exception.""" pages: list[list[str]] cmd_history: list[str] objects: str crash_handler = cast('CrashHandler', None) class CrashHandler(QObject): """Handler for crashes, reports and exceptions. Attributes: _app: The QApplication instance. _quitter: The Quitter instance. _args: The argparse namespace. _crash_dialog: The CrashDialog currently being shown. _crash_log_file: The file handle for the faulthandler crash log. _crash_log_data: Crash data read from the previous crash log. is_crashing: Used by mainwindow.py to skip confirm questions on crashes. """ def __init__(self, *, app, quitter, args, parent=None): super().__init__(parent) self._app = app self._quitter = quitter self._args = args self._crash_log_file = None self._crash_log_data = None self._crash_dialog = None self.is_crashing = False def activate(self): """Activate the exception hook.""" sys.excepthook = self.exception_hook def init_faulthandler(self): """Handle a segfault from a previous run and set up faulthandler.""" logname = os.path.join(standarddir.data(), 'crash.log') try: # First check if an old logfile exists. if os.path.exists(logname): with open(logname, 'r', encoding='ascii') as f: self._crash_log_data = f.read() os.remove(logname) self._init_crashlogfile() else: # There's no log file, so we can use this to display crashes to # the user on the next start. self._init_crashlogfile() except (OSError, UnicodeDecodeError): log.init.exception("Error while handling crash log file!") self._init_crashlogfile() def display_faulthandler(self): """If there was data in the crash log file, display a dialog.""" assert not self._args.no_err_windows if self._crash_log_data: # Crashlog exists and has data in it, so something crashed # previously. self._crash_dialog = crashdialog.FatalCrashDialog( self._args.debug, self._crash_log_data) self._crash_dialog.show() self._crash_log_data = None def _recover_pages(self, forgiving=False): """Try to recover all open pages. Called from exception_hook, so as forgiving as possible. Args: forgiving: Whether to ignore exceptions. Return: A list containing a list for each window, which in turn contain the opened URLs. """ pages = [] for win_id in objreg.window_registry: win_pages = [] tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) for tab in tabbed_browser.widgets(): try: urlstr = tab.url().toString( QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded) if urlstr: win_pages.append(urlstr) except Exception: if forgiving: log.destroy.exception("Error while recovering tab") else: raise pages.append(win_pages) return pages def _init_crashlogfile(self): """Start a new logfile and redirect faulthandler to it.""" logname = os.path.join(standarddir.data(), 'crash.log') try: # pylint: disable=consider-using-with self._crash_log_file = open(logname, 'w', encoding='ascii') except OSError: log.init.exception("Error while opening crash log file!") else: earlyinit.init_faulthandler(self._crash_log_file) @cmdutils.register(instance='crash-handler') def report(self, info=None, contact=None): """Report a bug in qutebrowser. Args: info: Information about the bug report. If given, no report dialog shows up. contact: Contact information for the report. """ pages = self._recover_pages() cmd_history = objreg.get('command-history')[-5:] all_objects = debug.get_all_objects() self._crash_dialog = crashdialog.ReportDialog(pages, cmd_history, all_objects) if info is None: self._crash_dialog.show() else: self._crash_dialog.report(info=info, contact=contact) @pyqtSlot() def shutdown(self): self.destroy_crashlogfile() def destroy_crashlogfile(self): """Clean up the crash log file and delete it.""" if self._crash_log_file is None: return # We use sys.__stderr__ instead of sys.stderr here so this will still # work when sys.stderr got replaced, e.g. by "Python Tools for Visual # Studio". if sys.__stderr__ is not None: faulthandler.enable(sys.__stderr__) else: faulthandler.disable() try: self._crash_log_file.close() os.remove(self._crash_log_file.name) except OSError: log.destroy.exception("Could not remove crash log!") def _get_exception_info(self): """Get info needed for the exception hook/dialog. Return: An ExceptionInfo object. """ try: pages = self._recover_pages(forgiving=True) except Exception as e: log.destroy.exception("Error while recovering pages: {}".format(e)) pages = [] try: cmd_history = objreg.get('command-history')[-5:] except Exception as e: log.destroy.exception("Error while getting history: {}".format(e)) cmd_history = [] try: all_objects = debug.get_all_objects() except Exception: log.destroy.exception("Error while getting objects") all_objects = "" return ExceptionInfo(pages, cmd_history, all_objects) def _handle_early_exits(self, exc): """Handle some special cases for the exception hook. Return value: True: Exception hook should be aborted. False: Continue handling exception. """ exctype, _excvalue, tb = exc if not self._quitter.quit_status['crash']: log.misc.error("ARGH, there was an exception while the crash " "dialog is already shown:", exc_info=exc) return True log.misc.error("Uncaught exception", exc_info=exc) is_ignored_exception = (exctype is bdb.BdbQuit or not issubclass(exctype, Exception)) if 'pdb-postmortem' in objects.debug_flags: if tb is None: pdb.set_trace() # noqa: T100 pylint: disable=forgotten-debug-statement else: pdb.post_mortem(tb) if is_ignored_exception or 'pdb-postmortem' in objects.debug_flags: # pdb exit, KeyboardInterrupt, ... sys.exit(usertypes.Exit.exception) if threading.current_thread() != threading.main_thread(): log.misc.error("Ignoring exception outside of main thread... " "Please report this as a bug.") return True return False def exception_hook(self, exctype, excvalue, tb): """Handle uncaught python exceptions. It'll try very hard to write all open tabs to a file, and then exit gracefully. """ exc = (exctype, excvalue, tb) if self._handle_early_exits(exc): return self._quitter.quit_status['crash'] = False info = self._get_exception_info() if ipc.server is not None: try: ipc.server.ignored = True except Exception: log.destroy.exception("Error while ignoring ipc") try: self._app.lastWindowClosed.disconnect( self._quitter.on_last_window_closed) except TypeError: log.destroy.exception("Error while preventing shutdown") self.is_crashing = True self._app.closeAllWindows() if self._args.no_err_windows: crashdialog.dump_exception_info(exc, info.pages, info.cmd_history, info.objects) else: self._crash_dialog = crashdialog.ExceptionCrashDialog( self._args.debug, info.pages, info.cmd_history, exc, info.objects) ret = self._crash_dialog.exec() if ret == crashdialog.Result.restore: self._quitter.restart(info.pages) # We might risk a segfault here, but that's better than continuing to # run in some undefined state, so we only do the most needed shutdown # here. qInstallMessageHandler(None) self.destroy_crashlogfile() sys.exit(usertypes.Exit.exception) def raise_crashdlg(self): """Raise the crash dialog if one exists.""" if self._crash_dialog is not None: self._crash_dialog.raise_() class SignalHandler(QObject): """Handler responsible for handling OS signals (SIGINT, SIGTERM, etc.). Attributes: _app: The QApplication instance. _quitter: The Quitter instance. _activated: Whether activate() was called. _notifier: A QSocketNotifier used for signals on Unix. _timer: A QTimer used to poll for signals on Windows. _orig_handlers: A {signal: handler} dict of original signal handlers. _orig_wakeup_fd: The original wakeup filedescriptor. """ def __init__(self, *, app, quitter, parent=None): super().__init__(parent) self._app = app self._quitter = quitter self._notifier = None self._timer = usertypes.Timer(self, 'python_hacks') self._orig_handlers: MutableMapping[int, 'signal._HANDLER'] = {} self._activated = False self._orig_wakeup_fd: Optional[int] = None self._handlers: dict[ signal.Signals, Callable[[int, Optional[types.FrameType]], None] ] = { signal.SIGINT: self.interrupt, signal.SIGTERM: self.interrupt, } platform_dependant_handlers = { "SIGHUP": self.reload_config, } for sig_str, handler in platform_dependant_handlers.items(): try: self._handlers[signal.Signals[sig_str]] = handler except KeyError: pass def activate(self): """Set up signal handlers. On Windows this uses a QTimer to periodically hand control over to Python so it can handle signals. On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get notified. """ for sig, handler in self._handlers.items(): self._orig_handlers[sig] = signal.signal(sig, handler) if utils.is_posix and hasattr(signal, 'set_wakeup_fd'): # pylint: disable=import-error,no-member,useless-suppression import fcntl read_fd, write_fd = os.pipe() for fd in [read_fd, write_fd]: flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) self._notifier = QSocketNotifier(cast(sip.voidptr, read_fd), QSocketNotifier.Type.Read, self) self._notifier.activated.connect(self.handle_signal_wakeup) self._orig_wakeup_fd = signal.set_wakeup_fd(write_fd) # pylint: enable=import-error,no-member,useless-suppression else: self._timer.start(1000) self._timer.timeout.connect(lambda: None) self._activated = True def deactivate(self): """Deactivate all signal handlers.""" if not self._activated: return if self._notifier is not None: assert self._orig_wakeup_fd is not None self._notifier.setEnabled(False) rfd = self._notifier.socket() wfd = signal.set_wakeup_fd(self._orig_wakeup_fd) os.close(int(rfd)) os.close(wfd) for sig, handler in self._orig_handlers.items(): signal.signal(sig, handler) self._timer.stop() self._activated = False @pyqtSlot() def handle_signal_wakeup(self): """Handle a newly arrived signal. This gets called via self._notifier when there's a signal. Python will get control here, so the signal will get handled. """ assert self._notifier is not None log.destroy.debug("Handling signal wakeup!") self._notifier.setEnabled(False) read_fd = self._notifier.socket() try: os.read(int(read_fd), 1) except OSError: log.destroy.exception("Failed to read wakeup fd.") self._notifier.setEnabled(True) def _log_later(self, *lines): """Log the given text line-wise with a QTimer.""" for line in lines: QTimer.singleShot(0, functools.partial(log.destroy.info, line)) def interrupt(self, signum, _frame): """Handler for signals to gracefully shutdown (SIGINT/SIGTERM). This calls shutdown and remaps the signal to call interrupt_forcefully the next time. """ signal.signal(signal.SIGINT, self.interrupt_forcefully) signal.signal(signal.SIGTERM, self.interrupt_forcefully) # Signals can arrive anywhere, so we do this in the main thread self._log_later("SIGINT/SIGTERM received, shutting down!", "Do the same again to forcefully quit.") QTimer.singleShot(0, functools.partial( self._quitter.shutdown, 128 + signum)) def interrupt_forcefully(self, signum, _frame): """Interrupt forcefully on the second SIGINT/SIGTERM request. This skips our shutdown routine and calls QApplication:exit instead. It then remaps the signals to call self.interrupt_really_forcefully the next time. """ signal.signal(signal.SIGINT, self.interrupt_really_forcefully) signal.signal(signal.SIGTERM, self.interrupt_really_forcefully) # Signals can arrive anywhere, so we do this in the main thread self._log_later("Forceful quit requested, goodbye cruel world!", "Do the same again to quit with even more force.") QTimer.singleShot(0, functools.partial(self._app.exit, 128 + signum)) def interrupt_really_forcefully(self, signum, _frame): """Interrupt with even more force on the third SIGINT/SIGTERM request. This doesn't run *any* Qt cleanup and simply exits via Python. It will most likely lead to a segfault. """ print("WHY ARE YOU DOING THIS TO ME? :(") sys.exit(128 + signum) def reload_config(self, _signum, _frame): """Reload the config.""" log.signals.info("SIGHUP received, reloading config.") filename = standarddir.config_py() try: configfiles.read_config_py(filename) except configexc.ConfigFileErrors as e: message.error(str(e)) def init(q_app: QApplication, args: argparse.Namespace, quitter: 'quitter.Quitter') -> None: """Initialize crash/signal handlers.""" global crash_handler crash_handler = CrashHandler( app=q_app, quitter=quitter, args=args, parent=q_app) objreg.register('crash-handler', crash_handler, command_only=True) crash_handler.activate() quitter.shutting_down.connect(crash_handler.shutdown) signal_handler = SignalHandler(app=q_app, quitter=quitter, parent=q_app) signal_handler.activate() quitter.shutting_down.connect(signal_handler.deactivate) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/debugcachestats.py0000644000175100017510000000212515102145205022564 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Implementation of the command debug-cache-stats. Because many modules depend on this command, this needs to have as few dependencies as possible to avoid cyclic dependencies. """ import weakref from typing import Any, Optional, TypeVar from collections.abc import MutableMapping, Callable from qutebrowser.utils import log # The callable should be a lru_cache wrapped function _CACHE_FUNCTIONS: MutableMapping[str, Any] = weakref.WeakValueDictionary() _T = TypeVar('_T', bound=Callable[..., Any]) def register(name: Optional[str] = None) -> Callable[[_T], _T]: """Register a lru_cache wrapped function for debug_cache_stats.""" def wrapper(fn: _T) -> _T: fn_name = fn.__name__ if name is None else name _CACHE_FUNCTIONS[fn_name] = fn return fn return wrapper def debug_cache_stats() -> None: """Print LRU cache stats.""" for name, fn in _CACHE_FUNCTIONS.items(): log.misc.info('{}: {}'.format(name, fn.cache_info())) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/earlyinit.py0000644000175100017510000002721215102145205021437 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Things which need to be done really early (e.g. before importing Qt). At this point we can be sure we have all python 3.9 features available. """ try: # Importing hunter to register its atexit handler early so it gets called # late. import hunter # pylint: disable=unused-import except ImportError: hunter = None import sys import faulthandler import traceback import signal import importlib import datetime from typing import NoReturn try: import tkinter except ImportError: tkinter = None # type: ignore[assignment] # NOTE: No qutebrowser or PyQt import should be done here, as some early # initialization needs to take place before that! # # The machinery module is an exception, as it also is required to never import Qt # itself at import time. from qutebrowser.qt import machinery START_TIME = datetime.datetime.now() def _missing_str(name, *, webengine=False): """Get an error string for missing packages. Args: name: The name of the package. webengine: Whether this is checking the QtWebEngine package """ blocks = ["Fatal error: {} is required to run qutebrowser but " "could not be imported! Maybe it's not installed?".format(name), "The error encountered was:
%ERROR%"] lines = ['Please search for the python3 version of {} in your ' 'distributions packages, or see ' 'https://github.com/qutebrowser/qutebrowser/blob/main/doc/install.asciidoc' .format(name)] blocks.append('
'.join(lines)) if not webengine: lines = ['If you installed a qutebrowser package for your ' 'distribution, please report this as a bug.'] blocks.append('
'.join(lines)) return '

'.join(blocks) def _die(message, exception=None): """Display an error message using Qt and quit. We import the imports here as we want to do other stuff before the imports. Args: message: The message to display. exception: The exception object if we're handling an exception. """ from qutebrowser.qt.widgets import QApplication, QMessageBox from qutebrowser.qt.core import Qt if (('--debug' in sys.argv or '--no-err-windows' in sys.argv) and exception is not None): print(file=sys.stderr) traceback.print_exc() app = QApplication(sys.argv) if '--no-err-windows' in sys.argv: print(message, file=sys.stderr) print("Exiting because of --no-err-windows.", file=sys.stderr) else: if exception is not None: message = message.replace('%ERROR%', str(exception)) msgbox = QMessageBox(QMessageBox.Icon.Critical, "qutebrowser: Fatal error!", message) msgbox.setTextFormat(Qt.TextFormat.RichText) msgbox.resize(msgbox.sizeHint()) msgbox.exec() app.quit() sys.exit(1) def init_faulthandler(fileobj=sys.__stderr__): """Enable faulthandler module if available. This print a nice traceback on segfaults. We use sys.__stderr__ instead of sys.stderr here so this will still work when sys.stderr got replaced, e.g. by "Python Tools for Visual Studio". Args: fileobj: An opened file object to write the traceback to. """ try: faulthandler.enable(fileobj) except (RuntimeError, AttributeError): # When run with pythonw.exe, sys.__stderr__ can be None: # https://docs.python.org/3/library/sys.html#sys.__stderr__ # # With PyInstaller, it can be a NullWriter raising AttributeError on # fileno: https://github.com/pyinstaller/pyinstaller/issues/4481 # # Later when we have our data dir available we re-enable faulthandler # to write to a file so we can display a crash to the user at the next # start. # # Note that we don't have any logging initialized yet at this point, so # this is a silent error. return if (hasattr(faulthandler, 'register') and hasattr(signal, 'SIGUSR1') and sys.stderr is not None): # If available, we also want a traceback on SIGUSR1. # pylint: disable=no-member,useless-suppression faulthandler.register(signal.SIGUSR1) # pylint: enable=no-member,useless-suppression def _fatal_qt_error(text: str) -> NoReturn: """Show a fatal error about Qt being missing.""" if tkinter and '--no-err-windows' not in sys.argv: root = tkinter.Tk() root.withdraw() tkinter.messagebox.showerror("qutebrowser: Fatal error!", text) else: print(text, file=sys.stderr) if '--debug' in sys.argv or '--no-err-windows' in sys.argv: print(file=sys.stderr) traceback.print_exc() sys.exit(1) def check_qt_available(info: machinery.SelectionInfo) -> None: """Check if Qt core modules (QtCore/QtWidgets) are installed.""" if info.wrapper is None: _fatal_qt_error(f"No Qt wrapper was importable.\n\n{info}") packages = [f'{info.wrapper}.QtCore', f'{info.wrapper}.QtWidgets'] for name in packages: try: importlib.import_module(name) except ImportError as e: text = _missing_str(name) text = text.replace('', '') text = text.replace('', '') text = text.replace('
', '\n') text = text.replace('%ERROR%', str(e)) text += '\n\n' + str(info) _fatal_qt_error(text) def qt_version(qversion=None, qt_version_str=None): """Get a Qt version string based on the runtime/compiled versions.""" if qversion is None: from qutebrowser.qt.core import qVersion qversion = qVersion() if qt_version_str is None: from qutebrowser.qt.core import QT_VERSION_STR qt_version_str = QT_VERSION_STR if qversion != qt_version_str: return '{} (compiled {})'.format(qversion, qt_version_str) else: return qversion def get_qt_version(): """Get the Qt version, or None if too old for QLibraryInfo.version().""" try: from qutebrowser.qt.core import QLibraryInfo return QLibraryInfo.version().normalized() except (ImportError, AttributeError): return None def check_qt_version(): """Check if the Qt version is recent enough.""" from qutebrowser.qt.core import QT_VERSION, PYQT_VERSION, PYQT_VERSION_STR from qutebrowser.qt.core import QVersionNumber qt_ver = get_qt_version() recent_qt_runtime = qt_ver is not None and qt_ver >= QVersionNumber(5, 15) if QT_VERSION < 0x050F00 or PYQT_VERSION < 0x050F00 or not recent_qt_runtime: text = ("Fatal error: Qt >= 5.15.0 and PyQt >= 5.15.0 are required, " "but Qt {} / PyQt {} is installed.".format(qt_version(), PYQT_VERSION_STR)) _die(text) if 0x060000 <= PYQT_VERSION < 0x060202: text = ("Fatal error: With Qt 6, PyQt >= 6.2.2 is required, but " "{} is installed.".format(PYQT_VERSION_STR)) _die(text) def check_ssl_support(): """Check if SSL support is available.""" try: from qutebrowser.qt.network import QSslSocket # pylint: disable=unused-import except ImportError: _die("Fatal error: Your Qt is built without SSL support.") def _check_modules(modules): """Make sure the given modules are available.""" from qutebrowser.utils import log for name, text in modules.items(): try: with log.py_warning_filter( category=DeprecationWarning, message=r'invalid escape sequence' ), log.py_warning_filter( category=ImportWarning, message=r'Not importing directory .*: missing __init__' ), log.py_warning_filter( category=DeprecationWarning, message=r'the imp module is deprecated', ), log.py_warning_filter( # WORKAROUND for https://github.com/pypa/setuptools/issues/2466 category=DeprecationWarning, message=r'Creating a LegacyVersion has been deprecated', ): importlib.import_module(name) except ImportError as e: _die(text, e) def check_libraries(): """Check if all needed Python libraries are installed.""" modules = { 'jinja2': _missing_str("jinja2"), 'yaml': _missing_str("PyYAML"), } for subpkg in ['QtQml', 'QtOpenGL', 'QtDBus']: package = f'{machinery.INFO.wrapper}.{subpkg}' modules[package] = _missing_str(package) if sys.platform.startswith('darwin'): from qutebrowser.qt.core import QVersionNumber qt_ver = get_qt_version() if qt_ver is not None and qt_ver < QVersionNumber(6, 3): # Used for resizable hide_decoration windows on macOS modules['objc'] = _missing_str("pyobjc-core") modules['AppKit'] = _missing_str("pyobjc-framework-Cocoa") _check_modules(modules) def configure_pyqt(): """Remove the PyQt input hook and enable overflow checking. Doing this means we can't use the interactive shell anymore (which we don't anyways), but we can use pdb instead. """ from qutebrowser.qt.core import pyqtRemoveInputHook pyqtRemoveInputHook() from qutebrowser.qt import sip if machinery.IS_QT5: # default in PyQt6 sip.enableoverflowchecking(True) def init_log(args): """Initialize logging. Args: args: The argparse namespace. """ from qutebrowser.utils import log log.init_log(args) log.init.debug("Log initialized.") def init_qtlog(args): """Initialize Qt logging. Args: args: The argparse namespace. """ from qutebrowser.utils import log, qtlog qtlog.init(args) log.init.debug("Qt log initialized.") def check_optimize_flag(): """Check whether qutebrowser is running with -OO.""" from qutebrowser.utils import log if sys.flags.optimize >= 2: log.init.warning("Running on optimize level higher than 1, " "unexpected behavior may occur.") def webengine_early_import(): """If QtWebEngine is available, import it early. We need to ensure that QtWebEngine is imported before a QApplication is created for everything to work properly. This needs to be done even when using the QtWebKit backend, to ensure that e.g. error messages in backendproblem.py are accurate. """ try: from qutebrowser.qt import webenginewidgets # pylint: disable=unused-import except ImportError: pass def early_init(args): """Do all needed early initialization. Note that it's vital the other earlyinit functions get called in the right order! Args: args: The argparse namespace. """ # Init logging as early as possible init_log(args) # First we initialize the faulthandler as early as possible, so we # theoretically could catch segfaults occurring later during earlyinit. init_faulthandler() # Then we configure the selected Qt wrapper info = machinery.init(args) # Here we check if QtCore is available, and if not, print a message to the # console or via Tk. check_qt_available(info) # Init Qt logging after machinery is initialized init_qtlog(args) # Now we can be sure QtCore is available, so we can print dialogs on # errors, so people only using the GUI notice them as well. check_libraries() check_qt_version() configure_pyqt() check_ssl_support() check_optimize_flag() webengine_early_import() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/editor.py0000644000175100017510000002306415102145205020726 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Launcher for an external editor.""" import os import tempfile from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, QObject, QProcess, QFileSystemWatcher) from qutebrowser.config import config from qutebrowser.utils import message, log from qutebrowser.misc import guiprocess from qutebrowser.qt import sip class ExternalEditor(QObject): """Class to simplify editing a text in an external editor. Attributes: _text: The current text before the editor is opened. _filename: The name of the file to be edited. _remove_file: Whether the file should be removed when the editor is closed. _proc: The GUIProcess of the editor. _watcher: A QFileSystemWatcher to watch the edited file for changes. Only set if watch=True. _content: The last-saved text of the editor. Signals: file_updated: The text in the edited file was updated. arg: The new text. editing_finished: The editor process was closed. """ file_updated = pyqtSignal(str) editing_finished = pyqtSignal() def __init__(self, parent=None, watch=False): super().__init__(parent) self._filename = None self._proc = None self._remove_file = None self._watcher = QFileSystemWatcher(parent=self) if watch else None self._content = None def _cleanup(self, *, successful): """Clean up temporary files after the editor closed. Args: successful: Whether the editor exited successfully, i.e. the file can be deleted. """ assert self._remove_file is not None if (self._watcher is not None and not sip.isdeleted(self._watcher) and self._watcher.files()): failed = self._watcher.removePaths(self._watcher.files()) if failed: log.procs.error("Failed to unwatch paths: {}".format(failed)) if self._filename is None or not self._remove_file: # Could not create initial file. return assert self._proc is not None if successful: try: os.remove(self._filename) except OSError as e: # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error("Failed to delete tempfile... ({})".format(e)) else: message.info(f"Keeping file {self._filename} as the editor process exited " "abnormally") @pyqtSlot(int, QProcess.ExitStatus) def _on_proc_closed(self, _exitcode, exitstatus): """Write the editor text into the form field and clean up tempfile. Callback for QProcess when the editor was closed. """ if sip.isdeleted(self): # pragma: no cover log.procs.debug("Ignoring _on_proc_closed for deleted editor") return log.procs.debug("Editor closed") if exitstatus != QProcess.ExitStatus.NormalExit: # No error/cleanup here, since we already handle this in # on_proc_error. return # do a final read to make sure we don't miss the last signal assert self._proc is not None self._on_file_changed(self._filename) self.editing_finished.emit() self._cleanup(successful=self._proc.outcome.was_successful()) @pyqtSlot(QProcess.ProcessError) def _on_proc_error(self, _err): self._cleanup(successful=False) def edit(self, text, caret_position=None): """Edit a given text. Args: text: The initial text to edit. caret_position: The position of the caret in the text. """ if self._filename is not None: raise ValueError("Already editing a file!") try: self._filename = self._create_tempfile(text, 'qutebrowser-editor-') except (OSError, UnicodeEncodeError) as e: message.error("Failed to create initial file: {}".format(e)) return self._remove_file = config.val.editor.remove_file line, column = self._calc_line_and_column(text, caret_position) self._start_editor(line=line, column=column) def backup(self): """Create a backup if the content has changed from the original.""" if not self._content: return try: fname = self._create_tempfile(self._content, 'qutebrowser-editor-backup-') message.info('Editor backup at {}'.format(fname)) except OSError as e: message.error('Failed to create editor backup: {}'.format(e)) def _create_tempfile(self, text, prefix): # Close while the external process is running, as otherwise systems # with exclusive write access (e.g. Windows) may fail to update # the file from the external editor, see # https://github.com/qutebrowser/qutebrowser/issues/1767 with tempfile.NamedTemporaryFile( mode='w', prefix=prefix, encoding=config.val.editor.encoding, delete=False) as fobj: if text: fobj.write(text) return fobj.name @pyqtSlot(str) def _on_file_changed(self, path): try: with open(path, 'r', encoding=config.val.editor.encoding) as f: text = f.read() except OSError as e: # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error("Failed to read back edited file: {}".format(e)) return log.procs.debug("Read back: {}".format(text)) if self._content != text: self._content = text self.file_updated.emit(text) def edit_file(self, filename): """Edit the file with the given filename.""" if not os.path.exists(filename): with open(filename, 'w', encoding='utf-8'): pass self._filename = filename self._remove_file = False self._start_editor() def _start_editor(self, line=1, column=1): """Start the editor with the file opened as self._filename. Args: line: the line number to pass to the editor column: the column number to pass to the editor """ self._proc = guiprocess.GUIProcess(what='editor') self._proc.finished.connect(self._on_proc_closed) self._proc.error.connect(self._on_proc_error) editor = config.val.editor.command executable = editor[0] if self._watcher: assert self._filename is not None ok = self._watcher.addPath(self._filename) if not ok: log.procs.error("Failed to watch path: {}" .format(self._filename)) self._watcher.fileChanged.connect(self._on_file_changed) args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args) def _calc_line_and_column(self, text, caret_position): r"""Calculate line and column numbers given a text and caret position. Both line and column are 1-based indexes, because that's what most editors use as line and column starting index. By "most" we mean at least vim, nvim, gvim, emacs, atom, sublimetext, notepad++, brackets, visual studio, QtCreator and so on. To find the line we just count how many newlines there are before the caret and add 1. To find the column we calculate the difference between the caret and the last newline before the caret. For example in the text `aaa\nbb|bbb` (| represents the caret): caret_position = 6 text[:caret_position] = `aaa\nbb` text[:caret_position].count('\n') = 1 caret_position - text[:caret_position].rfind('\n') = 3 Thus line, column = 2, 3, and the caret is indeed in the second line, third column Args: text: the text for which the numbers must be calculated caret_position: the position of the caret in the text, or None Return: A (line, column) tuple of (int, int) """ if caret_position is None: return 1, 1 line = text[:caret_position].count('\n') + 1 column = caret_position - text[:caret_position].rfind('\n') return line, column def _sub_placeholder(self, arg, line, column): """Substitute a single placeholder. If the `arg` input to this function is a valid placeholder it will be substituted with the appropriate value, otherwise it will be left unchanged. Args: arg: an argument of editor.command. line: the previously-calculated line number for the text caret. column: the previously-calculated column number for the text caret. Return: The substituted placeholder or the original argument. """ replacements = { '{}': self._filename, '{file}': self._filename, '{line}': str(line), '{line0}': str(line-1), '{column}': str(column), '{column0}': str(column-1) } for old, new in replacements.items(): arg = arg.replace(old, new) return arg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/elf.py0000644000175100017510000002410515102145205020203 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Simplistic ELF parser to get the QtWebEngine/Chromium versions. I know what you must be thinking when reading this: "Why on earth does qutebrowser have an ELF parser?!". For one, because writing one was an interesting learning exercise. But there's actually a reason it's here: QtWebEngine 5.15.x versions come with different underlying Chromium versions, but there is no API to get the version of QtWebEngine/Chromium... We can instead: a) Look at the Qt runtime version (qVersion()). This often doesn't actually correspond to the QtWebEngine version (as that can be older/newer). Since there will be a QtWebEngine 5.15.3 release, but not Qt itself (due to LTS licensing restrictions), this isn't a reliable source of information. b) Look at the PyQtWebEngine version (PyQt5.QtWebEngine.PYQT_WEBENGINE_VERSION_STR). This is a good first guess (especially for our Windows/macOS releases), but still isn't certain. Linux distributions often push a newer QtWebEngine before the corresponding PyQtWebEngine release, and some (*cough* Gentoo *cough*) even publish QtWebEngine "5.15.2" but upgrade the underlying Chromium. c) Parse the user agent. This is what qutebrowser did before this monstrosity was introduced (and still does as a fallback), but for some things (finding the proper commandline arguments to pass) it's too late in the initialization process. d) Spawn QtWebEngine in a subprocess and ask for its user-agent. This takes too long to do it on every startup. e) Ask the package manager for this information. This means we'd need to know (or guess) the package manager and package name. Also see: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=752114 Because of all those issues, we instead look for the (fixed!) version string as part of the user agent header. Because libQt5WebEngineCore is rather big (~120 MB), we don't want to search through the entire file, so we instead have a simplistic ELF parser here to find the .rodata section. This way, searching the version gets faster by some orders of magnitudes (a couple of us instead of ms). This is a "best effort" parser. If it errors out, we instead end up relying on the PyQtWebEngine version, which is the next best thing. """ import enum import re import dataclasses import mmap import pathlib from typing import IO, ClassVar, Optional, cast from qutebrowser.qt import machinery from qutebrowser.utils import log, version, qtutils from qutebrowser.misc import binparsing class Bitness(enum.Enum): """Whether the ELF file is 32- or 64-bit.""" x32 = 1 x64 = 2 class Endianness(enum.Enum): """Whether the ELF file is little- or big-endian.""" little = 1 big = 2 @dataclasses.dataclass class Ident: """File identification for ELF. See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header (first 16 bytes). """ magic: bytes klass: Bitness data: Endianness version: int osabi: int abiversion: int _FORMAT: ClassVar[str] = '<4sBBBBB7x' @classmethod def parse(cls, fobj: IO[bytes]) -> 'Ident': """Parse an ELF ident header from a file.""" magic, klass, data, elfversion, osabi, abiversion = binparsing.unpack(cls._FORMAT, fobj) try: bitness = Bitness(klass) except ValueError: raise binparsing.ParseError(f"Invalid bitness {klass}") try: endianness = Endianness(data) except ValueError: raise binparsing.ParseError(f"Invalid endianness {data}") return cls(magic, bitness, endianness, elfversion, osabi, abiversion) @dataclasses.dataclass class Header: """ELF header without file identification. See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header (without the first 16 bytes). """ typ: int machine: int version: int entry: int phoff: int shoff: int flags: int ehsize: int phentsize: int phnum: int shentsize: int shnum: int shstrndx: int _FORMATS: ClassVar[dict[Bitness, str]] = { Bitness.x64: ' 'Header': """Parse an ELF header from a file.""" fmt = cls._FORMATS[bitness] return cls(*binparsing.unpack(fmt, fobj)) @dataclasses.dataclass class SectionHeader: """ELF section header. See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#Section_header """ name: int typ: int flags: int addr: int offset: int size: int link: int info: int addralign: int entsize: int _FORMATS: ClassVar[dict[Bitness, str]] = { Bitness.x64: ' 'SectionHeader': """Parse an ELF section header from a file.""" fmt = cls._FORMATS[bitness] return cls(*binparsing.unpack(fmt, fobj)) def get_rodata_header(f: IO[bytes]) -> SectionHeader: """Parse an ELF file and find the .rodata section header.""" ident = Ident.parse(f) if ident.magic != b'\x7fELF': raise binparsing.ParseError(f"Invalid magic {ident.magic!r}") if ident.data != Endianness.little: raise binparsing.ParseError("Big endian is unsupported") if ident.version != 1: raise binparsing.ParseError(f"Only version 1 is supported, not {ident.version}") header = Header.parse(f, bitness=ident.klass) # Read string table binparsing.safe_seek(f, header.shoff + header.shstrndx * header.shentsize) shstr = SectionHeader.parse(f, bitness=ident.klass) binparsing.safe_seek(f, shstr.offset) string_table = binparsing.safe_read(f, shstr.size) # Back to all sections for i in range(header.shnum): binparsing.safe_seek(f, header.shoff + i * header.shentsize) sh = SectionHeader.parse(f, bitness=ident.klass) name = string_table[sh.name:].split(b'\x00')[0] if name == b'.rodata': return sh raise binparsing.ParseError("No .rodata section found") @dataclasses.dataclass class Versions: """The versions found in the ELF file.""" webengine: str chromium: str def _find_versions(data: bytes) -> Versions: """Find the version numbers in the given data. Note that 'data' can actually be a mmap.mmap, but typing doesn't handle that correctly: https://github.com/python/typeshed/issues/1467 """ pattern = br'\x00QtWebEngine/([0-9.]+) Chrome/([0-9.]+)\x00' match = re.search(pattern, data) if match is not None: try: return Versions( webengine=match.group(1).decode('ascii'), chromium=match.group(2).decode('ascii'), ) except UnicodeDecodeError as e: raise binparsing.ParseError(e) # Here it gets even more crazy: Sometimes, we don't have the full UA in one piece # in the string table somehow (?!). However, Qt 6.2 added a separate # qWebEngineChromiumVersion(), with PyQt wrappers following later. And *that* # apparently stores the full version in the string table separately from the UA. # As we clearly didn't have enough crazy heuristics yet so far, let's hunt for it! # We first get the partial Chromium version from the UA: match = re.search(pattern[:-4], data) # without trailing literal \x00 if match is None: raise binparsing.ParseError("No match in .rodata") webengine_bytes = match.group(1) partial_chromium_bytes = match.group(2) if b"." not in partial_chromium_bytes or len(partial_chromium_bytes) < 6: # some sanity checking raise binparsing.ParseError("Inconclusive partial Chromium bytes") # And then try to find the *full* string, stored separately, based on the # partial one we got above. pattern = br"\x00(" + re.escape(partial_chromium_bytes) + br"[0-9.]+)\x00" match = re.search(pattern, data) if match is None: raise binparsing.ParseError("No match in .rodata for full version") chromium_bytes = match.group(1) try: return Versions( webengine=webengine_bytes.decode('ascii'), chromium=chromium_bytes.decode('ascii'), ) except UnicodeDecodeError as e: raise binparsing.ParseError(e) def _parse_from_file(f: IO[bytes]) -> Versions: """Parse the ELF file from the given path.""" sh = get_rodata_header(f) rest = sh.offset % mmap.ALLOCATIONGRANULARITY mmap_offset = sh.offset - rest mmap_size = sh.size + rest try: with mmap.mmap( f.fileno(), mmap_size, offset=mmap_offset, access=mmap.ACCESS_READ, ) as mmap_data: return _find_versions(cast(bytes, mmap_data)) except (OSError, OverflowError) as e: log.misc.debug(f"mmap failed ({e}), falling back to reading", exc_info=True) binparsing.safe_seek(f, sh.offset) data = binparsing.safe_read(f, sh.size) return _find_versions(data) def parse_webenginecore() -> Optional[Versions]: """Parse the QtWebEngineCore library file.""" if version.is_flatpak(): # Flatpak has Qt in /usr/lib/x86_64-linux-gnu, but QtWebEngine in /app/lib. library_path = pathlib.Path("/app/lib") else: library_path = qtutils.library_path(qtutils.LibraryPath.libraries) suffix = "6" if machinery.IS_QT6 else "5" library_name = sorted(library_path.glob(f'libQt{suffix}WebEngineCore.so*')) if not library_name: log.misc.debug(f"No QtWebEngine .so found in {library_path}") return None else: lib_file = library_name[-1] log.misc.debug(f"QtWebEngine .so found at {lib_file}") try: with lib_file.open('rb') as f: versions = _parse_from_file(f) log.misc.debug(f"Got versions from ELF: {versions}") return versions except binparsing.ParseError as e: log.misc.debug(f"Failed to parse ELF: {e}", exc_info=True) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/guiprocess.py0000644000175100017510000003670615102145205021632 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """A QProcess which shows notifications in the GUI.""" import dataclasses import locale import shlex import shutil import signal from typing import Optional from collections.abc import Mapping, Sequence from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, QObject, QProcess, QProcessEnvironment, QByteArray, QUrl, Qt) from qutebrowser.utils import message, log, utils, usertypes, version, qtutils from qutebrowser.api import cmdutils, apitypes from qutebrowser.completion.models import miscmodels all_processes: dict[int, Optional['GUIProcess']] = {} last_pid: Optional[int] = None @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('pid', completion=miscmodels.process) @cmdutils.argument('action', choices=['show', 'terminate', 'kill']) def process(tab: apitypes.Tab, pid: int = None, action: str = 'show') -> None: """Manage processes spawned by qutebrowser. Note that processes with a successful exit get cleaned up after 1h. Args: pid: The process ID of the process to manage. action: What to do with the given process: - show: Show information about the process. - terminate: Try to gracefully terminate the process (SIGTERM). - kill: Kill the process forcefully (SIGKILL). """ if pid is None: if last_pid is None: raise cmdutils.CommandError("No process executed yet!") pid = last_pid try: proc = all_processes[pid] except KeyError: raise cmdutils.CommandError(f"No process found with pid {pid}") if proc is None: raise cmdutils.CommandError(f"Data for process {pid} got cleaned up") if action == 'show': tab.load_url(QUrl(f'qute://process/{pid}')) elif action == 'terminate': proc.terminate() elif action == 'kill': proc.terminate(kill=True) else: raise utils.Unreachable(action) @dataclasses.dataclass class ProcessOutcome: """The outcome of a finished process.""" what: str running: bool = False status: Optional[QProcess.ExitStatus] = None code: Optional[int] = None def was_successful(self) -> bool: """Whether the process exited successfully. This must not be called if the process didn't exit yet. """ assert self.status is not None, "Process didn't finish yet" assert self.code is not None return self.status == QProcess.ExitStatus.NormalExit and self.code == 0 def was_sigterm(self) -> bool: """Whether the process was terminated by a SIGTERM. This must not be called if the process didn't exit yet. """ assert self.status is not None, "Process didn't finish yet" assert self.code is not None return ( self.status == QProcess.ExitStatus.CrashExit and self.code == signal.SIGTERM ) def _crash_signal(self) -> Optional[signal.Signals]: """Get a Python signal (e.g. signal.SIGTERM) from a crashed process.""" assert self.status == QProcess.ExitStatus.CrashExit if self.code is None: return None try: return signal.Signals(self.code) except ValueError: return None def __str__(self) -> str: if self.running: return f"{self.what.capitalize()} is running." elif self.status is None: return f"{self.what.capitalize()} did not start." assert self.status is not None assert self.code is not None if self.status == QProcess.ExitStatus.CrashExit: msg = f"{self.what.capitalize()} {self.state_str()} with status {self.code}" sig = self._crash_signal() if sig is None: return f"{msg}." return f"{msg} ({sig.name})." elif self.was_successful(): return f"{self.what.capitalize()} exited successfully." assert self.status == QProcess.ExitStatus.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. return f"{self.what.capitalize()} exited with status {self.code}." def state_str(self) -> str: """Get a short string describing the state of the process. This is used in the :process completion. """ if self.running: return 'running' elif self.status is None: return 'not started' elif self.was_sigterm(): return 'terminated' elif self.status == QProcess.ExitStatus.CrashExit: return 'crashed' elif self.was_successful(): return 'successful' else: return 'unsuccessful' class GUIProcess(QObject): """An external process which shows notifications in the GUI. Args: cmd: The command which was started. args: A list of arguments which gets passed. verbose: Whether to show more messages. running: Whether the underlying process is started. what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. _output_messages: Show output as messages. _proc: The underlying QProcess. Signals: error/finished/started signals proxied from QProcess. """ error = pyqtSignal(QProcess.ProcessError) finished = pyqtSignal(int, QProcess.ExitStatus) started = pyqtSignal() def __init__( self, what: str, *, verbose: bool = False, additional_env: Mapping[str, str] = None, output_messages: bool = False, ): # We do not accept a parent, as GUIProcesses keep track of themselves # (see all_processes and _post_start() / _on_cleanup_timer()) super().__init__() self.what = what self.verbose = verbose self._output_messages = output_messages self.outcome = ProcessOutcome(what=what) self.cmd: Optional[str] = None self.resolved_cmd: Optional[str] = None self.args: Optional[Sequence[str]] = None self.pid: Optional[int] = None self.stdout: str = "" self.stderr: str = "" self._cleanup_timer = usertypes.Timer(self, 'process-cleanup') self._cleanup_timer.setTimerType(Qt.TimerType.VeryCoarseTimer) self._cleanup_timer.setInterval(3600 * 1000) # 1h self._cleanup_timer.timeout.connect(self._on_cleanup_timer) self._cleanup_timer.setSingleShot(True) self._proc = QProcess(self) self._proc.errorOccurred.connect(self._on_error) self._proc.errorOccurred.connect(self.error) self._proc.finished.connect(self._on_finished) self._proc.finished.connect(self.finished) self._proc.started.connect(self._on_started) self._proc.started.connect(self.started) self._proc.readyReadStandardOutput.connect(self._on_ready_read_stdout) self._proc.readyReadStandardError.connect(self._on_ready_read_stderr) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() for k, v in additional_env.items(): procenv.insert(k, v) self._proc.setProcessEnvironment(procenv) def __str__(self) -> str: if self.cmd is None or self.args is None: return f'' return ' '.join(shlex.quote(e) for e in [self.cmd] + list(self.args)) def _decode_data(self, qba: QByteArray) -> str: """Decode data coming from a process.""" encoding = locale.getpreferredencoding(do_setlocale=False) return qba.data().decode(encoding, 'replace') def _process_text(self, data: QByteArray, attr: str) -> None: """Process new stdout/stderr text. Arguments: data: The new process data. attr: Either 'stdout' or 'stderr'. """ text = self._decode_data(data) if '\r' in text and not utils.is_windows: # Crude handling of CR for e.g. progress output. # Discard everything before the last \r in the new input, then discard # everything after the last \n in self.stdout/self.stderr. text = text.rsplit('\r', maxsplit=1)[-1] existing = getattr(self, attr) if '\n' in existing: new = existing.rsplit('\n', maxsplit=1)[0] + '\n' else: new = '' setattr(self, attr, new) if attr == 'stdout': self.stdout += text elif attr == 'stderr': self.stderr += text else: raise utils.Unreachable(attr) @pyqtSlot() def _on_ready_read_stdout(self) -> None: if not self._output_messages: return self._process_text(self._proc.readAllStandardOutput(), 'stdout') message.info(self._elide_output(self.stdout), replace=f"stdout-{self.pid}") @pyqtSlot() def _on_ready_read_stderr(self) -> None: if not self._output_messages: return self._process_text(self._proc.readAllStandardError(), 'stderr') message.error(self._elide_output(self.stderr), replace=f"stderr-{self.pid}") @pyqtSlot(QProcess.ProcessError) def _on_error(self, error: QProcess.ProcessError) -> None: """Show a message if there was an error while spawning.""" if error == QProcess.ProcessError.Crashed and not utils.is_windows: # Already handled via ExitStatus in _on_finished return what = f"{self.what} {self.cmd!r}" error_descriptions = { QProcess.ProcessError.FailedToStart: f"{what.capitalize()} failed to start", QProcess.ProcessError.Crashed: f"{what.capitalize()} crashed", QProcess.ProcessError.Timedout: f"{what.capitalize()} timed out", QProcess.ProcessError.WriteError: f"Write error for {what}", QProcess.ProcessError.ReadError: f"Read error for {what}", } # We can't get some kind of error code from Qt... # https://bugreports.qt.io/browse/QTBUG-44769 # but we pre-resolve the executable in Python, which also checks if it's # runnable. if self.resolved_cmd is None: # No point in showing the "No program defined" we got due to # passing None into Qt. error_string = f"{self.cmd!r} doesn't exist or isn't executable" if version.is_flatpak(): error_string += " inside the Flatpak container" else: # pragma: no cover error_string = self._proc.errorString() msg = ': '.join([error_descriptions[error], error_string]) message.error(msg) def _elide_output(self, output: str) -> str: """Shorten long output before showing it.""" output = output.strip() lines = output.splitlines() count = len(lines) threshold = 20 if count > threshold: lines = [ f'[{count - threshold} lines hidden, see :process for the full output]' ] + lines[-threshold:] output = '\n'.join(lines) return output @pyqtSlot(int, QProcess.ExitStatus) def _on_finished(self, code: int, status: QProcess.ExitStatus) -> None: """Show a message when the process finished.""" log.procs.debug("Process finished with code {}, status {}.".format( code, status)) self.outcome.running = False self.outcome.code = code self.outcome.status = status self.stderr += self._decode_data(self._proc.readAllStandardError()) self.stdout += self._decode_data(self._proc.readAllStandardOutput()) if self._output_messages: if self.stdout: message.info( self._elide_output(self.stdout), replace=f"stdout-{self.pid}") if self.stderr: message.error( self._elide_output(self.stderr), replace=f"stderr-{self.pid}") msg = f"{self.outcome} See :process {self.pid} for details." if self.outcome.was_successful() or self.outcome.was_sigterm(): if self.verbose: message.info(msg) self._cleanup_timer.start() else: if self.stdout: log.procs.error("Process stdout:\n" + self.stdout.strip()) if self.stderr: log.procs.error("Process stderr:\n" + self.stderr.strip()) message.error(msg) @pyqtSlot() def _on_started(self) -> None: """Called when the process started successfully.""" log.procs.debug("Process started.") assert not self.outcome.running self.outcome.running = True def _pre_start(self, cmd: str, args: Sequence[str]) -> None: """Resolve the given command and prepare starting of a QProcess. Doing the resolving in Python here instead of letting Qt do it serves two purposes: - Being able to show a nicer error message without having to parse the string we get from Qt: https://bugreports.qt.io/browse/QTBUG-44769 - Not running the file from the current directory on Unix with Qt < 5.15.? and 6.2.4, as a WORKAROUND for CVE-2022-25255: https://invent.kde.org/qt/qt/qtbase/-/merge_requests/139 https://www.qt.io/blog/security-advisory-qprocess https://lists.qt-project.org/pipermail/announce/2022-February/000333.html """ if self.outcome.running: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.resolved_cmd = shutil.which(cmd) self.args = args log.procs.debug(f"Executing: {self}") if self.verbose: message.info(f'Executing: {self}') def start(self, cmd: str, args: Sequence[str]) -> None: """Convenience wrapper around QProcess::start.""" log.procs.debug("Starting process.") self._pre_start(cmd, args) self._proc.start( qtutils.remove_optional(self.resolved_cmd), args, ) self._post_start() self._proc.closeWriteChannel() def start_detached(self, cmd: str, args: Sequence[str]) -> bool: """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, self.pid = self._proc.startDetached( self.resolved_cmd, args, None, # workingDirectory ) # type: ignore[call-arg] if not ok: message.error("Error while spawning {}".format(self.what)) return False log.procs.debug("Process started.") self.outcome.running = True self._post_start() return True def _post_start(self) -> None: """Register this process and remember the process ID after starting.""" self.pid = self._proc.processId() all_processes[self.pid] = self global last_pid last_pid = self.pid @pyqtSlot() def _on_cleanup_timer(self) -> None: """Remove the process from all registered processes.""" log.procs.debug(f"Cleaning up data for {self.pid}") assert self.pid in all_processes all_processes[self.pid] = None self.deleteLater() def terminate(self, kill: bool = False) -> None: """Terminate or kill the process.""" if kill: self._proc.kill() else: self._proc.terminate() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/httpclient.py0000644000175100017510000000720215102145205021612 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """An HTTP client based on QNetworkAccessManager.""" import functools import urllib.parse from collections.abc import MutableMapping from qutebrowser.qt.core import pyqtSignal, QObject, QTimer from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkRequest, QNetworkReply) from qutebrowser.utils import qtlog, usertypes class HTTPRequest(QNetworkRequest): """A QNetworkRequest that follows (secure) redirects by default.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setAttribute(QNetworkRequest.Attribute.RedirectPolicyAttribute, QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy) class HTTPClient(QObject): """An HTTP client based on QNetworkAccessManager. Intended for APIs, automatically decodes data. Attributes: _nam: The QNetworkAccessManager used. _timers: A {QNetworkReply: QTimer} dict. Signals: success: Emitted when the operation succeeded. arg: The received data. error: Emitted when the request failed. arg: The error message, as string. """ success = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) with qtlog.disable_qt_msghandler(): # WORKAROUND for a hang when messages are printed, see our # NetworkAccessManager subclass for details. self._nam = QNetworkAccessManager(self) self._timers: MutableMapping[QNetworkReply, QTimer] = {} def post(self, url, data=None): """Create a new POST request. Args: url: The URL to post to, as QUrl. data: A dict of data to send. """ if data is None: data = {} encoded_data = urllib.parse.urlencode(data).encode('utf-8') request = HTTPRequest(url) request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, 'application/x-www-form-urlencoded;charset=utf-8') reply = self._nam.post(request, encoded_data) self._handle_reply(reply) def get(self, url): """Create a new GET request. Emits success/error when done. Args: url: The URL to access, as QUrl. """ request = HTTPRequest(url) reply = self._nam.get(request) self._handle_reply(reply) def _handle_reply(self, reply): """Handle a new QNetworkReply.""" if reply.isFinished(): self.on_reply_finished(reply) else: timer = usertypes.Timer(self) timer.setInterval(10000) timer.timeout.connect(reply.abort) timer.start() self._timers[reply] = timer reply.finished.connect(functools.partial( self.on_reply_finished, reply)) def on_reply_finished(self, reply): """Read the data and finish when the reply finished. Args: reply: The QNetworkReply which finished. """ timer = self._timers.pop(reply) if timer is not None: timer.stop() timer.deleteLater() if reply.error() != QNetworkReply.NetworkError.NoError: self.error.emit(reply.errorString()) return try: data = bytes(reply.readAll()).decode('utf-8') except UnicodeDecodeError: self.error.emit("Invalid UTF-8 data received in reply!") return self.success.emit(data) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/ipc.py0000644000175100017510000004703015102145205020212 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities for IPC with existing instances.""" import os import time import json import getpass import binascii import hashlib from typing import Optional from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt from qutebrowser.qt.network import QLocalSocket, QLocalServer, QAbstractSocket import qutebrowser from qutebrowser.utils import log, usertypes, error, standarddir, utils, debug, qtutils from qutebrowser.qt import sip CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting WRITE_TIMEOUT = 1000 READ_TIMEOUT = 5000 ATIME_INTERVAL = 5000 * 60 # 5 minutes PROTOCOL_VERSION = 1 # The ipc server instance server: Optional["IPCServer"] = None def _get_socketname_windows(basedir): """Get a socketname to use for Windows.""" try: username = getpass.getuser() except ImportError: # getpass.getuser() first tries a couple of environment variables. If # none of those are set (i.e., USERNAME is missing), it tries to import # the "pwd" module which is unavailable on Windows. raise Error("Could not find username. This should only happen if " "there is a bug in the application launching qutebrowser, " "preventing the USERNAME environment variable from being " "passed. If you know more about when this happens, please " "report this to mail@qutebrowser.org.") parts = ['qutebrowser', username] if basedir is not None: md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest() parts.append(md5) return '-'.join(parts) def _get_socketname(basedir): """Get a socketname to use.""" if utils.is_windows: # pragma: no cover return _get_socketname_windows(basedir) parts_to_hash = [getpass.getuser()] if basedir is not None: parts_to_hash.append(basedir) data_to_hash = '-'.join(parts_to_hash).encode('utf-8') md5 = hashlib.md5(data_to_hash).hexdigest() prefix = 'i-' if utils.is_mac else 'ipc-' filename = '{}{}'.format(prefix, md5) return os.path.join(standarddir.runtime(), filename) class Error(Exception): """Base class for IPC exceptions.""" class SocketError(Error): """Exception raised when there was an error with a QLocalSocket. Args: code: The error code. message: The error message. action: The action which was taken when the error happened. """ def __init__(self, action, socket): """Constructor. Args: action: The action which was taken when the error happened. socket: The QLocalSocket which has the error set. """ super().__init__() self.action = action self.code: QLocalSocket.LocalSocketError = socket.error() self.message: str = socket.errorString() def __str__(self): return "Error while {}: {} ({})".format( self.action, self.message, debug.qenum_key(QLocalSocket, self.code)) class ListenError(Error): """Exception raised when there was a problem with listening to IPC. Args: code: The error code. message: The error message. """ def __init__(self, local_server): """Constructor. Args: local_server: The QLocalServer which has the error set. """ super().__init__() self.code: QAbstractSocket.SocketError = local_server.serverError() self.message: str = local_server.errorString() def __str__(self): return "Error while listening to IPC server: {} ({})".format( self.message, debug.qenum_key(QAbstractSocket, self.code)) class AddressInUseError(ListenError): """Emitted when the server address is already in use.""" class IPCServer(QObject): """IPC server to which clients connect to. Attributes: ignored: Whether requests are ignored (in exception hook). _timer: A timer to handle timeouts. _server: A QLocalServer to accept new connections. _socket: The QLocalSocket we're currently connected to. _socketname: The socketname to use. _atime_timer: Timer to update the atime of the socket regularly. Signals: got_args: Emitted when there was an IPC connection and arguments were passed. got_args: Emitted with the raw data an IPC connection got. got_invalid_data: Emitted when there was invalid incoming data. """ got_args = pyqtSignal(list, str, str) got_raw = pyqtSignal(bytes) got_invalid_data = pyqtSignal() def __init__(self, socketname, parent=None): """Start the IPC server and listen to commands. Args: socketname: The socketname to use. parent: The parent to be used. """ super().__init__(parent) self.ignored = False self._socketname = socketname self._timer = usertypes.Timer(self, 'ipc-timeout') self._timer.setInterval(READ_TIMEOUT) self._timer.timeout.connect(self.on_timeout) if utils.is_windows: # pragma: no cover self._atime_timer = None else: self._atime_timer = usertypes.Timer(self, 'ipc-atime') self._atime_timer.setInterval(ATIME_INTERVAL) self._atime_timer.timeout.connect(self.update_atime) self._atime_timer.setTimerType(Qt.TimerType.VeryCoarseTimer) self._server: Optional[QLocalServer] = QLocalServer(self) self._server.newConnection.connect(self.handle_connection) self._socket = None self._old_socket = None if utils.is_windows: # pragma: no cover # As a WORKAROUND for a Qt bug, we can't use UserAccessOption on Unix. If we # do, we don't get an AddressInUseError anymore: # https://bugreports.qt.io/browse/QTBUG-48635 # # Thus, we only do so on Windows, and handle permissions manually in # listen() on Linux. log.ipc.debug("Calling setSocketOptions") self._server.setSocketOptions(QLocalServer.SocketOption.UserAccessOption) else: # pragma: no cover log.ipc.debug("Not calling setSocketOptions") def _remove_server(self): """Remove an existing server.""" ok = QLocalServer.removeServer(self._socketname) if not ok: raise Error("Error while removing server {}!".format( self._socketname)) def listen(self): """Start listening on self._socketname.""" assert self._server is not None log.ipc.debug("Listening as {}".format(self._socketname)) if self._atime_timer is not None: # pragma: no branch self._atime_timer.start() self._remove_server() ok = self._server.listen(self._socketname) if not ok: if self._server.serverError() == QAbstractSocket.SocketError.AddressInUseError: raise AddressInUseError(self._server) raise ListenError(self._server) if not utils.is_windows: # pragma: no cover # WORKAROUND for QTBUG-48635, see the comment in __init__ for details. try: os.chmod(self._server.fullServerName(), 0o700) except FileNotFoundError: # https://github.com/qutebrowser/qutebrowser/issues/1530 # The server doesn't actually exist even if ok was reported as # True, so report this as an error. raise ListenError(self._server) @pyqtSlot('QLocalSocket::LocalSocketError') def on_error(self, err): """Raise SocketError on fatal errors.""" if self._socket is None: # Sometimes this gets called from stale sockets. log.ipc.debug("In on_error with None socket!") return self._timer.stop() log.ipc.debug("Socket 0x{:x}: error {}: {}".format( id(self._socket), self._socket.error(), self._socket.errorString())) if err != QLocalSocket.LocalSocketError.PeerClosedError: raise SocketError("handling IPC connection", self._socket) @pyqtSlot() def handle_connection(self): """Handle a new connection to the server.""" if self.ignored or self._server is None: return if self._socket is not None: log.ipc.debug("Got new connection but ignoring it because we're " "still handling another one (0x{:x}).".format( id(self._socket))) return socket = qtutils.add_optional(self._server.nextPendingConnection()) if socket is None: log.ipc.debug("No new connection to handle.") return log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket))) self._socket = socket self._timer.start() socket.readyRead.connect(self.on_ready_read) if socket.canReadLine(): log.ipc.debug("We can read a line immediately.") self.on_ready_read() socket.errorOccurred.connect(self.on_error) # FIXME:v4 Ignore needed due to overloaded signal/method in Qt 5 socket_error = socket.error() # type: ignore[operator,unused-ignore] if socket_error not in [ QLocalSocket.LocalSocketError.UnknownSocketError, QLocalSocket.LocalSocketError.PeerClosedError ]: log.ipc.debug("We got an error immediately.") self.on_error(socket_error) socket.disconnected.connect(self.on_disconnected) if socket.state() == QLocalSocket.LocalSocketState.UnconnectedState: log.ipc.debug("Socket was disconnected immediately.") self.on_disconnected() @pyqtSlot() def on_disconnected(self): """Clean up socket when the client disconnected.""" log.ipc.debug("Client disconnected from socket 0x{:x}.".format( id(self._socket))) self._timer.stop() if self._old_socket is not None: self._old_socket.deleteLater() self._old_socket = self._socket self._socket = None # Maybe another connection is waiting. self.handle_connection() def _handle_invalid_data(self): """Handle invalid data we got from a QLocalSocket.""" assert self._socket is not None log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format( id(self._socket))) self.got_invalid_data.emit() self._socket.errorOccurred.connect(self.on_error) self._socket.disconnectFromServer() def _handle_data(self, data): """Handle data (as bytes) we got from on_ready_read.""" try: decoded = data.decode('utf-8') except UnicodeDecodeError: log.ipc.error("invalid utf-8: {!r}".format(binascii.hexlify(data))) self._handle_invalid_data() return log.ipc.debug("Processing: {}".format(decoded)) try: json_data = json.loads(decoded) except ValueError: log.ipc.error("invalid json: {}".format(decoded.strip())) self._handle_invalid_data() return for name in ['args', 'target_arg']: if name not in json_data: log.ipc.error("Missing {}: {}".format(name, decoded.strip())) self._handle_invalid_data() return try: protocol_version = int(json_data['protocol_version']) except (KeyError, ValueError): log.ipc.error("invalid version: {}".format(decoded.strip())) self._handle_invalid_data() return if protocol_version != PROTOCOL_VERSION: log.ipc.error("incompatible version: expected {}, got {}".format( PROTOCOL_VERSION, protocol_version)) self._handle_invalid_data() return args = json_data['args'] target_arg = json_data['target_arg'] if target_arg is None: # https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html target_arg = '' cwd = json_data.get('cwd', '') assert cwd is not None self.got_args.emit(args, target_arg, cwd) def _get_socket(self, warn=True): """Get the current socket for on_ready_read. Arguments: warn: Whether to warn if no socket was found. """ if self._socket is None: # pragma: no cover # This happens when doing a connection while another one is already # active for some reason. if self._old_socket is None: if warn: log.ipc.warning("In _get_socket with None socket and old_socket!") return None log.ipc.debug("In _get_socket with None socket!") socket = self._old_socket else: socket = self._socket if sip.isdeleted(socket): # pragma: no cover log.ipc.warning("Ignoring deleted IPC socket") return None return socket @pyqtSlot() def on_ready_read(self): """Read json data from the client.""" self._timer.stop() socket = self._get_socket() while socket is not None and socket.canReadLine(): data = bytes(socket.readLine()) self.got_raw.emit(data) log.ipc.debug("Read from socket 0x{:x}: {!r}".format( id(socket), data)) self._handle_data(data) socket = self._get_socket(warn=False) if self._socket is not None: self._timer.start() @pyqtSlot() def on_timeout(self): """Cancel the current connection if it was idle for too long.""" assert self._socket is not None if not self._timer.check_timeout_validity(): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-124496 log.ipc.debug("Ignoring early on_timeout call") return log.ipc.error("IPC connection timed out " "(socket 0x{:x}).".format(id(self._socket))) self._socket.disconnectFromServer() if self._socket is not None: # pragma: no cover # on_socket_disconnected sets it to None self._socket.waitForDisconnected(CONNECT_TIMEOUT) if self._socket is not None: # pragma: no cover # on_socket_disconnected sets it to None self._socket.abort() @pyqtSlot() def update_atime(self): """Update the atime of the socket file all few hours. From the XDG basedir spec: To ensure that your files are not removed, they should have their access time timestamp modified at least once every 6 hours of monotonic time or the 'sticky' bit should be set on the file. """ assert self._server is not None path = self._server.fullServerName() if not path: log.ipc.error("In update_atime with no server path!") return log.ipc.debug("Touching {}".format(path)) try: os.utime(path) except OSError: log.ipc.exception("Failed to update IPC socket, trying to " "re-listen...") self._server.close() self.listen() @pyqtSlot() def shutdown(self): """Shut down the IPC server cleanly.""" if self._server is None: # We can get called twice when using :restart -- there, IPC is shut down # early to avoid processing new connections while shutting down, and then # we get called again when the application is about to quit. return log.ipc.debug("Shutting down IPC (socket 0x{:x})".format( id(self._socket))) if self._socket is not None: self._socket.deleteLater() self._socket = None self._timer.stop() if self._atime_timer is not None: # pragma: no branch self._atime_timer.stop() try: self._atime_timer.timeout.disconnect(self.update_atime) except TypeError: pass self._server.close() self._server.deleteLater() self._remove_server() self._server = None def send_to_running_instance(socketname, command, target_arg, *, socket=None): """Try to send a commandline to a running instance. Blocks for CONNECT_TIMEOUT ms. Args: socketname: The name which should be used for the socket. command: The command to send to the running instance. target_arg: --target command line argument socket: The socket to read data from, or None. Return: True if connecting was successful, False if no connection was made. """ if socket is None: socket = QLocalSocket() log.ipc.debug("Connecting to {}".format(socketname)) socket.connectToServer(socketname) connected = socket.waitForConnected(CONNECT_TIMEOUT) if connected: log.ipc.info("Opening in existing instance") json_data = {'args': command, 'target_arg': target_arg, 'version': qutebrowser.__version__, 'protocol_version': PROTOCOL_VERSION} try: cwd = os.getcwd() except OSError: pass else: json_data['cwd'] = cwd line = json.dumps(json_data) + '\n' data = line.encode('utf-8') log.ipc.debug("Writing: {!r}".format(data)) socket.writeData(data) socket.waitForBytesWritten(WRITE_TIMEOUT) if socket.error() != QLocalSocket.LocalSocketError.UnknownSocketError: raise SocketError("writing to running instance", socket) socket.disconnectFromServer() if socket.state() != QLocalSocket.LocalSocketState.UnconnectedState: socket.waitForDisconnected(CONNECT_TIMEOUT) return True else: if socket.error() not in [QLocalSocket.LocalSocketError.ConnectionRefusedError, QLocalSocket.LocalSocketError.ServerNotFoundError]: raise SocketError("connecting to running instance", socket) log.ipc.debug("No existing instance present ({})".format( debug.qenum_key(QLocalSocket, socket.error()))) return False def display_error(exc, args): """Display a message box with an IPC error.""" error.handle_fatal_exc( exc, "Error while connecting to running instance!", no_err_windows=args.no_err_windows) def send_or_listen(args): """Send the args to a running instance or start a new IPCServer. Args: args: The argparse namespace. Return: The IPCServer instance if no running instance was detected. None if an instance was running and received our request. """ global server try: socketname = _get_socketname(args.basedir) try: sent = send_to_running_instance(socketname, args.command, args.target) if sent: return None log.init.debug("Starting IPC server...") server = IPCServer(socketname) server.listen() return server except AddressInUseError: # This could be a race condition... log.init.debug("Got AddressInUseError, trying again.") time.sleep(0.5) sent = send_to_running_instance(socketname, args.command, args.target) if sent: return None else: raise except Error as e: display_error(e, args) raise ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/keyhintwidget.py0000644000175100017510000001053615102145205022317 0ustar00runnerrunner# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) # # SPDX-License-Identifier: GPL-3.0-or-later """Small window that pops up to show hints for possible keystrings. When a user inputs a key that forms a partial match, this shows a small window with each possible completion of that keystring and the corresponding command. It is intended to help discoverability of keybindings. """ import html import re from qutebrowser.qt.widgets import QLabel, QSizePolicy from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt from qutebrowser.qt.gui import QKeySequence from qutebrowser.config import config, stylesheet from qutebrowser.utils import utils, usertypes from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils class KeyHintView(QLabel): """The view showing hints for key bindings based on the current key string. Attributes: _win_id: Window ID of parent. Signals: update_geometry: Emitted when this widget should be resized/positioned. """ STYLESHEET = """ QLabel { font: {{ conf.fonts.keyhint }}; color: {{ conf.colors.keyhint.fg }}; background-color: {{ conf.colors.keyhint.bg }}; padding: 6px; {% if conf.statusbar.position == 'top' %} border-bottom-right-radius: {{ conf.keyhint.radius }}px; {% else %} border-top-right-radius: {{ conf.keyhint.radius }}px; {% endif %} } """ update_geometry = pyqtSignal() def __init__(self, win_id, parent=None): super().__init__(parent) self.setTextFormat(Qt.TextFormat.RichText) self._win_id = win_id self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) self.hide() self._show_timer = usertypes.Timer(self, 'keyhint_show') self._show_timer.timeout.connect(self.show) self._show_timer.setSingleShot(True) stylesheet.set_register(self) def __repr__(self): return utils.get_repr(self, win_id=self._win_id) def showEvent(self, e): """Adjust the keyhint size when it's freshly shown.""" self.update_geometry.emit() super().showEvent(e) @pyqtSlot(usertypes.KeyMode, str) def update_keyhint(self, mode, prefix): """Show hints for the given prefix (or hide if prefix is empty). Args: mode: The key mode to show the keyhints for. prefix: The current partial keystring. """ match = re.fullmatch(r'(\d*)(.*)', prefix) assert match is not None, prefix countstr, prefix = match.groups() if not prefix: self._show_timer.stop() self.hide() return def blacklisted(keychain): excluded = config.val.keyhint.blacklist return utils.match_globs(excluded, keychain) is not None def takes_count(cmdstr): """Return true iff this command can take a count argument.""" cmdname = cmdstr.split(' ')[0] cmd = objects.commands.get(cmdname) return cmd and cmd.takes_count() bindings_dict = config.key_instance.get_bindings_for(mode.name) bindings = [ (k, v) for (k, v) in sorted(bindings_dict.items()) if keyutils.KeySequence.parse(prefix).matches(k) != QKeySequence.SequenceMatch.NoMatch and not blacklisted(str(k)) and (takes_count(v) or not countstr) ] if not bindings: self._show_timer.stop() return # delay so a quickly typed keychain doesn't display hints self._show_timer.setInterval(config.val.keyhint.delay) self._show_timer.start() suffix_color = html.escape(config.val.colors.keyhint.suffix.fg) text = '' for seq, cmd in bindings: text += ( "" "{}" "{}" "{}" "" ).format( html.escape(prefix), suffix_color, html.escape(str(seq).removeprefix(prefix)), html.escape(cmd) ) text = '{}
'.format(text) self.setText(text) self.adjustSize() self.update_geometry.emit() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/lineparser.py0000644000175100017510000001535015102145205021603 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Parser for line-based files like histories.""" import os import os.path import contextlib from collections.abc import Sequence from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QObject from qutebrowser.utils import log, utils, qtutils from qutebrowser.config import config class BaseLineParser(QObject): """A LineParser without any real data. Attributes: _configdir: Directory to read the config from, or None. _configfile: The config file path. _fname: Filename of the config. _binary: Whether to open the file in binary mode. Signals: changed: Emitted when the history was changed. """ changed = pyqtSignal() def __init__(self, configdir, fname, *, binary=False, parent=None): """Constructor. Args: configdir: Directory to read the config from. fname: Filename of the config file. binary: Whether to open the file in binary mode. _opened: Whether the underlying file is open """ super().__init__(parent) self._configdir = configdir self._configfile = os.path.join(self._configdir, fname) self._fname = fname self._binary = binary self._opened = False def __repr__(self): return utils.get_repr(self, constructor=True, configdir=self._configdir, fname=self._fname, binary=self._binary) def _prepare_save(self): """Prepare saving of the file. Return: True if the file should be saved, False otherwise. """ os.makedirs(self._configdir, 0o755, exist_ok=True) return True def _after_save(self): """Log a message after saving is done.""" log.destroy.debug("Saved to {}".format(self._configfile)) @contextlib.contextmanager def _open(self, mode): """Open self._configfile for reading. Args: mode: The mode to use ('a'/'r'/'w') Raises: OSError: if the file is already open Yields: a file object for the config file """ assert self._configfile is not None if self._opened: raise OSError("Refusing to double-open LineParser.") self._opened = True try: if self._binary: # pylint: disable=unspecified-encoding with open(self._configfile, mode + 'b') as f: yield f else: with open(self._configfile, mode, encoding='utf-8') as f: yield f finally: self._opened = False def _write(self, fp, data): """Write the data to a file. Args: fp: A file object to write the data to. data: The data to write. """ if not data: return if self._binary: fp.write(b'\n'.join(data)) fp.write(b'\n') else: fp.write('\n'.join(data)) fp.write('\n') def save(self): """Save the history to disk.""" raise NotImplementedError def clear(self): """Clear the contents of the file.""" raise NotImplementedError class LineParser(BaseLineParser): """Parser for configuration files which are simply line-based. Attributes: data: A list of lines. """ def __init__(self, configdir, fname, *, binary=False, parent=None): """Constructor. Args: configdir: Directory to read the config from. fname: Filename of the config file. binary: Whether to open the file in binary mode. """ super().__init__(configdir, fname, binary=binary, parent=parent) if not os.path.isfile(self._configfile): self.data: Sequence[str] = [] else: log.init.debug("Reading {}".format(self._configfile)) self._read() def __iter__(self): return iter(self.data) def __getitem__(self, key): return self.data[key] def _read(self): """Read the data from self._configfile.""" with self._open('r') as f: if self._binary: self.data = [line.rstrip(b'\n') for line in f] else: self.data = [line.rstrip('\n') for line in f] def save(self): """Save the config file.""" if self._opened: raise OSError("Refusing to double-open LineParser.") do_save = self._prepare_save() if not do_save: return self._opened = True try: assert self._configfile is not None with qtutils.savefile_open(self._configfile, self._binary) as f: self._write(f, self.data) finally: self._opened = False self._after_save() def clear(self): self.data = [] self.save() class LimitLineParser(LineParser): """A LineParser with a limited count of lines. Attributes: _limit: The config option used to limit the maximum number of lines. """ def __init__(self, configdir, fname, *, limit, binary=False, parent=None): """Constructor. Args: configdir: Directory to read the config from, or None. fname: Filename of the config file. limit: Config option which contains a limit. binary: Whether to open the file in binary mode. """ super().__init__(configdir, fname, binary=binary, parent=parent) self._limit = limit if limit is not None and configdir is not None: config.instance.changed.connect(self._cleanup_file) def __repr__(self): return utils.get_repr(self, constructor=True, configdir=self._configdir, fname=self._fname, limit=self._limit, binary=self._binary) @pyqtSlot(str) def _cleanup_file(self, option): """Delete the file if the limit was changed to 0.""" assert self._configfile is not None if option != self._limit: return value = config.instance.get(option) if value == 0: if os.path.exists(self._configfile): os.remove(self._configfile) def save(self): """Save the config file.""" limit = config.instance.get(self._limit) if limit == 0: return do_save = self._prepare_save() if not do_save: return assert self._configfile is not None with qtutils.savefile_open(self._configfile, self._binary) as f: self._write(f, self.data[-limit:]) self._after_save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/miscwidgets.py0000644000175100017510000003624215102145205021764 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Misc. widgets used at different places.""" from typing import Optional from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt, QSize, QTimer from qutebrowser.qt.widgets import (QLineEdit, QWidget, QHBoxLayout, QLabel, QStyleOption, QStyle, QLayout, QSplitter) from qutebrowser.qt.gui import QValidator, QPainter, QResizeEvent from qutebrowser.config import config, configfiles from qutebrowser.utils import utils, log, usertypes, debug, qtutils from qutebrowser.misc import cmdhistory from qutebrowser.browser import inspector from qutebrowser.keyinput import keyutils, modeman class CommandLineEdit(QLineEdit): """A QLineEdit with a history and prompt chars. Attributes: history: The command history object. _validator: The current command validator. _promptlen: The length of the current prompt. """ def __init__(self, parent=None): super().__init__(parent) self.history = cmdhistory.History(parent=self) self._validator = _CommandValidator(self) self.setValidator(self._validator) self.textEdited.connect(self.on_text_edited) self.cursorPositionChanged.connect(self.__on_cursor_position_changed) self._promptlen = 0 def __repr__(self): return utils.get_repr(self, text=self.text()) @pyqtSlot(str) def on_text_edited(self, _text): """Slot for textEdited. Stop history browsing.""" self.history.stop() @pyqtSlot(int, int) def __on_cursor_position_changed(self, _old, new): """Prevent the cursor moving to the prompt. We use __ here to avoid accidentally overriding it in subclasses. """ if new < self._promptlen: self.cursorForward(self.hasSelectedText(), self._promptlen - new) def set_prompt(self, text): """Set the current prompt to text. This updates the validator, and makes sure the user can't move the cursor behind the prompt. """ self._validator.prompt = text self._promptlen = len(text) class _CommandValidator(QValidator): """Validator to prevent the : from getting deleted. Attributes: prompt: The current prompt. """ def __init__(self, parent=None): super().__init__(parent) self.prompt = None def validate(self, string, pos): """Override QValidator::validate. Args: string: The string to validate. pos: The current cursor position. Return: A tuple (status, string, pos) as a QValidator should. """ if self.prompt is None or string.startswith(self.prompt): return (QValidator.State.Acceptable, string, pos) else: return (QValidator.State.Invalid, string, pos) class DetailFold(QWidget): """A "fold" widget with an arrow to show/hide details. Attributes: _folded: Whether the widget is currently folded or not. _hbox: The HBoxLayout the arrow/label are in. _arrow: The FoldArrow widget. Signals: toggled: Emitted when the widget was folded/unfolded. arg 0: bool, if the contents are currently visible. """ toggled = pyqtSignal(bool) def __init__(self, text, parent=None): super().__init__(parent) self._folded = True self._hbox = QHBoxLayout(self) self._hbox.setContentsMargins(0, 0, 0, 0) self._arrow = _FoldArrow() self._hbox.addWidget(self._arrow) label = QLabel(text) self._hbox.addWidget(label) self._hbox.addStretch() def toggle(self): """Toggle the fold of the widget.""" self._folded = not self._folded self._arrow.fold(self._folded) self.toggled.emit(not self._folded) def mousePressEvent(self, e): """Toggle the fold if the widget was pressed. Args: e: The QMouseEvent. """ if e.button() == Qt.MouseButton.LeftButton: e.accept() self.toggle() else: super().mousePressEvent(e) class _FoldArrow(QWidget): """The arrow shown for the DetailFold widget. Attributes: _folded: Whether the widget is currently folded or not. """ def __init__(self, parent=None): super().__init__(parent) self._folded = True def fold(self, folded): """Fold/unfold the widget. Args: folded: The new desired state. """ self._folded = folded self.update() def paintEvent(self, _event): """Paint the arrow. Args: _event: The QPaintEvent (unused). """ opt = QStyleOption() opt.initFrom(self) painter = QPainter(self) if self._folded: elem = QStyle.PrimitiveElement.PE_IndicatorArrowRight else: elem = QStyle.PrimitiveElement.PE_IndicatorArrowDown style = self.style() assert style is not None style.drawPrimitive(elem, opt, painter, self) def minimumSizeHint(self): """Return a sensible size.""" return QSize(8, 8) class WrapperLayout(QLayout): """A Qt layout which simply wraps a single widget. This is used so the widget is hidden behind a defined API and can't easily be accidentally accessed. """ def __init__(self, parent=None): super().__init__(parent) self._widget: Optional[QWidget] = None self._container: Optional[QWidget] = None def addItem(self, _widget): raise utils.Unreachable def sizeHint(self): """Get the size of the underlying widget.""" if self._widget is None: return QSize() return self._widget.sizeHint() def itemAt(self, _index): return None def takeAt(self, _index): raise utils.Unreachable def setGeometry(self, rect): """Pass through setGeometry calls to the underlying widget.""" if self._widget is None: return self._widget.setGeometry(rect) def wrap(self, container, widget): """Wrap the given widget in the given container.""" self._container = container self._widget = widget container.setFocusProxy(widget) widget.setParent(container) def unwrap(self): """Remove the widget from this layout. Does nothing if it nothing was wrapped before. """ if self._widget is None: return assert self._container is not None self._widget.setParent(qtutils.QT_NONE) self._widget.deleteLater() self._widget = None self._container.setFocusProxy(qtutils.QT_NONE) class FullscreenNotification(QLabel): """A label telling the user this page is now fullscreen.""" def __init__(self, parent=None): super().__init__(parent) self.setStyleSheet(""" background-color: rgba(50, 50, 50, 80%); color: white; border-radius: 20px; padding: 30px; """) all_bindings = config.key_instance.get_reverse_bindings_for('normal') bindings = all_bindings.get('fullscreen --leave') if bindings: key = bindings[0] self.setText("Press {} to exit fullscreen.".format(key)) else: self.setText("Page is now fullscreen.") self.resize(self.sizeHint()) if config.val.content.fullscreen.window: parent = self.parentWidget() assert parent is not None geom = parent.geometry() else: window = self.window() assert window is not None handle = window.windowHandle() assert handle is not None screen = handle.screen() assert screen is not None geom = screen.geometry() self.move((geom.width() - self.sizeHint().width()) // 2, 30) def set_timeout(self, timeout): """Hide the widget after the given timeout.""" QTimer.singleShot(timeout, self._on_timeout) @pyqtSlot() def _on_timeout(self): """Hide and delete the widget.""" self.hide() self.deleteLater() class InspectorSplitter(QSplitter): """Allows putting an inspector inside the tab. Attributes: _main_idx: index of the main webview widget _position: position of the inspector (right/left/top/bottom) _preferred_size: the preferred size of the inpector widget in pixels Class attributes: _PROTECTED_MAIN_SIZE: How much space should be reserved for the main content (website). _SMALL_SIZE_THRESHOLD: If the window size is under this threshold, we consider this a temporary "emergency" situation. """ _PROTECTED_MAIN_SIZE = 150 _SMALL_SIZE_THRESHOLD = 300 def __init__(self, win_id: int, main_webview: QWidget, parent: QWidget = None) -> None: super().__init__(parent) self._win_id = win_id self.addWidget(main_webview) self.setFocusProxy(main_webview) self.splitterMoved.connect(self._on_splitter_moved) self._main_idx: Optional[int] = None self._inspector_idx: Optional[int] = None self._position: Optional[inspector.Position] = None self._preferred_size: Optional[int] = None def cycle_focus(self): """Cycle keyboard focus between the main/inspector widget.""" if self.count() == 1: raise inspector.Error("No inspector inside main window") assert self._main_idx is not None assert self._inspector_idx is not None main_widget = self.widget(self._main_idx) inspector_widget = self.widget(self._inspector_idx) assert main_widget is not None assert inspector_widget is not None if not inspector_widget.isVisible(): raise inspector.Error("No inspector inside main window") if main_widget.hasFocus(): inspector_widget.setFocus() modeman.enter(self._win_id, usertypes.KeyMode.insert, reason='Inspector focused', only_if_normal=True) elif inspector_widget.hasFocus(): main_widget.setFocus() def set_inspector(self, inspector_widget: inspector.AbstractWebInspector, position: inspector.Position) -> None: """Set the position of the inspector.""" assert position != inspector.Position.window if position in [inspector.Position.right, inspector.Position.bottom]: self._main_idx = 0 self._inspector_idx = 1 else: self._inspector_idx = 0 self._main_idx = 1 self.setOrientation(Qt.Orientation.Horizontal if position in [inspector.Position.left, inspector.Position.right] else Qt.Orientation.Vertical) self.insertWidget(self._inspector_idx, inspector_widget) self._position = position self._load_preferred_size() self._adjust_size() def _save_preferred_size(self) -> None: """Save the preferred size of the inspector widget.""" assert self._position is not None size = str(self._preferred_size) configfiles.state['inspector'][self._position.name] = size def _load_preferred_size(self) -> None: """Load the preferred size of the inspector widget.""" assert self._position is not None full = (self.width() if self.orientation() == Qt.Orientation.Horizontal else self.height()) # If we first open the inspector with a window size of < 300px # (self._SMALL_SIZE_THRESHOLD), we don't want to default to half of the # window size as the small window is likely a temporary situation and # the inspector isn't very usable in that state. self._preferred_size = max(self._SMALL_SIZE_THRESHOLD, full // 2) try: size = int(configfiles.state['inspector'][self._position.name]) except KeyError: # First start pass except ValueError as e: log.misc.error("Could not read inspector size: {}".format(e)) else: self._preferred_size = int(size) def _adjust_size(self) -> None: """Adjust the size of the inspector similarly to Chromium. In general, we want to keep the absolute size of the inspector (rather than the ratio) the same, as it's confusing when the layout of its contents changes. We're essentially handling three different cases: 1) We have plenty of space -> Keep inspector at the preferred absolute size. 2) We're slowly running out of space. Make sure the page still has 150px (self._PROTECTED_MAIN_SIZE) left, give the rest to the inspector. 3) The window is very small (< 300px, self._SMALL_SIZE_THRESHOLD). Keep Qt's behavior of keeping the aspect ratio, as all hope is lost at this point. """ sizes = self.sizes() total = sizes[0] + sizes[1] assert self._main_idx is not None assert self._inspector_idx is not None assert self._preferred_size is not None if total >= self._preferred_size + self._PROTECTED_MAIN_SIZE: # Case 1 above sizes[self._inspector_idx] = self._preferred_size sizes[self._main_idx] = total - self._preferred_size self.setSizes(sizes) elif (sizes[self._main_idx] < self._PROTECTED_MAIN_SIZE and total >= self._SMALL_SIZE_THRESHOLD): # Case 2 above handle_size = self.handleWidth() sizes[self._main_idx] = ( self._PROTECTED_MAIN_SIZE - handle_size // 2) sizes[self._inspector_idx] = ( total - self._PROTECTED_MAIN_SIZE + handle_size // 2) self.setSizes(sizes) else: # Case 3 above pass @pyqtSlot() def _on_splitter_moved(self) -> None: assert self._inspector_idx is not None sizes = self.sizes() self._preferred_size = sizes[self._inspector_idx] self._save_preferred_size() def resizeEvent(self, e: Optional[QResizeEvent]) -> None: """Window resize event.""" assert e is not None super().resizeEvent(e) if self.count() == 2: self._adjust_size() class KeyTesterWidget(QWidget): """Widget displaying key presses.""" def __init__(self, parent=None): super().__init__(parent) self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self._layout = QHBoxLayout(self) self._label = QLabel(text="Waiting for keypress...") self._layout.addWidget(self._label) def keyPressEvent(self, e): """Show pressed keys.""" lines = [ str(keyutils.KeyInfo.from_event(e)), '', f"key: {debug.qenum_key(Qt, e.key(), klass=Qt.Key)}", f"modifiers: {debug.qflags_key(Qt, e.modifiers())}", 'text: {!r}'.format(e.text()), ] self._label.setText('\n'.join(lines)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/msgbox.py0000644000175100017510000000365115102145205020737 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Convenience functions to show message boxes.""" from qutebrowser.qt.core import Qt from qutebrowser.qt.widgets import QMessageBox from qutebrowser.misc import objects from qutebrowser.utils import log class DummyBox: """A dummy QMessageBox returned when --no-err-windows is used.""" def exec(self): pass def msgbox(parent, title, text, *, icon, buttons=QMessageBox.StandardButton.Ok, on_finished=None, plain_text=None): """Display a QMessageBox with the given icon. Args: parent: The parent to set for the message box. title: The title to set. text: The text to set. icon: The QIcon to show in the box. buttons: The buttons to set (QMessageBox::StandardButtons) on_finished: A slot to connect to the 'finished' signal. plain_text: Whether to force plain text (True) or rich text (False). None (the default) uses Qt's auto detection. Return: A new QMessageBox. """ if objects.args.no_err_windows: log.misc.info(f'{title}\n\n{text}') return DummyBox() box = QMessageBox(parent) box.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) box.setIcon(icon) box.setStandardButtons(buttons) if on_finished is not None: box.finished.connect(on_finished) if plain_text: box.setTextFormat(Qt.TextFormat.PlainText) elif plain_text is not None: box.setTextFormat(Qt.TextFormat.RichText) box.setWindowTitle(title) box.setText(text) box.show() return box def information(*args, **kwargs): """Display an information box. Args: *args: Passed to msgbox. **kwargs: Passed to msgbox. Return: A new QMessageBox. """ return msgbox(*args, icon=QMessageBox.Icon.Information, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/nativeeventfilter.py0000644000175100017510000001360415102145205023175 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Native Qt event filter. This entire file is a giant WORKAROUND for https://bugreports.qt.io/browse/QTBUG-114334. """ from typing import Union, Optional import enum import ctypes import ctypes.util from qutebrowser.qt import sip, machinery from qutebrowser.qt.core import QAbstractNativeEventFilter, QByteArray, qVersion from qutebrowser.misc import objects from qutebrowser.utils import log, qtutils # Needs to be saved to avoid garbage collection _instance: Optional["NativeEventFilter"] = None # Using C-style naming for C structures in this file # pylint: disable=invalid-name class xcb_ge_generic_event_t(ctypes.Structure): # noqa: N801 """See https://xcb.freedesktop.org/manual/structxcb__ge__generic__event__t.html. Also used for xcb_generic_event_t as the structures overlap: https://xcb.freedesktop.org/manual/structxcb__generic__event__t.html """ _fields_ = [ ("response_type", ctypes.c_uint8), ("extension", ctypes.c_uint8), ("sequence", ctypes.c_uint16), ("length", ctypes.c_uint32), ("event_type", ctypes.c_uint16), ("pad0", ctypes.c_uint8 * 22), ("full_sequence", ctypes.c_uint32), ] _XCB_GE_GENERIC = 35 class XcbInputOpcodes(enum.IntEnum): """https://xcb.freedesktop.org/manual/group__XCB__Input__API.html. NOTE: If adding anything new here, adjust _PROBLEMATIC_XINPUT_EVENTS below! """ HIERARCHY = 11 TOUCH_BEGIN = 18 TOUCH_UPDATE = 19 TOUCH_END = 20 GESTURE_PINCH_BEGIN = 27 GESTURE_PINCH_UPDATE = 28 GESTURE_PINCH_END = 29 GESTURE_SWIPE_BEGIN = 30 GESTURE_SWIPE_UPDATE = 31 GESTURE_SWIPE_END = 32 _PROBLEMATIC_XINPUT_EVENTS = set(XcbInputOpcodes) - {XcbInputOpcodes.HIERARCHY} class xcb_query_extension_reply_t(ctypes.Structure): # noqa: N801 """https://xcb.freedesktop.org/manual/structxcb__query__extension__reply__t.html.""" _fields_ = [ ("response_type", ctypes.c_uint8), ("pad0", ctypes.c_uint8), ("sequence", ctypes.c_uint16), ("length", ctypes.c_uint32), ("present", ctypes.c_uint8), ("major_opcode", ctypes.c_uint8), ("first_event", ctypes.c_uint8), ("first_error", ctypes.c_uint8), ] # pylint: enable=invalid-name if machinery.IS_QT6: _PointerRetType = sip.voidptr else: _PointerRetType = int class NativeEventFilter(QAbstractNativeEventFilter): """Event filter for XCB messages to work around Qt 6.5.1 crash.""" # Return values for nativeEventFilter. # # Tuple because PyQt uses the second value as the *result out-pointer, which # according to the Qt documentation is only used on Windows. _PASS_EVENT_RET = (False, qtutils.maybe_cast(_PointerRetType, machinery.IS_QT6, 0)) _FILTER_EVENT_RET = (True, qtutils.maybe_cast(_PointerRetType, machinery.IS_QT6, 0)) def __init__(self) -> None: super().__init__() self._active = False # Set to true when getting hierarchy event xcb = ctypes.CDLL(ctypes.util.find_library("xcb")) xcb.xcb_connect.restype = ctypes.POINTER(ctypes.c_void_p) xcb.xcb_query_extension_reply.restype = ctypes.POINTER( xcb_query_extension_reply_t ) conn = xcb.xcb_connect(None, None) assert conn try: assert not xcb.xcb_connection_has_error(conn) # Get major opcode ID of Xinput extension name = b"XInputExtension" cookie = xcb.xcb_query_extension(conn, len(name), name) reply = xcb.xcb_query_extension_reply(conn, cookie, None) assert reply if reply.contents.present: self.xinput_opcode = reply.contents.major_opcode else: self.xinput_opcode = None finally: xcb.xcb_disconnect(conn) def nativeEventFilter( self, evtype: Union[QByteArray, bytes, bytearray, memoryview], message: Optional[sip.voidptr], ) -> tuple[bool, _PointerRetType]: """Handle XCB events.""" # We're only installed when the platform plugin is xcb assert evtype == b"xcb_generic_event_t", evtype assert message is not None # We cast to xcb_ge_generic_event_t, which overlaps with xcb_generic_event_t. # .extension and .event_type will only make sense if this is an # XCB_GE_GENERIC event, but this is the first thing we check in the 'if' # below anyways. event = ctypes.cast( int(message), ctypes.POINTER(xcb_ge_generic_event_t) ).contents if ( event.response_type == _XCB_GE_GENERIC and event.extension == self.xinput_opcode ): if not self._active and event.event_type == XcbInputOpcodes.HIERARCHY: log.misc.warning( "Got XInput HIERARCHY event, future swipe/pinch/touch events will " "be ignored to avoid a Qt 6.5.1 crash. Restart qutebrowser to make " "them work again." ) self._active = True elif self._active and event.event_type in _PROBLEMATIC_XINPUT_EVENTS: name = XcbInputOpcodes(event.event_type).name log.misc.debug(f"Ignoring problematic XInput event {name}") return self._FILTER_EVENT_RET return self._PASS_EVENT_RET def init() -> None: """Install the native event filter if needed.""" global _instance platform = objects.qapp.platformName() qt_version = qVersion() log.misc.debug(f"Platform {platform}, Qt {qt_version}") if platform != "xcb" or qt_version != "6.5.1": return log.misc.debug("Installing native event filter to work around Qt 6.5.1 crash") _instance = NativeEventFilter() objects.qapp.installNativeEventFilter(_instance) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/objects.py0000644000175100017510000000165115102145205021067 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Various global objects.""" # NOTE: We need to be careful with imports here, as this is imported from # earlyinit. import argparse from typing import TYPE_CHECKING, Any, Union, cast if TYPE_CHECKING: from qutebrowser import app from qutebrowser.utils import usertypes from qutebrowser.commands import command class NoBackend: """Special object when there's no backend set so we notice that.""" @property def name(self) -> str: raise AssertionError("No backend set!") def __eq__(self, other: Any) -> bool: raise AssertionError("No backend set!") backend: Union['usertypes.Backend', NoBackend] = NoBackend() commands: dict[str, 'command.Command'] = {} debug_flags: set[str] = set() args = cast(argparse.Namespace, None) qapp = cast('app.Application', None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/pakjoy.py0000644000175100017510000002510115102145205020727 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Chromium .pak repacking. This entire file is a great WORKAROUND for https://bugreports.qt.io/browse/QTBUG-118157 and the fact we can't just simply disable the hangouts extension: https://bugreports.qt.io/browse/QTBUG-118452 It's yet another big hack. If you think this is bad, look at elf.py instead. The name of this file might or might not be inspired by a certain vegetable, as well as the "joy" this bug has caused me. Useful references: - https://sweetscape.com/010editor/repository/files/PAK.bt (010 editor <3) - https://textslashplain.com/2022/05/03/chromium-internals-pak-files/ - https://github.com/myfreeer/chrome-pak-customizer - https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/pak_util.py - https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/grit/format/data_pack.py This is a "best effort" parser. If it errors out, we don't apply the workaround instead of crashing. """ import os import sys import shutil import pathlib import dataclasses import contextlib from typing import ClassVar, IO, Optional from collections.abc import Iterator from qutebrowser.config import config from qutebrowser.misc import binparsing, objects from qutebrowser.qt import core from qutebrowser.utils import qtutils, standarddir, version, utils, log, message from qutebrowser.qt.webenginecore import QWebEngineProfile HANGOUTS_EXT_ID = "nkeimhogjdpnpccoofpliimaahmaaome" HANGOUTS_MARKER = f"// Extension ID: {HANGOUTS_EXT_ID}".encode("utf-8") HANGOUTS_IDS = [ # Linux 47222, # QtWebEngine 6.9 Beta 3 43932, # QtWebEngine 6.9 Beta 1 43722, # QtWebEngine 6.8 41262, # QtWebEngine 6.7 36197, # QtWebEngine 6.6 34897, # QtWebEngine 6.5 32707, # QtWebEngine 6.4 27537, # QtWebEngine 6.3 23607, # QtWebEngine 6.2 248, # macOS 381, # Windows ] PAK_VERSION = 5 RESOURCES_ENV_VAR = "QTWEBENGINE_RESOURCES_PATH" DISABLE_ENV_VAR = "QUTE_DISABLE_PAKJOY" CACHE_DIR_NAME = "webengine_resources_pak_quirk" PAK_FILENAME = ( "qtwebengine_resources.debug.pak" if core.QLibraryInfo.isDebugBuild() else "qtwebengine_resources.pak" ) TARGET_URL = b"https://*.google.com/*" REPLACEMENT_URL = b"https://qute.invalid/*" assert len(TARGET_URL) == len(REPLACEMENT_URL) @dataclasses.dataclass class PakHeader: """Chromium .pak header (version 5).""" encoding: int # uint32 resource_count: int # uint16 _alias_count: int # uint16 _FORMAT: ClassVar[str] = " "PakHeader": """Parse a PAK version 5 header from a file.""" return cls(*binparsing.unpack(cls._FORMAT, fobj)) @dataclasses.dataclass class PakEntry: """Entry description in a .pak file.""" resource_id: int # uint16 file_offset: int # uint32 size: int = 0 # not in file _FORMAT: ClassVar[str] = " "PakEntry": """Parse a PAK entry from a file.""" return cls(*binparsing.unpack(cls._FORMAT, fobj)) class PakParser: """Parse webengine pak and find patch location to disable Google Meet extension.""" def __init__(self, fobj: IO[bytes]) -> None: """Parse the .pak file from the given file object.""" pak_version = binparsing.unpack(" int: """Return byte offset of TARGET_URL into the pak file.""" try: return self.manifest_entry.file_offset + self.manifest.index(TARGET_URL) except ValueError: raise binparsing.ParseError("Couldn't find URL in manifest") def _maybe_get_hangouts_manifest(self, entry: PakEntry) -> Optional[bytes]: self.fobj.seek(entry.file_offset) data = self.fobj.read(entry.size) if not data.startswith(b"{") or not data.rstrip(b"\n").endswith(b"}"): # not JSON return None if HANGOUTS_MARKER not in data: return None return data def _read_header(self) -> dict[int, PakEntry]: """Read the header and entry index from the .pak file.""" entries = [] header = PakHeader.parse(self.fobj) for _ in range(header.resource_count + 1): # + 1 due to sentinel at end entries.append(PakEntry.parse(self.fobj)) for entry, next_entry in zip(entries, entries[1:]): if entry.resource_id == 0: raise binparsing.ParseError("Unexpected sentinel entry") entry.size = next_entry.file_offset - entry.file_offset if entries[-1].resource_id != 0: raise binparsing.ParseError("Missing sentinel entry") del entries[-1] return {entry.resource_id: entry for entry in entries} def _find_manifest(self, entries: dict[int, PakEntry]) -> tuple[PakEntry, bytes]: to_check = list(entries.values()) for hangouts_id in HANGOUTS_IDS: if hangouts_id in entries: # Most likely candidate, based on previous known ID to_check.insert(0, entries[hangouts_id]) for entry in to_check: manifest = self._maybe_get_hangouts_manifest(entry) if manifest is not None: return entry, manifest raise binparsing.ParseError("Couldn't find hangouts manifest") def _find_webengine_resources() -> pathlib.Path: """Find the QtWebEngine resources dir. Mirrors logic from QtWebEngine: https://github.com/qt/qtwebengine/blob/v6.6.0/src/core/web_engine_library_info.cpp#L293-L341 """ if RESOURCES_ENV_VAR in os.environ: return pathlib.Path(os.environ[RESOURCES_ENV_VAR]) candidates = [] qt_data_path = qtutils.library_path(qtutils.LibraryPath.data) if utils.is_mac: # pragma: no cover # I'm not sure how to arrive at this path without hardcoding it # ourselves. importlib.resources.files("PyQt6.Qt6") can serve as a # replacement for the qtutils bit but it doesn't seem to help find the # actual Resources folder. candidates.append( qt_data_path / "lib" / "QtWebEngineCore.framework" / "Resources" ) candidates += [ qt_data_path / "resources", qt_data_path, pathlib.Path(objects.qapp.applicationDirPath()), pathlib.Path.home() / f".{objects.qapp.applicationName()}", ] for candidate in candidates: if (candidate / PAK_FILENAME).exists(): return candidate candidates_str = "\n".join(f" {p}" for p in candidates) raise FileNotFoundError( f"Couldn't find webengine resources dir, candidates:\n{candidates_str}") def copy_webengine_resources() -> Optional[pathlib.Path]: """Copy qtwebengine resources to local dir for patching.""" resources_dir = _find_webengine_resources() work_dir = pathlib.Path(standarddir.cache()) / CACHE_DIR_NAME if work_dir.exists(): log.misc.debug(f"Removing existing {work_dir}") shutil.rmtree(work_dir) versions = version.qtwebengine_versions(avoid_init=True) if not ( # https://bugreports.qt.io/browse/QTBUG-118157 versions.webengine == utils.VersionNumber(6, 6) # https://bugreports.qt.io/browse/QTBUG-113369 or ( versions.webengine >= utils.VersionNumber(6, 5) and versions.webengine < utils.VersionNumber(6, 5, 3) and config.val.colors.webpage.darkmode.enabled ) # https://github.com/qutebrowser/qutebrowser/issues/8257 or config.val.qt.workarounds.disable_hangouts_extension ) or hasattr(QWebEngineProfile, "extensionManager"): # Qt 6.10+ # No patching needed return None log.misc.debug( "Copying webengine resources for quirk patching: " f"{resources_dir} -> {work_dir}" ) shutil.copytree(resources_dir, work_dir) return work_dir def _patch(file_to_patch: pathlib.Path) -> None: """Apply any patches to the given pak file.""" if not file_to_patch.exists(): _error( None, "Resource pak doesn't exist at expected location! " f"Not applying quirks. Expected location: {file_to_patch}" ) return with open(file_to_patch, "r+b") as f: try: parser = PakParser(f) log.misc.debug(f"Patching pak entry: {parser.manifest_entry}") offset = parser.find_patch_offset() binparsing.safe_seek(f, offset) f.write(REPLACEMENT_URL) except binparsing.ParseError as e: _error(e, "Failed to apply quirk to resources pak.") def _error(exc: Optional[BaseException], text: str) -> None: if config.val.qt.workarounds.disable_hangouts_extension: # Explicitly requested -> hard error lines = ["Failed to disable Hangouts extension:", text] if exc is None: lines.append(str(exc)) message.error("\n".join(lines)) elif exc is None: # Best effort -> just log log.misc.error(text) else: log.misc.exception(text) @contextlib.contextmanager def patch_webengine() -> Iterator[None]: """Apply any patches to webengine resource pak files.""" if os.environ.get(DISABLE_ENV_VAR): log.misc.debug(f"Not applying quirk due to {DISABLE_ENV_VAR}") yield return try: # Still calling this on Qt != 6.6 so that the directory is cleaned up # when not needed anymore. webengine_resources_path = copy_webengine_resources() except OSError as e: _error(e, "Failed to copy webengine resources, not applying quirk") yield return if webengine_resources_path is None: yield return _patch(webengine_resources_path / PAK_FILENAME) old_value = os.environ.get(RESOURCES_ENV_VAR) os.environ[RESOURCES_ENV_VAR] = str(webengine_resources_path) yield # Restore old value for subprocesses or :restart if old_value is None: del os.environ[RESOURCES_ENV_VAR] else: os.environ[RESOURCES_ENV_VAR] = old_value def main() -> None: with open(sys.argv[1], "rb") as f: parser = PakParser(f) print(parser.manifest.decode("utf-8")) print() print(f"entry: {parser.manifest_entry}") print(f"URL offset: {parser.find_patch_offset()}") if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/pastebin.py0000644000175100017510000000456715102145205021254 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Client for the pastebin.""" import urllib.parse from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QUrl class PastebinClient(QObject): """A client for Stikked pastebins using HTTPClient. Attributes: _client: The HTTPClient used. Class attributes: API_URL: The base API URL. Signals: success: Emitted when the paste succeeded. arg: The URL of the paste, as string. error: Emitted when the paste failed. arg: The error message, as string. """ API_URL = 'https://crashes.qutebrowser.org/api/' MISC_API_URL = 'https://paste.the-compiler.org/api/' success = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, client, parent=None, api_url=API_URL): """Constructor. Args: client: The HTTPClient to use. Will be reparented. api_url: The Stikked pastebin endpoint to use. """ super().__init__(parent) client.setParent(self) client.error.connect(self.error) client.success.connect(self.on_client_success) self._client = client self._api_url = api_url def paste(self, name, title, text, parent=None, private=False): """Paste the text into a pastebin and return the URL. Args: name: The username to post as. title: The post title. text: The text to post. parent: The parent paste to reply to. private: Whether to paste privately. """ data = { 'text': text, 'title': title, 'name': name, 'apikey': 'ihatespam', } if parent is not None: data['reply'] = parent if private: data['private'] = '1' url = QUrl(urllib.parse.urljoin(self._api_url, 'create')) self._client.post(url, data) @pyqtSlot(str) def on_client_success(self, data): """Process the data and finish when the client finished. Args: data: A string with the received data. """ if data.startswith('http://') or data.startswith('https://'): self.success.emit(data) else: self.error.emit("Invalid data received in reply!") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/quitter.py0000644000175100017510000002566715102145205021150 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Helpers related to quitting qutebrowser cleanly.""" import os import os.path import sys import json import atexit import shutil import argparse import tokenize import functools import warnings import subprocess from typing import cast from collections.abc import Iterable, Mapping, MutableSequence, Sequence from qutebrowser.qt.core import QObject, pyqtSignal, QTimer try: import hunter except ImportError: hunter = None import qutebrowser from qutebrowser.api import cmdutils from qutebrowser.utils import log, qtlog from qutebrowser.misc import sessions, ipc, objects from qutebrowser.mainwindow import prompt from qutebrowser.completion.models import miscmodels instance = cast('Quitter', None) class Quitter(QObject): """Utility class to quit/restart the QApplication. Attributes: quit_status: The current quitting status. is_shutting_down: Whether we're currently shutting down. _args: The argparse namespace. """ shutting_down = pyqtSignal() # Emitted immediately before shut down def __init__(self, *, args: argparse.Namespace, parent: QObject = None) -> None: super().__init__(parent) self.quit_status = { 'crash': True, 'tabs': False, 'main': False, } self.is_shutting_down = False self._args = args def on_last_window_closed(self) -> None: """Slot which gets invoked when the last window was closed.""" self.shutdown(last_window=True) def _compile_modules(self) -> None: """Compile all modules to catch SyntaxErrors.""" if os.path.basename(sys.argv[0]) == 'qutebrowser': # Launched via launcher script return elif hasattr(sys, 'frozen'): return else: path = os.path.abspath(os.path.dirname(qutebrowser.__file__)) if not os.path.isdir(path): # Probably running from a python egg. return for dirpath, _dirnames, filenames in os.walk(path): for fn in filenames: if os.path.splitext(fn)[1] == '.py' and os.path.isfile(fn): with tokenize.open(os.path.join(dirpath, fn)) as f: compile(f.read(), fn, 'exec') def _get_restart_args( self, pages: Iterable[str] = (), session: str = None, override_args: Mapping[str, str] = None ) -> Sequence[str]: """Get args to relaunch qutebrowser. Args: pages: The pages to re-open. session: The session to load, or None. override_args: Argument overrides as a dict. Return: The commandline as a list of strings. """ if os.path.basename(sys.argv[0]) == 'qutebrowser': # Launched via launcher script args = [sys.argv[0]] elif hasattr(sys, 'frozen'): args = [sys.executable] else: args = [sys.executable, '-m', 'qutebrowser'] # Add all open pages so they get reopened. page_args: MutableSequence[str] = [] for win in pages: page_args.extend(win) page_args.append('') # Serialize the argparse namespace into json and pass that to the new # process via --json-args. # We do this as there's no way to "unparse" the namespace while # ignoring some arguments. argdict = vars(self._args) argdict['session'] = None argdict['url'] = [] argdict['command'] = page_args[:-1] argdict['json_args'] = None # Ensure the given session (or none at all) gets opened. if session is None: argdict['session'] = None argdict['override_restore'] = True else: argdict['session'] = session argdict['override_restore'] = False # Ensure :restart works with --temp-basedir if self._args.temp_basedir: argdict['temp_basedir'] = False argdict['temp_basedir_restarted'] = True if override_args is not None: argdict.update(override_args) # Dump the data data = json.dumps(argdict) args += ['--json-args', data] log.destroy.debug("args: {}".format(args)) return args def restart(self, pages: Sequence[str] = (), session: str = None, override_args: Mapping[str, str] = None) -> bool: """Inner logic to restart qutebrowser. The "better" way to restart is to pass a session (_restart usually) as that'll save the complete state. However we don't do that (and pass a list of pages instead) when we restart because of an exception, as that's a lot simpler and we don't want to risk anything going wrong. Args: pages: A list of URLs to open. session: The session to load, or None. override_args: Argument overrides as a dict. Return: True if the restart succeeded, False otherwise. """ self._compile_modules() log.destroy.debug("sys.executable: {}".format(sys.executable)) log.destroy.debug("sys.path: {}".format(sys.path)) log.destroy.debug("sys.argv: {}".format(sys.argv)) log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen'))) # Save the session if one is given. if session is not None: sessions.session_manager.save(session, with_private=True) # Make sure we're not accepting a connection from the new process # before we fully exited. assert ipc.server is not None ipc.server.shutdown() if hasattr(sys, 'frozen'): # https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html#independent-subprocess env = os.environ.copy() env["PYINSTALLER_RESET_ENVIRONMENT"] = "1" else: env = None # Open a new process and immediately shutdown the existing one try: args = self._get_restart_args(pages, session, override_args) proc = subprocess.Popen(args, env=env) # pylint: disable=consider-using-with except OSError: log.destroy.exception("Failed to restart") return False else: log.destroy.debug(f"New process PID: {proc.pid}") # Avoid ResourceWarning about still running subprocess when quitting. warnings.filterwarnings( "ignore", category=ResourceWarning, message=f"subprocess {proc.pid} is still running", ) return True def shutdown(self, status: int = 0, session: sessions.ArgType = None, last_window: bool = False, is_restart: bool = False) -> None: """Quit qutebrowser. Args: status: The status code to exit with. session: A session name if saving should be forced. last_window: If the shutdown was triggered due to the last window closing. is_restart: If we're planning to restart. """ if self.is_shutting_down: return self.is_shutting_down = True log.destroy.debug("Shutting down with status {}, session {}...".format( status, session)) sessions.shutdown(session, last_window=last_window) if prompt.prompt_queue is not None: prompt.prompt_queue.shutdown() # If shutdown was called while we were asking a question, we're in # a still sub-eventloop (which gets quit now) and not in the main # one. # But there's also other situations where it's problematic to shut down # immediately (e.g. when we're just starting up). # This means we need to defer the real shutdown to when we're back # in the real main event loop, or we'll get a segfault. log.destroy.debug("Deferring shutdown stage 2") QTimer.singleShot( 0, functools.partial(self._shutdown_2, status, is_restart=is_restart)) def _shutdown_2(self, status: int, is_restart: bool) -> None: """Second stage of shutdown.""" log.destroy.debug("Stage 2 of shutting down...") # Tell everything to shut itself down self.shutting_down.emit() # Delete temp basedir if ((self._args.temp_basedir or self._args.temp_basedir_restarted) and not is_restart): atexit.register(shutil.rmtree, self._args.basedir, ignore_errors=True) # Now we can hopefully quit without segfaults log.destroy.debug("Deferring QApplication::exit...") # We use a singleshot timer to exit here to minimize the likelihood of # segfaults. QTimer.singleShot(0, functools.partial(self._shutdown_3, status)) def _shutdown_3(self, status: int) -> None: """Finally shut down the QApplication.""" log.destroy.debug("Now calling QApplication::exit.") if 'debug-exit' in objects.debug_flags: if hunter is None: print("Not logging late shutdown because hunter could not be " "imported!", file=sys.stderr) else: print("Now logging late shutdown.", file=sys.stderr) hunter.trace() objects.qapp.exit(status) @cmdutils.register(name='quit') @cmdutils.argument('session', completion=miscmodels.session) def quit_(save: bool = False, session: sessions.ArgType = None) -> None: """Quit qutebrowser. Args: save: When given, save the open windows even if auto_save.session is turned off. session: The name of the session to save. """ if session is not None and not save: raise cmdutils.CommandError("Session name given without --save!") if save and session is None: session = sessions.default instance.shutdown(session=session) @cmdutils.register() def restart() -> None: """Restart qutebrowser while keeping existing tabs open.""" try: ok = instance.restart(session='_restart') except sessions.SessionError as e: log.destroy.exception("Failed to save session!") raise cmdutils.CommandError("Failed to save session: {}!" .format(e)) except SyntaxError as e: log.destroy.exception("Got SyntaxError") raise cmdutils.CommandError("SyntaxError in {}:{}: {}".format( e.filename, e.lineno, e)) if ok: instance.shutdown(is_restart=True) def init(args: argparse.Namespace) -> None: """Initialize the global Quitter instance.""" global instance instance = Quitter(args=args, parent=objects.qapp) instance.shutting_down.connect(qtlog.shutdown_log) objects.qapp.lastWindowClosed.connect(instance.on_last_window_closed) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/savemanager.py0000644000175100017510000001721215102145205021727 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Saving things to disk periodically.""" import os.path import collections from collections.abc import MutableMapping from qutebrowser.qt.core import pyqtSlot, QObject, QTimer from qutebrowser.config import config from qutebrowser.api import cmdutils from qutebrowser.utils import utils, log, message, usertypes, error from qutebrowser.misc import objects class Saveable: """A single thing which can be saved. Attributes: _name: The name of the thing to be saved. _dirty: Whether the saveable was changed since the last save. _save_handler: The function to call to save this Saveable. _save_on_exit: Whether to always save this saveable on exit. _config_opt: A config option which decides whether to auto-save or not. None if no such option exists. _filename: The filename of the underlying file. """ def __init__(self, name, save_handler, changed=None, config_opt=None, filename=None): self._name = name self._dirty = False self._save_handler = save_handler self._config_opt = config_opt if changed is not None: changed.connect(self.mark_dirty) self._save_on_exit = False else: self._save_on_exit = True self._filename = filename if filename is not None and not os.path.exists(filename): self._dirty = True self.save() def __repr__(self): return utils.get_repr(self, name=self._name, dirty=self._dirty, save_handler=self._save_handler, config_opt=self._config_opt, save_on_exit=self._save_on_exit, filename=self._filename) def mark_dirty(self): """Mark this saveable as dirty (having changes).""" log.save.debug("Marking {} as dirty.".format(self._name)) self._dirty = True def save(self, is_exit=False, explicit=False, silent=False, force=False): """Save this saveable. Args: is_exit: Whether we're currently exiting qutebrowser. explicit: Whether the user explicitly requested this save. silent: Don't write information to log. force: Force saving, no matter what. """ if (self._config_opt is not None and (not config.instance.get(self._config_opt)) and (not explicit) and (not force)): if not silent: log.save.debug("Not saving {name} because autosaving has been " "disabled by {cfg[0]} -> {cfg[1]}.".format( name=self._name, cfg=self._config_opt)) return do_save = self._dirty or (self._save_on_exit and is_exit) or force if not silent: log.save.debug("Save of {} requested - dirty {}, save_on_exit {}, " "is_exit {}, force {} -> {}".format( self._name, self._dirty, self._save_on_exit, is_exit, force, do_save)) if do_save: self._save_handler() self._dirty = False class SaveManager(QObject): """Responsible to save 'saveables' periodically and on exit. Attributes: saveables: A dict mapping names to Saveable instances. _save_timer: The Timer used to periodically auto-save things. """ def __init__(self, parent=None): super().__init__(parent) self.saveables: MutableMapping[str, Saveable] = collections.OrderedDict() self._save_timer = usertypes.Timer(self, name='save-timer') self._save_timer.timeout.connect(self.autosave) self._set_autosave_interval() config.instance.changed.connect(self._set_autosave_interval) def __repr__(self): return utils.get_repr(self, saveables=self.saveables) @config.change_filter('auto_save.interval') def _set_autosave_interval(self): """Set the auto-save interval.""" interval = config.val.auto_save.interval if interval == 0: self._save_timer.stop() else: self._save_timer.setInterval(interval) self._save_timer.start() def add_saveable(self, name, save, changed=None, config_opt=None, filename=None, dirty=False): """Add a new saveable. Args: name: The name to use. save: The function to call to save this saveable. changed: The signal emitted when this saveable changed. config_opt: An option deciding whether to auto-save or not. filename: The filename of the underlying file, so we can force saving if it doesn't exist. dirty: Whether the saveable is already dirty. """ if name in self.saveables: raise ValueError("Saveable {} already registered!".format(name)) saveable = Saveable(name, save, changed, config_opt, filename) self.saveables[name] = saveable if dirty: saveable.mark_dirty() QTimer.singleShot(0, saveable.save) def save(self, name, is_exit=False, explicit=False, silent=False, force=False): """Save a saveable by name. Args: name: The name of the saveable to save. is_exit: Whether we're currently exiting qutebrowser. explicit: Whether this save operation was triggered explicitly. silent: Don't write information to log. Used to reduce log spam when autosaving. force: Force saving, no matter what. """ self.saveables[name].save(is_exit=is_exit, explicit=explicit, silent=silent, force=force) def save_all(self, *args, **kwargs): """Save all saveables.""" for saveable in self.saveables: self.save(saveable, *args, **kwargs) @pyqtSlot() def autosave(self): """Slot used when the configs are auto-saved.""" for (key, saveable) in self.saveables.items(): try: saveable.save(silent=True) except OSError as e: message.error("Failed to auto-save {}: {}".format(key, e)) @cmdutils.register(instance='save-manager', name='save', star_args_optional=True) def save_command(self, *what): """Save configs and state. Args: *what: What to save (`config`/`key-config`/`cookies`/...). If not given, everything is saved. """ if what: explicit = True else: what = tuple(self.saveables) explicit = False for key in what: if key not in self.saveables: message.error("{} is nothing which can be saved".format(key)) else: try: self.save(key, explicit=explicit, force=True) except OSError as e: message.error("Could not save {}: {}".format(key, e)) log.save.debug(":save saved {}".format(', '.join(what))) @pyqtSlot() def shutdown(self): """Save all saveables when shutting down.""" for key in self.saveables: try: self.save(key, is_exit=True) except OSError as e: error.handle_fatal_exc( e, "Error while saving!", pre_text="Error while saving {}".format(key), no_err_windows=objects.args.no_err_windows) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/sessions.py0000644000175100017510000006011215102145205021301 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Management of sessions - saved tabs/windows.""" import os import os.path import itertools import urllib import shutil import pathlib from typing import Any, Optional, Union, cast from collections.abc import Iterable, MutableMapping, MutableSequence from qutebrowser.qt.core import Qt, QUrl, QObject, QPoint, QTimer, QDateTime import yaml from qutebrowser.utils import (standarddir, objreg, qtutils, log, message, utils, usertypes) from qutebrowser.api import cmdutils from qutebrowser.config import config, configfiles from qutebrowser.completion.models import miscmodels from qutebrowser.mainwindow import mainwindow from qutebrowser.qt import sip from qutebrowser.misc import objects, throttle _JsonType = MutableMapping[str, Any] class Sentinel: """Sentinel value for default argument.""" default = Sentinel() session_manager = cast('SessionManager', None) ArgType = Union[str, Sentinel] def init(parent=None): """Initialize sessions. Args: parent: The parent to use for the SessionManager. """ base_path = pathlib.Path(standarddir.data()) / 'sessions' # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/5359 backup_path = base_path / 'before-qt-515' do_backup = objects.backend == usertypes.Backend.QtWebEngine if base_path.exists() and not backup_path.exists() and do_backup: backup_path.mkdir() for path in base_path.glob('*.yml'): shutil.copy(path, backup_path) base_path.mkdir(exist_ok=True) global session_manager session_manager = SessionManager(str(base_path), parent) def shutdown(session: Optional[ArgType], last_window: bool) -> None: """Handle a shutdown by saving sessions and removing the autosave file.""" if session_manager is None: return # type: ignore[unreachable] try: if session is not None: session_manager.save(session, last_window=last_window, load_next_time=True) elif config.val.auto_save.session: session_manager.save(default, last_window=last_window, load_next_time=True) except SessionError as e: log.sessions.error("Failed to save session: {}".format(e)) session_manager.delete_autosave() class SessionError(Exception): """Exception raised when a session failed to load/save.""" class SessionNotFoundError(SessionError): """Exception raised when a session to be loaded was not found.""" class TabHistoryItem: """A single item in the tab history. Attributes: url: The QUrl of this item. original_url: The QUrl of this item which was originally requested. title: The title as string of this item. active: Whether this item is the item currently navigated to. user_data: The user data for this item. """ def __init__(self, url, title, *, original_url=None, active=False, user_data=None, last_visited=None): self.url = url if original_url is None: self.original_url = url else: self.original_url = original_url self.title = title self.active = active self.user_data = user_data self.last_visited = last_visited def __repr__(self): return utils.get_repr(self, constructor=True, url=self.url, original_url=self.original_url, title=self.title, active=self.active, user_data=self.user_data, last_visited=self.last_visited) class SessionManager(QObject): """Manager for sessions. Attributes: _base_path: The path to store sessions under. _last_window_session: The session data of the last window which was closed. current: The name of the currently loaded session, or None. did_load: Set when a session was loaded. """ def __init__(self, base_path, parent=None): super().__init__(parent) self.current: Optional[str] = None self._base_path = base_path self._last_window_session = None self.did_load = False # throttle autosaves to one minute apart self.save_autosave = throttle.Throttle(self._save_autosave, 60 * 1000) def _get_session_path(self, name, check_exists=False): """Get the session path based on a session name or absolute path. Args: name: The name of the session. check_exists: Whether it should also be checked if the session exists. """ path = os.path.expanduser(name) if os.path.isabs(path) and ((not check_exists) or os.path.exists(path)): return path else: path = os.path.join(self._base_path, name + '.yml') if check_exists and not os.path.exists(path): raise SessionNotFoundError(path) return path def exists(self, name): """Check if a named session exists.""" try: self._get_session_path(name, check_exists=True) except SessionNotFoundError: return False else: return True def _save_tab_item(self, tab, idx, item): """Save a single history item in a tab. Args: tab: The tab to save. idx: The index of the current history item. item: The history item. Return: A dict with the saved data for this item. """ data: _JsonType = { 'url': bytes(item.url().toEncoded()).decode('ascii'), } if item.title(): data['title'] = item.title() elif tab.history.current_idx() == idx: # https://github.com/qutebrowser/qutebrowser/issues/879 data['title'] = tab.title() else: data['title'] = data['url'] if item.originalUrl() != item.url(): encoded = item.originalUrl().toEncoded() data['original-url'] = bytes(encoded).decode('ascii') if tab.history.current_idx() == idx: data['active'] = True try: user_data = item.userData() except AttributeError: # QtWebEngine user_data = None data['last_visited'] = item.lastVisited().toString(Qt.DateFormat.ISODate) if tab.history.current_idx() == idx: pos = tab.scroller.pos_px() data['zoom'] = tab.zoom.factor() data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} elif user_data is not None: if 'zoom' in user_data: data['zoom'] = user_data['zoom'] if 'scroll-pos' in user_data: pos = user_data['scroll-pos'] data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} data['pinned'] = tab.data.pinned return data def _save_tab(self, tab, active, with_history=True): """Get a dict with data for a single tab. Args: tab: The WebView to save. active: Whether the tab is currently active. with_history: Include the tab's history. """ data: _JsonType = {'history': []} if active: data['active'] = True history = tab.history if with_history else [tab.history.current_item()] for idx, item in enumerate(history): qtutils.ensure_valid(item) item_data = self._save_tab_item(tab, idx, item) if not item.url().isValid(): # WORKAROUND Qt 6.5 regression # https://github.com/qutebrowser/qutebrowser/issues/7696 log.sessions.debug(f"Skipping invalid history item: {item}") continue if item.url().scheme() == 'qute' and item.url().host() == 'back': # don't add qute://back to the session file if item_data.get('active', False) and data['history']: # mark entry before qute://back as active data['history'][-1]['active'] = True else: data['history'].append(item_data) return data def _save_all(self, *, only_window=None, with_private=False, with_history=True): """Get a dict with data for all windows/tabs.""" data: _JsonType = {'windows': []} if only_window is not None: winlist: Iterable[int] = [only_window] else: winlist = objreg.window_registry for win_id in sorted(winlist): tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) main_window = objreg.get('main-window', scope='window', window=win_id) # We could be in the middle of destroying a window here if sip.isdeleted(main_window): continue if tabbed_browser.is_private and not with_private: continue win_data: _JsonType = {} active_window = objects.qapp.activeWindow() if getattr(active_window, 'win_id', None) == win_id: win_data['active'] = True win_data['geometry'] = bytes(main_window.saveGeometry()) win_data['tabs'] = [] if tabbed_browser.is_private: win_data['private'] = True for i, tab in enumerate(tabbed_browser.widgets()): active = i == tabbed_browser.widget.currentIndex() win_data['tabs'].append(self._save_tab(tab, active, with_history=with_history)) data['windows'].append(win_data) return data def _get_session_name(self, name): """Helper for save to get the name to save the session to. Args: name: The name of the session to save, or the 'default' sentinel object. """ if name is default: name = config.val.session.default_name if name is None: if self.current is not None: name = self.current else: name = 'default' return name def save(self, name, last_window=False, load_next_time=False, only_window=None, with_private=False, with_history=True): """Save a named session. Args: name: The name of the session to save, or the 'default' sentinel object. last_window: If set, saves the saved self._last_window_session instead of the currently open state. load_next_time: If set, prepares this session to be load next time. only_window: If set, only tabs in the specified window is saved. with_private: Include private windows. with_history: Include tab history. Return: The name of the saved session. """ name = self._get_session_name(name) path = self._get_session_path(name) log.sessions.debug("Saving session {} to {}...".format(name, path)) if last_window: data = self._last_window_session if data is None: log.sessions.error("last_window_session is None while saving!") return None else: data = self._save_all(only_window=only_window, with_private=with_private, with_history=with_history) log.sessions.vdebug( # type: ignore[attr-defined] "Saving data: {}".format(data)) try: with qtutils.savefile_open(path) as f: utils.yaml_dump(data, f) except (OSError, UnicodeEncodeError, yaml.YAMLError) as e: raise SessionError(e) if load_next_time: configfiles.state['general']['session'] = name return name def _save_autosave(self): """Save the autosave session.""" try: self.save('_autosave') except SessionError as e: log.sessions.error("Failed to save autosave session: {}".format(e)) def delete_autosave(self): """Delete the autosave session.""" # cancel any in-flight saves self.save_autosave.cancel() try: self.delete('_autosave') except SessionNotFoundError: # Exiting before the first load finished pass except SessionError as e: log.sessions.error("Failed to delete autosave session: {}" .format(e)) def save_last_window_session(self): """Temporarily save the session for the last closed window.""" self._last_window_session = self._save_all() def _load_tab(self, new_tab, data): # noqa: C901 """Load yaml data into a newly opened tab.""" entries = [] lazy_load: MutableSequence[_JsonType] = [] # use len(data['history']) # -> dropwhile empty if not session.lazy_session lazy_index = len(data['history']) gen = itertools.chain( itertools.takewhile(lambda _: not lazy_load, enumerate(data['history'])), enumerate(lazy_load), itertools.dropwhile(lambda i: i[0] < lazy_index, enumerate(data['history']))) for i, histentry in gen: user_data = {} if 'zoom' in data: # The zoom was accidentally stored in 'data' instead of per-tab # earlier. # See https://github.com/qutebrowser/qutebrowser/issues/728 user_data['zoom'] = data['zoom'] elif 'zoom' in histentry: user_data['zoom'] = histentry['zoom'] if 'scroll-pos' in data: # The scroll position was accidentally stored in 'data' instead # of per-tab earlier. # See https://github.com/qutebrowser/qutebrowser/issues/728 pos = data['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) elif 'scroll-pos' in histentry: pos = histentry['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) if 'pinned' in histentry: new_tab.data.pinned = histentry['pinned'] if (config.val.session.lazy_restore and histentry.get('active', False) and not histentry['url'].startswith('qute://back')): # remove "active" mark and insert back page marked as active lazy_index = i + 1 lazy_load.append({ 'title': histentry['title'], 'url': 'qute://back#' + urllib.parse.quote(histentry['title']), 'active': True }) histentry['active'] = False active = histentry.get('active', False) url = QUrl.fromEncoded(histentry['url'].encode('ascii')) if 'original-url' in histentry: orig_url = QUrl.fromEncoded( histentry['original-url'].encode('ascii')) else: orig_url = url if histentry.get("last_visited"): last_visited: Optional[QDateTime] = QDateTime.fromString( histentry.get("last_visited"), Qt.DateFormat.ISODate, ) else: last_visited = None entry = TabHistoryItem(url=url, original_url=orig_url, title=histentry['title'], active=active, user_data=user_data, last_visited=last_visited) entries.append(entry) if active: new_tab.title_changed.emit(histentry['title']) try: new_tab.history.private_api.load_items(entries) except ValueError as e: raise SessionError(e) def _load_window(self, win): """Turn yaml data into windows.""" window = mainwindow.MainWindow(geometry=win['geometry'], private=win.get('private', None)) tabbed_browser = objreg.get('tabbed-browser', scope='window', window=window.win_id) tab_to_focus = None for i, tab in enumerate(win['tabs']): new_tab = tabbed_browser.tabopen(background=False) self._load_tab(new_tab, tab) if tab.get('active', False): tab_to_focus = i if new_tab.data.pinned: new_tab.set_pinned(True) if tab_to_focus is not None: tabbed_browser.widget.setCurrentIndex(tab_to_focus) window.show() if win.get('active', False): QTimer.singleShot(0, tabbed_browser.widget.activateWindow) def load(self, name, temp=False): """Load a named session. Args: name: The name of the session to load. temp: If given, don't set the current session. """ path = self._get_session_path(name, check_exists=True) try: with open(path, encoding='utf-8') as f: data = utils.yaml_load(f) except (OSError, UnicodeDecodeError, yaml.YAMLError) as e: raise SessionError(e) log.sessions.debug("Loading session {} from {}...".format(name, path)) if data is None: raise SessionError("Got empty session file") if qtutils.is_single_process(): if any(win.get('private') for win in data['windows']): raise SessionError("Can't load a session with private windows " "in single process mode.") for win in data['windows']: self._load_window(win) if data['windows']: self.did_load = True if not name.startswith('_') and not temp: self.current = name def delete(self, name): """Delete a session.""" path = self._get_session_path(name, check_exists=True) try: os.remove(path) except OSError as e: raise SessionError(e) def list_sessions(self): """Get a list of all session names.""" sessions = [] for filename in os.listdir(self._base_path): base, ext = os.path.splitext(filename) if ext == '.yml': sessions.append(base) return sorted(sessions) @cmdutils.register() @cmdutils.argument('name', completion=miscmodels.session) def session_load(name: str, *, clear: bool = False, temp: bool = False, force: bool = False, delete: bool = False) -> None: """Load a session. Args: name: The name of the session. clear: Close all existing windows. temp: Don't set the current session for :session-save. force: Force loading internal sessions (starting with an underline). delete: Delete the saved session once it has loaded. """ if name.startswith('_') and not force: raise cmdutils.CommandError("{} is an internal session, use --force " "to load anyways.".format(name)) old_windows = list(objreg.window_registry.values()) try: session_manager.load(name, temp=temp) except SessionNotFoundError: raise cmdutils.CommandError("Session {} not found!".format(name)) except SessionError as e: raise cmdutils.CommandError("Error while loading session: {}" .format(e)) if clear: for win in old_windows: win.close() if delete: try: session_manager.delete(name) except SessionError as e: log.sessions.exception("Error while deleting session!") raise cmdutils.CommandError("Error while deleting session: {}" .format(e)) log.sessions.debug("Loaded & deleted session {}.".format(name)) @cmdutils.register() @cmdutils.argument('name', completion=miscmodels.session) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('with_private', flag='p') @cmdutils.argument('no_history', flag='n') def session_save(name: ArgType = default, *, current: bool = False, quiet: bool = False, force: bool = False, only_active_window: bool = False, with_private: bool = False, no_history: bool = False, win_id: int = None) -> None: """Save a session. Args: name: The name of the session. If not given, the session configured in session.default_name is saved. current: Save the current session instead of the default. quiet: Don't show confirmation message. force: Force saving internal sessions (starting with an underline). only_active_window: Saves only tabs of the currently active window. with_private: Include private windows. no_history: Don't store tab history. """ if not isinstance(name, Sentinel) and name.startswith('_') and not force: raise cmdutils.CommandError("{} is an internal session, use --force " "to save anyways.".format(name)) if current: if session_manager.current is None: raise cmdutils.CommandError("No session loaded currently!") name = session_manager.current assert not name.startswith('_') try: if only_active_window: name = session_manager.save(name, only_window=win_id, with_private=True, with_history=not no_history) else: name = session_manager.save(name, with_private=with_private, with_history=not no_history) except SessionError as e: raise cmdutils.CommandError("Error while saving session: {}".format(e)) if quiet: log.sessions.debug("Saved session {}.".format(name)) else: message.info("Saved session {}.".format(name)) @cmdutils.register() @cmdutils.argument('name', completion=miscmodels.session) def session_delete(name: str, *, force: bool = False) -> None: """Delete a session. Args: name: The name of the session. force: Force deleting internal sessions (starting with an underline). """ if name.startswith('_') and not force: raise cmdutils.CommandError("{} is an internal session, use --force " "to delete anyways.".format(name)) try: session_manager.delete(name) except SessionNotFoundError: raise cmdutils.CommandError("Session {} not found!".format(name)) except SessionError as e: log.sessions.exception("Error while deleting session!") raise cmdutils.CommandError("Error while deleting session: {}" .format(e)) log.sessions.debug("Deleted session {}.".format(name)) def load_default(name): """Load the default session. Args: name: The name of the session to load, or None to read state file. """ if name is None and session_manager.exists('_autosave'): name = '_autosave' elif name is None: try: name = configfiles.state['general']['session'] except KeyError: # No session given as argument and none in the session file -> # start without loading a session return try: session_manager.load(name) except SessionNotFoundError: message.error("Session {} not found!".format(name)) except SessionError as e: message.error("Failed to load session {}: {}".format(name, e)) try: del configfiles.state['general']['session'] except KeyError: pass # If this was a _restart session, delete it. if name == '_restart': session_manager.delete('_restart') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/split.py0000644000175100017510000001356715102145205020602 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Our own fork of shlex.split with some added and removed features.""" import re from qutebrowser.utils import log, utils class ShellLexer: """A lexical analyzer class for simple shell-like syntaxes. Based on Python's shlex, but cleaned up, removed some features, and added some features useful for qutebrowser. Attributes: FIXME """ def __init__(self, s): self.string = s self.whitespace = ' \t\r' self.quotes = '\'"' self.escape = '\\' self.escapedquotes = '"' self.keep = False self.quoted = False self.escapedstate = ' ' self.token = '' self.state = ' ' def reset(self): """Reset the state machine state to the defaults.""" self.quoted = False self.escapedstate = ' ' self.token = '' self.state = ' ' def __iter__(self): # noqa: C901 pragma: no mccabe """Read a raw token from the input stream.""" self.reset() for nextchar in self.string: if self.state == ' ': if self.keep: self.token += nextchar if nextchar in self.whitespace: if self.token or self.quoted: yield self.token self.reset() elif nextchar in self.escape: self.escapedstate = 'a' self.state = nextchar elif nextchar in self.quotes: self.state = nextchar else: self.token = nextchar self.state = 'a' elif self.state in self.quotes: self.quoted = True if nextchar == self.state: if self.keep: self.token += nextchar self.state = 'a' elif (nextchar in self.escape and self.state in self.escapedquotes): if self.keep: self.token += nextchar self.escapedstate = self.state self.state = nextchar else: self.token += nextchar elif self.state in self.escape: # In posix shells, only the quote itself or the escape # character may be escaped within quotes. if (self.escapedstate in self.quotes and nextchar != self.state and nextchar != self.escapedstate and not self.keep): self.token += self.state self.token += nextchar self.state = self.escapedstate elif self.state == 'a': if nextchar in self.whitespace: self.state = ' ' assert self.token or self.quoted yield self.token self.reset() if self.keep: yield nextchar elif nextchar in self.quotes: if self.keep: self.token += nextchar self.state = nextchar elif nextchar in self.escape: if self.keep: self.token += nextchar self.escapedstate = 'a' self.state = nextchar else: self.token += nextchar else: raise utils.Unreachable( "Invalid state {!r}!".format(self.state)) if self.state in self.escape and not self.keep: self.token += self.state if self.token or self.quoted: yield self.token def split(s, keep=False): """Split a string via ShellLexer. Args: s: The string to split. keep: Whether to keep special chars in the split output. """ lexer = ShellLexer(s) lexer.keep = keep tokens = list(lexer) if not tokens: return [] out = [] spaces = "" log.shlexer.vdebug( # type: ignore[attr-defined] "{!r} -> {!r}".format(s, tokens)) for t in tokens: if t.isspace(): spaces += t else: out.append(spaces + t) spaces = "" if spaces: out.append(spaces) return out def _combine_ws(parts, whitespace): """Combine whitespace in a list with the element following it. Args: parts: A list of strings. whitespace: A string containing what's considered whitespace. Return: The modified list. """ out = [] ws = '' for part in parts: if not part: continue elif part in whitespace: ws += part else: out.append(ws + part) ws = '' if ws: out.append(ws) return out def simple_split(s, keep=False, maxsplit=None): """Split a string on whitespace, optionally keeping the whitespace. Args: s: The string to split. keep: Whether to keep whitespace. maxsplit: The maximum count of splits. Return: A list of split strings. """ whitespace = '\n\t ' if maxsplit == 0: # re.split with maxsplit=0 splits everything, while str.split splits # nothing (which is the behavior we want). if keep: return [s] else: return [s.strip(whitespace)] elif maxsplit is None: maxsplit = 0 if keep: pattern = '([' + whitespace + '])' parts = re.split(pattern, s, maxsplit=maxsplit) return _combine_ws(parts, whitespace) else: pattern = '[' + whitespace + ']' parts = re.split(pattern, s, maxsplit=maxsplit) parts[-1] = parts[-1].rstrip() return [p for p in parts if p] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/sql.py0000644000175100017510000005031515102145205020236 0ustar00runnerrunner# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) # # SPDX-License-Identifier: GPL-3.0-or-later """Provides access to sqlite databases.""" import enum import collections import contextlib import dataclasses import types from typing import Any, Optional, Union from collections.abc import Iterator, Mapping, MutableSequence from qutebrowser.qt.core import QObject, pyqtSignal from qutebrowser.qt.sql import QSqlDatabase, QSqlError, QSqlQuery from qutebrowser.qt import sip, machinery from qutebrowser.utils import debug, log @dataclasses.dataclass class UserVersion: """The version of data stored in the history database. When we originally started using user_version, we only used it to signify that the completion database should be regenerated. However, sometimes there are backwards-incompatible changes. Instead, we now (ab)use the fact that the user_version in sqlite is a 32-bit integer to store both a major and a minor part. If only the minor part changed, we can deal with it (there are only new URLs to clean up or somesuch). If the major part changed, there are backwards-incompatible changes in how the database works, so newer databases are not compatible with older qutebrowser versions. """ major: int minor: int @classmethod def from_int(cls, num: int) -> 'UserVersion': """Parse a number from sqlite into a major/minor user version.""" assert 0 <= num <= 0x7FFF_FFFF, num # signed integer, but shouldn't be negative major = (num & 0x7FFF_0000) >> 16 minor = num & 0x0000_FFFF return cls(major, minor) def to_int(self) -> int: """Get a sqlite integer from a major/minor user version.""" assert 0 <= self.major <= 0x7FFF # signed integer assert 0 <= self.minor <= 0xFFFF return self.major << 16 | self.minor def __str__(self) -> str: return f'{self.major}.{self.minor}' class SqliteErrorCode(enum.Enum): """Primary error codes as used by sqlite. See https://sqlite.org/rescode.html """ # pylint: disable=invalid-name OK = 0 # Successful result ERROR = 1 # Generic error INTERNAL = 2 # Internal logic error in SQLite PERM = 3 # Access permission denied ABORT = 4 # Callback routine requested an abort BUSY = 5 # The database file is locked LOCKED = 6 # A table in the database is locked NOMEM = 7 # A malloc() failed READONLY = 8 # Attempt to write a readonly database INTERRUPT = 9 # Operation terminated by sqlite3_interrupt()*/ IOERR = 10 # Some kind of disk I/O error occurred CORRUPT = 11 # The database disk image is malformed NOTFOUND = 12 # Unknown opcode in sqlite3_file_control() FULL = 13 # Insertion failed because database is full CANTOPEN = 14 # Unable to open the database file PROTOCOL = 15 # Database lock protocol error EMPTY = 16 # Internal use only SCHEMA = 17 # The database schema changed TOOBIG = 18 # String or BLOB exceeds size limit CONSTRAINT = 19 # Abort due to constraint violation MISMATCH = 20 # Data type mismatch MISUSE = 21 # Library used incorrectly NOLFS = 22 # Uses OS features not supported on host AUTH = 23 # Authorization denied FORMAT = 24 # Not used RANGE = 25 # 2nd parameter to sqlite3_bind out of range NOTADB = 26 # File opened that is not a database file NOTICE = 27 # Notifications from sqlite3_log() WARNING = 28 # Warnings from sqlite3_log() ROW = 100 # sqlite3_step() has another row ready DONE = 101 # sqlite3_step() has finished executing class Error(Exception): """Base class for all SQL related errors.""" def __init__(self, msg: str, error: Optional[QSqlError] = None) -> None: super().__init__(msg) self.error = error def text(self) -> str: """Get a short text description of the error. This is a string suitable to show to the user as error message. """ if self.error is None: return str(self) return self.error.databaseText() class KnownError(Error): """Raised on an error interacting with the SQL database. This is raised in conditions resulting from the environment (like a full disk or I/O errors), where qutebrowser isn't to blame. """ class BugError(Error): """Raised on an error interacting with the SQL database. This is raised for errors resulting from a qutebrowser bug. """ def raise_sqlite_error(msg: str, error: QSqlError) -> None: """Raise either a BugError or KnownError.""" error_code = error.nativeErrorCode() primary_error_code: Union[SqliteErrorCode, str] try: # https://sqlite.org/rescode.html#pve primary_error_code = SqliteErrorCode(int(error_code) & 0xff) except ValueError: # not an int, or unknown error code -> fall back to string primary_error_code = error_code database_text = error.databaseText() driver_text = error.driverText() log.sql.debug("SQL error:") log.sql.debug(f"type: {debug.qenum_key(QSqlError, error.type())}") log.sql.debug(f"database text: {database_text}") log.sql.debug(f"driver text: {driver_text}") log.sql.debug(f"error code: {error_code} -> {primary_error_code}") known_errors = [ SqliteErrorCode.BUSY, SqliteErrorCode.READONLY, SqliteErrorCode.IOERR, SqliteErrorCode.CORRUPT, SqliteErrorCode.FULL, SqliteErrorCode.CANTOPEN, SqliteErrorCode.PROTOCOL, SqliteErrorCode.NOTADB, ] # https://github.com/qutebrowser/qutebrowser/issues/4681 # If the query we built was too long too_long_err = ( primary_error_code == SqliteErrorCode.ERROR and (database_text.startswith("Expression tree is too large") or database_text in ["too many SQL variables", "LIKE or GLOB pattern too complex"])) if primary_error_code in known_errors or too_long_err: raise KnownError(msg, error) raise BugError(msg, error) class Database: """A wrapper over a QSqlDatabase connection.""" _USER_VERSION = UserVersion(0, 4) # The current / newest user version def __init__(self, path: str) -> None: if QSqlDatabase.database(path).isValid(): raise BugError(f'A connection to the database at "{path}" already exists') self._path = path database = QSqlDatabase.addDatabase('QSQLITE', path) if not database.isValid(): raise KnownError('Failed to add database. Are sqlite and Qt sqlite ' 'support installed?') database.setDatabaseName(path) if not database.open(): error = database.lastError() msg = f"Failed to open sqlite database at {path}: {error.text()}" raise_sqlite_error(msg, error) version_int = self.query('pragma user_version').run().value() self._user_version = UserVersion.from_int(version_int) if self._user_version.major > self._USER_VERSION.major: raise KnownError( "Database is too new for this qutebrowser version (database version " f"{self._user_version}, but {self._USER_VERSION.major}.x is supported)") if self.user_version_changed(): # Enable write-ahead-logging and reduce disk write frequency # see https://sqlite.org/pragma.html and issues #2930 and #3507 # # We might already have done this (without a migration) in earlier versions, # but as those are idempotent, let's make sure we run them once again. self.query("PRAGMA journal_mode=WAL").run() self.query("PRAGMA synchronous=NORMAL").run() def qt_database(self) -> QSqlDatabase: """Return the wrapped QSqlDatabase instance.""" database = QSqlDatabase.database(self._path, open=True) if not database.isValid(): raise BugError('Failed to get connection. Did you close() this Database ' 'instance?') return database def query(self, querystr: str, forward_only: bool = True) -> 'Query': """Return a Query instance linked to this Database.""" return Query(self, querystr, forward_only) def table(self, name: str, fields: list[str], constraints: Optional[dict[str, str]] = None, parent: Optional[QObject] = None) -> 'SqlTable': """Return a SqlTable instance linked to this Database.""" return SqlTable(self, name, fields, constraints, parent) def user_version_changed(self) -> bool: """Whether the version stored in the database differs from the current one.""" return self._user_version != self._USER_VERSION def upgrade_user_version(self) -> None: """Upgrade the user version to the latest version. This method should be called once all required operations to migrate from one version to another have been run. """ log.sql.debug(f"Migrating from version {self._user_version} " f"to {self._USER_VERSION}") self.query(f'PRAGMA user_version = {self._USER_VERSION.to_int()}').run() self._user_version = self._USER_VERSION def close(self) -> None: """Close the SQL connection.""" database = self.qt_database() database.close() sip.delete(database) QSqlDatabase.removeDatabase(self._path) def transaction(self) -> 'Transaction': """Return a Transaction object linked to this Database.""" return Transaction(self) class Transaction(contextlib.AbstractContextManager): # type: ignore[type-arg] """A Database transaction that can be used as a context manager.""" def __init__(self, database: Database) -> None: self._database = database def __enter__(self) -> None: log.sql.debug('Starting a transaction') db = self._database.qt_database() ok = db.transaction() if not ok: error = db.lastError() msg = f'Failed to start a transaction: "{error.text()}"' raise_sqlite_error(msg, error) def __exit__(self, _exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], _exc_tb: Optional[types.TracebackType]) -> None: db = self._database.qt_database() if exc_val: log.sql.debug('Rolling back a transaction') db.rollback() else: log.sql.debug('Committing a transaction') ok = db.commit() if not ok: error = db.lastError() msg = f'Failed to commit a transaction: "{error.text()}"' raise_sqlite_error(msg, error) class Query: """A prepared SQL query.""" def __init__(self, database: Database, querystr: str, forward_only: bool = True) -> None: """Prepare a new SQL query. Args: database: The Database object on which to operate. querystr: String to prepare query from. forward_only: Optimization for queries that will only step forward. Must be false for completion queries. """ self._database = database self.query = QSqlQuery(database.qt_database()) log.sql.vdebug(f'Preparing: {querystr}') # type: ignore[attr-defined] ok = self.query.prepare(querystr) self._check_ok('prepare', ok) self.query.setForwardOnly(forward_only) self._placeholders: list[str] = [] def __iter__(self) -> Iterator[Any]: if not self.query.isActive(): raise BugError("Cannot iterate inactive query") rec = self.query.record() fields = [rec.fieldName(i) for i in range(rec.count())] # pylint: disable=prefer-typing-namedtuple rowtype = collections.namedtuple( # type: ignore[misc] 'ResultRow', fields) while self.query.next(): rec = self.query.record() yield rowtype(*[rec.value(i) for i in range(rec.count())]) def _check_ok(self, step: str, ok: bool) -> None: if not ok: query = self.query.lastQuery() error = self.query.lastError() msg = f'Failed to {step} query "{query}": "{error.text()}"' raise_sqlite_error(msg, error) def _validate_bound_values(self) -> None: """Make sure all placeholders are bound.""" qt_bound_values = self.query.boundValues() if machinery.IS_QT5: # Qt 5: Returns a dict values = list(qt_bound_values.values()) else: # Qt 6: Returns a list values = qt_bound_values if None in values: raise BugError("Missing bound values!") def _bind_values(self, values: Mapping[str, Any]) -> dict[str, Any]: self._placeholders = list(values) for key, val in values.items(): self.query.bindValue(f':{key}', val) self._validate_bound_values() return self.bound_values() def run(self, **values: Any) -> 'Query': """Execute the prepared query.""" log.sql.debug(self.query.lastQuery()) bound_values = self._bind_values(values) if bound_values: log.sql.debug(f' {bound_values}') ok = self.query.exec() self._check_ok('exec', ok) return self def run_batch(self, values: Mapping[str, MutableSequence[Any]]) -> None: """Execute the query in batch mode.""" log.sql.debug(f'Running SQL query (batch): "{self.query.lastQuery()}"') self._bind_values(values) db = self._database.qt_database() ok = db.transaction() self._check_ok('transaction', ok) ok = self.query.execBatch() try: self._check_ok('execBatch', ok) except Error: # Not checking the return value here, as we're failing anyways... db.rollback() raise ok = db.commit() self._check_ok('commit', ok) def value(self) -> Any: """Return the result of a single-value query (e.g. an EXISTS).""" if not self.query.next(): raise BugError("No result for single-result query") return self.query.record().value(0) def rows_affected(self) -> int: """Return how many rows were affected by a non-SELECT query.""" assert not self.query.isSelect(), self assert self.query.isActive(), self rows = self.query.numRowsAffected() assert rows != -1 return rows def bound_values(self) -> dict[str, Any]: return { f":{key}": self.query.boundValue(f":{key}") for key in self._placeholders } class SqlTable(QObject): """Interface to a SQL table. Attributes: _name: Name of the SQL table this wraps. database: The Database to which this table belongs. Signals: changed: Emitted when the table is modified. """ changed = pyqtSignal() database: Database def __init__(self, database: Database, name: str, fields: list[str], constraints: Optional[dict[str, str]] = None, parent: Optional[QObject] = None) -> None: """Wrapper over a table in the SQL database. Args: database: The Database to which this table belongs. name: Name of the table. fields: A list of field names. constraints: A dict mapping field names to constraint strings. """ super().__init__(parent) self._name = name self.database = database self._create_table(fields, constraints) def _create_table(self, fields: list[str], constraints: Optional[dict[str, str]], *, force: bool = False) -> None: """Create the table if the database is uninitialized. If the table already exists, this does nothing (except with force=True), so it can e.g. be called on every user_version change. """ if not self.database.user_version_changed() and not force: return constraints = constraints or {} column_defs = [f'{field} {constraints.get(field, "")}' for field in fields] q = self.database.query( f"CREATE TABLE IF NOT EXISTS {self._name} ({', '.join(column_defs)})" ) q.run() def create_index(self, name: str, field: str) -> None: """Create an index over this table if the database is uninitialized. Args: name: Name of the index, should be unique. field: Name of the field to index. """ if not self.database.user_version_changed(): return q = self.database.query( f"CREATE INDEX IF NOT EXISTS {name} ON {self._name} ({field})" ) q.run() def __iter__(self) -> Iterator[Any]: """Iterate rows in the table.""" q = self.database.query(f"SELECT * FROM {self._name}") q.run() return iter(q) def contains_query(self, field: str) -> Query: """Return a prepared query that checks for the existence of an item. Args: field: Field to match. """ return self.database.query( f"SELECT EXISTS(SELECT * FROM {self._name} WHERE {field} = :val)" ) def __len__(self) -> int: """Return the count of rows in the table.""" q = self.database.query(f"SELECT count(*) FROM {self._name}") q.run() return q.value() def __bool__(self) -> bool: """Check whether there's any data in the table.""" q = self.database.query(f"SELECT 1 FROM {self._name} LIMIT 1") q.run() return q.query.next() def delete(self, field: str, value: Any) -> None: """Remove all rows for which `field` equals `value`. Args: field: Field to use as the key. value: Key value to delete. """ q = self.database.query(f"DELETE FROM {self._name} where {field} = :val") q.run(val=value) if not q.rows_affected(): raise KeyError(f'No row with {field} = {value!r}') self.changed.emit() def _insert_query(self, values: Mapping[str, Any], replace: bool) -> Query: params = ', '.join(f':{key}' for key in values) columns = ', '.join(values) verb = "REPLACE" if replace else "INSERT" return self.database.query( f"{verb} INTO {self._name} ({columns}) values({params})" ) def insert(self, values: Mapping[str, Any], replace: bool = False) -> None: """Append a row to the table. Args: values: A dict with a value to insert for each field name. replace: If set, replace existing values. """ q = self._insert_query(values, replace) q.run(**values) self.changed.emit() def insert_batch(self, values: Mapping[str, MutableSequence[Any]], replace: bool = False) -> None: """Performantly append multiple rows to the table. Args: values: A dict with a list of values to insert for each field name. replace: If true, overwrite rows with a primary key match. """ q = self._insert_query(values, replace) q.run_batch(values) self.changed.emit() def delete_all(self) -> None: """Remove all rows from the table.""" self.database.query(f"DELETE FROM {self._name}").run() self.changed.emit() def select(self, sort_by: str, sort_order: str, limit: int = -1) -> Query: """Prepare, run, and return a select statement on this table. Args: sort_by: name of column to sort by. sort_order: 'asc' or 'desc'. limit: max number of rows in result, defaults to -1 (unlimited). Return: A prepared and executed select query. """ q = self.database.query( f"SELECT * FROM {self._name} ORDER BY {sort_by} {sort_order} LIMIT :limit" ) q.run(limit=limit) return q def version() -> str: """Return the sqlite version string.""" try: with contextlib.closing(Database(':memory:')) as in_memory_db: return in_memory_db.query("select sqlite_version()").run().value() except KnownError as e: return f'UNAVAILABLE ({e})' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/throttle.py0000644000175100017510000000566115102145205021310 0ustar00runnerrunner# SPDX-FileCopyrightText: Jay Kamat # # SPDX-License-Identifier: GPL-3.0-or-later """A throttle for throttling function calls.""" import dataclasses import time from typing import Any, Optional from collections.abc import Mapping, Sequence, Callable from qutebrowser.qt.core import QObject from qutebrowser.utils import usertypes @dataclasses.dataclass class _CallArgs: args: Sequence[Any] kwargs: Mapping[str, Any] class Throttle(QObject): """A throttle to throttle calls. If a request comes in, it will be processed immediately. If another request comes in too soon, it is ignored, but will be processed when a timeout ends. If another request comes in, it will update the pending request. """ def __init__(self, func: Callable[..., None], delay_ms: int, parent: QObject = None) -> None: """Constructor. Args: delay_ms: The time to wait before allowing another call of the function. -1 disables the wrapper. func: The function/method to call on __call__. parent: The parent object. """ super().__init__(parent) self._delay_ms = delay_ms self._func = func self._pending_call: Optional[_CallArgs] = None self._last_call_ms: Optional[int] = None self._timer = usertypes.Timer(self, 'throttle-timer') self._timer.setSingleShot(True) def _call_pending(self) -> None: """Start a pending call.""" assert self._pending_call is not None self._func(*self._pending_call.args, **self._pending_call.kwargs) self._pending_call = None self._last_call_ms = int(time.monotonic() * 1000) def __call__(self, *args: Any, **kwargs: Any) -> Any: cur_time_ms = int(time.monotonic() * 1000) if self._pending_call is None: if (self._last_call_ms is None or cur_time_ms - self._last_call_ms > self._delay_ms): # Call right now self._last_call_ms = cur_time_ms self._func(*args, **kwargs) return self._timer.setInterval(self._delay_ms - (cur_time_ms - self._last_call_ms)) # Disconnect any existing calls, continue if no connections. try: self._timer.timeout.disconnect() except TypeError: pass self._timer.timeout.connect(self._call_pending) self._timer.start() # Update arguments for an existing pending call self._pending_call = _CallArgs(args=args, kwargs=kwargs) def set_delay(self, delay_ms: int) -> None: """Set the delay to wait between invocation of this function.""" self._delay_ms = delay_ms def cancel(self) -> None: """Cancel any pending instance of this timer.""" self._timer.stop() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/utilcmds.py0000644000175100017510000002167215102145205021267 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Misc. utility commands exposed to the user.""" # QApplication and objects are imported so they're usable in :debug-pyeval import functools import os import traceback from typing import Optional from qutebrowser.qt.core import QUrl from qutebrowser.qt.widgets import QApplication from qutebrowser.browser import qutescheme from qutebrowser.utils import log, objreg, usertypes, message, debug, utils from qutebrowser.keyinput import modeman from qutebrowser.commands import runners from qutebrowser.api import cmdutils from qutebrowser.misc import ( # pylint: disable=unused-import consolewidget, debugcachestats, objects, miscwidgets) from qutebrowser.utils.version import pastebin_version from qutebrowser.qt import sip @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True, deprecated_name='later') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) def cmd_later(duration: str, command: str, win_id: int) -> None: """Execute a command after some time. Args: duration: Duration to wait in format XhYmZs or a number for milliseconds. command: The command to run, with optional args. """ try: ms = utils.parse_duration(duration) except ValueError as e: raise cmdutils.CommandError(e) commandrunner = runners.CommandRunner(win_id) timer = usertypes.Timer(name='later', parent=QApplication.instance()) try: timer.setSingleShot(True) try: timer.setInterval(ms) except OverflowError: raise cmdutils.CommandError("Numeric argument is too large for " "internal int representation.") timer.timeout.connect( functools.partial(commandrunner.run_safely, command)) timer.timeout.connect(timer.deleteLater) timer.start() except: timer.deleteLater() raise @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True, deprecated_name='repeat') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('count', value=cmdutils.Value.count) def cmd_repeat(times: int, command: str, win_id: int, count: int = None) -> None: """Repeat a given command. Args: times: How many times to repeat. command: The command to run, with optional args. count: Multiplies with 'times' when given. """ if count is not None: times *= count if times < 0: raise cmdutils.CommandError("A negative count doesn't make sense.") commandrunner = runners.CommandRunner(win_id) for _ in range(times): commandrunner.run_safely(command) @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True, deprecated_name='run-with-count') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('count', value=cmdutils.Value.count) def cmd_run_with_count(count_arg: int, command: str, win_id: int, count: int = 1) -> None: """Run a command with the given count. If cmd_run_with_count itself is run with a count, it multiplies count_arg. Args: count_arg: The count to pass to the command. command: The command to run, with optional args. count: The count that run_with_count itself received. """ runners.CommandRunner(win_id).run(command, count_arg * count) @cmdutils.register() def clear_messages() -> None: """Clear all message notifications.""" message.global_bridge.clear_messages.emit() @cmdutils.register(debug=True) def debug_all_objects() -> None: """Print a list of all objects to the debug log.""" s = debug.get_all_objects() log.misc.debug(s) @cmdutils.register(debug=True) def debug_cache_stats() -> None: """Print LRU cache stats.""" debugcachestats.debug_cache_stats() @cmdutils.register(debug=True) def debug_console() -> None: """Show the debugging console.""" if consolewidget.console_widget is None: log.misc.debug('initializing debug console') consolewidget.init() assert consolewidget.console_widget is not None if consolewidget.console_widget.isVisible(): log.misc.debug('hiding debug console') consolewidget.console_widget.hide() else: log.misc.debug('showing debug console') consolewidget.console_widget.show() @cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True) def debug_pyeval(s: str, file: bool = False, quiet: bool = False) -> None: """Evaluate a python string and display the results as a web page. Args: s: The string to evaluate. file: Interpret s as a path to file, also implies --quiet. quiet: Don't show the output in a new tab. """ if file: quiet = True path = os.path.expanduser(s) try: with open(path, 'r', encoding='utf-8') as f: s = f.read() except OSError as e: raise cmdutils.CommandError(str(e)) try: exec(s) out = "No error" except Exception: out = traceback.format_exc() else: try: r = eval(s) out = repr(r) except Exception: out = traceback.format_exc() qutescheme.pyeval_output = out if quiet: log.misc.debug("pyeval output: {}".format(out)) else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') tabbed_browser.load_url(QUrl('qute://pyeval'), newtab=True) @cmdutils.register(debug=True) def debug_set_fake_clipboard(s: str = None) -> None: """Put data into the fake clipboard and enable logging, used for tests. Args: s: The text to put into the fake clipboard, or unset to enable logging. """ if s is None: utils.log_clipboard = True else: utils.fake_clipboard = s @cmdutils.register(deprecated_name='repeat-command') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('count', value=cmdutils.Value.count) def cmd_repeat_last(win_id: int, count: int = None) -> None: """Repeat the last executed command. Args: count: Which count to pass the command. """ mode_manager = modeman.instance(win_id) if mode_manager.mode not in runners.last_command: raise cmdutils.CommandError("You didn't do anything yet.") cmd = runners.last_command[mode_manager.mode] commandrunner = runners.CommandRunner(win_id) commandrunner.run(cmd[0], count if count is not None else cmd[1]) @cmdutils.register(debug=True, name='debug-log-capacity') def log_capacity(capacity: int) -> None: """Change the number of log lines to be stored in RAM. Args: capacity: Number of lines for the log. """ if capacity < 0: raise cmdutils.CommandError("Can't set a negative log capacity!") assert log.ram_handler is not None log.ram_handler.change_log_capacity(capacity) @cmdutils.register(debug=True) def debug_log_filter(filters: str) -> None: """Change the log filter for console logging. Args: filters: A comma separated list of logger names. Can also be "none" to clear any existing filters. """ if log.console_filter is None: raise cmdutils.CommandError("No log.console_filter. Not attached " "to a console?") try: new_filter = log.LogFilter.parse(filters) except log.InvalidLogFilterError as e: raise cmdutils.CommandError(e) log.console_filter.update_from(new_filter) @cmdutils.register() @cmdutils.argument('current_win_id', value=cmdutils.Value.win_id) def window_only(current_win_id: int) -> None: """Close all windows except for the current one.""" for win_id, window in objreg.window_registry.items(): # We could be in the middle of destroying a window here if sip.isdeleted(window): continue if win_id != current_win_id: window.close() @cmdutils.register() @cmdutils.argument('win_id', value=cmdutils.Value.win_id) def version(win_id: int, paste: bool = False) -> None: """Show version information. Args: paste: Paste to pastebin. """ tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.load_url(QUrl('qute://version/'), newtab=True) if paste: pastebin_version() _keytester_widget: Optional[miscwidgets.KeyTesterWidget] = None @cmdutils.register(debug=True) def debug_keytester() -> None: """Show a keytester widget.""" global _keytester_widget if (_keytester_widget and not sip.isdeleted(_keytester_widget) and _keytester_widget.isVisible()): _keytester_widget.close() else: _keytester_widget = miscwidgets.KeyTesterWidget() _keytester_widget.show() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/misc/wmname.py0000644000175100017510000002366715102145205020735 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities to get the name of the window manager (X11) / compositor (Wayland).""" from typing import NewType from collections.abc import Iterator import ctypes import socket import struct import pathlib import dataclasses import contextlib import ctypes.util class Error(Exception): """Base class for errors in this module.""" class _WaylandDisplayStruct(ctypes.Structure): pass _WaylandDisplay = NewType("_WaylandDisplay", "ctypes._Pointer[_WaylandDisplayStruct]") def _load_libwayland_client() -> ctypes.CDLL: """Load the Wayland client library.""" try: return ctypes.CDLL("libwayland-client.so") except OSError as e: raise Error(f"Failed to load libwayland-client: {e}") def _pid_from_fd(fd: int) -> int: """Get the process ID from a file descriptor using SO_PEERCRED. https://stackoverflow.com/a/35827184 """ if not hasattr(socket, "SO_PEERCRED"): raise Error("Missing socket.SO_PEERCRED") # struct ucred { # pid_t pid; # uid_t uid; #  gid_t gid; # }; // where all of those are integers ucred_format = "3i" ucred_size = struct.calcsize(ucred_format) try: sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) except OSError as e: raise Error(f"Error creating socket for fd {fd}: {e}") try: ucred = sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, ucred_size) except OSError as e: raise Error(f"Error getting SO_PEERCRED for fd {fd}: {e}") finally: sock.close() pid, _uid, _gid = struct.unpack(ucred_format, ucred) return pid def _process_name_from_pid(pid: int) -> str: """Get the process name from a PID by reading /proc/[pid]/cmdline.""" proc_path = pathlib.Path(f"/proc/{pid}/cmdline") try: return proc_path.read_text(encoding="utf-8").replace("\0", " ").strip() except OSError as e: raise Error(f"Error opening {proc_path}: {e}") @contextlib.contextmanager def _wayland_display(wayland_client: ctypes.CDLL) -> Iterator[_WaylandDisplay]: """Context manager to connect to a Wayland display.""" wayland_client.wl_display_connect.argtypes = [ctypes.c_char_p] # name wayland_client.wl_display_connect.restype = ctypes.POINTER(_WaylandDisplayStruct) wayland_client.wl_display_disconnect.argtypes = [ ctypes.POINTER(_WaylandDisplayStruct) ] wayland_client.wl_display_disconnect.restype = None display = wayland_client.wl_display_connect(None) if not display: raise Error("Can't connect to display") try: yield display finally: wayland_client.wl_display_disconnect(display) def _wayland_get_fd(wayland_client: ctypes.CDLL, display: _WaylandDisplay) -> int: """Get the file descriptor for the Wayland display.""" wayland_client.wl_display_get_fd.argtypes = [ctypes.POINTER(_WaylandDisplayStruct)] wayland_client.wl_display_get_fd.restype = ctypes.c_int fd = wayland_client.wl_display_get_fd(display) if fd < 0: raise Error(f"Failed to get Wayland display file descriptor: {fd}") return fd def wayland_compositor_name() -> str: """Get the name of the running Wayland compositor. Approach based on: https://stackoverflow.com/questions/69302630/wayland-client-get-compositor-name """ wayland_client = _load_libwayland_client() with _wayland_display(wayland_client) as display: fd = _wayland_get_fd(wayland_client, display) pid = _pid_from_fd(fd) process_name = _process_name_from_pid(pid) return process_name @dataclasses.dataclass class _X11Atoms: NET_SUPPORTING_WM_CHECK: int NET_WM_NAME: int UTF8_STRING: int class _X11DisplayStruct(ctypes.Structure): pass _X11Display = NewType("_X11Display", "ctypes._Pointer[_X11DisplayStruct]") _X11Window = NewType("_X11Window", int) def _x11_load_lib() -> ctypes.CDLL: """Load the X11 library.""" lib = ctypes.util.find_library("X11") if lib is None: raise Error("X11 library not found") try: return ctypes.CDLL(lib) except OSError as e: raise Error(f"Failed to load X11 library: {e}") @contextlib.contextmanager def _x11_open_display(xlib: ctypes.CDLL) -> Iterator[_X11Display]: """Open a connection to the X11 display.""" xlib.XOpenDisplay.argtypes = [ctypes.c_char_p] xlib.XOpenDisplay.restype = ctypes.POINTER(_X11DisplayStruct) xlib.XCloseDisplay.argtypes = [ctypes.POINTER(_X11DisplayStruct)] xlib.XCloseDisplay.restype = None display = xlib.XOpenDisplay(None) if not display: raise Error("Cannot open display") try: yield display finally: xlib.XCloseDisplay(display) def _x11_intern_atom( xlib: ctypes.CDLL, display: _X11Display, name: bytes, only_if_exists: bool = True ) -> int: """Call xlib's XInternAtom function.""" xlib.XInternAtom.argtypes = [ ctypes.POINTER(_X11DisplayStruct), # Display ctypes.c_char_p, # Atom name ctypes.c_int, # Only if exists (bool) ] xlib.XInternAtom.restype = ctypes.c_ulong atom = xlib.XInternAtom(display, name, only_if_exists) if atom == 0: raise Error(f"Failed to intern atom: {name!r}") return atom @contextlib.contextmanager def _x11_get_window_property( xlib: ctypes.CDLL, display: _X11Display, *, window: _X11Window, prop: int, req_type: int, length: int, offset: int = 0, delete: bool = False, ) -> Iterator[tuple["ctypes._Pointer[ctypes.c_ubyte]", ctypes.c_ulong]]: """Call xlib's XGetWindowProperty function.""" ret_actual_type = ctypes.c_ulong() ret_actual_format = ctypes.c_int() ret_nitems = ctypes.c_ulong() ret_bytes_after = ctypes.c_ulong() ret_prop = ctypes.POINTER(ctypes.c_ubyte)() xlib.XGetWindowProperty.argtypes = [ ctypes.POINTER(_X11DisplayStruct), # Display ctypes.c_ulong, # Window ctypes.c_ulong, # Property ctypes.c_long, # Offset ctypes.c_long, # Length ctypes.c_int, # Delete (bool) ctypes.c_ulong, # Required type (Atom) ctypes.POINTER(ctypes.c_ulong), # return: Actual type (Atom) ctypes.POINTER(ctypes.c_int), # return: Actual format ctypes.POINTER(ctypes.c_ulong), # return: Number of items ctypes.POINTER(ctypes.c_ulong), # return: Bytes after ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte)), # return: Property value ] xlib.XGetWindowProperty.restype = ctypes.c_int result = xlib.XGetWindowProperty( display, window, prop, offset, length, delete, req_type, ctypes.byref(ret_actual_type), ctypes.byref(ret_actual_format), ctypes.byref(ret_nitems), ctypes.byref(ret_bytes_after), ctypes.byref(ret_prop), ) if result != 0: raise Error(f"XGetWindowProperty for {prop} failed: {result}") if not ret_prop: raise Error(f"Property {prop} is NULL") if ret_actual_type.value != req_type: raise Error( f"Expected type {req_type}, got {ret_actual_type.value} for property {prop}" ) if ret_bytes_after.value != 0: raise Error( f"Expected no bytes after property {prop}, got {ret_bytes_after.value}" ) try: yield ret_prop, ret_nitems finally: xlib.XFree(ret_prop) def _x11_get_wm_window( xlib: ctypes.CDLL, display: _X11Display, *, atoms: _X11Atoms ) -> _X11Window: """Get the _NET_SUPPORTING_WM_CHECK window.""" xlib.XDefaultScreen.argtypes = [ctypes.POINTER(_X11DisplayStruct)] xlib.XDefaultScreen.restype = ctypes.c_int xlib.XRootWindow.argtypes = [ ctypes.POINTER(_X11DisplayStruct), # Display ctypes.c_int, # Screen number ] xlib.XRootWindow.restype = ctypes.c_ulong screen = xlib.XDefaultScreen(display) root_window = xlib.XRootWindow(display, screen) with _x11_get_window_property( xlib, display, window=root_window, prop=atoms.NET_SUPPORTING_WM_CHECK, req_type=33, # XA_WINDOW length=1, ) as (prop, _nitems): win = ctypes.cast(prop, ctypes.POINTER(ctypes.c_ulong)).contents.value return _X11Window(win) def _x11_get_wm_name( xlib: ctypes.CDLL, display: _X11Display, *, atoms: _X11Atoms, wm_window: _X11Window, ) -> str: """Get the _NET_WM_NAME property of the window manager.""" with _x11_get_window_property( xlib, display, window=wm_window, prop=atoms.NET_WM_NAME, req_type=atoms.UTF8_STRING, length=1024, # somewhat arbitrary ) as (prop, nitems): if nitems.value <= 0: raise Error(f"{nitems.value} items found in _NET_WM_NAME property") wm_name = ctypes.string_at(prop, nitems.value).decode("utf-8") if not wm_name: raise Error("Window manager name is empty") return wm_name def x11_wm_name() -> str: """Get the name of the running X11 window manager.""" xlib = _x11_load_lib() with _x11_open_display(xlib) as display: atoms = _X11Atoms( NET_SUPPORTING_WM_CHECK=_x11_intern_atom( xlib, display, b"_NET_SUPPORTING_WM_CHECK" ), NET_WM_NAME=_x11_intern_atom(xlib, display, b"_NET_WM_NAME"), UTF8_STRING=_x11_intern_atom(xlib, display, b"UTF8_STRING"), ) wm_window = _x11_get_wm_window(xlib, display, atoms=atoms) return _x11_get_wm_name(xlib, display, atoms=atoms, wm_window=wm_window) if __name__ == "__main__": try: wayland_name = wayland_compositor_name() print(f"Wayland compositor name: {wayland_name}") except Error as e: print(f"Wayland error: {e}") try: x11_name = x11_wm_name() print(f"X11 window manager name: {x11_name}") except Error as e: print(f"X11 error: {e}") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5506387 qutebrowser-3.6.1/qutebrowser/qt/0000755000175100017510000000000015102145351016554 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/__init__.py0000644000175100017510000000017515102145205020666 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/_core_pyqtproperty.py0000644000175100017510000000550115102145205023076 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """WORKAROUND for missing pyqtProperty typing, ported from PyQt5-stubs: FIXME:mypy PyQt6-stubs issue https://github.com/python-qt-tools/PyQt5-stubs/blob/5.15.6.0/PyQt5-stubs/QtCore.pyi#L68-L111 """ # flake8: noqa # pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument,deprecated-typing-alias import typing from PyQt6.QtCore import QObject, pyqtSignal if typing.TYPE_CHECKING: QObjectT = typing.TypeVar("QObjectT", bound=QObject) TPropertyTypeVal = typing.TypeVar("TPropertyTypeVal") TPropGetter = typing.TypeVar( "TPropGetter", bound=typing.Callable[[QObjectT], TPropertyTypeVal] ) TPropSetter = typing.TypeVar( "TPropSetter", bound=typing.Callable[[QObjectT, TPropertyTypeVal], None] ) TPropDeleter = typing.TypeVar( "TPropDeleter", bound=typing.Callable[[QObjectT], None] ) TPropResetter = typing.TypeVar( "TPropResetter", bound=typing.Callable[[QObjectT], None] ) class pyqtProperty: def __init__( # pylint: disable=too-many-positional-arguments self, type: typing.Union[type, str], fget: typing.Optional[typing.Callable[[QObjectT], TPropertyTypeVal]] = None, fset: typing.Optional[ typing.Callable[[QObjectT, TPropertyTypeVal], None] ] = None, freset: typing.Optional[typing.Callable[[QObjectT], None]] = None, fdel: typing.Optional[typing.Callable[[QObjectT], None]] = None, doc: typing.Optional[str] = "", designable: bool = True, scriptable: bool = True, stored: bool = True, user: bool = True, constant: bool = True, final: bool = True, notify: typing.Optional[pyqtSignal] = None, revision: int = 0, ) -> None: ... type: typing.Union[type, str] fget: typing.Optional[typing.Callable[[], TPropertyTypeVal]] fset: typing.Optional[typing.Callable[[TPropertyTypeVal], None]] freset: typing.Optional[typing.Callable[[], None]] fdel: typing.Optional[typing.Callable[[], None]] def read(self, func: TPropGetter) -> "pyqtProperty": ... def write(self, func: TPropSetter) -> "pyqtProperty": ... def reset(self, func: TPropResetter) -> "pyqtProperty": ... def getter(self, func: TPropGetter) -> "pyqtProperty": ... def setter(self, func: TPropSetter) -> "pyqtProperty": ... def deleter(self, func: TPropDeleter) -> "pyqtProperty": ... def __call__(self, func: TPropGetter) -> "pyqtProperty": ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/core.py0000644000175100017510000000166415102145205020063 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt Core. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtcore-index.html """ from typing import TYPE_CHECKING from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtCore import * elif machinery.USE_PYQT5: from PyQt5.QtCore import * elif machinery.USE_PYQT6: from PyQt6.QtCore import * if TYPE_CHECKING: from qutebrowser.qt._core_pyqtproperty import pyqtProperty else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/dbus.py0000644000175100017510000000147115102145205020064 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt DBus. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtdbus-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtDBus import * elif machinery.USE_PYQT5: from PyQt5.QtDBus import * elif machinery.USE_PYQT6: from PyQt6.QtDBus import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/gui.py0000644000175100017510000000165615102145205017720 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import,unused-import """Wrapped Qt imports for Qt Gui. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtgui-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtGui import * elif machinery.USE_PYQT5: from PyQt5.QtGui import * from PyQt5.QtWidgets import QFileSystemModel del QOpenGLVersionProfile # moved to QtOpenGL in Qt 6 elif machinery.USE_PYQT6: from PyQt6.QtGui import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/machinery.py0000644000175100017510000002352615102145205021113 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pyright: reportConstantRedefinition=false """Qt wrapper selection. Contains selection logic and globals for Qt wrapper selection. All other files in this package are intended to be simple wrappers around Qt imports. Depending on what is set in this module, they import from PyQt5 or PyQt6. The import wrappers are intended to be as thin as possible. They will not unify API-level differences between Qt 5 and Qt 6. This is best handled by the calling code, which has a better picture of what changed between APIs and how to best handle it. What they *will* do is handle simple 1:1 renames of classes, or moves between modules (where they aim to always expose the Qt 6 API). See e.g. webenginecore.py. """ # NOTE: No qutebrowser or PyQt import should be done here (at import time), # as some early initialization needs to take place before that! import os import sys import enum import html import argparse import warnings import importlib import dataclasses from typing import Optional from qutebrowser.utils import log # Packagers: Patch the line below to enforce a Qt wrapper, e.g.: # sed -i 's/_WRAPPER_OVERRIDE = .*/_WRAPPER_OVERRIDE = "PyQt6"/' qutebrowser/qt/machinery.py # # Users: Set the QUTE_QT_WRAPPER environment variable to change the default wrapper. _WRAPPER_OVERRIDE = None # type: ignore[var-annotated] WRAPPERS = [ "PyQt6", "PyQt5", # Needs more work # "PySide6", ] class Error(Exception): """Base class for all exceptions in this module.""" class Unavailable(Error, ModuleNotFoundError): """Raised when a module is unavailable with the given wrapper.""" def __init__(self) -> None: super().__init__(f"Unavailable with {INFO.wrapper}") class NoWrapperAvailableError(Error, ImportError): """Raised when no Qt wrapper is available.""" def __init__(self, info: "SelectionInfo") -> None: super().__init__(f"No Qt wrapper was importable.\n\n{info}") class UnknownWrapper(Error): """Raised when an Qt module is imported but the wrapper values are unknown. Should never happen (unless a new wrapper is added). """ class SelectionReason(enum.Enum): """Reasons for selecting a Qt wrapper.""" #: The wrapper was selected via --qt-wrapper. cli = "--qt-wrapper" #: The wrapper was selected via the QUTE_QT_WRAPPER environment variable. env = "QUTE_QT_WRAPPER" #: The wrapper was selected via autoselection. auto = "autoselect" #: The default wrapper was selected. default = "default" #: The wrapper was faked/patched out (e.g. in tests). fake = "fake" #: The wrapper was overridden by patching _WRAPPER_OVERRIDE. override = "override" #: The reason was not set. unknown = "unknown" @dataclasses.dataclass class SelectionInfo: """Information about outcomes of importing Qt wrappers.""" wrapper: Optional[str] = None outcomes: dict[str, str] = dataclasses.field(default_factory=dict) reason: SelectionReason = SelectionReason.unknown def set_module_error(self, name: str, error: Exception) -> None: """Set the outcome for a module import.""" self.outcomes[name] = f"{type(error).__name__}: {error}" def use_wrapper(self, wrapper: str) -> None: """Set the wrapper to use.""" self.wrapper = wrapper self.outcomes[wrapper] = "success" def __str__(self) -> str: if not self.outcomes: # No modules were tried to be imported (no autoselection) # Thus, we can have a shorter output instead of adding noise. return f"Qt wrapper: {self.wrapper} (via {self.reason.value})" lines = ["Qt wrapper info:"] for wrapper in WRAPPERS: outcome = self.outcomes.get(wrapper, "not imported") lines.append(f" {wrapper}: {outcome}") lines.append(f" -> selected: {self.wrapper} (via {self.reason.value})") return "\n".join(lines) def to_html(self) -> str: return html.escape(str(self)).replace("\n", "
") def _autoselect_wrapper() -> SelectionInfo: """Autoselect a Qt wrapper. This goes through all wrappers defined in WRAPPER. The first one which can be imported is returned. """ info = SelectionInfo(reason=SelectionReason.auto) for wrapper in WRAPPERS: try: importlib.import_module(wrapper) except ModuleNotFoundError as e: # Wrapper not available -> try the next one. info.set_module_error(wrapper, e) continue except ImportError as e: # Any other ImportError -> stop to surface the error. info.set_module_error(wrapper, e) break # Wrapper imported successfully -> use it. info.use_wrapper(wrapper) return info # SelectionInfo with wrapper=None but all error reports return info def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: """Select a Qt wrapper. - If --qt-wrapper is given, use that. - Otherwise, if the QUTE_QT_WRAPPER environment variable is set, use that. - Otherwise, try the wrappers in WRAPPER in order (PyQt6 -> PyQt5) """ # If any Qt wrapper has been imported before this, something strange might # be happening. With PyInstaller, it imports the Qt bindings early. for name in WRAPPERS: if name in sys.modules and not hasattr(sys, "frozen"): warnings.warn(f"{name} already imported", stacklevel=1) if args is not None and args.qt_wrapper is not None: assert args.qt_wrapper in WRAPPERS, args.qt_wrapper # ensured by argparse return SelectionInfo(wrapper=args.qt_wrapper, reason=SelectionReason.cli) env_var = "QUTE_QT_WRAPPER" env_wrapper = os.environ.get(env_var) if env_wrapper: if env_wrapper == "auto": return _autoselect_wrapper() elif env_wrapper not in WRAPPERS: raise Error( f"Unknown wrapper {env_wrapper} set via {env_var}, " f"allowed: {', '.join(WRAPPERS)}" ) return SelectionInfo(wrapper=env_wrapper, reason=SelectionReason.env) if _WRAPPER_OVERRIDE is not None: assert _WRAPPER_OVERRIDE in WRAPPERS return SelectionInfo(wrapper=_WRAPPER_OVERRIDE, reason=SelectionReason.override) return _autoselect_wrapper() # Values are set in init(). If you see a NameError here, it means something tried to # import Qt (or check for its availability) before machinery.init() was called. #: Information about the wrapper that ended up being selected. #: Should not be used directly, use one of the USE_* or IS_* constants below #: instead, as those are supported by type checking. INFO: SelectionInfo #: Whether we're using PyQt5. Consider using IS_QT5 or IS_PYQT instead. USE_PYQT5: bool #: Whether we're using PyQt6. Consider using IS_QT6 or IS_PYQT instead. USE_PYQT6: bool #: Whether we're using PySide6. Consider using IS_QT6 or IS_PYSIDE instead. USE_PYSIDE6: bool #: Whether we are using any Qt 5 wrapper. IS_QT5: bool #: Whether we are using any Qt 6 wrapper. IS_QT6: bool #: Whether we are using any PyQt wrapper. IS_PYQT: bool #: Whether we are using any PySide wrapper. IS_PYSIDE: bool _initialized = False def _set_globals(info: SelectionInfo) -> None: """Set all global variables in this module based on the given SelectionInfo. Those are split into multiple global variables because that way we can teach mypy about them via --always-true and --always-false, see tox.ini. """ global INFO, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, IS_PYQT, IS_PYSIDE, _initialized assert info.wrapper is not None, info assert not _initialized _initialized = True INFO = info USE_PYQT5 = info.wrapper == "PyQt5" USE_PYQT6 = info.wrapper == "PyQt6" USE_PYSIDE6 = info.wrapper == "PySide6" assert USE_PYQT5 + USE_PYQT6 + USE_PYSIDE6 == 1 IS_QT5 = USE_PYQT5 IS_QT6 = USE_PYQT6 or USE_PYSIDE6 IS_PYQT = USE_PYQT5 or USE_PYQT6 IS_PYSIDE = USE_PYSIDE6 assert IS_QT5 ^ IS_QT6 assert IS_PYQT ^ IS_PYSIDE def init_implicit() -> None: """Initialize Qt wrapper globals implicitly at Qt import time. This gets called when any qutebrowser.qt module is imported, and implicitly initializes the Qt wrapper globals. After this is called, no explicit initialization via machinery.init() is possible anymore - thus, this should never be called before init() when running qutebrowser as an application (and any further calls will be a no-op). However, this ensures that any qutebrowser module can be imported without having to worry about machinery.init(). This is useful for e.g. tests or manual interactive usage of the qutebrowser code. """ if _initialized: # Implicit initialization can happen multiple times # (all subsequent calls are a no-op) return info = _select_wrapper(args=None) if info.wrapper is None: raise NoWrapperAvailableError(info) _set_globals(info) def init(args: argparse.Namespace) -> SelectionInfo: """Initialize Qt wrapper globals during qutebrowser application start. This gets called from earlyinit.py, i.e. after we have an argument parser, but before any kinds of Qt usage. This allows `args` to be passed, which is used to select the Qt wrapper (if --qt-wrapper is given). If any qutebrowser.qt module is imported before this, init_implicit() will be called instead, which means this can't be called anymore. """ if _initialized: raise Error("init() already called before application init") info = _select_wrapper(args) if info.wrapper is not None: _set_globals(info) log.init.debug(str(info)) # If info is None here (no Qt wrapper available), we'll show an error later # in earlyinit.py. return info ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/network.py0000644000175100017510000000151015102145205020612 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt Network. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtnetwork-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtNetwork import * elif machinery.USE_PYQT5: from PyQt5.QtNetwork import * elif machinery.USE_PYQT6: from PyQt6.QtNetwork import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/opengl.py0000644000175100017510000000154215102145205020412 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-import,unused-wildcard-import """Wrapped Qt imports for Qt OpenGL. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtopengl-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtOpenGL import * elif machinery.USE_PYQT5: from PyQt5.QtGui import QOpenGLVersionProfile elif machinery.USE_PYQT6: from PyQt6.QtOpenGL import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/printsupport.py0000644000175100017510000000154215102145205021717 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt Print Support. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtprintsupport-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtPrintSupport import * elif machinery.USE_PYQT5: from PyQt5.QtPrintSupport import * elif machinery.USE_PYQT6: from PyQt6.QtPrintSupport import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/qml.py0000644000175100017510000000146415102145205017722 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt QML. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtqml-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtQml import * elif machinery.USE_PYQT5: from PyQt5.QtQml import * elif machinery.USE_PYQT6: from PyQt6.QtQml import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/sip.py0000644000175100017510000000247615102145205017730 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=wildcard-import,unused-wildcard-import """Wrapped Qt imports for PyQt5.sip/PyQt6.sip. All code in qutebrowser should use this module instead of importing from PyQt/sip directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the PyQt6.sip API: https://www.riverbankcomputing.com/static/Docs/PyQt6/api/sip/sip-module.html Note that we don't yet abstract between PySide/PyQt here. """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: # pylint: disable=no-else-raise raise machinery.Unavailable() elif machinery.USE_PYQT5: try: from PyQt5.sip import * except ImportError: from sip import * # type: ignore[import-not-found] elif machinery.USE_PYQT6: try: from PyQt6.sip import * except ImportError: # While upstream recommends using PyQt5.sip ever since PyQt5 5.11, some # distributions still package later versions of PyQt5 with a top-level # "sip" rather than "PyQt5.sip". from sip import * # type: ignore[import-not-found] else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/sql.py0000644000175100017510000000146415102145205017730 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt SQL. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtsql-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtSql import * elif machinery.USE_PYQT5: from PyQt5.QtSql import * elif machinery.USE_PYQT6: from PyQt6.QtSql import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/test.py0000644000175100017510000000147115102145205020106 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt Test. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qttest-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtTest import * elif machinery.USE_PYQT5: from PyQt5.QtTest import * elif machinery.USE_PYQT6: from PyQt6.QtTest import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/webenginecore.py0000644000175100017510000000267115102145205021746 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import,unused-import """Wrapped Qt imports for Qt WebEngine Core. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtwebenginecore-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtWebEngineCore import * elif machinery.USE_PYQT5: from PyQt5.QtWebEngineCore import * from PyQt5.QtWebEngineWidgets import ( QWebEngineSettings, QWebEngineProfile, QWebEngineDownloadItem as QWebEngineDownloadRequest, QWebEnginePage, QWebEngineCertificateError, QWebEngineScript, QWebEngineHistory, QWebEngineHistoryItem, QWebEngineScriptCollection, QWebEngineClientCertificateSelection, QWebEngineFullScreenRequest, QWebEngineContextMenuData as QWebEngineContextMenuRequest, ) from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION, PYQT_WEBENGINE_VERSION_STR elif machinery.USE_PYQT6: from PyQt6.QtWebEngineCore import * else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/webenginewidgets.py0000644000175100017510000000252015102145205022455 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt WebEngine Widgets. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtwebenginewidgets-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtWebEngineWidgets import * elif machinery.USE_PYQT5: from PyQt5.QtWebEngineWidgets import * elif machinery.USE_PYQT6: from PyQt6.QtWebEngineWidgets import * else: raise machinery.UnknownWrapper() if machinery.IS_QT5: # pylint: disable=undefined-variable # moved to WebEngineCore in Qt 6 del QWebEngineSettings del QWebEngineProfile del QWebEngineDownloadItem del QWebEnginePage del QWebEngineCertificateError del QWebEngineScript del QWebEngineHistory del QWebEngineHistoryItem del QWebEngineScriptCollection del QWebEngineClientCertificateSelection del QWebEngineFullScreenRequest del QWebEngineContextMenuData ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/webkit.py0000644000175100017510000000241115102145205020407 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=wildcard-import """Wrapped Qt imports for Qt WebKit. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6 (though WebKit is only supported with Qt 5). See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the QtWebKit 5.212 API: https://qtwebkit.github.io/doc/qtwebkit/qtwebkit-index.html """ import typing from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: # pylint: disable=no-else-raise raise machinery.Unavailable() elif machinery.USE_PYQT5 or typing.TYPE_CHECKING: # If we use mypy (even on Qt 6), we pretend to have WebKit available. # This avoids central API (like BrowserTab) being Any because the webkit part of # the unions there is missing. # This causes various issues inside browser/webkit/, but we ignore those in # .mypy.ini because we don't really care much about QtWebKit anymore. from PyQt5.QtWebKit import * elif machinery.USE_PYQT6: raise machinery.Unavailable() else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/webkitwidgets.py0000644000175100017510000000241515102145205022002 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=wildcard-import,no-else-raise """Wrapped Qt imports for Qt WebKit Widgets. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6 (though WebKit is only supported with Qt 5). See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the QtWebKit 5.212 API: https://qtwebkit.github.io/doc/qtwebkit/qtwebkitwidgets-index.html """ import typing from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: raise machinery.Unavailable() elif machinery.USE_PYQT5 or typing.TYPE_CHECKING: # If we use mypy (even on Qt 6), we pretend to have WebKit available. # This avoids central API (like BrowserTab) being Any because the webkit part of # the unions there is missing. # This causes various issues inside browser/webkit/, but we ignore those in # .mypy.ini because we don't really care much about QtWebKit anymore. from PyQt5.QtWebKitWidgets import * elif machinery.USE_PYQT6: raise machinery.Unavailable() else: raise machinery.UnknownWrapper() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qt/widgets.py0000644000175100017510000000167215102145205020600 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=import-error,wildcard-import,unused-wildcard-import """Wrapped Qt imports for Qt Widgets. All code in qutebrowser should use this module instead of importing from PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6. See machinery.py for details on how Qt wrapper selection works. Any API exported from this module is based on the Qt 6 API: https://doc.qt.io/qt-6/qtwidgets-index.html """ from qutebrowser.qt import machinery machinery.init_implicit() if machinery.USE_PYSIDE6: from PySide6.QtWidgets import * elif machinery.USE_PYQT5: from PyQt5.QtWidgets import * elif machinery.USE_PYQT6: from PyQt6.QtWidgets import * else: raise machinery.UnknownWrapper() if machinery.IS_QT5: # pylint: disable=undefined-variable del QFileSystemModel # moved to QtGui in Qt 6 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/qutebrowser.py0000644000175100017510000002406615102145205021072 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Early initialization and main entry point. qutebrowser's initialization process roughly looks like this: - This file gets imported, either via the setuptools entry point or __main__.py. - At import time, we check for the correct Python version and show an error if it's too old. - The main() function in this file gets invoked - Argument parsing takes place - earlyinit.early_init() gets invoked to do various low-level initialization and checks whether all dependencies are met. - app.run() gets called, which takes over. See the docstring of app.py for details. """ import sys import json import qutebrowser try: from qutebrowser.misc.checkpyver import check_python_version except ImportError: try: # python2 from .misc.checkpyver import check_python_version except (SystemError, ValueError): # Import without module - SystemError on Python3, ValueError (?!?) on # Python2 sys.stderr.write("Please don't run this script directly, do something " "like python3 -m qutebrowser instead.\n") sys.stderr.flush() sys.exit(100) check_python_version() import argparse from qutebrowser.misc import earlyinit from qutebrowser.qt import machinery def get_argparser(): """Get the argparse parser.""" parser = argparse.ArgumentParser(prog='qutebrowser', description=qutebrowser.__description__) parser.add_argument('-B', '--basedir', help="Base directory for all " "storage.") parser.add_argument('-C', '--config-py', help="Path to config.py.", metavar='CONFIG') parser.add_argument('-V', '--version', help="Show version and quit.", action='store_true') parser.add_argument('-s', '--set', help="Set a temporary setting for " "this session.", nargs=2, action='append', dest='temp_settings', default=[], metavar=('OPTION', 'VALUE')) parser.add_argument('-r', '--restore', help="Restore a named session.", dest='session') parser.add_argument('-R', '--override-restore', help="Don't restore a " "session even if one would be restored.", action='store_true') parser.add_argument('--target', choices=['auto', 'tab', 'tab-bg', 'tab-silent', 'tab-bg-silent', 'window', 'private-window'], help="How URLs should be opened if there is already a " "qutebrowser instance running.") parser.add_argument('--backend', choices=['webkit', 'webengine'], help="Which backend to use.") parser.add_argument('--qt-wrapper', choices=machinery.WRAPPERS, help="Which Qt wrapper to use. This can also be set " "via the QUTE_QT_WRAPPER environment variable. " "If both are set, the command line argument takes " "precedence.") parser.add_argument('--desktop-file-name', default="org.qutebrowser.qutebrowser", help="Set the base name of the desktop entry for this " "application. Used to set the app_id under Wayland. See " "https://doc.qt.io/qt-6/qguiapplication.html#desktopFileName-prop") parser.add_argument('--untrusted-args', action='store_true', help="Mark all following arguments as untrusted, which " "enforces that they are URLs/search terms (and not flags or " "commands)") parser.add_argument('--json-args', help=argparse.SUPPRESS) parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS, action='store_true') debug = parser.add_argument_group('debug arguments') debug.add_argument('-l', '--loglevel', dest='loglevel', help="Override the configured console loglevel", choices=['critical', 'error', 'warning', 'info', 'debug', 'vdebug']) debug.add_argument('--logfilter', type=logfilter_error, help="Comma-separated list of things to be logged " "to the debug log on stdout.") debug.add_argument('--loglines', help="How many lines of the debug log to keep in RAM " "(-1: unlimited).", default=2000, type=int) debug.add_argument('-d', '--debug', help="Turn on debugging options.", action='store_true') debug.add_argument('--json-logging', action='store_true', help="Output log" " lines in JSON format (one object per line).") debug.add_argument('--nocolor', help="Turn off colored logging.", action='store_false', dest='color') debug.add_argument('--force-color', help="Force colored logging", action='store_true') debug.add_argument('--nowindow', action='store_true', help="Don't show " "the main window.") debug.add_argument('-T', '--temp-basedir', action='store_true', help="Use " "a temporary basedir.") debug.add_argument('--no-err-windows', action='store_true', help="Don't " "show any error windows (used for tests/smoke.py).") debug.add_argument('--qt-arg', help="Pass an argument with a value to Qt. " "For example, you can do " "`--qt-arg geometry 650x555+200+300` to set the window " "geometry.", nargs=2, metavar=('NAME', 'VALUE'), action='append') debug.add_argument('--qt-flag', help="Pass an argument to Qt as flag.", nargs=1, action='append') debug.add_argument('-D', '--debug-flag', type=debug_flag_error, default=[], help="Pass name of debugging feature to be" " turned on.", action='append', dest='debug_flags') parser.add_argument('command', nargs='*', help="Commands to execute on " "startup.", metavar=':command') # URLs will actually be in command parser.add_argument('url', nargs='*', help="URLs to open on startup " "(empty as a window separator).") return parser def directory(arg): if not arg: raise argparse.ArgumentTypeError("Invalid empty value") def logfilter_error(logfilter): """Validate logger names passed to --logfilter. Args: logfilter: A comma separated list of logger names. """ from qutebrowser.utils import log try: log.LogFilter.parse(logfilter) except log.InvalidLogFilterError as e: raise argparse.ArgumentTypeError(e) return logfilter def debug_flag_error(flag): """Validate flags passed to --debug-flag. Available flags: debug-exit: Turn on debugging of late exit. pdb-postmortem: Drop into pdb on exceptions. no-sql-history: Don't store history items. no-scroll-filtering: Process all scrolling updates. log-requests: Log all network requests. log-cookies: Log cookies in cookie filter. log-scroll-pos: Log all scrolling changes. log-sensitive-keys: Log keypresses in passthrough modes. stack: Enable Chromium stack logging. chromium: Enable Chromium logging. wait-renderer-process: Wait for debugger in renderer process. avoid-chromium-init: Enable `--version` without initializing Chromium. werror: Turn Python warnings into errors. test-notification-service: Use the testing libnotify service. caret: Enable debug logging for caret.js. no-system-pdfjs: Ignore system-wide PDF.js installations """ valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history', 'no-scroll-filtering', 'log-requests', 'log-cookies', 'log-scroll-pos', 'log-sensitive-keys', 'stack', 'chromium', 'wait-renderer-process', 'avoid-chromium-init', 'werror', 'test-notification-service', 'log-qt-events', 'caret', 'no-system-pdfjs'] if flag in valid_flags: return flag else: raise argparse.ArgumentTypeError("Invalid debug flag - valid flags: {}" .format(', '.join(valid_flags))) def _unpack_json_args(args): """Restore arguments from --json-args after a restart. When restarting, we serialize the argparse namespace into json, and construct a "fake" argparse.Namespace here based on the data loaded from json. """ new_args = vars(args) data = json.loads(args.json_args) new_args.update(data) return argparse.Namespace(**new_args) def _validate_untrusted_args(argv): # NOTE: Do not use f-strings here, as this should run with older Python # versions (so that a proper error can be displayed) try: untrusted_idx = argv.index('--untrusted-args') except ValueError: return rest = argv[untrusted_idx + 1:] if len(rest) > 1: sys.exit( "Found multiple arguments ({}) after --untrusted-args, " "aborting.".format(' '.join(rest))) for arg in rest: if arg.startswith(('-', ':')): sys.exit("Found {} after --untrusted-args, aborting.".format(arg)) def main(): _validate_untrusted_args(sys.argv) parser = get_argparser() argv = sys.argv[1:] args = parser.parse_args(argv) if args.json_args is not None: args = _unpack_json_args(args) earlyinit.early_init(args) # We do this imports late as earlyinit needs to be run first (because of # version checking and other early initialization) from qutebrowser import app return app.run(args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5536387 qutebrowser-3.6.1/qutebrowser/utils/0000755000175100017510000000000015102145351017270 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/__init__.py0000644000175100017510000000023415102145205021376 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Misc utility functions.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/debug.py0000644000175100017510000003046115102145205020732 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities used for debugging.""" import re import enum import inspect import logging import functools import datetime import types from typing import ( Any, Optional, Union) from collections.abc import Mapping, MutableSequence, Sequence, Callable from qutebrowser.qt.core import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSignal from qutebrowser.qt.widgets import QApplication from qutebrowser.utils import log, utils, qtutils, objreg from qutebrowser.misc import objects from qutebrowser.qt import sip, machinery def log_events(klass: type[QObject]) -> type[QObject]: """Class decorator to log Qt events.""" old_event = klass.event @functools.wraps(old_event) def new_event(self: Any, e: QEvent) -> bool: """Wrapper for event() which logs events.""" # Passing klass as a WORKAROUND because with PyQt6, QEvent.type() returns int: # https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044583.html log.misc.debug("Event in {}: {}".format( utils.qualname(klass), qenum_key(QEvent, e.type(), klass=QEvent.Type))) return old_event(self, e) klass.event = new_event # type: ignore[assignment] return klass def log_signals(obj: Union[QObject, type[QObject]]) -> Union[QObject, type[QObject]]: """Log all signals of an object or class. Can be used as class decorator. """ def log_slot(obj: QObject, signal: pyqtBoundSignal, *args: Any) -> None: """Slot connected to a signal to log it.""" dbg = dbg_signal(signal, args) try: r = repr(obj) except RuntimeError: # pragma: no cover r = '' log.signals.debug("Signal in {}: {}".format(r, dbg)) def connect_log_slot(obj: QObject) -> None: """Helper function to connect all signals to a logging slot.""" metaobj = obj.metaObject() assert metaobj is not None for i in range(metaobj.methodCount()): meta_method = metaobj.method(i) qtutils.ensure_valid(meta_method) if meta_method.methodType() == QMetaMethod.MethodType.Signal: name = meta_method.name().data().decode('ascii') if name != 'destroyed': signal = getattr(obj, name) try: signal.connect(functools.partial( log_slot, obj, signal)) except TypeError: # pragma: no cover pass if inspect.isclass(obj): old_init = obj.__init__ @functools.wraps(old_init) def new_init(self: Any, *args: Any, **kwargs: Any) -> None: """Wrapper for __init__() which logs signals.""" old_init(self, *args, **kwargs) connect_log_slot(self) obj.__init__ = new_init else: assert isinstance(obj, QObject) connect_log_slot(obj) return obj if machinery.IS_QT6: _EnumValueType = Union[enum.Enum, int] else: _EnumValueType = Union[sip.simplewrapper, int] def _qenum_key_python( value: _EnumValueType, klass: type[_EnumValueType], ) -> Optional[str]: """New-style PyQt6: Try getting value from Python enum.""" if isinstance(value, enum.Enum) and value.name: return value.name # We got an int with klass passed: Try asking Python enum for member if issubclass(klass, enum.Enum): try: assert isinstance(value, int) name = klass(value).name if name is not None and name != str(value): return name except ValueError: pass return None def _qenum_key_qt( base: type[sip.simplewrapper], value: _EnumValueType, klass: type[_EnumValueType], ) -> Optional[str]: # On PyQt5, or PyQt6 with int passed: Try to ask Qt's introspection. # However, not every Qt enum value has a staticMetaObject try: meta_obj = base.staticMetaObject # type: ignore[attr-defined] idx = meta_obj.indexOfEnumerator(klass.__name__) meta_enum = meta_obj.enumerator(idx) key = meta_enum.valueToKey(int(value)) # type: ignore[arg-type] if key is not None: return key except AttributeError: pass # PyQt5: Try finding value match in class for name, obj in vars(base).items(): if isinstance(obj, klass) and obj == value: return name return None def qenum_key( base: type[sip.simplewrapper], value: _EnumValueType, klass: type[_EnumValueType] = None, ) -> str: """Convert a Qt Enum value to its key as a string. Args: base: The object the enum is in, e.g. QFrame. value: The value to get. klass: The enum class the value belongs to. If None, the class will be auto-guessed. Return: The key associated with the value as a string if it could be found. The original value as a string if not. """ if klass is None: klass = value.__class__ if klass == int: raise TypeError("Can't guess enum class of an int!") assert klass is not None name = _qenum_key_python(value=value, klass=klass) if name is not None: return name name = _qenum_key_qt(base=base, value=value, klass=klass) if name is not None: return name # Last resort fallback: Hex value return '0x{:04x}'.format(int(value)) # type: ignore[arg-type] def qflags_key(base: type[sip.simplewrapper], value: _EnumValueType, klass: type[_EnumValueType] = None) -> str: """Convert a Qt QFlags value to its keys as string. Note: Passing a combined value (such as Qt.AlignmentFlag.AlignCenter) will get the names for the individual bits (e.g. Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignHCenter). FIXME https://github.com/qutebrowser/qutebrowser/issues/42 Args: base: The object the flags are in, e.g. QtCore.Qt value: The value to get. klass: The flags class the value belongs to. If None, the class will be auto-guessed. Return: The keys associated with the flags as a '|' separated string if they could be found. Hex values as a string if not. """ if klass is None: # We have to store klass here because it will be lost when iterating # over the bits. klass = value.__class__ if klass == int: raise TypeError("Can't guess enum class of an int!") if not value: return qenum_key(base, value, klass) bits = [] names = [] mask = 0x01 intval = qtutils.extract_enum_val(value) while mask <= intval: if intval & mask: bits.append(mask) mask <<= 1 for bit in bits: # We have to re-convert to an enum type here or we'll sometimes get an # empty string back. enum_value = klass(bit) # type: ignore[call-arg,unused-ignore] names.append(qenum_key(base, enum_value, klass)) return '|'.join(names) def signal_name(sig: pyqtBoundSignal) -> str: """Get a cleaned up name of a signal. Unfortunately, the way to get the name of a signal differs based on bound vs. unbound signals. Here, we try to get the name from .signal or .signatures, or if all else fails, extract it from the repr(). Args: sig: A bound signal. Return: The cleaned up signal name. """ if hasattr(sig, 'signal'): # Bound signal # Examples: # sig.signal == '2signal1' # sig.signal == '2signal2(QString,QString)' m = re.fullmatch(r'[0-9]+(?P.*)\(.*\)', sig.signal) else: # Unbound signal, PyQt >= 5.11 # Examples: # sig.signatures == ('signal1()',) # sig.signatures == ('signal2(QString,QString)',) m = re.fullmatch(r'(?P.*)\(.*\)', sig.signatures[0]) # type: ignore[attr-defined] assert m is not None, sig return m.group('name') def format_args(args: Sequence[Any] = None, kwargs: Mapping[str, Any] = None) -> str: """Format a list of arguments/kwargs to a function-call like string.""" if args is not None: arglist = [utils.compact_text(repr(arg), 200) for arg in args] else: arglist = [] if kwargs is not None: for k, v in kwargs.items(): arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 200))) return ', '.join(arglist) def dbg_signal(sig: pyqtBoundSignal, args: Any) -> str: """Get a string representation of a signal for debugging. Args: sig: A bound signal. args: The arguments as list of strings. Return: A human-readable string representation of signal/args. """ return '{}({})'.format(signal_name(sig), format_args(args)) def format_call(func: Callable[..., Any], args: Sequence[Any] = None, kwargs: Mapping[str, Any] = None, full: bool = True) -> str: """Get a string representation of a function calls with the given args. Args: func: The callable to print. args: A list of positional arguments. kwargs: A dict of named arguments. full: Whether to print the full name Return: A string with the function call. """ if full: name = utils.qualname(func) else: name = func.__name__ return '{}({})'.format(name, format_args(args, kwargs)) class log_time: # noqa: N801,N806 pylint: disable=invalid-name """Log the time an operation takes. Usable as context manager or as decorator. """ def __init__(self, logger: Union[logging.Logger, str], action: str = 'operation') -> None: """Constructor. Args: logger: The logging.Logger to use for logging, or a logger name. action: A description of what's being done. """ if isinstance(logger, str): self._logger = logging.getLogger(logger) else: self._logger = logger self._started: Optional[datetime.datetime] = None self._action = action def __enter__(self) -> None: self._started = datetime.datetime.now() def __exit__(self, _exc_type: Optional[type[BaseException]], _exc_val: Optional[BaseException], _exc_tb: Optional[types.TracebackType]) -> None: assert self._started is not None finished = datetime.datetime.now() delta = (finished - self._started).total_seconds() self._logger.debug("{} took {} seconds.".format( self._action.capitalize(), delta)) def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: @functools.wraps(func) def wrapped(*args: Any, **kwargs: Any) -> Any: """Call the original function.""" with self: return func(*args, **kwargs) return wrapped def _get_widgets(qapp: QApplication) -> Sequence[str]: """Get a string list of all widgets.""" widgets = qapp.allWidgets() widgets.sort(key=repr) return [repr(w) for w in widgets] def _get_pyqt_objects(lines: MutableSequence[str], obj: QObject, depth: int = 0) -> None: """Recursive method for get_all_objects to get Qt objects.""" for kid in obj.findChildren(QObject, '', Qt.FindChildOption.FindDirectChildrenOnly): lines.append(' ' * depth + repr(kid)) _get_pyqt_objects(lines, kid, depth + 1) def get_all_objects(start_obj: QObject = None, *, qapp: QApplication = None) -> str: """Get all children of an object recursively as a string.""" if qapp is None: assert objects.qapp is not None qapp = objects.qapp output = [''] widget_lines = _get_widgets(qapp) widget_lines = [' ' + e for e in widget_lines] widget_lines.insert(0, "Qt widgets - {} objects:".format( len(widget_lines))) output += widget_lines if start_obj is None: start_obj = qapp pyqt_lines: list[str] = [] _get_pyqt_objects(pyqt_lines, start_obj) pyqt_lines = [' ' + e for e in pyqt_lines] pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(len(pyqt_lines))) output += [''] output += pyqt_lines output += objreg.dump_objects() return '\n'.join(output) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/docutils.py0000644000175100017510000001353415102145205021474 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities used for the documentation and built-in help.""" import re import sys import inspect import os.path import collections import enum from typing import Any, Optional, Union from collections.abc import MutableMapping, Callable import qutebrowser from qutebrowser.utils import log, utils def is_git_repo() -> bool: """Check if we're running from a git repository.""" gitfolder = os.path.join(qutebrowser.basedir, os.path.pardir, '.git') return os.path.isdir(gitfolder) def docs_up_to_date(path: str) -> bool: """Check if the generated html documentation is up to date. Args: path: The path of the document to check. Return: True if they are up to date or we couldn't check. False if they are outdated. """ if hasattr(sys, 'frozen') or not is_git_repo(): return True html_path = os.path.join(qutebrowser.basedir, 'html', 'doc', path) filename = os.path.splitext(path)[0] asciidoc_path = os.path.join(qutebrowser.basedir, os.path.pardir, 'doc', 'help', filename + '.asciidoc') try: html_time = os.path.getmtime(html_path) asciidoc_time = os.path.getmtime(asciidoc_path) except FileNotFoundError: return True return asciidoc_time <= html_time class DocstringParser: """Generate documentation based on a docstring of a command handler. The docstring needs to follow the format described in doc/contributing. Attributes: _state: The current state of the parser state machine. _cur_arg_name: The name of the argument we're currently handling. _short_desc_parts: The short description of the function as list. _long_desc_parts: The long description of the function as list. short_desc: The short description of the function. long_desc: The long description of the function. arg_descs: A dict of argument names to their descriptions """ class State(enum.Enum): """The current state of the parser.""" short = enum.auto() desc = enum.auto() desc_hidden = enum.auto() arg_start = enum.auto() arg_inside = enum.auto() misc = enum.auto() def __init__(self, func: Callable[..., Any]) -> None: """Constructor. Args: func: The function to parse the docstring for. """ self._state = self.State.short self._cur_arg_name: Optional[str] = None self._short_desc_parts: list[str] = [] self._long_desc_parts: list[str] = [] self.arg_descs: MutableMapping[ str, Union[str, list[str]]] = collections.OrderedDict() doc = inspect.getdoc(func) handlers = { self.State.short: self._parse_short, self.State.desc: self._parse_desc, self.State.desc_hidden: self._skip, self.State.arg_start: self._parse_arg_start, self.State.arg_inside: self._parse_arg_inside, self.State.misc: self._skip, } if doc is None: if sys.flags.optimize < 2: log.commands.warning( "Function {}() from {} has no docstring".format( utils.qualname(func), inspect.getsourcefile(func))) self.long_desc = "" self.short_desc = "" return for line in doc.splitlines(): handler = handlers[self._state] stop = handler(line) if stop: break for k, v in self.arg_descs.items(): desc = ' '.join(v) desc = re.sub(r', or None($|\.)', r'\1', desc) desc = re.sub(r', or None', r', or not given', desc) self.arg_descs[k] = desc self.long_desc = ' '.join(self._long_desc_parts) self.short_desc = ' '.join(self._short_desc_parts) def _process_arg(self, line: str) -> None: """Helper method to process a line like 'fooarg: Blah blub'.""" self._cur_arg_name, argdesc = line.split(':', maxsplit=1) self._cur_arg_name = self._cur_arg_name.strip().lstrip('*') self.arg_descs[self._cur_arg_name] = [argdesc.strip()] def _skip(self, line: str) -> None: """Handler to ignore everything until we get 'Args:'.""" if line.startswith('Args:'): self._state = self.State.arg_start def _parse_short(self, line: str) -> None: """Parse the short description (first block) in the docstring.""" if not line: self._state = self.State.desc else: self._short_desc_parts.append(line.strip()) def _parse_desc(self, line: str) -> None: """Parse the long description in the docstring.""" if line.startswith('Args:'): self._state = self.State.arg_start elif line.strip() == '//' or line.startswith('Attributes:'): self._state = self.State.desc_hidden elif line.strip(): self._long_desc_parts.append(line.strip()) def _parse_arg_start(self, line: str) -> None: """Parse first argument line.""" self._process_arg(line) self._state = self.State.arg_inside def _parse_arg_inside(self, line: str) -> bool: """Parse subsequent argument lines.""" argname = self._cur_arg_name assert argname is not None descs = self.arg_descs[argname] assert isinstance(descs, list) if re.fullmatch(r'[A-Z][a-z]+:', line): if not descs[-1].strip(): del descs[-1] return True elif not line.strip(): descs.append('\n\n') elif line[4:].startswith(' '): descs.append(line.strip() + '\n') else: self._process_arg(line) return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/error.py0000644000175100017510000000375615102145205021004 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Tools related to error printing/displaying.""" from qutebrowser.qt.widgets import QMessageBox from qutebrowser.utils import log, utils def _get_name(exc: BaseException) -> str: """Get a suitable exception name as a string.""" prefixes = ['qutebrowser.', 'builtins.'] name = utils.qualname(exc.__class__) for prefix in prefixes: if name.startswith(prefix): name = name.removeprefix(prefix) break return name def handle_fatal_exc(exc: BaseException, title: str, *, no_err_windows: bool, pre_text: str = '', post_text: str = '') -> None: """Handle a fatal "expected" exception by displaying an error box. If --no-err-windows is given as argument, the text is logged to the error logger instead. Args: exc: The Exception object being handled. no_err_windows: Show text in log instead of error window. title: The title to be used for the error message. pre_text: The text to be displayed before the exception text. post_text: The text to be displayed after the exception text. """ if no_err_windows: lines = [ "Handling fatal {} with --no-err-windows!".format(_get_name(exc)), "", "title: {}".format(title), "pre_text: {}".format(pre_text), "post_text: {}".format(post_text), "exception text: {}".format(str(exc) or 'none'), ] log.misc.error('\n'.join(lines)) else: log.misc.error("Fatal exception:") if pre_text: msg_text = '{}: {}'.format(pre_text, exc) else: msg_text = str(exc) if post_text: msg_text += '\n\n{}'.format(post_text) msgbox = QMessageBox(QMessageBox.Icon.Critical, title, msg_text) msgbox.exec() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/javascript.py0000644000175100017510000000512115102145205022005 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities related to javascript interaction.""" from typing import Union from collections.abc import Sequence _InnerJsArgType = Union[None, str, bool, int, float] _JsArgType = Union[_InnerJsArgType, Sequence[_InnerJsArgType]] def string_escape(text: str) -> str: """Escape values special to javascript in strings. With this we should be able to use something like: elem.evaluateJavaScript("this.value='{}'".format(string_escape(...))) And all values should work. """ # This is a list of tuples because order matters, and using OrderedDict # makes no sense because we don't actually need dict-like properties. replacements = ( ('\\', r'\\'), # First escape all literal \ signs as \\. ("'", r"\'"), # Then escape ' and " as \' and \". ('"', r'\"'), # (note it won't hurt when we escape the wrong one). ('\n', r'\n'), # We also need to escape newlines for some reason. ('\r', r'\r'), ('\x00', r'\x00'), ('\ufeff', r'\ufeff'), # https://stackoverflow.com/questions/2965293/ ('\u2028', r'\u2028'), ('\u2029', r'\u2029'), ) for orig, repl in replacements: text = text.replace(orig, repl) return text def to_js(arg: _JsArgType) -> str: """Convert the given argument so it's the equivalent in JS.""" if arg is None: return 'undefined' elif isinstance(arg, str): return '"{}"'.format(string_escape(arg)) elif isinstance(arg, bool): return str(arg).lower() elif isinstance(arg, (int, float)): return str(arg) elif isinstance(arg, list): return '[{}]'.format(', '.join(to_js(e) for e in arg)) else: raise TypeError("Don't know how to handle {!r} of type {}!".format( arg, type(arg).__name__)) def assemble(module: str, function: str, *args: _JsArgType) -> str: """Assemble a javascript file and a function call.""" js_args = ', '.join(to_js(arg) for arg in args) if module == 'window': parts = ['window', function] else: parts = ['window', '_qutebrowser', module, function] code = '"use strict";\n{}({});'.format('.'.join(parts), js_args) return code def wrap_global(name: str, *sources: str) -> str: """Wrap a script using window._qutebrowser.""" from qutebrowser.utils import jinja # circular import template = jinja.js_environment.get_template('global_wrapper.js') return template.render(code='\n'.join(sources), name=name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/jinja.py0000644000175100017510000001167015102145205020740 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities related to jinja2.""" import os import os.path import posixpath import functools import contextlib import html from typing import Any from collections.abc import Iterator, Callable import jinja2 import jinja2.nodes from qutebrowser.qt.core import QUrl from qutebrowser.utils import utils, urlutils, log, qtutils, resources from qutebrowser.misc import debugcachestats html_fallback = """ Error while loading template

The %FILE% template could not be found!
Please check your qutebrowser installation

%ERROR%

""" class Loader(jinja2.BaseLoader): """Jinja loader which uses resources.read_file to load templates. Attributes: _subdir: The subdirectory to find templates in. """ def __init__(self, subdir: str) -> None: self._subdir = subdir def get_source( self, _env: jinja2.Environment, template: str ) -> tuple[str, str, Callable[[], bool]]: path = os.path.join(self._subdir, template) try: source = resources.read_file(path) except OSError as e: source = html_fallback.replace("%ERROR%", html.escape(str(e))) source = source.replace("%FILE%", html.escape(template)) log.misc.exception("The {} template could not be loaded from {}" .format(template, path)) # Currently we don't implement auto-reloading, so we always return True # for up-to-date. return source, path, lambda: True class Environment(jinja2.Environment): """Our own jinja environment which is more strict.""" def __init__(self) -> None: super().__init__(loader=Loader('html'), autoescape=lambda _name: self._autoescape, undefined=jinja2.StrictUndefined) self.globals['resource_url'] = self._resource_url self.globals['file_url'] = urlutils.file_url self.globals['data_url'] = self._data_url self.globals['qcolor_to_qsscolor'] = qtutils.qcolor_to_qsscolor self._autoescape = True @contextlib.contextmanager def no_autoescape(self) -> Iterator[None]: """Context manager to temporarily turn off autoescaping.""" self._autoescape = False yield self._autoescape = True def _resource_url(self, path: str) -> str: """Load qutebrowser resource files. Arguments: path: The relative path to the resource. """ assert not posixpath.isabs(path), path url = QUrl('qute://resource') url.setPath('/' + path) urlutils.ensure_valid(url) urlstr = url.toString(urlutils.FormatOption.ENCODED) return urlstr def _data_url(self, path: str) -> str: """Get a data: url for the broken qutebrowser logo.""" data = resources.read_file_binary(path) mimetype = utils.guess_mimetype(path) return urlutils.data_url(mimetype, data).toString() def getattr(self, obj: Any, attribute: str) -> Any: """Override jinja's getattr() to be less clever. This means it doesn't fall back to __getitem__, and it doesn't hide AttributeError. """ return getattr(obj, attribute) def render(template: str, **kwargs: Any) -> str: """Render the given template and pass the given arguments to it.""" return environment.get_template(template).render(**kwargs) environment = Environment() js_environment = jinja2.Environment(loader=Loader('javascript')) @debugcachestats.register() @functools.lru_cache def template_config_variables(template: str) -> frozenset[str]: """Return the config variables used in the template.""" unvisted_nodes: list[jinja2.nodes.Node] = [environment.parse(template)] result: set[str] = set() while unvisted_nodes: node = unvisted_nodes.pop() if not isinstance(node, jinja2.nodes.Getattr): unvisted_nodes.extend(node.iter_child_nodes()) continue # List of attribute names in reverse order. # For example it's ['ab', 'c', 'd'] for 'conf.d.c.ab'. attrlist: list[str] = [] while isinstance(node, jinja2.nodes.Getattr): attrlist.append(node.attr) node = node.node if isinstance(node, jinja2.nodes.Name): if node.name == 'conf': result.add('.'.join(reversed(attrlist))) # otherwise, the node is a Name node so it doesn't have any # child nodes else: unvisted_nodes.append(node) from qutebrowser.config import config for option in result: config.instance.ensure_has_opt(option) return frozenset(result) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/log.py0000644000175100017510000005073715102145205020435 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Loggers and utilities related to logging.""" import os import sys import html as pyhtml import logging import contextlib import collections import copy import warnings import json import inspect import argparse from typing import (TYPE_CHECKING, Any, Optional, Union, TextIO, Literal, cast) from collections.abc import Iterator, Mapping, MutableSequence # NOTE: This is a Qt-free zone! All imports related to Qt logging should be done in # qutebrowser.utils.qtlog (see https://github.com/qutebrowser/qutebrowser/issues/7769). # Optional imports try: import colorama except ImportError: colorama = None # type: ignore[assignment] if TYPE_CHECKING: from qutebrowser.config import config as configmodule _log_inited = False _args: Optional[argparse.Namespace] = None COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white'] COLOR_ESCAPES = {color: '\033[{}m'.format(i) for i, color in enumerate(COLORS, start=30)} RESET_ESCAPE = '\033[0m' # Log formats to use. SIMPLE_FMT = ('{green}{asctime:8}{reset} {log_color}{levelname}{reset}: ' '{message}') EXTENDED_FMT = ('{green}{asctime:8}{reset} ' '{log_color}{levelname:8}{reset} ' '{cyan}{name:10} {module}:{funcName}:{lineno}{reset} ' '{log_color}{message}{reset}') EXTENDED_FMT_HTML = ( '' '
%(green)s%(asctime)-8s%(reset)s
' '
%(log_color)s%(levelname)-8s%(reset)s
' '%(cyan)s%(name)-10s' '
%(cyan)s%(module)s:%(funcName)s:%(lineno)s%(reset)s
' '
%(log_color)s%(message)s%(reset)s
' '' ) DATEFMT = '%H:%M:%S' LOG_COLORS = { 'VDEBUG': 'white', 'DEBUG': 'white', 'INFO': 'green', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red', } # We first monkey-patch logging to support our VDEBUG level before getting the # loggers. Based on https://stackoverflow.com/a/13638084 # mypy doesn't know about this, so we need to ignore it. VDEBUG_LEVEL = 9 logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG') logging.VDEBUG = VDEBUG_LEVEL # type: ignore[attr-defined] LOG_LEVELS = { 'VDEBUG': logging.VDEBUG, # type: ignore[attr-defined] 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'WARNING': logging.WARNING, 'ERROR': logging.ERROR, 'CRITICAL': logging.CRITICAL, } def vdebug(self: logging.Logger, msg: str, *args: Any, **kwargs: Any) -> None: """Log with a VDEBUG level. VDEBUG is used when a debug message is rather verbose, and probably of little use to the end user or for post-mortem debugging, i.e. the content probably won't change unless the code changes. """ if self.isEnabledFor(VDEBUG_LEVEL): # pylint: disable=protected-access self._log(VDEBUG_LEVEL, msg, args, **kwargs) # pylint: enable=protected-access logging.Logger.vdebug = vdebug # type: ignore[attr-defined] # The different loggers used. statusbar = logging.getLogger('statusbar') completion = logging.getLogger('completion') destroy = logging.getLogger('destroy') modes = logging.getLogger('modes') webview = logging.getLogger('webview') mouse = logging.getLogger('mouse') misc = logging.getLogger('misc') url = logging.getLogger('url') procs = logging.getLogger('procs') commands = logging.getLogger('commands') init = logging.getLogger('init') signals = logging.getLogger('signals') hints = logging.getLogger('hints') keyboard = logging.getLogger('keyboard') downloads = logging.getLogger('downloads') js = logging.getLogger('js') # Javascript console messages qt = logging.getLogger('qt') # Warnings produced by Qt ipc = logging.getLogger('ipc') shlexer = logging.getLogger('shlexer') save = logging.getLogger('save') message = logging.getLogger('message') config = logging.getLogger('config') sessions = logging.getLogger('sessions') webelem = logging.getLogger('webelem') prompt = logging.getLogger('prompt') network = logging.getLogger('network') sql = logging.getLogger('sql') greasemonkey = logging.getLogger('greasemonkey') extensions = logging.getLogger('extensions') LOGGER_NAMES = [ 'statusbar', 'completion', 'init', 'url', 'destroy', 'modes', 'webview', 'misc', 'mouse', 'procs', 'hints', 'keyboard', 'commands', 'signals', 'downloads', 'js', 'qt', 'ipc', 'shlexer', 'save', 'message', 'config', 'sessions', 'webelem', 'prompt', 'network', 'sql', 'greasemonkey', 'extensions', ] ram_handler: Optional['RAMHandler'] = None console_handler: Optional[logging.Handler] = None console_filter: Optional["LogFilter"] = None def stub(suffix: str = '') -> None: """Show a STUB: message for the calling function.""" try: function = inspect.stack()[1][3] except IndexError: # pragma: no cover misc.exception("Failed to get stack") function = '' text = "STUB: {}".format(function) if suffix: text = '{} ({})'.format(text, suffix) misc.warning(text) def init_log(args: argparse.Namespace) -> None: """Init loggers based on the argparse namespace passed.""" level = (args.loglevel or "info").upper() try: numeric_level = getattr(logging, level) except AttributeError: raise ValueError("Invalid log level: {}".format(args.loglevel)) if numeric_level > logging.DEBUG and args.debug: numeric_level = logging.DEBUG console, ram = _init_handlers(numeric_level, args.color, args.force_color, args.json_logging, args.loglines) root = logging.getLogger() global console_filter if console is not None: console_filter = LogFilter.parse(args.logfilter) console.addFilter(console_filter) root.addHandler(console) if ram is not None: root.addHandler(ram) else: # If we add no handler, we shouldn't process non visible logs at all # # disable blocks the current level (while setHandler shows the current # level), so -1 to avoid blocking handled messages. logging.disable(numeric_level - 1) global _log_inited, _args _args = args root.setLevel(logging.NOTSET) logging.captureWarnings(True) _init_py_warnings() _log_inited = True def _init_py_warnings() -> None: """Initialize Python warning handling.""" assert _args is not None warnings.simplefilter('error' if 'werror' in _args.debug_flags else 'default') warnings.filterwarnings('ignore', module='pdb', category=ResourceWarning) # This happens in many qutebrowser dependencies... warnings.filterwarnings('ignore', category=DeprecationWarning, message=r"Using or importing the ABCs from " r"'collections' instead of from 'collections.abc' " r"is deprecated.*") # PyQt 5.15/6.2/6.3/6.4: # https://riverbankcomputing.com/news/SIP_v6.7.12_Released warnings.filterwarnings( 'ignore', category=DeprecationWarning, message=( r"sipPyTypeDict\(\) is deprecated, the extension module should use " r"sipPyTypeDictRef\(\) instead" ) ) @contextlib.contextmanager def py_warning_filter( action: Literal['default', 'error', 'ignore', 'always', 'module', 'once'] = 'ignore', **kwargs: Any, ) -> Iterator[None]: """Contextmanager to temporarily disable certain Python warnings.""" warnings.filterwarnings(action, **kwargs) yield if _log_inited: _init_py_warnings() def _init_handlers( level: int, color: bool, force_color: bool, json_logging: bool, ram_capacity: int ) -> tuple[Optional["logging.StreamHandler[TextIO]"], Optional['RAMHandler']]: """Init log handlers. Args: level: The numeric logging level. color: Whether to use color if available. force_color: Force colored output. json_logging: Output log lines in JSON (this disables all colors). """ global ram_handler global console_handler console_fmt, ram_fmt, html_fmt, use_colorama = _init_formatters( level, color, force_color, json_logging) if sys.stderr is None: console_handler = None else: strip = False if force_color else None if use_colorama: stream = cast(TextIO, colorama.AnsiToWin32(sys.stderr, strip=strip)) else: stream = sys.stderr console_handler = logging.StreamHandler(stream) console_handler.setLevel(level) console_handler.setFormatter(console_fmt) if ram_capacity == 0: ram_handler = None else: ram_handler = RAMHandler(capacity=ram_capacity) ram_handler.setLevel(logging.DEBUG) ram_handler.setFormatter(ram_fmt) ram_handler.html_formatter = html_fmt return console_handler, ram_handler def get_console_format(level: int) -> str: """Get the log format the console logger should use. Args: level: The numeric logging level. Return: Format of the requested level. """ return EXTENDED_FMT if level <= logging.DEBUG else SIMPLE_FMT def _init_formatters( level: int, color: bool, force_color: bool, json_logging: bool, ) -> tuple[ Union['JSONFormatter', 'ColoredFormatter', None], 'ColoredFormatter', 'HTMLFormatter', bool, ]: """Init log formatters. Args: level: The numeric logging level. color: Whether to use color if available. force_color: Force colored output. json_logging: Format lines as JSON (disables all color). Return: A (console_formatter, ram_formatter, use_colorama) tuple. console_formatter/ram_formatter: logging.Formatter instances. use_colorama: Whether to use colorama. """ console_fmt = get_console_format(level) ram_formatter = ColoredFormatter(EXTENDED_FMT, DATEFMT, '{', use_colors=False) html_formatter = HTMLFormatter(EXTENDED_FMT_HTML, DATEFMT, log_colors=LOG_COLORS) use_colorama = False if sys.stderr is None: console_formatter = None return console_formatter, ram_formatter, html_formatter, use_colorama if json_logging: json_formatter = JSONFormatter() return json_formatter, ram_formatter, html_formatter, use_colorama color_supported = os.name == 'posix' or colorama if color_supported and (sys.stderr.isatty() or force_color) and color: use_colors = True if colorama and os.name != 'posix': use_colorama = True else: use_colors = False console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{', use_colors=use_colors) return console_formatter, ram_formatter, html_formatter, use_colorama def change_console_formatter(level: int) -> None: """Change console formatter based on level. Args: level: The numeric logging level """ assert console_handler is not None old_formatter = console_handler.formatter if isinstance(old_formatter, ColoredFormatter): console_fmt = get_console_format(level) console_formatter = ColoredFormatter( console_fmt, DATEFMT, '{', use_colors=old_formatter.use_colors) console_handler.setFormatter(console_formatter) else: # Same format for all levels assert isinstance(old_formatter, JSONFormatter), old_formatter def init_from_config(conf: 'configmodule.ConfigContainer') -> None: """Initialize logging settings from the config. init_log is called before the config module is initialized, so config-based initialization cannot be performed there. Args: conf: The global ConfigContainer. This is passed rather than accessed via the module to avoid a cyclic import. """ assert _args is not None if _args.debug: init.debug("--debug flag overrides log configs") return if ram_handler: ramlevel = conf.logging.level.ram init.debug("Configuring RAM loglevel to %s", ramlevel) ram_handler.setLevel(LOG_LEVELS[ramlevel.upper()]) if console_handler: consolelevel = conf.logging.level.console if _args.loglevel: init.debug("--loglevel flag overrides logging.level.console") else: init.debug("Configuring console loglevel to %s", consolelevel) level = LOG_LEVELS[consolelevel.upper()] console_handler.setLevel(level) change_console_formatter(level) class InvalidLogFilterError(Exception): """Raised when an invalid filter string is passed to LogFilter.parse().""" def __init__(self, names: set[str]): invalid = names - set(LOGGER_NAMES) super().__init__("Invalid log category {} - valid categories: {}" .format(', '.join(sorted(invalid)), ', '.join(LOGGER_NAMES))) class LogFilter(logging.Filter): """Filter to filter log records based on the commandline argument. The default Filter only supports one name to show - we support a comma-separated list instead. Attributes: names: A set of logging names to allow. negated: Whether names is a set of names to log or to suppress. only_debug: Only filter debug logs, always show anything more important than debug. """ def __init__(self, names: set[str], *, negated: bool = False, only_debug: bool = True) -> None: super().__init__() self.names = names self.negated = negated self.only_debug = only_debug @classmethod def parse(cls, filter_str: Optional[str], *, only_debug: bool = True) -> 'LogFilter': """Parse a log filter from a string.""" if filter_str is None or filter_str == 'none': names = set() negated = False else: filter_str = filter_str.lower() if filter_str.startswith('!'): negated = True filter_str = filter_str[1:] else: negated = False names = {e.strip() for e in filter_str.split(',')} if not names.issubset(LOGGER_NAMES): raise InvalidLogFilterError(names) return cls(names=names, negated=negated, only_debug=only_debug) def update_from(self, other: 'LogFilter') -> None: """Update this filter's properties from another filter.""" self.names = other.names self.negated = other.negated self.only_debug = other.only_debug def filter(self, record: logging.LogRecord) -> bool: """Determine if the specified record is to be logged.""" if not self.names: # No filter return True elif record.levelno > logging.DEBUG and self.only_debug: # More important than DEBUG, so we won't filter at all return True elif record.name.split('.')[0] in self.names: return not self.negated return self.negated class RAMHandler(logging.Handler): """Logging handler which keeps the messages in a deque in RAM. Loosely based on logging.BufferingHandler which is unsuitable because it uses a simple list rather than a deque. Attributes: _data: A deque containing the logging records. """ def __init__(self, capacity: int) -> None: super().__init__() self.html_formatter: Optional[HTMLFormatter] = None if capacity != -1: self._data: MutableSequence[logging.LogRecord] = collections.deque( maxlen=capacity ) else: self._data = collections.deque() def emit(self, record: logging.LogRecord) -> None: self._data.append(record) def dump_log(self, html: bool = False, level: str = 'vdebug', logfilter: LogFilter = None) -> str: """Dump the complete formatted log data as string. FIXME: We should do all the HTML formatting via jinja2. (probably obsolete when moving to a widget for logging, https://github.com/qutebrowser/qutebrowser/issues/34 Args: html: Produce HTML rather than plaintext output. level: The minimal loglevel to show. logfilter: A LogFilter instance used to filter log lines. """ minlevel = LOG_LEVELS.get(level.upper(), VDEBUG_LEVEL) if logfilter is None: logfilter = LogFilter(set()) if html: assert self.html_formatter is not None fmt = self.html_formatter.format else: fmt = self.format self.acquire() try: lines = [fmt(record) for record in self._data if record.levelno >= minlevel and logfilter.filter(record)] finally: self.release() return '\n'.join(lines) def change_log_capacity(self, capacity: int) -> None: self._data = collections.deque(self._data, maxlen=capacity) class ColoredFormatter(logging.Formatter): """Logging formatter to output colored logs. Attributes: use_colors: Whether to do colored logging or not. """ def __init__(self, fmt: str, datefmt: str, style: Literal["%", "{", "$"], *, use_colors: bool) -> None: super().__init__(fmt, datefmt, style) self.use_colors = use_colors def format(self, record: logging.LogRecord) -> str: if self.use_colors: color_dict = dict(COLOR_ESCAPES) color_dict['reset'] = RESET_ESCAPE log_color = LOG_COLORS[record.levelname] color_dict['log_color'] = COLOR_ESCAPES[log_color] else: color_dict = dict.fromkeys(COLOR_ESCAPES, "") color_dict['reset'] = '' color_dict['log_color'] = '' record.__dict__.update(color_dict) return super().format(record) class HTMLFormatter(logging.Formatter): """Formatter for HTML-colored log messages. Attributes: _log_colors: The colors to use for logging levels. _colordict: The colordict passed to the logger. """ def __init__(self, fmt: str, datefmt: str, log_colors: Mapping[str, str]) -> None: """Constructor. Args: fmt: The format string to use. datefmt: The date format to use. log_colors: The colors to use for logging levels. """ super().__init__(fmt, datefmt) self._log_colors: Mapping[str, str] = log_colors self._colordict: Mapping[str, str] = {} # We could solve this nicer by using CSS, but for this simple case this # works. for color in COLORS: self._colordict[color] = ''.format(color) self._colordict['reset'] = '' def format(self, record: logging.LogRecord) -> str: record_clone = copy.copy(record) record_clone.__dict__.update(self._colordict) if record_clone.levelname in self._log_colors: color = self._log_colors[record_clone.levelname] color_str = self._colordict[color] record_clone.log_color = color_str else: record_clone.log_color = '' for field in ['msg', 'filename', 'funcName', 'levelname', 'module', 'name', 'pathname', 'processName', 'threadName']: data = str(getattr(record_clone, field)) setattr(record_clone, field, pyhtml.escape(data)) msg = super().format(record_clone) if not msg.endswith(self._colordict['reset']): msg += self._colordict['reset'] return msg def formatTime(self, record: logging.LogRecord, datefmt: str = None) -> str: out = super().formatTime(record, datefmt) return pyhtml.escape(out) class JSONFormatter(logging.Formatter): """Formatter for JSON-encoded log messages.""" def format(self, record: logging.LogRecord) -> str: obj = {} for field in ['created', 'msecs', 'levelname', 'name', 'module', 'funcName', 'lineno', 'levelno']: obj[field] = getattr(record, field) obj['message'] = record.getMessage() if record.exc_info is not None: obj['traceback'] = super().formatException(record.exc_info) return json.dumps(obj) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/message.py0000644000175100017510000002166615102145205021277 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # Because every method needs to have a log_stack argument # and because we use *args a lot # pylint: disable=unused-argument,differing-param-doc """Message singleton so we don't have to define unneeded signals.""" import dataclasses import traceback from typing import Any, Union, Optional from collections.abc import Iterable, Callable from qutebrowser.qt.core import pyqtSignal, pyqtBoundSignal, QObject from qutebrowser.utils import usertypes, log @dataclasses.dataclass class MessageInfo: """Information associated with a message to be displayed.""" level: usertypes.MessageLevel text: str replace: Optional[str] = None rich: bool = False def _log_stack(typ: str, stack: str) -> None: """Log the given message stacktrace. Args: typ: The type of the message. stack: An optional stacktrace. """ lines = stack.splitlines() stack_text = '\n'.join(line.rstrip() for line in lines) log.message.debug("Stack for {} message:\n{}".format(typ, stack_text)) def error( message: str, *, stack: str = None, replace: str = None, rich: bool = False, ) -> None: """Display an error message. Args: message: The message to show. stack: The stack trace to show (if any). replace: Replace existing messages which are still being shown. rich: Show message as rich text. """ if stack is None: stack = ''.join(traceback.format_stack()) typ = 'error' else: typ = 'error (from exception)' _log_stack(typ, stack) log.message.error(message) global_bridge.show( level=usertypes.MessageLevel.error, text=message, replace=replace, rich=rich, ) def warning(message: str, *, replace: str = None, rich: bool = False) -> None: """Display a warning message. Args: message: The message to show. replace: Replace existing messages which are still being shown. rich: Show message as rich text. """ _log_stack('warning', ''.join(traceback.format_stack())) log.message.warning(message) global_bridge.show( level=usertypes.MessageLevel.warning, text=message, replace=replace, rich=rich, ) def info(message: str, *, replace: str = None, rich: bool = False) -> None: """Display an info message. Args: message: The message to show. replace: Replace existing messages which are still being shown. rich: Show message as rich text. """ log.message.info(message) global_bridge.show( level=usertypes.MessageLevel.info, text=message, replace=replace, rich=rich, ) def _build_question(title: str, text: str = None, *, mode: usertypes.PromptMode, default: Union[None, bool, str] = None, abort_on: Iterable[pyqtBoundSignal] = (), url: str = None, option: bool = None) -> usertypes.Question: """Common function for ask/ask_async.""" question = usertypes.Question() question.title = title question.text = text question.mode = mode question.default = default question.url = url if option is not None: if mode != usertypes.PromptMode.yesno: raise ValueError("Can only 'option' with PromptMode.yesno") if url is None: raise ValueError("Need 'url' given when 'option' is given") question.option = option for sig in abort_on: sig.connect(question.abort) return question def ask(*args: Any, **kwargs: Any) -> Any: """Ask a modular question in the statusbar (blocking). Args: title: The message to display to the user. mode: A PromptMode. default: The default value to display. text: Additional text to show option: The option for always/never question answers. Only available with PromptMode.yesno. abort_on: A list of signals which abort the question if emitted. Return: The answer the user gave or None if the prompt was cancelled. """ question = _build_question(*args, **kwargs) global_bridge.ask(question, blocking=True) answer = question.answer question.deleteLater() return answer def ask_async(title: str, mode: usertypes.PromptMode, handler: Callable[[Any], None], **kwargs: Any) -> None: """Ask an async question in the statusbar. Args: title: The message to display to the user. mode: A PromptMode. handler: The function to get called with the answer as argument. default: The default value to display. text: Additional text to show. """ question = _build_question(title, mode=mode, **kwargs) question.answered.connect(handler) question.completed.connect(question.deleteLater) global_bridge.ask(question, blocking=False) _ActionType = Callable[[], Any] def confirm_async(*, yes_action: _ActionType, no_action: _ActionType = None, cancel_action: _ActionType = None, **kwargs: Any) -> usertypes.Question: """Ask a yes/no question to the user and execute the given actions. Args: title: The message to display to the user. yes_action: Callable to be called when the user answered yes. no_action: Callable to be called when the user answered no. cancel_action: Callable to be called when the user cancelled the question. default: True/False to set a default value, or None. option: The option for always/never question answers. text: Additional text to show. Return: The question object. """ kwargs['mode'] = usertypes.PromptMode.yesno question = _build_question(**kwargs) question.answered_yes.connect(yes_action) if no_action is not None: question.answered_no.connect(no_action) if cancel_action is not None: question.cancelled.connect(cancel_action) question.completed.connect(question.deleteLater) global_bridge.ask(question, blocking=False) return question class GlobalMessageBridge(QObject): """Global (not per-window) message bridge for errors/infos/warnings. Attributes: _connected: Whether a slot is connected and we can show messages. _cache: Messages shown while we were not connected. Signals: show_message: Show a message arg 0: A MessageLevel member arg 1: The text to show arg 2: A message ID (as string) to replace, or None. prompt_done: Emitted when a prompt was answered somewhere. ask_question: Ask a question to the user. arg 0: The Question object to ask. arg 1: Whether to block (True) or ask async (False). IMPORTANT: Slots need to be connected to this signal via a Qt.ConnectionType.DirectConnection! mode_left: Emitted when a keymode was left in any window. """ show_message = pyqtSignal(MessageInfo) prompt_done = pyqtSignal(usertypes.KeyMode) ask_question = pyqtSignal(usertypes.Question, bool) mode_left = pyqtSignal(usertypes.KeyMode) clear_messages = pyqtSignal() def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._connected = False self._cache: list[MessageInfo] = [] def ask(self, question: usertypes.Question, blocking: bool, *, log_stack: bool = False) -> None: """Ask a question to the user. Note this method doesn't return the answer, it only blocks. The caller needs to construct a Question object and get the answer. Args: question: A Question object. blocking: Whether to return immediately or wait until the question is answered. log_stack: ignored """ self.ask_question.emit(question, blocking) def show( self, level: usertypes.MessageLevel, text: str, replace: str = None, rich: bool = False, ) -> None: """Show the given message.""" msg = MessageInfo(level=level, text=text, replace=replace, rich=rich) if self._connected: self.show_message.emit(msg) else: self._cache.append(msg) def flush(self) -> None: """Flush messages which accumulated while no handler was connected. This is so we don't miss messages shown during some early init phase. It needs to be called once the show_message signal is connected. """ self._connected = True for msg in self._cache: self.show(**dataclasses.asdict(msg)) self._cache = [] global_bridge = GlobalMessageBridge() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/objreg.py0000644000175100017510000002715515102145205021122 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """The global object registry and related utility functions.""" import collections import functools from typing import (TYPE_CHECKING, Any, Optional, Union) from collections.abc import MutableMapping, MutableSequence, Sequence, Callable from qutebrowser.qt.core import QObject, QTimer from qutebrowser.qt.widgets import QApplication from qutebrowser.qt.widgets import QWidget from qutebrowser.utils import log, usertypes, utils if TYPE_CHECKING: from qutebrowser.mainwindow import mainwindow _WindowTab = Union[str, int, None] class RegistryUnavailableError(Exception): """Exception raised when a certain registry does not exist yet.""" class NoWindow(Exception): """Exception raised by last_window if no window is available.""" class CommandOnlyError(Exception): """Raised when an object is requested which is used for commands only.""" _IndexType = Union[str, int] # UserDict is only generic in Python 3.9+ class ObjectRegistry(collections.UserDict): # type: ignore[type-arg] """A registry of long-living objects in qutebrowser. Inspired by the eric IDE code (E5Gui/E5Application.py). Attributes: _partial_objs: A dictionary of the connected partial objects. command_only: Objects which are only registered for commands. """ def __init__(self) -> None: super().__init__() self._partial_objs: MutableMapping[_IndexType, Callable[[], None]] = {} self.command_only: MutableSequence[str] = [] def __setitem__(self, name: _IndexType, obj: Any) -> None: """Register an object in the object registry. Sets a slot to remove QObjects when they are destroyed. """ if name is None: raise TypeError("Registering '{}' with name 'None'!".format(obj)) if obj is None: raise TypeError("Registering object None with name '{}'!".format( name)) self._disconnect_destroyed(name) if isinstance(obj, QObject): func = functools.partial(self.on_destroyed, name) obj.destroyed.connect(func) self._partial_objs[name] = func super().__setitem__(name, obj) def __delitem__(self, name: _IndexType) -> None: """Extend __delitem__ to disconnect the destroyed signal.""" self._disconnect_destroyed(name) super().__delitem__(name) def _disconnect_destroyed(self, name: _IndexType) -> None: """Disconnect the destroyed slot if it was connected.""" try: partial_objs = self._partial_objs except AttributeError: # This sometimes seems to happen on CI during # test_history.test_adding_item_during_async_read # and I have no idea why... return if name in partial_objs: func = partial_objs[name] try: self[name].destroyed.disconnect(func) except RuntimeError: # If C++ has deleted the object, the slot is already # disconnected. pass del partial_objs[name] def on_destroyed(self, name: _IndexType) -> None: """Schedule removing of a destroyed QObject. We don't remove the destroyed object immediately because it might still be destroying its children, which might still use the object registry. """ log.destroy.debug("schedule removal: {}".format(name)) QTimer.singleShot(0, functools.partial(self._on_destroyed, name)) def _on_destroyed(self, name: _IndexType) -> None: """Remove a destroyed QObject.""" log.destroy.debug("removed: {}".format(name)) if not hasattr(self, 'data'): # This sometimes seems to happen on CI during # test_history.test_adding_item_during_async_read # and I have no idea why... return try: del self[name] del self._partial_objs[name] except KeyError: pass def dump_objects(self) -> Sequence[str]: """Dump all objects as a list of strings.""" lines = [] for name, obj in self.data.items(): try: obj_repr = repr(obj) except RuntimeError: # Underlying object deleted probably obj_repr = '' suffix = (" (for commands only)" if name in self.command_only else "") lines.append("{}: {}{}".format(name, obj_repr, suffix)) return lines # The registry for global objects global_registry = ObjectRegistry() # The window registry. window_registry = ObjectRegistry() def _get_tab_registry(win_id: _WindowTab, tab_id: _WindowTab) -> ObjectRegistry: """Get the registry of a tab.""" if tab_id is None: raise ValueError("Got tab_id None (win_id {})".format(win_id)) if tab_id == 'current' and win_id is None: window: Optional[QWidget] = QApplication.activeWindow() if window is None or not hasattr(window, 'win_id'): raise RegistryUnavailableError('tab') win_id = window.win_id elif win_id is None: raise TypeError("window is None with scope tab!") if tab_id == 'current': tabbed_browser = get('tabbed-browser', scope='window', window=win_id) tab = tabbed_browser.widget.currentWidget() if tab is None: raise RegistryUnavailableError('window') tab_id = tab.tab_id tab_registry = get('tab-registry', scope='window', window=win_id) try: return tab_registry[tab_id].registry except AttributeError: raise RegistryUnavailableError('tab') def _get_window_registry(window: _WindowTab) -> ObjectRegistry: """Get the registry of a window.""" if window is None: raise TypeError("window is None with scope window!") try: if window == 'current': win: Optional[QWidget] = QApplication.activeWindow() elif window == 'last-focused': win = last_focused_window() else: win = window_registry[window] except (KeyError, NoWindow): win = None if win is None: raise RegistryUnavailableError('window') try: return win.registry # type: ignore[attr-defined] except AttributeError: raise RegistryUnavailableError('window') def _get_registry(scope: str, window: _WindowTab = None, tab: _WindowTab = None) -> ObjectRegistry: """Get the correct registry for a given scope.""" if window is not None and scope not in ['window', 'tab']: raise TypeError("window is set with scope {}".format(scope)) if tab is not None and scope != 'tab': raise TypeError("tab is set with scope {}".format(scope)) if scope == 'global': return global_registry elif scope == 'tab': return _get_tab_registry(window, tab) elif scope == 'window': return _get_window_registry(window) else: raise ValueError("Invalid scope '{}'!".format(scope)) def get(name: str, default: Any = usertypes.UNSET, scope: str = 'global', window: _WindowTab = None, tab: _WindowTab = None, from_command: bool = False) -> Any: """Helper function to get an object. Args: default: A default to return if the object does not exist. """ reg = _get_registry(scope, window, tab) if name in reg.command_only and not from_command: raise CommandOnlyError("{} is only registered for commands" .format(name)) try: return reg[name] except KeyError: if default is not usertypes.UNSET: return default else: raise def register(name: str, obj: Any, *, update: bool = False, scope: str = None, registry: ObjectRegistry = None, window: _WindowTab = None, tab: _WindowTab = None, command_only: bool = False) -> None: """Helper function to register an object. Args: name: The name the object will be registered as. obj: The object to register. update: If True, allows to update an already registered object. """ if scope is not None and registry is not None: raise ValueError("scope ({}) and registry ({}) can't be given at the " "same time!".format(scope, registry)) if registry is not None: reg = registry else: if scope is None: scope = 'global' reg = _get_registry(scope, window, tab) if not update and name in reg: raise KeyError("Object '{}' is already registered ({})!".format( name, repr(reg[name]))) reg[name] = obj if command_only: reg.command_only.append(name) def delete(name: str, scope: str = 'global', window: _WindowTab = None, tab: _WindowTab = None) -> None: """Helper function to unregister an object.""" reg = _get_registry(scope, window, tab) del reg[name] def dump_objects() -> Sequence[str]: """Get all registered objects in all registries as a string.""" blocks = [] lines = [] blocks.append(('global', global_registry.dump_objects())) for win_id in window_registry: registry = _get_registry('window', window=win_id) blocks.append(('window-{}'.format(win_id), registry.dump_objects())) tab_registry = get('tab-registry', scope='window', window=win_id) for tab_id, tab in tab_registry.items(): dump = tab.registry.dump_objects() data = [' ' + line for line in dump] blocks.append((' tab-{}'.format(tab_id), data)) for name, block_data in blocks: lines.append("") lines.append("{} object registry - {} objects:".format( name, len(block_data))) for line in block_data: lines.append(" {}".format(line)) return lines def last_visible_window() -> 'mainwindow.MainWindow': """Get the last visible window, or the last focused window if none.""" try: window = get('last-visible-main-window') except KeyError: return last_focused_window() if window.tabbed_browser.is_shutting_down: return last_focused_window() return window def last_focused_window() -> 'mainwindow.MainWindow': """Get the last focused window, or the last window if none.""" try: window = get('last-focused-main-window') except KeyError: return last_opened_window() if window.tabbed_browser.is_shutting_down: return last_opened_window() return window def _window_by_index(idx: int) -> 'mainwindow.MainWindow': """Get the Nth opened window object.""" if not window_registry: raise NoWindow() key = sorted(window_registry)[idx] return window_registry[key] def last_opened_window() -> 'mainwindow.MainWindow': """Get the last opened window object.""" if not window_registry: raise NoWindow() for idx in range(-1, -(len(window_registry)+1), -1): window = _window_by_index(idx) if not window.tabbed_browser.is_shutting_down: return window raise utils.Unreachable() def first_opened_window() -> 'mainwindow.MainWindow': """Get the first opened window object.""" if not window_registry: raise NoWindow() for idx in range(0, len(window_registry)+1): window = _window_by_index(idx) if not window.tabbed_browser.is_shutting_down: return window raise utils.Unreachable() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/qtlog.py0000644000175100017510000002036015102145205020767 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Loggers and utilities related to Qt logging.""" import argparse import contextlib import faulthandler import logging import sys import traceback from typing import Optional from collections.abc import Iterator from qutebrowser.qt import core as qtcore from qutebrowser.utils import log _args: Optional[argparse.Namespace] = None def init(args: argparse.Namespace) -> None: """Install Qt message handler based on the argparse namespace passed.""" global _args _args = args qtcore.qInstallMessageHandler(qt_message_handler) @qtcore.pyqtSlot() def shutdown_log() -> None: qtcore.qInstallMessageHandler(None) @contextlib.contextmanager def disable_qt_msghandler() -> Iterator[None]: """Contextmanager which temporarily disables the Qt message handler.""" old_handler = qtcore.qInstallMessageHandler(None) try: yield finally: qtcore.qInstallMessageHandler(old_handler) def qt_message_handler(msg_type: qtcore.QtMsgType, context: qtcore.QMessageLogContext, msg: Optional[str]) -> None: """Qt message handler to redirect qWarning etc. to the logging system. Args: msg_type: The level of the message. context: The source code location of the message. msg: The message text. """ # Mapping from Qt logging levels to the matching logging module levels. # Note we map critical to ERROR as it's actually "just" an error, and fatal # to critical. qt_to_logging = { qtcore.QtMsgType.QtDebugMsg: logging.DEBUG, qtcore.QtMsgType.QtWarningMsg: logging.WARNING, qtcore.QtMsgType.QtCriticalMsg: logging.ERROR, qtcore.QtMsgType.QtFatalMsg: logging.CRITICAL, qtcore.QtMsgType.QtInfoMsg: logging.INFO, } # Change levels of some well-known messages to debug so they don't get # shown to the user. # # If a message starts with any text in suppressed_msgs, it's not logged as # error. suppressed_msgs = [ # PNGs in Qt with broken color profile # https://bugreports.qt.io/browse/QTBUG-39788 ('libpng warning: iCCP: Not recognizing known sRGB profile that has ' 'been edited'), 'libpng warning: iCCP: known incorrect sRGB profile', # Hopefully harmless warning 'OpenType support missing for script ', # Error if a QNetworkReply gets two different errors set. Harmless Qt # bug on some pages. # https://bugreports.qt.io/browse/QTBUG-30298 ('QNetworkReplyImplPrivate::error: Internal problem, this method must ' 'only be called once.'), # Sometimes indicates missing text, but most of the time harmless 'load glyph failed ', # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479 ('content-type missing in HTTP POST, defaulting to ' 'application/x-www-form-urlencoded. ' 'Use QNetworkRequest::setHeader() to fix this problem.'), # https://bugreports.qt.io/browse/QTBUG-43118 'Using blocking call!', # Hopefully harmless ('"Method "GetAll" with signature "s" on interface ' '"org.freedesktop.DBus.Properties" doesn\'t exist'), ('"Method \\"GetAll\\" with signature \\"s\\" on interface ' '\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"'), 'WOFF support requires QtWebKit to be built with zlib support.', # Weird Enlightment/GTK X extensions 'QXcbWindow: Unhandled client message: "_E_', 'QXcbWindow: Unhandled client message: "_ECORE_', 'QXcbWindow: Unhandled client message: "_GTK_', # Happens on AppVeyor CI 'SetProcessDpiAwareness failed:', # https://bugreports.qt.io/browse/QTBUG-49174 ('QObject::connect: Cannot connect (null)::stateChanged(' 'QNetworkSession::State) to ' 'QNetworkReplyHttpImpl::_q_networkSessionStateChanged(' 'QNetworkSession::State)'), # https://bugreports.qt.io/browse/QTBUG-53989 ("Image of format '' blocked because it is not considered safe. If " "you are sure it is safe to do so, you can white-list the format by " "setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST="), # Installing Qt from the installer may cause it looking for SSL3 or # OpenSSL 1.0 which may not be available on the system "QSslSocket: cannot resolve ", "QSslSocket: cannot call unresolved function ", # When enabling debugging with QtWebEngine ("Remote debugging server started successfully. Try pointing a " "Chromium-based browser to "), # https://github.com/qutebrowser/qutebrowser/issues/1287 "QXcbClipboard: SelectionRequest too old", # https://github.com/qutebrowser/qutebrowser/issues/2071 'QXcbWindow: Unhandled client message: ""', # https://codereview.qt-project.org/176831 "QObject::disconnect: Unexpected null parameter", # https://bugreports.qt.io/browse/QTBUG-76391 "Attribute Qt::AA_ShareOpenGLContexts must be set before " "QCoreApplication is created.", # Qt 6.4 beta 1: https://bugreports.qt.io/browse/QTBUG-104741 "GL format 0 is not supported", # WORKAROUND https://bugreports.qt.io/browse/QTBUG-137424 ("QObject::disconnect: wildcard call disconnects from destroyed signal of " "QNativeSocketEngine::unnamed"), ] # not using utils.is_mac here, because we can't be sure we can successfully # import the utils module here. if sys.platform == 'darwin': suppressed_msgs += [ # https://bugreports.qt.io/browse/QTBUG-47154 ('virtual void QSslSocketBackendPrivate::transmit() SSLRead ' 'failed with: -9805'), ] if not msg: msg = "Logged empty message!" if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): level = logging.DEBUG elif context.category == "qt.webenginecontext" and ( msg.strip().startswith("GL Type: ") or # Qt 6.3 msg.strip().startswith("GLImplementation:") # Qt 6.2 ): level = logging.DEBUG else: level = qt_to_logging[msg_type] if context.line is None: lineno = -1 # type: ignore[unreachable] else: lineno = context.line if context.function is None: func = 'none' # type: ignore[unreachable] elif ':' in context.function: func = '"{}"'.format(context.function) else: func = context.function if context.category is None or context.category == 'default': name = 'qt' else: name = 'qt-' + context.category if msg.splitlines()[0] == ('This application failed to start because it ' 'could not find or load the Qt platform plugin ' '"xcb".'): # Handle this message specially. msg += ("\n\nOn Archlinux, this should fix the problem:\n" " pacman -S libxkbcommon-x11") faulthandler.disable() assert _args is not None if _args.debug: stack: Optional[str] = ''.join(traceback.format_stack()) else: stack = None record = log.qt.makeRecord(name=name, level=level, fn=context.file, lno=lineno, msg=msg, args=(), exc_info=None, func=func, sinfo=stack) log.qt.handle(record) class QtWarningFilter(logging.Filter): """Filter to filter Qt warnings. Attributes: _pattern: The start of the message. """ def __init__(self, pattern: str) -> None: super().__init__() self._pattern = pattern def filter(self, record: logging.LogRecord) -> bool: """Determine if the specified record is to be logged.""" do_log = not record.msg.strip().startswith(self._pattern) return do_log @contextlib.contextmanager def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]: """Hide Qt warnings matching the given regex.""" log_filter = QtWarningFilter(pattern) logger_obj = logging.getLogger(logger) logger_obj.addFilter(log_filter) try: yield finally: logger_obj.removeFilter(log_filter) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/qtutils.py0000644000175100017510000005502615102145205021355 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Misc. utilities related to Qt. Module attributes: MAXVALS: A dictionary of C/Qt types (as string) mapped to their maximum value. MINVALS: A dictionary of C/Qt types (as string) mapped to their minimum value. MAX_WORLD_ID: The highest world ID allowed by QtWebEngine. """ import io import enum import pathlib import operator import contextlib from typing import (Any, TYPE_CHECKING, BinaryIO, IO, Literal, Optional, Union, Protocol, cast, overload, TypeVar) from collections.abc import Iterator from qutebrowser.qt import machinery, sip from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR, PYQT_VERSION_STR, QObject, QUrl, QLibraryInfo) from qutebrowser.qt.gui import QColor try: from qutebrowser.qt.webkit import qWebKitVersion except ImportError: # pragma: no cover qWebKitVersion = None # type: ignore[assignment] # noqa: N816 if TYPE_CHECKING: from qutebrowser.qt.webkit import QWebHistory from qutebrowser.qt.webenginecore import QWebEngineHistory from qutebrowser.misc import objects from qutebrowser.utils import usertypes, utils MAXVALS = { 'int': 2 ** 31 - 1, 'int64': 2 ** 63 - 1, } MINVALS = { 'int': -(2 ** 31), 'int64': -(2 ** 63), } class QtOSError(OSError): """An OSError triggered by a QIODevice. Attributes: qt_errno: The error attribute of the given QFileDevice, if applicable. """ def __init__(self, dev: QIODevice, msg: str = None) -> None: if msg is None: msg = dev.errorString() self.qt_errno: Optional[QFileDevice.FileError] = None if isinstance(dev, QFileDevice): msg = self._init_filedev(dev, msg) super().__init__(msg) def _init_filedev(self, dev: QFileDevice, msg: str) -> str: self.qt_errno = dev.error() filename = dev.fileName() msg += ": {!r}".format(filename) return msg def version_check(version: str, exact: bool = False, compiled: bool = True) -> bool: """Check if the Qt runtime version is the version supplied or newer. By default this function will check `version` against: 1. the runtime Qt version (from qVersion()) 2. the Qt version that PyQt was compiled against (from QT_VERSION_STR) 3. the PyQt version (from PYQT_VERSION_STR) With `compiled=False` only the runtime Qt version (1) is checked. You can often run older PyQt versions against newer Qt versions, but you won't be able to access any APIs that were only added in the newer Qt version. So if you want to check if a new feature is supported, use the default behavior. If you just want to check the underlying Qt version, pass `compiled=False`. Args: version: The version to check against. exact: if given, check with == instead of >= compiled: Set to False to not check the compiled Qt version or the PyQt version. """ if compiled and exact: raise ValueError("Can't use compiled=True with exact=True!") parsed = utils.VersionNumber.parse(version) op = operator.eq if exact else operator.ge qversion = qVersion() assert qversion is not None result = op(utils.VersionNumber.parse(qversion), parsed) if compiled and result: # qVersion() ==/>= parsed, now check if QT_VERSION_STR ==/>= parsed. result = op(utils.VersionNumber.parse(QT_VERSION_STR), parsed) if compiled and result: # Finally, check PYQT_VERSION_STR as well. result = op(utils.VersionNumber.parse(PYQT_VERSION_STR), parsed) return result MAX_WORLD_ID = 256 def is_new_qtwebkit() -> bool: """Check if the given version is a new QtWebKit.""" assert qWebKitVersion is not None return (utils.VersionNumber.parse(qWebKitVersion()) > utils.VersionNumber.parse('538.1')) def is_single_process() -> bool: """Check whether QtWebEngine is running in single-process mode.""" if objects.backend == usertypes.Backend.QtWebKit: return False assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend args = objects.qapp.arguments() return '--single-process' in args def is_wayland() -> bool: """Check if we are running on Wayland.""" return objects.qapp.platformName() in ["wayland", "wayland-egl"] def check_overflow(arg: int, ctype: str, fatal: bool = True) -> int: """Check if the given argument is in bounds for the given type. Args: arg: The argument to check ctype: The C/Qt type to check as a string. fatal: Whether to raise exceptions (True) or truncate values (False) Return The truncated argument if fatal=False The original argument if it's in bounds. """ maxval = MAXVALS[ctype] minval = MINVALS[ctype] if arg > maxval: if fatal: raise OverflowError(arg) return maxval elif arg < minval: if fatal: raise OverflowError(arg) return minval else: return arg class Validatable(Protocol): """An object with an isValid() method (e.g. QUrl).""" def isValid(self) -> bool: ... def ensure_valid(obj: Validatable) -> None: """Ensure a Qt object with an .isValid() method is valid.""" if not obj.isValid(): raise QtValueError(obj) def check_qdatastream(stream: QDataStream) -> None: """Check the status of a QDataStream and raise OSError if it's not ok.""" status_to_str = { QDataStream.Status.Ok: "The data stream is operating normally.", QDataStream.Status.ReadPastEnd: ("The data stream has read past the end of " "the data in the underlying device."), QDataStream.Status.ReadCorruptData: "The data stream has read corrupt data.", QDataStream.Status.WriteFailed: ("The data stream cannot write to the " "underlying device."), } if machinery.IS_QT6: try: status_to_str[QDataStream.Status.SizeLimitExceeded] = ( "The data stream cannot read or write the data because its size is larger " "than supported by the current platform." ) except AttributeError: # Added in Qt 6.7 pass if stream.status() != QDataStream.Status.Ok: raise OSError(status_to_str[stream.status()]) _QtSerializableType = Union[ QObject, QByteArray, QUrl, 'QWebEngineHistory', 'QWebHistory' ] def serialize(obj: _QtSerializableType) -> QByteArray: """Serialize an object into a QByteArray.""" data = QByteArray() stream = QDataStream(data, QIODevice.OpenModeFlag.WriteOnly) serialize_stream(stream, obj) return data def deserialize(data: QByteArray, obj: _QtSerializableType) -> None: """Deserialize an object from a QByteArray.""" stream = QDataStream(data, QIODevice.OpenModeFlag.ReadOnly) deserialize_stream(stream, obj) def serialize_stream(stream: QDataStream, obj: _QtSerializableType) -> None: """Serialize an object into a QDataStream.""" # pylint: disable=pointless-statement check_qdatastream(stream) stream << obj # type: ignore[operator] check_qdatastream(stream) def deserialize_stream(stream: QDataStream, obj: _QtSerializableType) -> None: """Deserialize a QDataStream into an object.""" # pylint: disable=pointless-statement check_qdatastream(stream) stream >> obj # type: ignore[operator] check_qdatastream(stream) @overload @contextlib.contextmanager def savefile_open( filename: str, binary: Literal[False] = ..., encoding: str = 'utf-8' ) -> Iterator[IO[str]]: ... @overload @contextlib.contextmanager def savefile_open( filename: str, binary: Literal[True], encoding: str = 'utf-8' ) -> Iterator[IO[bytes]]: ... @contextlib.contextmanager def savefile_open( filename: str, binary: bool = False, encoding: str = 'utf-8' ) -> Iterator[Union[IO[str], IO[bytes]]]: """Context manager to easily use a QSaveFile.""" f = QSaveFile(filename) cancelled = False try: open_ok = f.open(QIODevice.OpenModeFlag.WriteOnly) if not open_ok: raise QtOSError(f) dev = cast(BinaryIO, PyQIODevice(f)) if binary: new_f: Union[IO[str], IO[bytes]] = dev else: new_f = io.TextIOWrapper(dev, encoding=encoding) yield new_f new_f.flush() except: f.cancelWriting() cancelled = True raise finally: commit_ok = f.commit() if not commit_ok and not cancelled: raise QtOSError(f, msg="Commit failed!") def qcolor_to_qsscolor(c: QColor) -> str: """Convert a QColor to a string that can be used in a QStyleSheet.""" ensure_valid(c) return "rgba({}, {}, {}, {})".format( c.red(), c.green(), c.blue(), c.alpha()) class PyQIODevice(io.BufferedIOBase): """Wrapper for a QIODevice which provides a python interface. Attributes: dev: The underlying QIODevice. """ def __init__(self, dev: QIODevice) -> None: super().__init__() self.dev = dev def __len__(self) -> int: return self.dev.size() def _check_open(self) -> None: """Check if the device is open, raise ValueError if not.""" if not self.dev.isOpen(): raise ValueError("IO operation on closed device!") def _check_random(self) -> None: """Check if the device supports random access, raise OSError if not.""" if not self.seekable(): raise OSError("Random access not allowed!") def _check_readable(self) -> None: """Check if the device is readable, raise OSError if not.""" if not self.dev.isReadable(): raise OSError("Trying to read unreadable file!") def _check_writable(self) -> None: """Check if the device is writable, raise OSError if not.""" if not self.writable(): raise OSError("Trying to write to unwritable file!") # contextlib.closing is only generic in Python 3.9+ def open( self, mode: QIODevice.OpenModeFlag, ) -> contextlib.closing: # type: ignore[type-arg] """Open the underlying device and ensure opening succeeded. Raises OSError if opening failed. Args: mode: QIODevice::OpenMode flags. Return: A contextlib.closing() object so this can be used as contextmanager. """ ok = self.dev.open(mode) if not ok: raise QtOSError(self.dev) return contextlib.closing(self) def close(self) -> None: """Close the underlying device.""" self.dev.close() def fileno(self) -> int: raise io.UnsupportedOperation def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: self._check_open() self._check_random() if whence == io.SEEK_SET: ok = self.dev.seek(offset) elif whence == io.SEEK_CUR: ok = self.dev.seek(self.tell() + offset) elif whence == io.SEEK_END: ok = self.dev.seek(len(self) + offset) else: raise io.UnsupportedOperation("whence = {} is not " "supported!".format(whence)) if not ok: raise QtOSError(self.dev, msg="seek failed!") return self.dev.pos() def truncate(self, size: int = None) -> int: raise io.UnsupportedOperation @property def closed(self) -> bool: return not self.dev.isOpen() def flush(self) -> None: self._check_open() self.dev.waitForBytesWritten(-1) def isatty(self) -> bool: self._check_open() return False def readable(self) -> bool: return self.dev.isReadable() def readline(self, size: Optional[int] = -1) -> bytes: self._check_open() self._check_readable() if size is None or size < 0: qt_size = None # no maximum size elif size == 0: return b'' else: qt_size = size + 1 # Qt also counts the NUL byte buf: Union[QByteArray, bytes, None] = None if self.dev.canReadLine(): if qt_size is None: buf = self.dev.readLine() else: buf = self.dev.readLine(qt_size) elif size is None or size < 0: buf = self.dev.readAll() else: buf = self.dev.read(size) if buf is None: raise QtOSError(self.dev) if isinstance(buf, QByteArray): # The type (bytes or QByteArray) seems to depend on what data we # feed in... buf = buf.data() return buf def seekable(self) -> bool: return not self.dev.isSequential() def tell(self) -> int: self._check_open() self._check_random() return self.dev.pos() def writable(self) -> bool: return self.dev.isWritable() def write( # type: ignore[override] self, data: Union[bytes, bytearray] ) -> int: self._check_open() self._check_writable() num = self.dev.write(data) if num == -1 or num < len(data): raise QtOSError(self.dev) return num def read(self, size: Optional[int] = None) -> bytes: self._check_open() self._check_readable() buf: Union[QByteArray, bytes, None] = None if size in [None, -1]: buf = self.dev.readAll() else: assert size is not None buf = self.dev.read(size) if buf is None: raise QtOSError(self.dev) if isinstance(buf, QByteArray): # The type (bytes or QByteArray) seems to depend on what data we # feed in... buf = buf.data() return buf class QtValueError(ValueError): """Exception which gets raised by ensure_valid.""" def __init__(self, obj: Validatable) -> None: try: self.reason = obj.errorString() # type: ignore[attr-defined] except AttributeError: self.reason = None err = "{} is not valid".format(obj) if self.reason: err += ": {}".format(self.reason) super().__init__(err) if machinery.IS_QT6: _ProcessEventFlagType = QEventLoop.ProcessEventsFlag else: _ProcessEventFlagType = Union[ QEventLoop.ProcessEventsFlag, QEventLoop.ProcessEventsFlags] class EventLoop(QEventLoop): """A thin wrapper around QEventLoop. Raises an exception when doing exec() multiple times. """ def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._executing = False def exec( self, flags: _ProcessEventFlagType = QEventLoop.ProcessEventsFlag.AllEvents, ) -> int: """Override exec_ to raise an exception when re-running.""" if self._executing: raise AssertionError("Eventloop is already running!") self._executing = True if machinery.IS_QT5: flags = cast(QEventLoop.ProcessEventsFlags, flags) status = super().exec(flags) self._executing = False return status def _get_color_percentage( # pylint: disable=too-many-positional-arguments x1: int, y1: int, z1: int, a1: int, x2: int, y2: int, z2: int, a2: int, percent: int ) -> tuple[int, int, int, int]: """Get a color which is percent% interpolated between start and end. Args: x1, y1, z1, a1 : Start color components (R, G, B, A / H, S, V, A / H, S, L, A) x2, y2, z2, a2 : End color components (R, G, B, A / H, S, V, A / H, S, L, A) percent: Percentage to interpolate, 0-100. 0: Start color will be returned. 100: End color will be returned. Return: A (x, y, z, alpha) tuple with the interpolated color components. """ if not 0 <= percent <= 100: raise ValueError("percent needs to be between 0 and 100!") x = round(x1 + (x2 - x1) * percent / 100) y = round(y1 + (y2 - y1) * percent / 100) z = round(z1 + (z2 - z1) * percent / 100) a = round(a1 + (a2 - a1) * percent / 100) return (x, y, z, a) def interpolate_color( start: QColor, end: QColor, percent: int, colorspace: Optional[QColor.Spec] = QColor.Spec.Rgb ) -> QColor: """Get an interpolated color value. Args: start: The start color. end: The end color. percent: Which value to get (0 - 100) colorspace: The desired interpolation color system, QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum) If None, start is used except when percent is 100. Return: The interpolated QColor, with the same spec as the given start color. """ ensure_valid(start) ensure_valid(end) if colorspace is None: if percent == 100: r, g, b, a = end.getRgb() assert r is not None assert g is not None assert b is not None assert a is not None return QColor(r, g, b, a) else: r, g, b, a = start.getRgb() assert r is not None assert g is not None assert b is not None assert a is not None return QColor(r, g, b, a) out = QColor() if colorspace == QColor.Spec.Rgb: r1, g1, b1, a1 = start.getRgb() r2, g2, b2, a2 = end.getRgb() assert r1 is not None assert g1 is not None assert b1 is not None assert a1 is not None assert r2 is not None assert g2 is not None assert b2 is not None assert a2 is not None components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent) out.setRgb(*components) elif colorspace == QColor.Spec.Hsv: h1, s1, v1, a1 = start.getHsv() h2, s2, v2, a2 = end.getHsv() assert h1 is not None assert s1 is not None assert v1 is not None assert a1 is not None assert h2 is not None assert s2 is not None assert v2 is not None assert a2 is not None components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent) out.setHsv(*components) elif colorspace == QColor.Spec.Hsl: h1, s1, l1, a1 = start.getHsl() h2, s2, l2, a2 = end.getHsl() assert h1 is not None assert s1 is not None assert l1 is not None assert a1 is not None assert h2 is not None assert s2 is not None assert l2 is not None assert a2 is not None components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent) out.setHsl(*components) else: raise ValueError("Invalid colorspace!") out = out.convertTo(start.spec()) ensure_valid(out) return out class LibraryPath(enum.Enum): """A path to be passed to QLibraryInfo. Should mirror QLibraryPath (Qt 5) and QLibraryLocation (Qt 6). Values are the respective Qt names. """ prefix = "PrefixPath" documentation = "DocumentationPath" headers = "HeadersPath" libraries = "LibrariesPath" library_executables = "LibraryExecutablesPath" binaries = "BinariesPath" plugins = "PluginsPath" qml2_imports = "Qml2ImportsPath" arch_data = "ArchDataPath" data = "DataPath" translations = "TranslationsPath" examples = "ExamplesPath" tests = "TestsPath" settings = "SettingsPath" def library_path(which: LibraryPath) -> pathlib.Path: """Wrapper around QLibraryInfo.path / .location.""" if machinery.IS_QT6: val = getattr(QLibraryInfo.LibraryPath, which.value) ret = QLibraryInfo.path(val) else: # Qt 5 val = getattr(QLibraryInfo.LibraryLocation, which.value) ret = QLibraryInfo.location(val) assert ret return pathlib.Path(ret) def extract_enum_val(val: Union[sip.simplewrapper, int, enum.Enum]) -> int: """Extract an int value from a Qt enum value. For Qt 5, enum values are basically Python integers. For Qt 6, they are usually enum.Enum instances, with the value set to the integer. """ if isinstance(val, enum.Enum): return val.value elif isinstance(val, sip.simplewrapper): return int(val) # type: ignore[call-overload] return val def qobj_repr(obj: Optional[QObject]) -> str: """Show nicer debug information for a QObject.""" py_repr = repr(obj) if obj is None: return py_repr try: object_name = obj.objectName() meta_object = obj.metaObject() except AttributeError: # Technically not possible if obj is a QObject, but crashing when trying to get # some debug info isn't helpful. return py_repr class_name = "" if meta_object is None else meta_object.className() if py_repr.startswith("<") and py_repr.endswith(">"): # With a repr such as , we want to end up with: # # But if we have RichRepr() as existing repr, we want: # py_repr = py_repr[1:-1] parts = [py_repr] if object_name: parts.append(f"objectName={object_name!r}") if class_name and f".{class_name} object at 0x" not in py_repr: parts.append(f"className={class_name!r}") return f"<{', '.join(parts)}>" _T = TypeVar("_T") if machinery.IS_QT5: # On Qt 5, add/remove Optional where type annotations don't have it. # Also we have a special QT_NONE, which (being Any) we can pass to functions # where PyQt type hints claim that it's not allowed. def remove_optional(obj: Optional[_T]) -> _T: return cast(_T, obj) def add_optional(obj: _T) -> Optional[_T]: return cast(Optional[_T], obj) QT_NONE: Any = None else: # On Qt 6, all those things are handled correctly by type annotations, so we # have a no-op below. def remove_optional(obj: Optional[_T]) -> Optional[_T]: return obj def add_optional(obj: Optional[_T]) -> Optional[_T]: return obj QT_NONE: None = None def maybe_cast(to_type: type[_T], do_cast: bool, value: Any) -> _T: """Cast `value` to `to_type` only if `do_cast` is true.""" if do_cast: return cast( to_type, # type: ignore[valid-type] value, ) return value ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/resources.py0000644000175100017510000000656315102145205021664 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Resources related utilities.""" import os.path import sys import contextlib import posixpath import pathlib import importlib.resources from typing import Union from collections.abc import Iterator, Iterable if sys.version_info >= (3, 11): # pragma: no cover # https://github.com/python/cpython/issues/90276 from importlib.resources.abc import Traversable else: from importlib.abc import Traversable import qutebrowser _cache: dict[str, str] = {} _bin_cache: dict[str, bytes] = {} _ResourceType = Union[Traversable, pathlib.Path] def _path(filename: str) -> _ResourceType: """Get a pathlib.Path object for a resource.""" assert not posixpath.isabs(filename), filename assert os.path.pardir not in filename.split(posixpath.sep), filename return importlib.resources.files(qutebrowser) / filename @contextlib.contextmanager def _keyerror_workaround() -> Iterator[None]: """Re-raise KeyErrors as FileNotFoundErrors. WORKAROUND for zipfile.Path resources raising KeyError when a file was notfound: https://bugs.python.org/issue43063 Only needed for Python 3.9. """ try: yield except KeyError as e: raise FileNotFoundError(str(e)) def _glob( resource_path: _ResourceType, subdir: str, ext: str, ) -> Iterable[str]: """Find resources with the given extension. Yields a resource name like "html/log.html" (as string). """ assert '*' not in ext, ext assert ext.startswith('.'), ext glob_path = resource_path / subdir if isinstance(resource_path, pathlib.Path): assert isinstance(glob_path, pathlib.Path) for full_path in glob_path.glob(f'*{ext}'): # . is contained in ext yield full_path.relative_to(resource_path).as_posix() else: # zipfile.Path or other importlib.resources.abc.Traversable assert glob_path.is_dir(), glob_path for subpath in glob_path.iterdir(): if subpath.name.endswith(ext): yield posixpath.join(subdir, subpath.name) def preload() -> None: """Load resource files into the cache.""" resource_path = _path('') for subdir, ext in [ ('html', '.html'), ('javascript', '.js'), ('javascript/quirks', '.js'), ]: for name in _glob(resource_path, subdir, ext): _cache[name] = read_file(name) for name in _glob(resource_path, 'img', '.png'): # e.g. broken_qutebrowser_logo.png _bin_cache[name] = read_file_binary(name) def read_file(filename: str) -> str: """Get the contents of a file contained with qutebrowser. Args: filename: The filename to open as string. Return: The file contents as string. """ if filename in _cache: return _cache[filename] path = _path(filename) with _keyerror_workaround(): return path.read_text(encoding='utf-8') def read_file_binary(filename: str) -> bytes: """Get the contents of a binary file contained with qutebrowser. Args: filename: The filename to open as string. Return: The file contents as a bytes object. """ if filename in _bin_cache: return _bin_cache[filename] path = _path(filename) with _keyerror_workaround(): return path.read_bytes() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/standarddir.py0000644000175100017510000002747215102145205022153 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities to get and initialize data/config paths.""" import os import os.path import sys import contextlib import enum import argparse import tempfile from typing import Optional from collections.abc import Iterator from qutebrowser.qt.core import QStandardPaths from qutebrowser.qt.widgets import QApplication from qutebrowser.utils import log, debug, utils, version, qtutils # The cached locations _locations: dict["_Location", str] = {} class _Location(enum.Enum): """A key for _locations.""" config = enum.auto() auto_config = enum.auto() data = enum.auto() system_data = enum.auto() cache = enum.auto() download = enum.auto() runtime = enum.auto() config_py = enum.auto() APPNAME = 'qutebrowser' class EmptyValueError(Exception): """Error raised when QStandardPaths returns an empty value.""" @contextlib.contextmanager def _unset_organization() -> Iterator[None]: """Temporarily unset QApplication.organizationName(). This is primarily needed in config.py. """ qapp = QApplication.instance() if qapp is not None: orgname = qapp.organizationName() qapp.setOrganizationName(qtutils.QT_NONE) try: yield finally: if qapp is not None: qapp.setOrganizationName(orgname) def _init_config(args: Optional[argparse.Namespace]) -> None: """Initialize the location for configs.""" typ = QStandardPaths.StandardLocation.ConfigLocation path = _from_args(typ, args) if path is None: if utils.is_windows: app_data_path = _writable_location( QStandardPaths.StandardLocation.AppDataLocation) path = os.path.join(app_data_path, 'config') else: path = _writable_location(typ) _create(path) _locations[_Location.config] = path _locations[_Location.auto_config] = path # Override the normal (non-auto) config on macOS if utils.is_mac: path = _from_args(typ, args) if path is None: # pragma: no branch path = os.path.expanduser('~/.' + APPNAME) _create(path) _locations[_Location.config] = path config_py_file = os.path.join(_locations[_Location.config], 'config.py') if getattr(args, 'config_py', None) is not None: assert args is not None config_py_file = os.path.abspath(args.config_py) _locations[_Location.config_py] = config_py_file def config(auto: bool = False) -> str: """Get the location for the config directory. If auto=True is given, get the location for the autoconfig.yml directory, which is different on macOS. """ if auto: return _locations[_Location.auto_config] return _locations[_Location.config] def config_py() -> str: """Get the location for config.py. Usually, config.py is in standarddir.config(), but this can be overridden with the --config-py argument. """ return _locations[_Location.config_py] def _init_data(args: Optional[argparse.Namespace]) -> None: """Initialize the location for data.""" typ = QStandardPaths.StandardLocation.AppDataLocation path = _from_args(typ, args) if path is None: if utils.is_windows: app_data_path = _writable_location(typ) # same location as config path = os.path.join(app_data_path, 'data') elif sys.platform.startswith('haiku'): # HaikuOS returns an empty value for AppDataLocation config_path = _writable_location(QStandardPaths.StandardLocation.ConfigLocation) path = os.path.join(config_path, 'data') else: path = _writable_location(typ) _create(path) _locations[_Location.data] = path # system_data _locations.pop(_Location.system_data, None) # Remove old state if utils.is_linux: prefix = '/app' if version.is_flatpak() else '/usr' path = f'{prefix}/share/{APPNAME}' if os.path.exists(path): _locations[_Location.system_data] = path def data(system: bool = False) -> str: """Get the data directory. If system=True is given, gets the system-wide (probably non-writable) data directory. """ if system: try: return _locations[_Location.system_data] except KeyError: pass return _locations[_Location.data] def _init_cache(args: Optional[argparse.Namespace]) -> None: """Initialize the location for the cache.""" typ = QStandardPaths.StandardLocation.CacheLocation path = _from_args(typ, args) if path is None: if utils.is_windows: # Local, not Roaming! data_path = _writable_location(QStandardPaths.StandardLocation.AppLocalDataLocation) path = os.path.join(data_path, 'cache') else: path = _writable_location(typ) _create(path) _locations[_Location.cache] = path def cache() -> str: return _locations[_Location.cache] def _init_download(args: Optional[argparse.Namespace]) -> None: """Initialize the location for downloads. Note this is only the default directory as found by Qt. Therefore, we also don't create it. """ typ = QStandardPaths.StandardLocation.DownloadLocation path = _from_args(typ, args) if path is None: path = _writable_location(typ) _locations[_Location.download] = path def download() -> str: return _locations[_Location.download] def _init_runtime(args: Optional[argparse.Namespace]) -> None: """Initialize location for runtime data.""" if utils.is_mac or utils.is_windows: # RuntimeLocation is a weird path on macOS and Windows. typ = QStandardPaths.StandardLocation.TempLocation else: typ = QStandardPaths.StandardLocation.RuntimeLocation path = _from_args(typ, args) if path is None: try: path = _writable_location(typ) except EmptyValueError: # Fall back to TempLocation when RuntimeLocation is misconfigured if typ == QStandardPaths.StandardLocation.TempLocation: raise path = _writable_location( # pragma: no cover QStandardPaths.StandardLocation.TempLocation) # This is generic, but per-user. # _writable_location makes sure we have a qutebrowser-specific subdir. # # For TempLocation: # "The returned value might be application-specific, shared among # other applications for this user, or even system-wide." # # Unfortunately this path could get too long for sockets (which have a # maximum length of 104 chars), so we don't add the username here... if version.is_flatpak(): # We need a path like # /run/user/1000/app/org.qutebrowser.qutebrowser rather than # /run/user/1000/qutebrowser on Flatpak, since that's bind-mounted # in a way that it is accessible by any other qutebrowser # instances. *parts, app_name = os.path.split(path) assert app_name == APPNAME, app_name flatpak_id = version.flatpak_id() assert flatpak_id is not None path = os.path.join(*parts, 'app', flatpak_id) _create(path) _locations[_Location.runtime] = path def runtime() -> str: return _locations[_Location.runtime] def _writable_location(typ: QStandardPaths.StandardLocation) -> str: """Wrapper around QStandardPaths.writableLocation. Arguments: typ: A QStandardPaths::StandardLocation member. """ typ_str = debug.qenum_key(QStandardPaths, typ) # Types we are sure we handle correctly below. assert typ in [ QStandardPaths.StandardLocation.ConfigLocation, QStandardPaths.StandardLocation.AppLocalDataLocation, QStandardPaths.StandardLocation.CacheLocation, QStandardPaths.StandardLocation.DownloadLocation, QStandardPaths.StandardLocation.RuntimeLocation, QStandardPaths.StandardLocation.TempLocation, QStandardPaths.StandardLocation.AppDataLocation], typ_str with _unset_organization(): path = QStandardPaths.writableLocation(typ) log.misc.debug("writable location for {}: {}".format(typ_str, path)) if not path: raise EmptyValueError("QStandardPaths returned an empty value!") # Qt seems to use '/' as path separator even on Windows... path = path.replace('/', os.sep) # Add the application name to the given path if needed. # This is in order for this to work without a QApplication (and thus # QStandardsPaths not knowing the application name). if (typ != QStandardPaths.StandardLocation.DownloadLocation and path.split(os.sep)[-1] != APPNAME): path = os.path.join(path, APPNAME) return path def _from_args( typ: QStandardPaths.StandardLocation, args: Optional[argparse.Namespace] ) -> Optional[str]: """Get the standard directory from an argparse namespace. Return: The overridden path, or None if there is no override. """ basedir_suffix = { QStandardPaths.StandardLocation.ConfigLocation: 'config', QStandardPaths.StandardLocation.AppDataLocation: 'data', QStandardPaths.StandardLocation.AppLocalDataLocation: 'data', QStandardPaths.StandardLocation.CacheLocation: 'cache', QStandardPaths.StandardLocation.DownloadLocation: 'download', QStandardPaths.StandardLocation.RuntimeLocation: 'runtime', } if getattr(args, 'basedir', None) is None: return None assert args is not None try: suffix = basedir_suffix[typ] except KeyError: # pragma: no cover return None return os.path.abspath(os.path.join(args.basedir, suffix)) def _create(path: str) -> None: """Create the `path` directory. From the XDG basedir spec: If, when attempting to write a file, the destination directory is non-existent an attempt should be made to create it with permission 0700. If the destination directory exists already the permissions should not be changed. """ if APPNAME == 'qute_test': if path.startswith('/home') and not path.startswith(tempfile.gettempdir()): # pragma: no cover for k, v in os.environ.items(): if k == 'HOME' or k.startswith('XDG_'): log.init.debug(f"{k} = {v}") raise AssertionError( "Trying to create directory inside /home during " "tests, this should not happen." ) os.makedirs(path, 0o700, exist_ok=True) def _init_dirs(args: argparse.Namespace = None) -> None: """Create and cache standard directory locations. Mainly in a separate function because we need to call it in tests. """ _init_config(args) _init_data(args) _init_cache(args) _init_download(args) _init_runtime(args) def init(args: Optional[argparse.Namespace]) -> None: """Initialize all standard dirs.""" if args is not None: # args can be None during tests log.init.debug("Base directory: {}".format(args.basedir)) _init_dirs(args) _init_cachedir_tag() def _init_cachedir_tag() -> None: """Create CACHEDIR.TAG if it doesn't exist. See https://bford.info/cachedir/ """ cachedir_tag = os.path.join(cache(), 'CACHEDIR.TAG') if not os.path.exists(cachedir_tag): try: with open(cachedir_tag, 'w', encoding='utf-8') as f: f.write("Signature: 8a477f597d28d172789f06886806bc55\n") f.write("# This file is a cache directory tag created by " "qutebrowser.\n") f.write("# For information about cache directory tags, see:\n") f.write("# https://bford.info/cachedir/\n") except OSError: log.init.exception("Failed to create CACHEDIR.TAG") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/testfile0000644000175100017510000000010015102145205021017 0ustar00runnerrunnerHello World! This file is used to test the read_file function. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/urlmatch.py0000644000175100017510000002542415102145205021466 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """A Chromium-like URL matching pattern. See: https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns https://cs.chromium.org/chromium/src/extensions/common/url_pattern.cc https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h Based on the following commit in Chromium: https://chromium.googlesource.com/chromium/src/+/6f4a6681eae01c2036336c18b06303e16a304a7c (October 10 2020, newest commit as per October 28th 2020) """ import ipaddress import fnmatch import urllib.parse from typing import Any, Optional from qutebrowser.qt.core import QUrl from qutebrowser.utils import utils, qtutils class ParseError(Exception): """Raised when a pattern could not be parsed.""" class UrlPattern: """A Chromium-like URL matching pattern. Class attributes: _DEFAULT_PORTS: The default ports used for schemes which support ports. _SCHEMES_WITHOUT_HOST: Schemes which don't need a host. Attributes: host: The host to match to, or None for any host. _pattern: The given pattern as string. _match_all: Whether the pattern should match all URLs. _match_subdomains: Whether the pattern should match subdomains of the given host. _scheme: The scheme to match to, or None to match any scheme. Note that with Chromium, '*'/None only matches http/https and not file/ftp. We deviate from that as per-URL settings aren't security relevant. _path: The path to match to, or None for any path. _port: The port to match to as integer, or None for any port. """ _DEFAULT_PORTS = {'https': 443, 'http': 80, 'ftp': 21} _SCHEMES_WITHOUT_HOST = ['about', 'file', 'data', 'javascript'] def __init__(self, pattern: str) -> None: # Make sure all attributes are initialized if we exit early. self._pattern = pattern self._match_all = False self._match_subdomains: bool = False self._scheme: Optional[str] = None self.host: Optional[str] = None self._path: Optional[str] = None self._port: Optional[int] = None # > The special pattern matches any URL that starts with a # > permitted scheme. if pattern == '': self._match_all = True return if '\0' in pattern: raise ParseError("May not contain NUL byte") pattern = self._fixup_pattern(pattern) # We use urllib.parse instead of QUrl here because it can handle # hosts with * in them. try: parsed = urllib.parse.urlparse(pattern) except ValueError as e: raise ParseError(str(e)) assert parsed is not None self._init_scheme(parsed) self._init_host(parsed) self._init_path(parsed) self._init_port(parsed) def _to_tuple(self) -> tuple[ bool, # _match_all bool, # _match_subdomains Optional[str], # _scheme Optional[str], # host Optional[str], # _path Optional[int], # _port ]: """Get a pattern with information used for __eq__/__hash__.""" return (self._match_all, self._match_subdomains, self._scheme, self.host, self._path, self._port) def __hash__(self) -> int: return hash(self._to_tuple()) def __eq__(self, other: Any) -> bool: if not isinstance(other, UrlPattern): return NotImplemented return self._to_tuple() == other._to_tuple() def __repr__(self) -> str: return utils.get_repr(self, pattern=self._pattern, constructor=True) def __str__(self) -> str: return self._pattern def _fixup_pattern(self, pattern: str) -> str: """Make sure the given pattern is parseable by urllib.parse.""" if pattern.startswith('*:'): # Any scheme, but *:// is unparsable pattern = 'any:' + pattern[2:] schemes = tuple(s + ':' for s in self._SCHEMES_WITHOUT_HOST) if '://' not in pattern and not pattern.startswith(schemes): pattern = 'any://' + pattern # Chromium handles file://foo like file:///foo # FIXME This doesn't actually strip the hostname correctly. if (pattern.startswith('file://') and not pattern.startswith('file:///')): pattern = 'file:///' + pattern.removeprefix("file://") return pattern def _init_scheme(self, parsed: urllib.parse.ParseResult) -> None: """Parse the scheme from the given URL. Deviation from Chromium: - We assume * when no scheme has been given. """ if not parsed.scheme: raise ParseError("Missing scheme") if parsed.scheme == 'any': self._scheme = None return self._scheme = parsed.scheme def _init_path(self, parsed: urllib.parse.ParseResult) -> None: """Parse the path from the given URL. Deviation from Chromium: - We assume * when no path has been given. """ if self._scheme == 'about' and not parsed.path.strip(): raise ParseError("Pattern without path") if parsed.path == '/*': self._path = None elif not parsed.path: # When the user doesn't add a trailing slash, we assume the pattern # matches any path. self._path = None else: self._path = parsed.path def _init_host(self, parsed: urllib.parse.ParseResult) -> None: """Parse the host from the given URL. Deviation from Chromium: - http://:1234/ is not a valid URL because it has no host. - We don't allow patterns for dot/space hosts which QUrl considers invalid. """ if parsed.hostname is None or not parsed.hostname.strip(): if self._scheme not in self._SCHEMES_WITHOUT_HOST: raise ParseError("Pattern without host") assert self.host is None return if parsed.netloc.startswith('['): # Using QUrl parsing to minimize ipv6 addresses url = QUrl() url.setHost(parsed.hostname) if not url.isValid(): raise ParseError(url.errorString()) self.host = url.host() return if parsed.hostname == '*': self._match_subdomains = True hostname = None elif parsed.hostname.startswith('*.'): if len(parsed.hostname) == 2: # We don't allow just '*.' as a host. raise ParseError("Pattern without host") self._match_subdomains = True hostname = parsed.hostname[2:] elif set(parsed.hostname) in {frozenset('.'), frozenset('. ')}: raise ParseError("Invalid host") else: hostname = parsed.hostname if hostname is None: self.host = None elif '*' in hostname: # Only * or *.foo is allowed as host. raise ParseError("Invalid host wildcard") else: self.host = hostname.rstrip('.') def _init_port(self, parsed: urllib.parse.ParseResult) -> None: """Parse the port from the given URL. Deviation from Chromium: - We use None instead of "*" if there's no port filter. """ if parsed.netloc.endswith(':*'): # We can't access parsed.port as it tries to run int() self._port = None elif parsed.netloc.endswith(':'): raise ParseError("Invalid port: Port is empty") else: try: self._port = parsed.port except ValueError as e: raise ParseError("Invalid port: {}".format(e)) scheme_has_port = (self._scheme in list(self._DEFAULT_PORTS) or self._scheme is None) if self._port is not None and not scheme_has_port: raise ParseError("Ports are unsupported with {} scheme".format( self._scheme)) def _matches_scheme(self, scheme: str) -> bool: return self._scheme is None or self._scheme == scheme def _matches_host(self, host: str) -> bool: # FIXME what about multiple dots? host = host.rstrip('.') # If we have no host in the match pattern, that means that we're # matching all hosts, which means we have a match no matter what the # test host is. # Contrary to Chromium, we don't need to check for # self._match_subdomains, as we want to return True here for e.g. # file:// as well. if self.host is None: return True # If the hosts are exactly equal, we have a match. if host == self.host: return True # Otherwise, we can only match if our match pattern matches subdomains. if not self._match_subdomains: return False # We don't do subdomain matching against IP addresses, so we can give # up now if the test host is an IP address. if not utils.raises(ValueError, ipaddress.ip_address, host): return False # Check if the test host is a subdomain of our host. if len(host) <= (len(self.host) + 1): return False if not host.endswith(self.host): return False return host[len(host) - len(self.host) - 1] == '.' def _matches_port(self, scheme: str, port: int) -> bool: if port == -1 and scheme in self._DEFAULT_PORTS: port = self._DEFAULT_PORTS[scheme] return self._port is None or self._port == port def _matches_path(self, path: str) -> bool: """Match the URL's path. Deviations from Chromium: - Chromium only matches with "javascript:" (pathless); but we also match *://*/* and friends. """ if self._path is None: return True # Match 'google.com' with 'google.com/' if path + '/*' == self._path: return True # FIXME Chromium seems to have a more optimized glob matching which # doesn't rely on regexes. Do we need that too? return fnmatch.fnmatchcase(path, self._path) def matches(self, qurl: QUrl) -> bool: """Check if the pattern matches the given QUrl.""" qtutils.ensure_valid(qurl) if self._match_all: return True if not self._matches_scheme(qurl.scheme()): return False # FIXME ignore for file:// like Chromium? if not self._matches_host(qurl.host()): return False if not self._matches_port(qurl.scheme(), qurl.port()): return False if not self._matches_path(qurl.path()): return False return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/urlutils.py0000644000175100017510000005270415102145205021533 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utils regarding URL handling.""" import re import base64 import os.path import ipaddress import posixpath import urllib.parse import mimetypes from typing import Optional, Union, cast from collections.abc import Iterable from qutebrowser.qt import machinery from qutebrowser.qt.core import QUrl, QUrlQuery from qutebrowser.qt.network import QHostInfo, QHostAddress, QNetworkProxy from qutebrowser.api import cmdutils from qutebrowser.config import config from qutebrowser.utils import log, qtutils, message, utils from qutebrowser.browser.network import pac # FIXME: we probably could raise some exceptions on invalid URLs # https://github.com/qutebrowser/qutebrowser/issues/108 if machinery.IS_QT6: UrlFlagsType = Union[QUrl.UrlFormattingOption, QUrl.ComponentFormattingOption] class FormatOption: """Simple wrapper around Qt enums to fix typing problems on Qt 5.""" ENCODED = QUrl.ComponentFormattingOption.FullyEncoded ENCODE_UNICODE = QUrl.ComponentFormattingOption.EncodeUnicode DECODE_RESERVED = QUrl.ComponentFormattingOption.DecodeReserved REMOVE_SCHEME = QUrl.UrlFormattingOption.RemoveScheme REMOVE_PASSWORD = QUrl.UrlFormattingOption.RemovePassword REMOVE_QUERY = QUrl.UrlFormattingOption.RemoveQuery else: UrlFlagsType = Union[ QUrl.FormattingOptions, QUrl.UrlFormattingOption, QUrl.ComponentFormattingOption, QUrl.ComponentFormattingOptions, ] class _QtFormattingOptions(QUrl.FormattingOptions): """WORKAROUND for invalid stubs. Teach mypy that | works for QUrl.FormattingOptions. """ def __or__(self, f: UrlFlagsType) -> '_QtFormattingOptions': return super() | f # type: ignore[operator,return-value] class FormatOption: """WORKAROUND for invalid stubs. Pretend that all ComponentFormattingOption values are also valid QUrl.FormattingOptions values, i.e. can be passed to QUrl.toString(). """ ENCODED = cast( _QtFormattingOptions, QUrl.ComponentFormattingOption.FullyEncoded) ENCODE_UNICODE = cast( _QtFormattingOptions, QUrl.ComponentFormattingOption.EncodeUnicode) DECODE_RESERVED = cast( _QtFormattingOptions, QUrl.ComponentFormattingOption.DecodeReserved) REMOVE_SCHEME = cast( _QtFormattingOptions, QUrl.UrlFormattingOption.RemoveScheme) REMOVE_PASSWORD = cast( _QtFormattingOptions, QUrl.UrlFormattingOption.RemovePassword) REMOVE_QUERY = cast( _QtFormattingOptions, QUrl.UrlFormattingOption.RemoveQuery) # URL schemes supported by QtWebEngine WEBENGINE_SCHEMES = [ 'about', 'data', 'file', 'filesystem', 'ftp', 'http', 'https', 'javascript', 'ws', 'wss', ] class Error(Exception): """Base class for errors in this module.""" class InvalidUrlError(Error): """Error raised if a function got an invalid URL.""" def __init__(self, url: QUrl) -> None: if url.isValid(): raise ValueError("Got valid URL {}!".format(url.toDisplayString())) self.url = url self.msg = get_errstring(url) super().__init__(self.msg) def _parse_search_term(s: str) -> tuple[Optional[str], Optional[str]]: """Get a search engine name and search term from a string. Args: s: The string to get a search engine for. Return: A (engine, term) tuple, where engine is None for the default engine. """ s = s.strip() split = s.split(maxsplit=1) if not split: raise ValueError("Empty search term!") if len(split) == 2: if split[0] in config.val.url.searchengines: engine: Optional[str] = split[0] term: Optional[str] = split[1] else: engine = None term = s else: # pylint: disable=else-if-used if config.val.url.open_base_url and s in config.val.url.searchengines: engine = s term = None else: engine = None term = s log.url.debug("engine {}, term {!r}".format(engine, term)) return (engine, term) def _get_search_url(txt: str) -> QUrl: """Get a search engine URL for a text. Args: txt: Text to search for. Return: The search URL as a QUrl. """ log.url.debug("Finding search engine for {!r}".format(txt)) engine, term = _parse_search_term(txt) if not engine: engine = 'DEFAULT' if term: template = config.val.url.searchengines[engine] semiquoted_term = urllib.parse.quote(term) quoted_term = urllib.parse.quote(term, safe='') evaluated = template.format(semiquoted_term, unquoted=term, quoted=quoted_term, semiquoted=semiquoted_term) url = QUrl.fromUserInput(evaluated) else: url = QUrl.fromUserInput(config.val.url.searchengines[engine]) url.setPath(qtutils.QT_NONE) url.setFragment(qtutils.QT_NONE) url.setQuery(qtutils.QT_NONE) qtutils.ensure_valid(url) return url def _is_url_naive(urlstr: str) -> bool: """Naive check if given URL is really a URL. Args: urlstr: The URL to check for, as string. Return: True if the URL really is a URL, False otherwise. """ url = QUrl.fromUserInput(urlstr) assert url.isValid() host = url.host() # Valid IPv4/IPv6 address. Qt converts things like "23.42" or "1337" or # "0xDEAD" to IP addresses, which we don't like, so we check if the host # from Qt is part of the input. if (not utils.raises(ValueError, ipaddress.ip_address, host) and host in urlstr): return True tld = r'\.([^.0-9_-]+|xn--[a-z0-9-]+)$' forbidden = r'[\u0000-\u002c\u002f\u003a-\u0060\u007b-\u00b6]' return bool(re.search(tld, host) and not re.search(forbidden, host)) def _is_url_dns(urlstr: str) -> bool: """Check if a URL is really a URL via DNS. Args: url: The URL to check for as a string. Return: True if the URL really is a URL, False otherwise. """ url = QUrl.fromUserInput(urlstr) assert url.isValid() if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and not QHostAddress(urlstr).isNull()): log.url.debug("Bogus IP URL -> False") # Qt treats things like "23.42" or "1337" or "0xDEAD" as valid URLs # which we don't want to. return False host = url.host() if not host: log.url.debug("URL has no host -> False") return False log.url.debug("Doing DNS request for {}".format(host)) info = QHostInfo.fromName(host) return info.error() == QHostInfo.HostInfoError.NoError def fuzzy_url(urlstr: str, cwd: str = None, relative: bool = False, do_search: bool = True, force_search: bool = False) -> QUrl: """Get a QUrl based on a user input which is URL or search term. Args: urlstr: URL to load as a string. cwd: The current working directory, or None. relative: Whether to resolve relative files. do_search: Whether to perform a search on non-URLs. force_search: Whether to force a search even if the content can be interpreted as a URL or a path. Return: A target QUrl to a search page or the original URL. """ urlstr = urlstr.strip() path = get_path_if_valid(urlstr, cwd=cwd, relative=relative, check_exists=True) if not force_search and path is not None: url = QUrl.fromLocalFile(path) elif force_search or (do_search and not is_url(urlstr)): # probably a search term log.url.debug("URL is a fuzzy search term") try: url = _get_search_url(urlstr) except ValueError: # invalid search engine url = QUrl.fromUserInput(urlstr) else: # probably an address log.url.debug("URL is a fuzzy address") url = QUrl.fromUserInput(urlstr) log.url.debug("Converting fuzzy term {!r} to URL -> {}".format( urlstr, url.toDisplayString())) ensure_valid(url) return url def _has_explicit_scheme(url: QUrl) -> bool: """Check if a url has an explicit scheme given. Args: url: The URL as QUrl. """ # Note that generic URI syntax actually would allow a second colon # after the scheme delimiter. Since we don't know of any URIs # using this and want to support e.g. searching for scoped C++ # symbols, we treat this as not a URI anyways. return bool(url.isValid() and url.scheme() and (url.host() or url.path()) and not url.path().startswith(':')) def is_special_url(url: QUrl) -> bool: """Return True if url is an about:... or other special URL. Args: url: The URL as QUrl. """ if not url.isValid(): return False special_schemes = ('about', 'qute', 'file') return url.scheme() in special_schemes def is_url(urlstr: str) -> bool: """Check if url seems to be a valid URL. Args: urlstr: The URL as string. Return: True if it is a valid URL, False otherwise. """ autosearch = config.val.url.auto_search log.url.debug("Checking if {!r} is a URL (autosearch={}).".format( urlstr, autosearch)) urlstr = urlstr.strip() qurl = QUrl(urlstr) qurl_userinput = QUrl.fromUserInput(urlstr) if autosearch == 'never': # no autosearch, so everything is a URL unless it has an explicit # search engine. try: engine, _term = _parse_search_term(urlstr) except ValueError: return False else: return engine is None if not qurl_userinput.isValid(): # This will also catch non-URLs containing spaces. return False if _has_explicit_scheme(qurl) and ' ' not in urlstr: # URLs with explicit schemes are always URLs log.url.debug("Contains explicit scheme") url = True elif (autosearch == 'schemeless' and (not _has_explicit_scheme(qurl) or ' ' in urlstr)): # When autosearch=schemeless, URLs must contain schemes to be valid log.url.debug("No explicit scheme in given URL, treating as non-URL") url = False elif qurl_userinput.host() in ['localhost', '127.0.0.1', '::1']: log.url.debug("Is localhost.") url = True elif is_special_url(qurl): # Special URLs are always URLs, even with autosearch=never log.url.debug("Is a special URL.") url = True elif autosearch == 'dns': log.url.debug("Checking via DNS check") # We want to use QUrl.fromUserInput here, as the user might enter # "foo.de" and that should be treated as URL here. url = ' ' not in qurl_userinput.userName() and _is_url_dns(urlstr) elif autosearch == 'naive': log.url.debug("Checking via naive check") url = ' ' not in qurl_userinput.userName() and _is_url_naive(urlstr) else: # pragma: no cover raise ValueError("Invalid autosearch value") log.url.debug("url = {}".format(url)) return url def ensure_valid(url: QUrl) -> None: if not url.isValid(): raise InvalidUrlError(url) def invalid_url_error(url: QUrl, action: str) -> None: """Display an error message for a URL. Args: url: The URL to display a message for. action: The action which was interrupted by the error. """ if url.isValid(): raise ValueError("Calling invalid_url_error with valid URL {}".format( url.toDisplayString())) errstring = get_errstring( url, "Trying to {} with invalid URL".format(action)) message.error(errstring) def raise_cmdexc_if_invalid(url: QUrl) -> None: """Check if the given QUrl is invalid, and if so, raise a CommandError.""" try: ensure_valid(url) except InvalidUrlError as e: raise cmdutils.CommandError(str(e)) def get_path_if_valid(pathstr: str, cwd: str = None, relative: bool = False, check_exists: bool = False) -> Optional[str]: """Check if path is a valid path. Args: pathstr: The path as string. cwd: The current working directory, or None. relative: Whether to resolve relative files. check_exists: Whether to check if the file actually exists of filesystem. Return: The path if it is a valid path, None otherwise. """ pathstr = pathstr.strip() log.url.debug("Checking if {!r} is a path".format(pathstr)) expanded = os.path.expanduser(pathstr) if os.path.isabs(expanded): path: Optional[str] = expanded elif relative and cwd: path = os.path.join(cwd, expanded) elif relative: try: path = os.path.abspath(expanded) except OSError: path = None else: path = None if check_exists: if path is not None: try: if os.path.exists(path): log.url.debug("URL is a local file") else: path = None except UnicodeEncodeError: log.url.debug( "URL contains characters which are not present in the " "current locale") path = None return path def filename_from_url(url: QUrl, fallback: str = None) -> Optional[str]: """Get a suitable filename from a URL. Args: url: The URL to parse, as a QUrl. fallback: Value to use if no name can be determined. Return: The suggested filename as a string, or None. """ if not url.isValid(): return fallback if url.scheme().lower() == 'data': mimetype, _encoding = mimetypes.guess_type(url.toString()) if not mimetype: return fallback ext = utils.mimetype_extension(mimetype) or '' return 'download' + ext pathname = posixpath.basename(url.path()) if pathname: return pathname elif url.host(): return url.host() + '.html' else: return fallback HostTupleType = tuple[str, str, int] def host_tuple(url: QUrl) -> HostTupleType: """Get a (scheme, host, port) tuple from a QUrl. This is suitable to identify a connection, e.g. for SSL errors. """ ensure_valid(url) scheme, host, port = url.scheme(), url.host(), url.port() assert scheme if not host: raise ValueError("Got URL {} without host.".format( url.toDisplayString())) if port == -1: port_mapping = { 'http': 80, 'https': 443, 'ftp': 21, } try: port = port_mapping[scheme] except KeyError: raise ValueError("Got URL {} with unknown port.".format( url.toDisplayString())) return scheme, host, port def get_errstring(url: QUrl, base: str = "Invalid URL") -> str: """Get an error string for a URL. Args: url: The URL as a QUrl. base: The base error string. Return: A new string with url.errorString() is appended if available. """ url_error = url.errorString() if url_error: return base + " - {}".format(url_error) else: return base def same_domain(url1: QUrl, url2: QUrl) -> bool: """Check if url1 and url2 belong to the same website. This will use a "public suffix list" to determine what a "top level domain" is. All further domains are ignored. For example example.com and www.example.com are considered the same. but example.co.uk and test.co.uk are not. If the URL's schemes or ports are different, they are always treated as not equal. Return: True if the domains are the same, False otherwise. """ ensure_valid(url1) ensure_valid(url2) if url1.scheme() != url2.scheme(): return False if url1.port() != url2.port(): return False # QUrl.topLevelDomain() got removed in Qt 6: # https://bugreports.qt.io/browse/QTBUG-80308 # # However, we should never land here if we are on Qt 6: # # On QtWebEngine, we don't have a QNetworkAccessManager attached to a tab # (all tab-specific downloads happen via the QtWebEngine network stack). # Thus, ensure_valid(url2) above will raise InvalidUrlError, which is # handled in NetworkManager. # # There are no other callers of same_domain, and url2 will only be ever valid when # we use a NetworkManager from QtWebKit. However, QtWebKit is Qt 5 only. assert machinery.IS_QT5, machinery.INFO suffix1 = url1.topLevelDomain() # type: ignore[attr-defined,unused-ignore] suffix2 = url2.topLevelDomain() # type: ignore[attr-defined,unused-ignore] if not suffix1: return url1.host() == url2.host() if suffix1 != suffix2: return False domain1 = url1.host().removesuffix(suffix1).split('.')[-1] domain2 = url2.host().removesuffix(suffix2).split('.')[-1] return domain1 == domain2 def encoded_url(url: QUrl) -> str: """Return the fully encoded url as string. Args: url: The url to encode as QUrl. """ return url.toEncoded().data().decode('ascii') def file_url(path: str) -> str: """Return a file:// url (as string) to the given local path. Arguments: path: The absolute path to the local file """ url = QUrl.fromLocalFile(path) return url.toString(FormatOption.ENCODED) def data_url(mimetype: str, data: bytes) -> QUrl: """Get a data: QUrl for the given data.""" b64 = base64.b64encode(data).decode('ascii') url = QUrl('data:{};base64,{}'.format(mimetype, b64)) qtutils.ensure_valid(url) return url def safe_display_string(qurl: QUrl) -> str: """Get a IDN-homograph phishing safe form of the given QUrl. If we're dealing with a Punycode-encoded URL, this prepends the hostname in its encoded form, to make sure those URLs are distinguishable. See https://github.com/qutebrowser/qutebrowser/issues/2547 and https://bugreports.qt.io/browse/QTBUG-60365 """ ensure_valid(qurl) host = qurl.host(QUrl.ComponentFormattingOption.FullyEncoded) assert '..' not in host, qurl # https://bugreports.qt.io/browse/QTBUG-60364 for part in host.split('.'): url_host = qurl.host(QUrl.ComponentFormattingOption.FullyDecoded) if part.startswith('xn--') and host != url_host: return '({}) {}'.format(host, qurl.toDisplayString()) return qurl.toDisplayString() class InvalidProxyTypeError(Exception): """Error raised when proxy_from_url gets an unknown proxy type.""" def __init__(self, typ: str) -> None: super().__init__("Invalid proxy type {}!".format(typ)) def proxy_from_url(url: QUrl) -> Union[QNetworkProxy, pac.PACFetcher]: """Create a QNetworkProxy from QUrl and a proxy type. Args: url: URL of a proxy (possibly with credentials). Return: New QNetworkProxy. """ ensure_valid(url) scheme = url.scheme() if scheme in ['pac+http', 'pac+https', 'pac+file']: fetcher = pac.PACFetcher(url) fetcher.fetch() return fetcher types = { 'http': QNetworkProxy.ProxyType.HttpProxy, 'socks': QNetworkProxy.ProxyType.Socks5Proxy, 'socks5': QNetworkProxy.ProxyType.Socks5Proxy, 'direct': QNetworkProxy.ProxyType.NoProxy, } if scheme not in types: raise InvalidProxyTypeError(scheme) proxy = QNetworkProxy(types[scheme], url.host()) if url.port() != -1: proxy.setPort(url.port()) if url.userName(): proxy.setUser(url.userName()) if url.password(): proxy.setPassword(url.password()) return proxy def parse_javascript_url(url: QUrl) -> str: """Get JavaScript source from the given URL. See https://wiki.whatwg.org/wiki/URL_schemes#javascript:_URLs and https://github.com/whatwg/url/issues/385 """ ensure_valid(url) if url.scheme() != 'javascript': raise Error("Expected a javascript:... URL") if url.authority(): raise Error("URL contains unexpected components: {}" .format(url.authority())) urlstr = url.toString(FormatOption.ENCODED) urlstr = urllib.parse.unquote(urlstr) code = urlstr.removeprefix('javascript:') if not code: raise Error("Resulted in empty JavaScript code") return code def widened_hostnames(hostname: str) -> Iterable[str]: """A generator for widening string hostnames. Ex: a.c.foo -> [a.c.foo, c.foo, foo]""" while hostname: yield hostname hostname = hostname.partition(".")[-1] def get_url_yank_text(url: QUrl, *, pretty: bool) -> str: """Get the text that should be yanked for the given URL.""" flags = FormatOption.REMOVE_PASSWORD if url.scheme() == 'mailto': flags |= FormatOption.REMOVE_SCHEME if pretty: flags |= FormatOption.DECODE_RESERVED else: flags |= FormatOption.ENCODED url_query = QUrlQuery() url_query_str = url.query() if '&' not in url_query_str and ';' in url_query_str: url_query.setQueryDelimiters('=', ';') url_query.setQuery(url_query_str) for key in dict(url_query.queryItems()): if key in config.val.url.yank_ignored_parameters: url_query.removeQueryItem(key) url.setQuery(url_query) return url.toString(flags) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/usertypes.py0000644000175100017510000004255115102145205021712 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Custom useful data types.""" import html import operator import enum import time import dataclasses import logging from typing import Optional, TypeVar, Union from collections.abc import Sequence from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QTimer from qutebrowser.qt.core import QUrl from qutebrowser.utils import log, qtutils, utils _T = TypeVar('_T', bound=utils.Comparable) class Unset: """Class for an unset object.""" __slots__ = () def __repr__(self) -> str: return '' UNSET = Unset() class NeighborList(Sequence[_T]): """A list of items which saves its current position. Class attributes: Modes: Different modes, see constructor documentation. Attributes: fuzzyval: The value which is currently set but not in the list. _idx: The current position in the list. _items: A list of all items, accessed through item property. _mode: The current mode. """ class Modes(enum.Enum): """Behavior for the 'mode' argument.""" edge = enum.auto() exception = enum.auto() def __init__(self, items: Sequence[_T] = None, default: Union[_T, Unset] = UNSET, mode: Modes = Modes.exception) -> None: """Constructor. Args: items: The list of items to iterate in. _default: The initially selected value. _mode: Behavior when the first/last item is reached. Modes.edge: Go to the first/last item Modes.exception: Raise an IndexError. """ if not isinstance(mode, self.Modes): raise TypeError("Mode {} is not a Modes member!".format(mode)) if items is None: self._items: Sequence[_T] = [] else: self._items = list(items) self._default = default if not isinstance(default, Unset): idx = self._items.index(default) self._idx: Optional[int] = idx else: self._idx = None self._mode = mode self.fuzzyval: Optional[int] = None def __getitem__(self, key: int) -> _T: # type: ignore[override] return self._items[key] def __len__(self) -> int: return len(self._items) def __repr__(self) -> str: return utils.get_repr(self, items=self._items, mode=self._mode, idx=self._idx, fuzzyval=self.fuzzyval) def _snap_in(self, offset: int) -> bool: """Set the current item to the closest item to self.fuzzyval. Args: offset: negative to get the next smaller item, positive for the next bigger one. Return: True if the value snapped in (changed), False when the value already was in the list. """ assert isinstance(self.fuzzyval, (int, float)), self.fuzzyval op = operator.le if offset < 0 else operator.ge items = [(idx, e) for (idx, e) in enumerate(self._items) if op(e, self.fuzzyval)] if items: item = min( items, key=lambda tpl: abs(self.fuzzyval - tpl[1])) # type: ignore[operator] else: sorted_items = sorted(enumerate(self.items), key=lambda e: e[1]) idx = 0 if offset < 0 else -1 item = sorted_items[idx] self._idx = item[0] return self.fuzzyval not in self._items def _get_new_item(self, offset: int) -> _T: """Logic for getitem to get the item at offset. Args: offset: The offset of the current item, relative to the last one. Return: The new item. """ assert self._idx is not None try: if self._idx + offset >= 0: new = self._items[self._idx + offset] else: raise IndexError except IndexError: if self._mode == self.Modes.edge: assert offset != 0 if offset > 0: new = self.lastitem() else: new = self.firstitem() elif self._mode == self.Modes.exception: # pragma: no branch raise else: self._idx += offset return new @property def items(self) -> Sequence[_T]: """Getter for items, which should not be set.""" return self._items def getitem(self, offset: int) -> _T: """Get the item with a relative position. Args: offset: The offset of the current item, relative to the last one. Return: The new item. """ log.misc.debug("{} items, idx {}, offset {}".format( len(self._items), self._idx, offset)) if not self._items: raise IndexError("No items found!") if self.fuzzyval is not None: # Value has been set to something not in the list, so we snap in to # the closest value in the right direction and count this as one # step towards offset. snapped = self._snap_in(offset) if snapped and offset > 0: offset -= 1 elif snapped: offset += 1 self.fuzzyval = None return self._get_new_item(offset) def curitem(self) -> _T: """Get the current item in the list.""" if self._idx is not None: return self._items[self._idx] else: raise IndexError("No current item!") def nextitem(self) -> _T: """Get the next item in the list.""" return self.getitem(1) def previtem(self) -> _T: """Get the previous item in the list.""" return self.getitem(-1) def firstitem(self) -> _T: """Get the first item in the list.""" if not self._items: raise IndexError("No items found!") self._idx = 0 return self.curitem() def lastitem(self) -> _T: """Get the last item in the list.""" if not self._items: raise IndexError("No items found!") self._idx = len(self._items) - 1 return self.curitem() def reset(self) -> _T: """Reset the position to the default.""" if self._default is UNSET: raise ValueError("No default set!") self._idx = self._items.index(self._default) return self.curitem() class PromptMode(enum.Enum): """The mode of a Question.""" yesno = enum.auto() text = enum.auto() user_pwd = enum.auto() alert = enum.auto() download = enum.auto() class ClickTarget(enum.Enum): """How to open a clicked link.""" normal = enum.auto() #: Open the link in the current tab tab = enum.auto() #: Open the link in a new foreground tab tab_bg = enum.auto() #: Open the link in a new background tab window = enum.auto() #: Open the link in a new window hover = enum.auto() #: Only hover over the link class KeyMode(enum.Enum): """Key input modes.""" normal = enum.auto() #: Normal mode (no mode was entered) hint = enum.auto() #: Hint mode (showing labels for links) command = enum.auto() #: Command mode (after pressing the colon key) yesno = enum.auto() #: Yes/No prompts prompt = enum.auto() #: Text prompts insert = enum.auto() #: Insert mode (passing through most keys) passthrough = enum.auto() #: Passthrough mode (passing through all keys) caret = enum.auto() #: Caret mode (moving cursor with keys) set_mark = enum.auto() jump_mark = enum.auto() record_macro = enum.auto() run_macro = enum.auto() # 'register' is a bit of an oddball here: It's not really a "real" mode, # but it's used in the config for common bindings for # set_mark/jump_mark/record_macro/run_macro. register = enum.auto() class Exit(enum.IntEnum): """Exit statuses for errors. Needs to be an int for sys.exit.""" ok = 0 reserved = 1 exception = 2 err_ipc = 3 err_init = 4 class LoadStatus(enum.Enum): """Load status of a tab.""" none = enum.auto() success = enum.auto() success_https = enum.auto() error = enum.auto() warn = enum.auto() loading = enum.auto() class Backend(enum.Enum): """The backend being used (usertypes.backend).""" # pylint: disable=invalid-name QtWebKit = enum.auto() QtWebEngine = enum.auto() class JsWorld(enum.Enum): """World/context to run JavaScript code in.""" main = enum.auto() #: Same world as the web page's JavaScript. application = enum.auto() #: Application world, used by qutebrowser internally. user = enum.auto() #: User world, currently not used. jseval = enum.auto() #: World used for the jseval-command. class JsLogLevel(enum.Enum): """Log level of a JS message. This needs to match up with the keys allowed for the content.javascript.log setting. """ unknown = enum.auto() info = enum.auto() warning = enum.auto() error = enum.auto() class MessageLevel(enum.Enum): """The level of a message being shown.""" error = enum.auto() warning = enum.auto() info = enum.auto() class IgnoreCase(enum.Enum): """Possible values for the 'search.ignore_case' setting.""" smart = enum.auto() never = enum.auto() always = enum.auto() class CommandValue(enum.Enum): """Special values which are injected when running a command handler.""" count = enum.auto() win_id = enum.auto() cur_tab = enum.auto() count_tab = enum.auto() class Question(QObject): """A question asked to the user, e.g. via the status bar. Note the creator is responsible for cleaning up the question after it doesn't need it anymore, e.g. via connecting Question.completed to Question.deleteLater. Attributes: mode: A PromptMode enum member. yesno: A question which can be answered with yes/no. text: A question which requires a free text answer. user_pwd: A question for a username and password. default: The default value. For yesno, None (no default), True or False. For text, a default text as string. For user_pwd, a default username as string. title: The question title to show. text: The prompt text to display to the user. url: Any URL referenced in prompts. option: Boolean option to be set when answering always/never. answer: The value the user entered (as password for user_pwd). is_aborted: Whether the question was aborted. interrupted: Whether the question was interrupted by another one. Signals: answered: Emitted when the question has been answered by the user. arg: The answer to the question. cancelled: Emitted when the question has been cancelled by the user. aborted: Emitted when the question was aborted programmatically. In this case, cancelled is not emitted. answered_yes: Convenience signal emitted when a yesno question was answered with yes. answered_no: Convenience signal emitted when a yesno question was answered with no. completed: Emitted when the question was completed in any way. """ answered = pyqtSignal(object) cancelled = pyqtSignal() aborted = pyqtSignal() answered_yes = pyqtSignal() answered_no = pyqtSignal() completed = pyqtSignal() def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self.mode: Optional[PromptMode] = None self.default: Union[bool, str, None] = None self.title: Optional[str] = None self.text: Optional[str] = None self.url: Optional[str] = None self.option: Optional[bool] = None self.answer: Union[str, bool, None] = None self.is_aborted = False self.interrupted = False def __repr__(self) -> str: return utils.get_repr(self, title=self.title, text=self.text, mode=self.mode, default=self.default, option=self.option) @pyqtSlot() def done(self) -> None: """Must be called when the question was answered completely.""" self.answered.emit(self.answer) if self.mode == PromptMode.yesno: if self.answer: self.answered_yes.emit() else: self.answered_no.emit() self.completed.emit() @pyqtSlot() def cancel(self) -> None: """Cancel the question (resulting from user-input).""" self.cancelled.emit() self.completed.emit() @pyqtSlot() def abort(self) -> None: """Abort the question.""" if self.is_aborted: log.misc.debug("Question was already aborted") return self.is_aborted = True self.aborted.emit() self.completed.emit() class Timer(QTimer): """A timer which has a name to show in __repr__ and checks for overflows. Attributes: _name: The name of the timer. """ def __init__(self, parent: QObject = None, name: str = None) -> None: super().__init__(parent) self._start_time: Optional[float] = None self.timeout.connect(self._validity_check_handler) if name is None: self._name = "unnamed" else: self.setObjectName(name) self._name = name def __repr__(self) -> str: return utils.get_repr(self, name=self._name) @pyqtSlot() def _validity_check_handler(self) -> None: if not self.check_timeout_validity() and self._start_time is not None: elapsed = time.monotonic() - self._start_time level = logging.WARNING if utils.is_windows and self._name == "ipc-timeout": level = logging.DEBUG log.misc.log( level, ( f"Timer {self._name} (id {self.timerId()}) triggered too early: " f"interval {self.interval()} but only {elapsed:.3f}s passed" ) ) def check_timeout_validity(self) -> bool: """Check to see if the timeout signal was fired at the expected time. WORKAROUND for https://bugreports.qt.io/browse/QTBUG-124496 """ if self._start_time is None: # manual emission? return True elapsed = time.monotonic() - self._start_time # Checking for half the interval is pretty arbitrary. In the bug case # the timer typically fires immediately since the expiry event is # already pending when it is created. if elapsed < self.interval() / 1000 / 2: return False return True def setInterval(self, msec: int) -> None: """Extend setInterval to check for overflows.""" qtutils.check_overflow(msec, 'int') super().setInterval(msec) def start(self, msec: int = None) -> None: """Extend start to check for overflows.""" self._start_time = time.monotonic() if msec is not None: qtutils.check_overflow(msec, 'int') super().start(msec) else: super().start() class UndeferrableError(Exception): """An AbstractCertificateErrorWrapper isn't deferrable.""" class AbstractCertificateErrorWrapper: """A wrapper over an SSL/certificate error.""" def __init__(self) -> None: self._certificate_accepted: Optional[bool] = None def __str__(self) -> str: raise NotImplementedError def __repr__(self) -> str: raise NotImplementedError def is_overridable(self) -> bool: raise NotImplementedError def html(self) -> str: return f'

{html.escape(str(self))}

' def accept_certificate(self) -> None: self._certificate_accepted = True def reject_certificate(self) -> None: self._certificate_accepted = False def defer(self) -> None: raise NotImplementedError def certificate_was_accepted(self) -> bool: """Check whether the certificate was accepted by the user.""" if not self.is_overridable(): return False if self._certificate_accepted is None: raise ValueError("No decision taken yet") return self._certificate_accepted @dataclasses.dataclass class NavigationRequest: """A request to navigate to the given URL.""" class Type(enum.Enum): """The type of a request. Based on QWebEngineUrlRequestInfo::NavigationType and QWebPage::NavigationType. """ #: Navigation initiated by clicking a link. link_clicked = 1 #: Navigation explicitly initiated by typing a URL (QtWebEngine only). typed = 2 #: Navigation submits a form. form_submitted = 3 #: An HTML form was submitted a second time (QtWebKit only). form_resubmitted = 4 #: Navigation initiated by a history action. back_forward = 5 #: Navigation initiated by refreshing the page. reload = 6 #: Navigation triggered automatically by page content or remote server #: (QtWebEngine >= 5.14 only) redirect = 7 #: None of the above. other = 8 url: QUrl navigation_type: Type is_main_frame: bool accepted: bool = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/utils.py0000644000175100017510000006567515102145205021023 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Other utilities which don't fit anywhere else.""" import os import os.path import io import re import sys import enum import json import fnmatch import datetime import traceback import functools import contextlib import shlex import sysconfig import mimetypes from typing import (Any, IO, Optional, Union, TypeVar, Protocol) from collections.abc import Iterator, Sequence, Callable from qutebrowser.qt.core import QUrl, QVersionNumber, QRect, QPoint from qutebrowser.qt.gui import QClipboard, QDesktopServices from qutebrowser.qt.widgets import QApplication import yaml try: from yaml import (CSafeLoader as YamlLoader, CSafeDumper as YamlDumper) YAML_C_EXT = True except ImportError: # pragma: no cover from yaml import (SafeLoader as YamlLoader, # type: ignore[assignment] SafeDumper as YamlDumper) YAML_C_EXT = False from qutebrowser.utils import log fake_clipboard: Optional[str] = None log_clipboard = False is_mac = sys.platform.startswith('darwin') is_linux = sys.platform.startswith('linux') is_windows = sys.platform.startswith('win') is_posix = os.name == 'posix' _C = TypeVar("_C", bound="Comparable") class Comparable(Protocol): """Protocol for a "comparable" object.""" def __lt__(self: _C, other: _C) -> bool: ... def __ge__(self: _C, other: _C) -> bool: ... class VersionNumber: """A representation of a version number.""" def __init__(self, *args: int) -> None: self._ver = QVersionNumber(args) # not *args, to support >3 components if self._ver.isNull(): raise ValueError("Can't construct a null version") normalized = self._ver.normalized() if normalized != self._ver: raise ValueError( f"Refusing to construct non-normalized version from {args} " f"(normalized: {tuple(normalized.segments())}).") self.major = self._ver.majorVersion() self.minor = self._ver.minorVersion() self.patch = self._ver.microVersion() self.segments = self._ver.segments() def __str__(self) -> str: return ".".join(str(s) for s in self.segments) def __repr__(self) -> str: args = ", ".join(str(s) for s in self.segments) return f'VersionNumber({args})' def strip_patch(self) -> 'VersionNumber': """Get a new VersionNumber with the patch version removed.""" return VersionNumber(*self.segments[:2]) @classmethod def parse(cls, s: str) -> 'VersionNumber': """Parse a version number from a string.""" ver, _suffix = QVersionNumber.fromString(s) # FIXME: Should we support a suffix? if ver.isNull(): raise ValueError(f"Failed to parse {s}") return cls(*ver.normalized().segments()) def __hash__(self) -> int: return hash(self._ver) def __eq__(self, other: object) -> bool: if not isinstance(other, VersionNumber): return NotImplemented return self._ver == other._ver def __ne__(self, other: object) -> bool: if not isinstance(other, VersionNumber): return NotImplemented return self._ver != other._ver # FIXME:mypy type ignores below needed for PyQt5-stubs: # Unsupported left operand type for ... ("QVersionNumber") def __ge__(self, other: 'VersionNumber') -> bool: return self._ver >= other._ver # type: ignore[operator,unused-ignore] def __gt__(self, other: 'VersionNumber') -> bool: return self._ver > other._ver # type: ignore[operator,unused-ignore] def __le__(self, other: 'VersionNumber') -> bool: return self._ver <= other._ver # type: ignore[operator,unused-ignore] def __lt__(self, other: 'VersionNumber') -> bool: return self._ver < other._ver # type: ignore[operator,unused-ignore] class Unreachable(Exception): """Raised when there was unreachable code.""" class ClipboardError(Exception): """Raised if the clipboard contents are unavailable for some reason.""" class SelectionUnsupportedError(ClipboardError): """Raised if [gs]et_clipboard is used and selection=True is unsupported.""" def __init__(self) -> None: super().__init__("Primary selection is not supported on this " "platform!") class ClipboardEmptyError(ClipboardError): """Raised if get_clipboard is used and the clipboard is empty.""" def elide(text: str, length: int) -> str: """Elide text so it uses a maximum of length chars.""" if length < 1: raise ValueError("length must be >= 1!") if len(text) <= length: return text else: return text[:length - 1] + '\u2026' def elide_filename(filename: str, length: int) -> str: """Elide a filename to the given length. The difference to the elide() is that the text is removed from the middle instead of from the end. This preserves file name extensions. Additionally, standard ASCII dots are used ("...") instead of the unicode "…" (U+2026) so it works regardless of the filesystem encoding. This function does not handle path separators. Args: filename: The filename to elide. length: The maximum length of the filename, must be at least 3. Return: The elided filename. """ elidestr = '...' if length < len(elidestr): raise ValueError('length must be greater or equal to 3') if len(filename) <= length: return filename # Account for '...' length -= len(elidestr) left = length // 2 right = length - left if right == 0: return filename[:left] + elidestr else: return filename[:left] + elidestr + filename[-right:] def compact_text(text: str, elidelength: int = None) -> str: """Remove leading whitespace and newlines from a text and maybe elide it. Args: text: The text to compact. elidelength: To how many chars to elide. """ lines = [] for line in text.splitlines(): lines.append(line.strip()) out = ''.join(lines) if elidelength is not None: out = elide(out, elidelength) return out def format_seconds(total_seconds: int) -> str: """Format a count of seconds to get a [H:]M:SS string.""" prefix = '-' if total_seconds < 0 else '' hours, rem = divmod(abs(round(total_seconds)), 3600) minutes, seconds = divmod(rem, 60) chunks = [] if hours: chunks.append(str(hours)) min_format = '{:02}' else: min_format = '{}' chunks.append(min_format.format(minutes)) chunks.append('{:02}'.format(seconds)) return prefix + ':'.join(chunks) def format_size(size: Optional[float], base: int = 1024, suffix: str = '') -> str: """Format a byte size so it's human readable. Inspired by https://stackoverflow.com/q/1094841 """ prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] if size is None: return '?.??' + suffix for p in prefixes: if -base < size < base: return '{:.02f}{}{}'.format(size, p, suffix) size /= base return '{:.02f}{}{}'.format(size, prefixes[-1], suffix) class FakeIOStream(io.TextIOBase): """A fake file-like stream which calls a function for write-calls.""" def __init__(self, write_func: Callable[[str], int]) -> None: super().__init__() self.write = write_func # type: ignore[method-assign] @contextlib.contextmanager def fake_io(write_func: Callable[[str], int]) -> Iterator[None]: """Run code with stdout and stderr replaced by FakeIOStreams. Args: write_func: The function to call when write is called. """ old_stdout = sys.stdout old_stderr = sys.stderr fake_stderr = FakeIOStream(write_func) fake_stdout = FakeIOStream(write_func) sys.stderr = fake_stderr sys.stdout = fake_stdout try: yield finally: # If the code we did run did change sys.stdout/sys.stderr, we leave it # unchanged. Otherwise, we reset it. if sys.stdout is fake_stdout: sys.stdout = old_stdout if sys.stderr is fake_stderr: sys.stderr = old_stderr @contextlib.contextmanager def disabled_excepthook() -> Iterator[None]: """Run code with the exception hook temporarily disabled.""" old_excepthook = sys.excepthook sys.excepthook = sys.__excepthook__ try: yield finally: # If the code we did run did change sys.excepthook, we leave it # unchanged. Otherwise, we reset it. if sys.excepthook is sys.__excepthook__: sys.excepthook = old_excepthook class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name """Decorator to ignore and log exceptions. This needs to be used for some places where PyQt segfaults on exceptions or silently ignores them. We used to re-raise the exception with a single-shot QTimer in a similar case, but that lead to a strange problem with a KeyError with some random jinja template stuff as content. For now, we only log it, so it doesn't pass 100% silently. This could also be a function, but as a class (with a "wrong" name) it's much cleaner to implement. Attributes: _retval: The value to return in case of an exception. _predicate: The condition which needs to be True to prevent exceptions """ def __init__(self, retval: Any, predicate: bool = True) -> None: """Save decorator arguments. Gets called on parse-time with the decorator arguments. Args: See class attributes. """ self._retval = retval self._predicate = predicate def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: """Called when a function should be decorated. Args: func: The function to be decorated. Return: The decorated function. """ if not self._predicate: return func retval = self._retval @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: """Call the original function.""" try: return func(*args, **kwargs) except BaseException: # noqa: B036 log.misc.exception("Error in {}".format(qualname(func))) return retval return wrapper def is_enum(obj: Any) -> bool: """Check if a given object is an enum.""" try: return issubclass(obj, enum.Enum) except TypeError: return False def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str: """Get a suitable __repr__ string for an object. Args: obj: The object to get a repr for. constructor: If True, show the Foo(one=1, two=2) form instead of . **attrs: The attributes to add. """ cls = qualname(obj.__class__) parts = [] items = sorted(attrs.items()) for name, val in items: parts.append('{}={!r}'.format(name, val)) if constructor: return '{}({})'.format(cls, ', '.join(parts)) elif parts: return '<{} {}>'.format(cls, ' '.join(parts)) else: return '<{}>'.format(cls) def qualname(obj: Any) -> str: """Get the fully qualified name of an object. Based on twisted.python.reflect.fullyQualifiedName. Should work with: - functools.partial objects - functions - classes - methods - modules """ if isinstance(obj, functools.partial): obj = obj.func if hasattr(obj, '__module__'): prefix = '{}.'.format(obj.__module__) else: prefix = '' if hasattr(obj, '__qualname__'): return '{}{}'.format(prefix, obj.__qualname__) elif hasattr(obj, '__name__'): return '{}{}'.format(prefix, obj.__name__) else: return repr(obj) _ExceptionType = Union[type[BaseException], tuple[type[BaseException]]] def raises(exc: _ExceptionType, func: Callable[..., Any], *args: Any) -> bool: """Check if a function raises a given exception. Args: exc: A single exception or an iterable of exceptions. func: A function to call. *args: The arguments to pass to the function. Returns: True if the exception was raised, False otherwise. """ try: func(*args) except exc: return True else: return False def force_encoding(text: str, encoding: str) -> str: """Make sure a given text is encodable with the given encoding. This replaces all chars not encodable with question marks. """ return text.encode(encoding, errors='replace').decode(encoding) def sanitize_filename(name: str, replacement: Optional[str] = '_', shorten: bool = False) -> str: """Replace invalid filename characters. Note: This should be used for the basename, as it also removes the path separator. Args: name: The filename. replacement: The replacement character (or None). shorten: Shorten the filename if it's too long for the filesystem. """ if replacement is None: replacement = '' # Remove chars which can't be encoded in the filename encoding. # See https://github.com/qutebrowser/qutebrowser/issues/427 encoding = sys.getfilesystemencoding() name = force_encoding(name, encoding) # See also # https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words if is_windows: bad_chars = '\\/:*?"<>|' elif is_mac: # Colons can be confusing in finder https://superuser.com/a/326627 bad_chars = '/:' else: bad_chars = '/' for bad_char in bad_chars: name = name.replace(bad_char, replacement) if not shorten: return name # Truncate the filename if it's too long. # Most filesystems have a maximum filename length of 255 bytes: # https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits # We also want to keep some space for QtWebEngine's ".download" suffix, as # well as deduplication counters. max_bytes = 255 - len("(123).download") root, ext = os.path.splitext(name) root = root[:max_bytes - len(ext)] excess = len(os.fsencode(root + ext)) - max_bytes while excess > 0 and root: # Max 4 bytes per character is assumed. # Integer division floors to -∞, not to 0. root = root[:(-excess // 4)] excess = len(os.fsencode(root + ext)) - max_bytes if not root: # Trimming the root is not enough. We must trim the extension. # We leave one character in the root, so that the filename # doesn't start with a dot, which makes the file hidden. root = name[0] excess = len(os.fsencode(root + ext)) - max_bytes while excess > 0 and ext: ext = ext[:(-excess // 4)] excess = len(os.fsencode(root + ext)) - max_bytes assert ext, name name = root + ext return name def _clipboard() -> QClipboard: """Get the QClipboard and make sure it's not None.""" clipboard = QApplication.clipboard() assert clipboard is not None return clipboard def set_clipboard(data: str, selection: bool = False) -> None: """Set the clipboard to some given data.""" global fake_clipboard if selection and not supports_selection(): raise SelectionUnsupportedError if log_clipboard: what = 'primary selection' if selection else 'clipboard' log.misc.debug("Setting fake {}: {}".format(what, json.dumps(data))) fake_clipboard = data else: mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard _clipboard().setText(data, mode=mode) def get_clipboard(selection: bool = False, fallback: bool = False) -> str: """Get data from the clipboard. Args: selection: Use the primary selection. fallback: Fall back to the clipboard if primary selection is unavailable. """ global fake_clipboard if fallback and not selection: raise ValueError("fallback given without selection!") if selection and not supports_selection(): if fallback: selection = False else: raise SelectionUnsupportedError if fake_clipboard is not None: data = fake_clipboard fake_clipboard = None else: mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard data = _clipboard().text(mode=mode) target = "Primary selection" if selection else "Clipboard" if not data.strip(): raise ClipboardEmptyError("{} is empty.".format(target)) log.misc.debug("{} contained: {!r}".format(target, data)) return data def supports_selection() -> bool: """Check if the OS supports primary selection.""" return _clipboard().supportsSelection() def open_file(filename: str, cmdline: str = None) -> None: """Open the given file. If cmdline is not given, downloads.open_dispatcher is used. If open_dispatcher is unset, the system's default application is used. Args: filename: The filename to open. cmdline: The command to use as string. A `{}` is expanded to the filename. None means to use the system's default application or `downloads.open_dispatcher` if set. If no `{}` is found, the filename is appended to the cmdline. """ # Import late to avoid circular imports: # - usertypes -> utils -> guiprocess -> message -> usertypes # - usertypes -> utils -> config -> configdata -> configtypes -> # cmdutils -> command -> message -> usertypes from qutebrowser.config import config from qutebrowser.misc import guiprocess from qutebrowser.utils import version, message # the default program to open downloads with - will be empty string # if we want to use the default override = config.val.downloads.open_dispatcher if version.is_flatpak(): if cmdline: message.error("Cannot spawn download dispatcher from sandbox") return if override: message.warning("Ignoring download dispatcher from config in " "sandbox environment") override = None # precedence order: cmdline > downloads.open_dispatcher > openUrl if cmdline is None and not override: log.misc.debug("Opening {} with the system application" .format(filename)) url = QUrl.fromLocalFile(filename) QDesktopServices.openUrl(url) return if cmdline is None and override: cmdline = override assert cmdline is not None cmd, *args = shlex.split(cmdline) args = [arg.replace('{}', filename) for arg in args] if '{}' not in cmdline: args.append(filename) log.misc.debug("Opening {} with {}" .format(filename, [cmd] + args)) proc = guiprocess.GUIProcess(what='open-file') proc.start_detached(cmd, args) def unused(_arg: Any) -> None: """Function which does nothing to avoid pylint complaining.""" def expand_windows_drive(path: str) -> str: r"""Expand a drive-path like E: into E:\. Does nothing for other paths. Args: path: The path to expand. """ # Usually, "E:" on Windows refers to the current working directory on drive # E:\. The correct way to specify drive E: is "E:\", but most users # probably don't use the "multiple working directories" feature and expect # "E:" and "E:\" to be equal. if re.fullmatch(r'[A-Z]:', path, re.IGNORECASE): return path + "\\" else: return path def yaml_load(f: Union[str, IO[str]]) -> Any: """Wrapper over yaml.load using the C loader if possible.""" start = datetime.datetime.now() # WORKAROUND for https://github.com/yaml/pyyaml/pull/181 with log.py_warning_filter( category=DeprecationWarning, message=r"Using or importing the ABCs from 'collections' instead " r"of from 'collections\.abc' is deprecated.*"): try: data = yaml.load(f, Loader=YamlLoader) except ValueError as e: # pragma: no cover pyyaml_error = 'could not convert string to float' if str(e).startswith(pyyaml_error): # WORKAROUND for https://github.com/yaml/pyyaml/issues/168 raise yaml.YAMLError(e) raise end = datetime.datetime.now() delta = (end - start).total_seconds() if "CI" in os.environ or sysconfig.get_config_var("Py_DEBUG"): deadline = 10 else: deadline = 2 if delta > deadline: # pragma: no cover log.misc.warning( "YAML load took unusually long, please report this at " "https://github.com/qutebrowser/qutebrowser/issues/2777\n" "duration: {}s\n" "PyYAML version: {}\n" "C extension: {}\n" "Stack:\n\n" "{}".format( delta, yaml.__version__, YAML_C_EXT, ''.join(traceback.format_stack()))) return data def yaml_dump(data: Any, f: IO[str] = None) -> Optional[str]: """Wrapper over yaml.dump using the C dumper if possible. Also returns a str instead of bytes. """ yaml_data = yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False, encoding='utf-8', allow_unicode=True) if yaml_data is None: return None else: return yaml_data.decode('utf-8') _T = TypeVar('_T') def chunk(elems: Sequence[_T], n: int) -> Iterator[Sequence[_T]]: """Yield successive n-sized chunks from elems. If elems % n != 0, the last chunk will be smaller. """ if n < 1: raise ValueError("n needs to be at least 1!") for i in range(0, len(elems), n): yield elems[i:i + n] def guess_mimetype(filename: str, fallback: bool = False) -> str: """Guess a mimetype based on a filename. Args: filename: The filename to check. fallback: Fall back to application/octet-stream if unknown. """ mimetype, _encoding = mimetypes.guess_type(filename) if os.path.splitext(filename)[1] == '.mjs' and mimetype == "text/plain": # Windows can sometimes have .mjs registered wrongly as text/plain: # https://github.com/golang/go/issues/68591 return "text/javascript" if mimetype is None: if fallback: return 'application/octet-stream' else: raise ValueError("Got None mimetype for {}".format(filename)) return mimetype def ceil_log(number: int, base: int) -> int: """Compute max(1, ceil(log(number, base))). Use only integer arithmetic in order to avoid numerical error. """ if number < 1 or base < 2: raise ValueError("math domain error") result = 1 accum = base while accum < number: result += 1 accum *= base return result def parse_duration(duration: str) -> int: """Parse duration in format XhYmZs into milliseconds duration.""" if duration.isdigit(): # For backward compatibility return milliseconds return int(duration) match = re.fullmatch( r'(?P[0-9]+(\.[0-9])?h)?\s*' r'(?P[0-9]+(\.[0-9])?m)?\s*' r'(?P[0-9]+(\.[0-9])?s)?', duration ) if not match or not match.group(0): raise ValueError( f"Invalid duration: {duration} - " "expected XhYmZs or a number of milliseconds" ) seconds_string = match.group('seconds') if match.group('seconds') else '0' seconds = float(seconds_string.rstrip('s')) minutes_string = match.group('minutes') if match.group('minutes') else '0' minutes = float(minutes_string.rstrip('m')) hours_string = match.group('hours') if match.group('hours') else '0' hours = float(hours_string.rstrip('h')) milliseconds = int((seconds + minutes * 60 + hours * 3600) * 1000) return milliseconds def mimetype_extension(mimetype: str) -> Optional[str]: """Get a suitable extension for a given mimetype. This mostly delegates to Python's mimetypes.guess_extension(), but backports some changes (via a simple override dict) which are missing from earlier Python versions. """ overrides = {} if sys.version_info[:2] < (3, 13): overrides.update({ "text/rtf": ".rtf", "text/markdown": ".md", "text/x-rst": ".rst", }) if sys.version_info[:2] < (3, 12): overrides.update({ "text/javascript": ".js", }) if sys.version_info[:2] < (3, 11): overrides.update({ "application/n-quads": ".nq", "application/n-triples": ".nt", "application/trig": ".trig", "image/avif": ".avif", "image/webp": ".webp", "text/n3": ".n3", "text/vtt": ".vtt", }) if sys.version_info[:2] < (3, 10): overrides.update({ "application/x-hdf5": ".h5", "audio/3gpp": ".3gp", "audio/3gpp2": ".3g2", "audio/aac": ".aac", "audio/opus": ".opus", "image/heic": ".heic", "image/heif": ".heif", }) if mimetype in overrides: return overrides[mimetype] return mimetypes.guess_extension(mimetype, strict=False) @contextlib.contextmanager def cleanup_file(filepath: str) -> Iterator[None]: """Context that deletes a file upon exit or error. Args: filepath: The file path """ try: yield finally: try: os.remove(filepath) except OSError as e: log.misc.error(f"Failed to delete tempfile {filepath} ({e})!") _RECT_PATTERN = re.compile(r'(?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+)') def parse_rect(s: str) -> QRect: """Parse a rectangle string like 20x20+5+3. Negative offsets aren't supported, and neither is leaving off parts of the string. """ match = _RECT_PATTERN.match(s) if not match: raise ValueError(f"String {s} does not match WxH+X+Y") w = int(match.group('w')) h = int(match.group('h')) x = int(match.group('x')) y = int(match.group('y')) try: rect = QRect(x, y, w, h) except OverflowError as e: raise ValueError(e) if not rect.isValid(): raise ValueError("Invalid rectangle") return rect def parse_point(s: str) -> QPoint: """Parse a point string like 13,-42.""" try: x, y = map(int, s.split(',', maxsplit=1)) except ValueError: raise ValueError(f"String {s} does not match X,Y") try: return QPoint(x, y) except OverflowError as e: raise ValueError(e) def match_globs(patterns: list[str], value: str) -> Optional[str]: """Match a list of glob-like patterns against a value. Return: The first matching pattern if there was a match, None with no match. """ for pattern in patterns: if fnmatch.fnmatchcase(name=value, pat=pattern): return pattern return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser/utils/version.py0000644000175100017510000012624515102145205021337 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Utilities to show various version information.""" import re import sys import glob import os.path import platform import subprocess import importlib import pathlib import configparser import enum import datetime import getpass import functools import dataclasses import importlib.metadata from typing import (Optional, ClassVar, Any, TYPE_CHECKING) from collections.abc import Mapping, Sequence from qutebrowser.qt import machinery from qutebrowser.qt.core import PYQT_VERSION_STR from qutebrowser.qt.network import QSslSocket from qutebrowser.qt.gui import QOpenGLContext, QOffscreenSurface from qutebrowser.qt.opengl import QOpenGLVersionProfile from qutebrowser.qt.widgets import QApplication try: from qutebrowser.qt.webkit import qWebKitVersion except ImportError: # pragma: no cover qWebKitVersion = None # type: ignore[assignment] # noqa: N816 try: from qutebrowser.qt.webenginecore import PYQT_WEBENGINE_VERSION_STR except ImportError: # pragma: no cover # QtWebKit PYQT_WEBENGINE_VERSION_STR = None # type: ignore[assignment] import qutebrowser from qutebrowser.utils import (log, utils, standarddir, usertypes, message, resources, qtutils) from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf, wmname from qutebrowser.browser import pdfjs from qutebrowser.config import config if TYPE_CHECKING: from qutebrowser.config import websettings _LOGO = r''' ______ ,, ,.-"` | ,-` | .^ || | / ,-*^| || | ; / | || ;-*```^*. ; ; | |;,-*` \ | | | ,-*` ,-"""\ \ | \ ,-"` ,-^`| \ | \ `^^ ,-;| | ; | *; ,-*` || | / ;; `^^`` | || | ,^ / | || `^^` ,^ | _,"| _,-" -*` ****"""`` ''' @dataclasses.dataclass class DistributionInfo: """Information about the running distribution.""" id: Optional[str] parsed: 'Distribution' pretty: str pastebin_url: Optional[str] = None class Distribution(enum.Enum): """A known Linux distribution. Usually lines up with ID=... in /etc/os-release. """ unknown = enum.auto() ubuntu = enum.auto() debian = enum.auto() void = enum.auto() arch = enum.auto() # includes rolling-release derivatives gentoo = enum.auto() # includes funtoo fedora = enum.auto() opensuse = enum.auto() linuxmint = enum.auto() manjaro = enum.auto() kde_flatpak = enum.auto() # org.kde.Platform neon = enum.auto() nixos = enum.auto() alpine = enum.auto() solus = enum.auto() def _parse_os_release() -> Optional[dict[str, str]]: """Parse an /etc/os-release file.""" filename = os.environ.get('QUTE_FAKE_OS_RELEASE', '/etc/os-release') info = {} try: with open(filename, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if (not line) or line.startswith('#') or '=' not in line: continue k, v = line.split("=", maxsplit=1) info[k] = v.strip('"') except (OSError, UnicodeDecodeError): return None return info def distribution() -> Optional[DistributionInfo]: """Get some information about the running Linux distribution. Returns: A DistributionInfo object, or None if no info could be determined. parsed: A Distribution enum member pretty: Always a string (might be "Unknown") """ info = _parse_os_release() if info is None: return None pretty = info.get('PRETTY_NAME', None) if pretty in ['Linux', None]: # Funtoo has PRETTY_NAME=Linux pretty = info.get('NAME', 'Unknown') assert pretty is not None dist_id = info.get('ID', None) id_mappings = { 'funtoo': 'gentoo', # does not have ID_LIKE=gentoo 'artix': 'arch', 'org.kde.Platform': 'kde_flatpak', } ids = [] if dist_id is not None: ids.append(id_mappings.get(dist_id, dist_id)) if 'ID_LIKE' in info: ids.extend(info['ID_LIKE'].split()) parsed = Distribution.unknown for cur_id in ids: try: parsed = Distribution[cur_id] except KeyError: pass else: break return DistributionInfo(parsed=parsed, pretty=pretty, id=dist_id) def is_flatpak() -> bool: """Whether qutebrowser is running via Flatpak. If packaged via Flatpak, the environment is has restricted access to the host system. """ return flatpak_id() is not None _FLATPAK_INFO_PATH = '/.flatpak-info' def flatpak_id() -> Optional[str]: """Get the ID of the currently running Flatpak (or None if outside of Flatpak).""" if 'FLATPAK_ID' in os.environ: return os.environ['FLATPAK_ID'] # 'FLATPAK_ID' was only added in Flatpak 1.2.0: # https://lists.freedesktop.org/archives/flatpak/2019-January/001464.html # but e.g. Ubuntu 18.04 ships 1.0.9. info_file = pathlib.Path(_FLATPAK_INFO_PATH) if not info_file.exists(): return None parser = configparser.ConfigParser() parser.read(info_file) return parser['Application']['name'] def _git_str() -> Optional[str]: """Try to find out git version. Return: string containing the git commit ID. None if there was an error or we're not in a git repo. """ # First try via subprocess if possible commit = None if not hasattr(sys, "frozen"): try: gitpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir, os.path.pardir) except (NameError, OSError): log.misc.exception("Error while getting git path") else: commit = _git_str_subprocess(gitpath) if commit is not None: return commit # If that fails, check the git-commit-id file. try: return resources.read_file('git-commit-id') except (OSError, ImportError): return None def _call_git(gitpath: str, *args: str) -> str: """Call a git subprocess.""" return subprocess.run( ['git'] + list(args), cwd=gitpath, check=True, stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() def _git_str_subprocess(gitpath: str) -> Optional[str]: """Try to get the git commit ID and timestamp by calling git. Args: gitpath: The path where the .git folder is. Return: The ID/timestamp on success, None on failure. """ if not os.path.isdir(os.path.join(gitpath, ".git")): return None try: # https://stackoverflow.com/questions/21017300/21017394#21017394 commit_hash = _call_git(gitpath, 'describe', '--match=NeVeRmAtCh', '--always', '--dirty') date = _call_git(gitpath, 'show', '-s', '--format=%ci', 'HEAD') branch = _call_git(gitpath, 'rev-parse', '--abbrev-ref', 'HEAD') return '{} on {} ({})'.format(commit_hash, branch, date) except (subprocess.CalledProcessError, OSError): return None def _release_info() -> Sequence[tuple[str, str]]: """Try to gather distribution release information. Return: list of (filename, content) tuples. """ blacklisted = ['ANSI_COLOR=', 'HOME_URL=', 'SUPPORT_URL=', 'BUG_REPORT_URL='] data = [] for fn in glob.glob("/etc/*-release"): lines = [] try: with open(fn, 'r', encoding='utf-8') as f: for line in f.read().strip().splitlines(): if not any(line.startswith(bl) for bl in blacklisted): lines.append(line) if lines: data.append((fn, '\n'.join(lines))) except OSError: log.misc.exception("Error while reading {}.".format(fn)) return data class ModuleInfo: """Class to query version information of qutebrowser dependencies. Attributes: name: Name of the module as it is imported. _version_attributes: Sequence of attribute names belonging to the module which may hold version information. min_version: Minimum version of this module which qutebrowser can use. _installed: Is the module installed? Determined at runtime. _version: Version of the module. Determined at runtime. _initialized: Set to `True` if the `self._installed` and `self._version` attributes have been set. """ def __init__( self, name: str, version_attributes: Sequence[str], min_version: Optional[str] = None ): self.name = name self._version_attributes = version_attributes self.min_version = min_version self._installed = False self._version: Optional[str] = None self._initialized = False def _reset_cache(self) -> None: """Reset the version cache. It is necessary to call this method in unit tests that mock a module's version number. """ self._installed = False self._version = None self._initialized = False def _initialize_info(self) -> None: """Import module and set `self.installed` and `self.version`.""" try: module = importlib.import_module(self.name) except (ImportError, ValueError): self._installed = False return self._installed = True for attribute_name in self._version_attributes: if hasattr(module, attribute_name): version = getattr(module, attribute_name) assert isinstance(version, (str, float)) self._version = str(version) break if self._version is None: try: self._version = importlib.metadata.version(self.name) except importlib.metadata.PackageNotFoundError: log.misc.debug(f"{self.name} not found") self._version = None self._initialized = True def get_version(self) -> Optional[str]: """Finds the module version if it exists.""" if not self._initialized: self._initialize_info() return self._version def is_installed(self) -> bool: """Checks whether the module is installed.""" if not self._initialized: self._initialize_info() return self._installed def is_outdated(self) -> Optional[bool]: """Checks whether the module is outdated. Return: A boolean when the version and minimum version are both defined. Otherwise `None`. """ version = self.get_version() if ( not self.is_installed() or version is None or self.min_version is None ): return None return version < self.min_version def is_usable(self) -> bool: """Whether the module is both installed and not outdated.""" return self.is_installed() and not self.is_outdated() def __str__(self) -> str: if not self.is_installed(): return f'{self.name}: no' version = self.get_version() if version is None: return f'{self.name}: unknown' text = f'{self.name}: {version}' if self.is_outdated(): text += f" (< {self.min_version}, outdated)" return text def _create_module_info() -> dict[str, ModuleInfo]: packages = [ ('colorama', ['VERSION', '__version__']), ('jinja2', []), ('pygments', ['__version__']), ('yaml', ['__version__']), ('adblock', ['__version__'], "0.3.2"), ('objc', ['__version__']), ] if machinery.IS_QT5: packages += [ ('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']), ('PyQt5.QtWebKitWidgets', []), ('PyQt5.sip', ['SIP_VERSION_STR']), ] elif machinery.IS_QT6: packages += [ ('PyQt6.QtWebEngineCore', ['PYQT_WEBENGINE_VERSION_STR']), ('PyQt6.sip', ['SIP_VERSION_STR']), ] else: raise utils.Unreachable() # Mypy doesn't understand this. See https://github.com/python/mypy/issues/9706 return { name: ModuleInfo(name, *args) # type: ignore[arg-type, misc] for (name, *args) in packages } MODULE_INFO: Mapping[str, ModuleInfo] = _create_module_info() def _module_versions() -> Sequence[str]: """Get versions of optional modules. Return: A list of lines with version info. """ return [str(mod_info) for mod_info in MODULE_INFO.values()] def _path_info() -> Mapping[str, str]: """Get info about important path names. Return: A dictionary of descriptive to actual path names. """ info = { 'config': standarddir.config(), 'data': standarddir.data(), 'cache': standarddir.cache(), 'runtime': standarddir.runtime(), } if standarddir.config() != standarddir.config(auto=True): info['auto config'] = standarddir.config(auto=True) if standarddir.data() != standarddir.data(system=True): info['system data'] = standarddir.data(system=True) return info def _os_info() -> Sequence[str]: """Get operating system info. Return: A list of lines with version info. """ lines = [] releaseinfo = None if utils.is_linux: osver = '' releaseinfo = _release_info() elif utils.is_windows: osver = ', '.join(platform.win32_ver()) elif utils.is_mac: release, info_tpl, machine = platform.mac_ver() if all(not e for e in info_tpl): versioninfo = '' else: versioninfo = '.'.join(info_tpl) osver = ', '.join(e for e in [release, versioninfo, machine] if e) elif utils.is_posix: osver = ' '.join(platform.uname()) else: osver = '?' lines.append('OS Version: {}'.format(osver)) if releaseinfo is not None: for (fn, data) in releaseinfo: lines += ['', '--- {} ---'.format(fn), data] return lines def _pdfjs_version() -> str: """Get the pdf.js version. Return: A string with the version number. """ try: pdfjs_file, file_path = pdfjs.get_pdfjs_res_and_path(pdfjs.get_pdfjs_js_path()) except pdfjs.PDFJSNotFound: return 'no' else: pdfjs_file = pdfjs_file.decode('utf-8') version_re = re.compile( r"""^ *(PDFJS\.version|(var|const|\*) pdfjsVersion) = ['"]?(?P[^'"\n]+)['"]?;?$""", re.MULTILINE) match = version_re.search(pdfjs_file) pdfjs_version = 'unknown' if not match else match.group('version') if file_path is None: file_path = 'bundled' return '{} ({})'.format(pdfjs_version, file_path) def _get_pyqt_webengine_qt_version() -> Optional[str]: """Get the version of the PyQtWebEngine-Qt package. With PyQtWebEngine 5.15.3, the QtWebEngine binary got split into its own PyQtWebEngine-Qt PyPI package: https://www.riverbankcomputing.com/pipermail/pyqt/2021-February/043591.html https://www.riverbankcomputing.com/pipermail/pyqt/2021-February/043638.html PyQtWebEngine 5.15.4 renamed it to PyQtWebEngine-Qt5...: https://www.riverbankcomputing.com/pipermail/pyqt/2021-March/043699.html Here, we try to use importlib.metadata to figure out that version number. If PyQtWebEngine is installed via pip, this will give us an accurate answer. """ names = ( ['PyQt6-WebEngine-Qt6'] if machinery.IS_QT6 else ['PyQtWebEngine-Qt5', 'PyQtWebEngine-Qt'] ) for name in names: try: return importlib.metadata.version(name) except importlib.metadata.PackageNotFoundError: log.misc.debug(f"{name} not found") return None @dataclasses.dataclass class WebEngineVersions: """Version numbers for QtWebEngine and the underlying Chromium.""" webengine: utils.VersionNumber chromium: Optional[str] source: str chromium_security: Optional[str] = None chromium_major: Optional[int] = dataclasses.field(init=False) # Dates based on https://chromium.googlesource.com/chromium/src/+refs _BASES: ClassVar[dict[int, str]] = { 83: '83.0.4103.122', # 2020-06-27, Qt 5.15.2 87: '87.0.4280.144', # 2021-01-08, Qt 5.15 90: '90.0.4430.228', # 2021-06-22, Qt 6.2 94: '94.0.4606.126', # 2021-11-17, Qt 6.3 102: '102.0.5005.177', # 2022-09-01, Qt 6.4 # (.220 claimed by code, .181 claimed by CHROMIUM_VERSION) 108: '108.0.5359.220', # 2023-01-27, Qt 6.5 112: '112.0.5615.213', # 2023-05-24, Qt 6.6 118: '118.0.5993.220', # 2024-01-25, Qt 6.7 122: '122.0.6261.171', # 2024-04-15, Qt 6.8 130: '130.0.6723.192', # 2025-01-06, Qt 6.9 134: '134.0.6998.208', # 2025-04-16, Qt 6.10 } # Dates based on https://chromereleases.googleblog.com/ _CHROMIUM_VERSIONS: ClassVar[dict[utils.VersionNumber, tuple[str, Optional[str]]]] = { # ====== UNSUPPORTED ===== # Qt 5.12: Chromium 69 # (LTS) 69.0.3497.128 (~2018-09-11) # 5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06) # Qt 5.13: Chromium 73 # 73.0.3683.105 (~2019-02-28) # 5.13.2: Security fixes up to 77.0.3865.120 (2019-10-10) # Qt 5.14: Chromium 77 # 77.0.3865.129 (~2019-10-10) # 5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03) # Qt 5.15: Chromium 80 # 80.0.3987.163 (2020-04-02) # 5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05) # 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25) # ====== SUPPORTED ===== # base security ## Qt 5.15 utils.VersionNumber(5, 15, 2): (_BASES[83], '86.0.4240.183'), # 2020-11-02 utils.VersionNumber(5, 15): (_BASES[87], None), # >= 5.15.3 utils.VersionNumber(5, 15, 3): (_BASES[87], '88.0.4324.150'), # 2021-02-04 # 5.15.4 to 5.15.6: unknown security fixes utils.VersionNumber(5, 15, 7): (_BASES[87], '94.0.4606.61'), # 2021-09-24 utils.VersionNumber(5, 15, 8): (_BASES[87], '96.0.4664.110'), # 2021-12-13 utils.VersionNumber(5, 15, 9): (_BASES[87], '98.0.4758.102'), # 2022-02-14 utils.VersionNumber(5, 15, 10): (_BASES[87], '98.0.4758.102'), # (?) 2022-02-14 utils.VersionNumber(5, 15, 11): (_BASES[87], '98.0.4758.102'), # (?) 2022-02-14 utils.VersionNumber(5, 15, 12): (_BASES[87], '98.0.4758.102'), # (?) 2022-02-14 utils.VersionNumber(5, 15, 13): (_BASES[87], '108.0.5359.124'), # 2022-12-13 utils.VersionNumber(5, 15, 14): (_BASES[87], '113.0.5672.64'), # 2023-05-02 # 5.15.15: unknown security fixes utils.VersionNumber(5, 15, 16): (_BASES[87], '119.0.6045.123'), # 2023-11-07 utils.VersionNumber(5, 15, 17): (_BASES[87], '123.0.6312.58'), # 2024-03-19 utils.VersionNumber(5, 15, 18): (_BASES[87], '130.0.6723.59'), # 2024-10-14 utils.VersionNumber(5, 15, 19): (_BASES[87], '135.0.7049.95'), # 2025-04-14 ## Qt 6.2 utils.VersionNumber(6, 2): (_BASES[90], '93.0.4577.63'), # 2021-08-31 utils.VersionNumber(6, 2, 1): (_BASES[90], '94.0.4606.61'), # 2021-09-24 utils.VersionNumber(6, 2, 2): (_BASES[90], '96.0.4664.45'), # 2021-11-15 utils.VersionNumber(6, 2, 3): (_BASES[90], '96.0.4664.45'), # 2021-11-15 utils.VersionNumber(6, 2, 4): (_BASES[90], '98.0.4758.102'), # 2022-02-14 # 6.2.5 / 6.2.6: unknown security fixes utils.VersionNumber(6, 2, 7): (_BASES[90], '107.0.5304.110'), # 2022-11-08 utils.VersionNumber(6, 2, 8): (_BASES[90], '111.0.5563.110'), # 2023-03-21 ## Qt 6.3 utils.VersionNumber(6, 3): (_BASES[94], '99.0.4844.84'), # 2022-03-25 utils.VersionNumber(6, 3, 1): (_BASES[94], '101.0.4951.64'), # 2022-05-10 utils.VersionNumber(6, 3, 2): (_BASES[94], '104.0.5112.81'), # 2022-08-01 ## Qt 6.4 utils.VersionNumber(6, 4): (_BASES[102], '104.0.5112.102'), # 2022-08-16 utils.VersionNumber(6, 4, 1): (_BASES[102], '107.0.5304.88'), # 2022-10-27 utils.VersionNumber(6, 4, 2): (_BASES[102], '108.0.5359.94'), # 2022-12-02 utils.VersionNumber(6, 4, 3): (_BASES[102], '110.0.5481.78'), # 2023-02-07 ## Qt 6.5 utils.VersionNumber(6, 5): (_BASES[108], '110.0.5481.104'), # 2023-02-16 utils.VersionNumber(6, 5, 1): (_BASES[108], '112.0.5615.138'), # 2023-04-18 utils.VersionNumber(6, 5, 2): (_BASES[108], '114.0.5735.133'), # 2023-06-13 utils.VersionNumber(6, 5, 3): (_BASES[108], '117.0.5938.63'), # 2023-09-12 ## Qt 6.6 utils.VersionNumber(6, 6): (_BASES[112], '117.0.5938.63'), # 2023-09-12 utils.VersionNumber(6, 6, 1): (_BASES[112], '119.0.6045.123'), # 2023-11-07 utils.VersionNumber(6, 6, 2): (_BASES[112], '121.0.6167.160'), # 2024-02-06 utils.VersionNumber(6, 6, 3): (_BASES[112], '122.0.6261.128'), # 2024-03-12 ## Qt 6.7 utils.VersionNumber(6, 7): (_BASES[118], '122.0.6261.128'), # 2024-03-12 utils.VersionNumber(6, 7, 1): (_BASES[118], '124.0.6367.202'), # 2024-05-09 utils.VersionNumber(6, 7, 2): (_BASES[118], '125.0.6422.142'), # 2024-05-30 utils.VersionNumber(6, 7, 3): (_BASES[118], '129.0.6668.58'), # 2024-09-17 ## Qt 6.8 utils.VersionNumber(6, 8): (_BASES[122], '129.0.6668.70'), # 2024-09-24 utils.VersionNumber(6, 8, 1): (_BASES[122], '131.0.6778.70'), # 2024-11-12 utils.VersionNumber(6, 8, 2): (_BASES[122], '132.0.6834.111'), # 2025-01-22 utils.VersionNumber(6, 8, 3): (_BASES[122], '134.0.6998.89'), # 2025-03-10 ## Qt 6.9 utils.VersionNumber(6, 9): (_BASES[130], '133.0.6943.141'), # 2025-02-25 utils.VersionNumber(6, 9, 1): (_BASES[130], '136.0.7103.114'), # 2025-05-13 utils.VersionNumber(6, 9, 2): (_BASES[130], '139.0.7258.67'), # 2025-07-29 ## Qt 6.10 utils.VersionNumber(6, 10): (_BASES[134], '140.0.7339.207'), # 2025-09-22 } def __post_init__(self) -> None: """Set the major Chromium version.""" if self.chromium is None: self.chromium_major = None else: self.chromium_major = int(self.chromium.split('.')[0]) def __str__(self) -> str: lines = [f'QtWebEngine {self.webengine}'] if self.chromium is not None: lines.append(f' based on Chromium {self.chromium}') if self.chromium_security is not None: lines.append(f' with security patches up to {self.chromium_security} (plus any distribution patches)') lines.append(f' (source: {self.source})') return "\n".join(lines) @classmethod def from_ua(cls, ua: 'websettings.UserAgent') -> 'WebEngineVersions': """Get the versions parsed from a user agent. This is the most reliable and "default" way to get this information for older Qt versions that don't provide an API for it. However, it needs a fully initialized QtWebEngine, and we sometimes need this information before that is available. """ assert ua.qt_version is not None, ua webengine = utils.VersionNumber.parse(ua.qt_version) chromium_inferred, chromium_security = cls._infer_chromium_version(webengine) if ua.upstream_browser_version != chromium_inferred: # pragma: no cover # should never happen, but let's play it safe log.misc.debug( f"Chromium version mismatch: {ua.upstream_browser_version} (UA) != " f"{chromium_inferred} (inferred)") chromium_security = None return cls( webengine=webengine, chromium=ua.upstream_browser_version, chromium_security=chromium_security, source='UA', ) @classmethod def from_elf(cls, versions: elf.Versions) -> 'WebEngineVersions': """Get the versions based on an ELF file. This only works on Linux, and even there, depends on various assumption on how QtWebEngine is built (e.g. that the version string is in the .rodata section). On Windows/macOS, we instead rely on from_pyqt, but especially on Linux, people sometimes mix and match Qt/QtWebEngine versions, so this is a more reliable (though hackish) way to get a more accurate result. """ webengine = utils.VersionNumber.parse(versions.webengine) chromium_inferred, chromium_security = cls._infer_chromium_version(webengine) if versions.chromium != chromium_inferred: # pragma: no cover # should never happen, but let's play it safe log.misc.debug( f"Chromium version mismatch: {versions.chromium} (ELF) != " f"{chromium_inferred} (inferred)") chromium_security = None return cls( webengine=webengine, chromium=versions.chromium, chromium_security=chromium_security, source='ELF', ) @classmethod def _infer_chromium_version( cls, pyqt_webengine_version: utils.VersionNumber, ) -> tuple[Optional[str], Optional[str]]: """Infer the Chromium version based on the PyQtWebEngine version. Returns: A tuple of the Chromium version and the security patch version. """ chromium_version, security_version = cls._CHROMIUM_VERSIONS.get( pyqt_webengine_version, (None, None)) if chromium_version is not None: return chromium_version, security_version # 5.15 patch versions change their QtWebEngine version, but no changes are # expected after 5.15.3 and 5.15.[01] are unsupported. assert pyqt_webengine_version != utils.VersionNumber(5, 15, 2) # e.g. 5.15.4 -> 5.15 # we ignore the security version as that one will have changed from .0 # and is thus unknown. minor_version = pyqt_webengine_version.strip_patch() chromium_ver, _security_ver = cls._CHROMIUM_VERSIONS.get( minor_version, (None, None)) return chromium_ver, None @classmethod def from_api( cls, qtwe_version: str, chromium_version: Optional[str], chromium_security: Optional[str] = None, ) -> 'WebEngineVersions': """Get the versions based on the exact versions. This is called if we have proper APIs to get the versions easily (Qt 6.2 with PyQt 6.3.1+). """ parsed = utils.VersionNumber.parse(qtwe_version) return cls( webengine=parsed, chromium=chromium_version, chromium_security=chromium_security, source='api', ) @classmethod def from_webengine( cls, pyqt_webengine_qt_version: str, source: str, ) -> 'WebEngineVersions': """Get the versions based on the PyQtWebEngine version. This is called if we don't want to fully initialize QtWebEngine (so from_ua isn't possible), we're not on Linux (or ELF parsing failed), but we have a PyQtWebEngine-Qt{,5} package from PyPI, so we could query its exact version. """ parsed = utils.VersionNumber.parse(pyqt_webengine_qt_version) chromium, chromium_security = cls._infer_chromium_version(parsed) return cls( webengine=parsed, chromium=chromium, chromium_security=chromium_security, source=source, ) @classmethod def from_pyqt(cls, pyqt_webengine_version: str, source: str = "PyQt") -> 'WebEngineVersions': """Get the versions based on the PyQtWebEngine version. This is the "last resort" if we don't want to fully initialize QtWebEngine (so from_ua isn't possible), we're not on Linux (or ELF parsing failed), and PyQtWebEngine-Qt{5,} isn't available from PyPI. Here, we assume that the PyQtWebEngine version is the same as the QtWebEngine version, and infer the Chromium version from that. This assumption isn't generally true, but good enough for some scenarios, especially the prebuilt Windows/macOS releases. """ parsed = utils.VersionNumber.parse(pyqt_webengine_version) if utils.VersionNumber(5, 15, 3) <= parsed < utils.VersionNumber(6): # If we land here, we're in a tricky situation where we are forced to guess: # # PyQt 5.15.3 and 5.15.4 from PyPI come with QtWebEngine 5.15.2 (Chromium # 83), not 5.15.3 (Chromium 87). Given that there was no binary release of # QtWebEngine 5.15.3, this is unlikely to change before Qt 6. # # However, at this point: # # - ELF parsing failed # (so we're likely on macOS or Windows, but not definitely) # # - Getting infos from a PyPI-installed PyQtWebEngine failed # (so we're either in a PyInstaller-deployed qutebrowser, or a self-built # or distribution-installed Qt) # # PyQt 5.15.3 and 5.15.4 come with QtWebEngine 5.15.2 (83-based), but if # someone lands here with the last Qt/PyQt installed from source, they might # be using QtWebEngine 5.15.3 (87-based). For now, we play it safe, and only # do this kind of "downgrade" when we know we're using PyInstaller. frozen = hasattr(sys, 'frozen') log.misc.debug(f"PyQt5 >= 5.15.3, frozen {frozen}") if frozen: parsed = utils.VersionNumber(5, 15, 2) chromium, chromium_security = cls._infer_chromium_version(parsed) return cls( webengine=parsed, chromium=chromium, chromium_security=chromium_security, source=source, ) def qtwebengine_versions(*, avoid_init: bool = False) -> WebEngineVersions: """Get the QtWebEngine and Chromium version numbers. If we have a parsed user agent, we use it here. If not, we avoid initializing things at all costs (because this gets called early to find out about commandline arguments). Instead, we fall back on looking at the ELF file (on Linux), or, if that fails, use the PyQtWebEngine version. This can also be checked by looking at this file with the right Qt tag: https://code.qt.io/cgit/qt/qtwebengine.git/tree/tools/scripts/version_resolver.py#n41 See WebEngineVersions above for a quick reference. Also see: - https://chromiumdash.appspot.com/schedule - https://www.chromium.org/developers/calendar - https://chromereleases.googleblog.com/ """ override = os.environ.get('QUTE_QTWEBENGINE_VERSION_OVERRIDE') if override is not None: return WebEngineVersions.from_pyqt(override, source='override') if machinery.IS_QT6: try: from qutebrowser.qt.webenginecore import ( qWebEngineVersion, qWebEngineChromiumVersion, ) except ImportError: pass # Needs QtWebEngine 6.2+ with PyQtWebEngine 6.3.1+ else: try: from qutebrowser.qt.webenginecore import ( qWebEngineChromiumSecurityPatchVersion, ) chromium_security = qWebEngineChromiumSecurityPatchVersion() except ImportError: chromium_security = None # Needs QtWebEngine 6.3+ qtwe_version = qWebEngineVersion() assert qtwe_version is not None return WebEngineVersions.from_api( qtwe_version=qtwe_version, chromium_version=qWebEngineChromiumVersion(), chromium_security=chromium_security, ) from qutebrowser.browser.webengine import webenginesettings if webenginesettings.parsed_user_agent is None and not avoid_init: webenginesettings.init_user_agent() if webenginesettings.parsed_user_agent is not None: return WebEngineVersions.from_ua(webenginesettings.parsed_user_agent) versions = elf.parse_webenginecore() if versions is not None: return WebEngineVersions.from_elf(versions) pyqt_webengine_qt_version = _get_pyqt_webengine_qt_version() if pyqt_webengine_qt_version is not None: return WebEngineVersions.from_webengine( pyqt_webengine_qt_version, source='importlib') assert PYQT_WEBENGINE_VERSION_STR is not None return WebEngineVersions.from_pyqt(PYQT_WEBENGINE_VERSION_STR) def _backend() -> str: """Get the backend line with relevant information.""" if objects.backend == usertypes.Backend.QtWebKit: return 'new QtWebKit (WebKit {})'.format(qWebKitVersion()) elif objects.backend == usertypes.Backend.QtWebEngine: return str(qtwebengine_versions( avoid_init='avoid-chromium-init' in objects.debug_flags)) raise utils.Unreachable(objects.backend) def _webengine_extensions() -> Sequence[str]: """Get a list of WebExtensions enabled in QtWebEngine.""" lines: list[str] = [] if ( objects.backend == usertypes.Backend.QtWebEngine and "avoid-chromium-init" not in objects.debug_flags and machinery.IS_QT6 # mypy; TODO early return once Qt 5 is dropped ): from qutebrowser.qt.webenginecore import QWebEngineProfile profile = QWebEngineProfile.defaultProfile() assert profile is not None # mypy try: ext_manager = profile.extensionManager() except AttributeError: # Added in QtWebEngine 6.10 return [] assert ext_manager is not None # mypy lines.append("WebExtensions:") if not ext_manager.extensions(): lines[0] += " none" for info in ext_manager.extensions(): tags = [ ("[x]" if info.isEnabled() else "[ ]") + " enabled", ("[x]" if info.isLoaded() else "[ ]") + " loaded", ("[x]" if info.isInstalled() else "[ ]") + " installed", ] lines.append(f" {info.name()} ({info.id()})") lines.append(f" {' '.join(tags)}") lines.append(f" {info.path()}") url = info.actionPopupUrl() if url.isValid(): lines.append(f" {url.toDisplayString()}") lines.append("") return lines def _uptime() -> datetime.timedelta: time_delta = datetime.datetime.now() - objects.qapp.launch_time # Round off microseconds time_delta -= datetime.timedelta(microseconds=time_delta.microseconds) return time_delta def _autoconfig_loaded() -> str: return "yes" if config.instance.yaml_loaded else "no" def _config_py_loaded() -> str: if config.instance.config_py_loaded: return "{} has been loaded".format(standarddir.config_py()) else: return "no config.py was loaded" def version_info() -> str: """Return a string with various version information.""" lines = _LOGO.lstrip('\n').splitlines() lines.append("qutebrowser v{}".format(qutebrowser.__version__)) gitver = _git_str() if gitver is not None: lines.append("Git commit: {}".format(gitver)) lines.append('Backend: {}'.format(_backend())) lines.append('Qt: {}'.format(earlyinit.qt_version())) lines += [ '', '{}: {}'.format(platform.python_implementation(), platform.python_version()), 'PyQt: {}'.format(PYQT_VERSION_STR), '', str(machinery.INFO), '', ] lines += _module_versions() lines += [ 'pdf.js: {}'.format(_pdfjs_version()), 'sqlite: {}'.format(sql.version()), 'QtNetwork SSL: {}\n'.format(QSslSocket.sslLibraryVersionString() if QSslSocket.supportsSsl() else 'no'), ] lines += _webengine_extensions() if objects.qapp: style = objects.qapp.style() assert style is not None metaobj = style.metaObject() assert metaobj is not None lines.append('Style: {}'.format(metaobj.className())) lines.append('Qt Platform: {}'.format(gui_platform_info())) lines.append('OpenGL: {}'.format(opengl_info())) importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__)) lines += [ 'Platform: {}, {}'.format(platform.platform(), platform.architecture()[0]), ] dist = distribution() if dist is not None: lines += [ 'Linux distribution: {} ({})'.format(dist.pretty, dist.parsed.name) ] lines += [ 'Frozen: {}'.format(hasattr(sys, 'frozen')), "Imported from {}".format(importpath), "Using Python from {}".format(sys.executable), "Qt library executable path: {}, data path: {}".format( qtutils.library_path(qtutils.LibraryPath.library_executables), qtutils.library_path(qtutils.LibraryPath.data), ) ] if not dist or dist.parsed == Distribution.unknown: lines += _os_info() lines += [ '', 'Paths:', ] for name, path in sorted(_path_info().items()): lines += ['{}: {}'.format(name, path)] lines += [ '', 'Autoconfig loaded: {}'.format(_autoconfig_loaded()), 'Config.py: {}'.format(_config_py_loaded()), 'Uptime: {}'.format(_uptime()) ] return '\n'.join(lines) @dataclasses.dataclass class OpenGLInfo: """Information about the OpenGL setup in use.""" # If we're using OpenGL ES. If so, no further information is available. gles: bool = False # The name of the vendor. Examples: # - nouveau # - "Intel Open Source Technology Center", "Intel", "Intel Inc." vendor: Optional[str] = None # The OpenGL version as a string. See tests for examples. version_str: Optional[str] = None # The parsed version as a (major, minor) tuple of ints version: Optional[tuple[int, ...]] = None # The vendor specific information following the version number vendor_specific: Optional[str] = None def __str__(self) -> str: if self.gles: return 'OpenGL ES' return '{}, {}'.format(self.vendor, self.version_str) @classmethod def parse(cls, *, vendor: str, version: str) -> 'OpenGLInfo': """Parse OpenGL version info from a string. The arguments should be the strings returned by OpenGL for GL_VENDOR and GL_VERSION, respectively. According to the OpenGL reference, the version string should have the following format: .[.] """ if ' ' not in version: log.misc.warning("Failed to parse OpenGL version (missing space): " "{}".format(version)) return cls(vendor=vendor, version_str=version) num_str, vendor_specific = version.split(' ', maxsplit=1) try: parsed_version = tuple(int(i) for i in num_str.split('.')) except ValueError: log.misc.warning("Failed to parse OpenGL version (parsing int): " "{}".format(version)) return cls(vendor=vendor, version_str=version) return cls(vendor=vendor, version_str=version, version=parsed_version, vendor_specific=vendor_specific) @functools.lru_cache(maxsize=1) def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover """Get the OpenGL vendor used. This returns a string such as 'nouveau' or 'Intel Open Source Technology Center'; or None if the vendor can't be determined. """ assert QApplication.instance() override = os.environ.get('QUTE_FAKE_OPENGL') if override is not None: log.init.debug("Using override {}".format(override)) vendor, version = override.split(', ', maxsplit=1) return OpenGLInfo.parse(vendor=vendor, version=version) old_context: Optional[QOpenGLContext] = QOpenGLContext.currentContext() old_surface = None if old_context is None else old_context.surface() surface = QOffscreenSurface() surface.create() ctx = QOpenGLContext() ok = ctx.create() if not ok: log.init.debug("Creating context failed!") return None ok = ctx.makeCurrent(surface) if not ok: log.init.debug("Making context current failed!") return None try: if ctx.isOpenGLES(): # Can't use versionFunctions there return OpenGLInfo(gles=True) vp = QOpenGLVersionProfile() vp.setVersion(2, 0) try: if machinery.IS_QT5: vf = ctx.versionFunctions(vp) else: # Qt 6 from qutebrowser.qt.opengl import QOpenGLVersionFunctionsFactory vf: Any = QOpenGLVersionFunctionsFactory.get(vp, ctx) except ImportError as e: log.init.debug("Importing version functions failed: {}".format(e)) return None if vf is None: log.init.debug("Getting version functions failed!") return None # FIXME:mypy PyQt6-stubs issue? vendor = vf.glGetString(vf.GL_VENDOR) version = vf.glGetString(vf.GL_VERSION) return OpenGLInfo.parse(vendor=vendor, version=version) finally: ctx.doneCurrent() if old_context and old_surface: old_context.makeCurrent(old_surface) def gui_platform_info() -> str: """Get the Qt GUI platform name, optionally with the WM/compositor name.""" info = objects.qapp.platformName() try: if info == "xcb": info += f" ({wmname.x11_wm_name()})" elif info in ["wayland", "wayland-egl"]: info += f" ({wmname.wayland_compositor_name()})" except wmname.Error as e: info += f" (Error: {e})" return info def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None: """Pastebin the version and log the url to messages.""" def _yank_url(url: str) -> None: utils.set_clipboard(url) message.info("Version url {} yanked to clipboard.".format(url)) def _on_paste_version_success(url: str) -> None: assert pbclient is not None global pastebin_url url = url.strip() _yank_url(url) pbclient.deleteLater() pastebin_url = url def _on_paste_version_err(text: str) -> None: assert pbclient is not None message.error("Failed to pastebin version" " info: {}".format(text)) pbclient.deleteLater() if pastebin_url: _yank_url(pastebin_url) return app = QApplication.instance() http_client = httpclient.HTTPClient() misc_api = pastebin.PastebinClient.MISC_API_URL pbclient = pbclient or pastebin.PastebinClient(http_client, parent=app, api_url=misc_api) pbclient.success.connect(_on_paste_version_success) pbclient.error.connect(_on_paste_version_err) pbclient.paste(getpass.getuser(), "qute version info {}".format(qutebrowser.__version__), version_info(), private=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6426377 qutebrowser-3.6.1/qutebrowser.egg-info/0000755000175100017510000000000015102145351017622 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183912.0 qutebrowser-3.6.1/qutebrowser.egg-info/PKG-INFO0000644000175100017510000003662115102145350020726 0ustar00runnerrunnerMetadata-Version: 2.4 Name: qutebrowser Version: 3.6.1 Summary: A keyboard-driven, vim-like browser based on Python and Qt. Home-page: https://www.qutebrowser.org/ Author: Florian Bruhin Author-email: mail@qutebrowser.org License: GPL-3.0-or-later Keywords: pyqt browser web qt webkit qtwebkit qtwebengine Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: X11 Applications :: Qt Classifier: Intended Audience :: End Users/Desktop Classifier: Natural Language :: English Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: MacOS Classifier: Operating System :: POSIX :: BSD Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Browsers Requires-Python: >=3.9 Description-Content-Type: text/plain License-File: LICENSE Requires-Dist: jinja2 Requires-Dist: PyYAML Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: requires-dist Dynamic: requires-python Dynamic: summary // SPDX-License-Identifier: GPL-3.0-or-later // If you are reading this in plaintext or on PyPi: // // A rendered version is available at: // https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc qutebrowser =========== // QUTE_WEB_HIDE image:qutebrowser/icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on Python and Qt.* image:https://github.com/qutebrowser/qutebrowser/workflows/CI/badge.svg["Build Status", link="https://github.com/qutebrowser/qutebrowser/actions?query=workflow%3ACI"] image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=main["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=main"] link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | https://github.com/qutebrowser/qutebrowser/blob/main/doc/faq.asciidoc[FAQ] | https://www.qutebrowser.org/doc/contributing.html[contributing] | link:https://github.com/qutebrowser/qutebrowser/releases[releases] | https://github.com/qutebrowser/qutebrowser/blob/main/doc/install.asciidoc[installing] // QUTE_WEB_HIDE_END qutebrowser is a keyboard-focused browser with a minimal GUI. It's based on Python and Qt and free software, licensed under the GPL. It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. // QUTE_WEB_HIDE **qutebrowser's primary maintainer, The-Compiler, is currently working part-time on qutebrowser, funded by donations.** To sustain this for a long time, your help is needed! See the https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] or https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc#donating[alternative donation methods] for more information. Depending on your sign-up date and how long you keep a certain level, you can get qutebrowser t-shirts, stickers and more! // QUTE_WEB_HIDE_END Screenshots ----------- image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"] image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"] image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"] image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"] Downloads --------- See the https://github.com/qutebrowser/qutebrowser/releases[GitHub releases page] for available downloads and the link:doc/install.asciidoc[INSTALL] file for detailed instructions on how to get qutebrowser running on various platforms. Documentation and getting help ------------------------------ Please see the link:doc/help/index.asciidoc[help page] for available documentation pages and support channels. Contributions / Bugs -------------------- You want to contribute to qutebrowser? Awesome! Please read link:doc/contributing.asciidoc[the contribution guidelines] for details and useful hints. If you found a bug or have a feature request, you can report it in several ways: * Use the built-in `:report` command or the automatic crash dialog. * Open an issue in the Github issue tracker. * Write a mail to the https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[]. Please report security bugs to security@qutebrowser.org (or if GPG encryption is desired, contact me@the-compiler.org with GPG ID https://www.the-compiler.org/pubkey.asc[0x916EB0C8FD55A072]). Alternatively, https://github.com/qutebrowser/qutebrowser/security/advisories/new[report a vulnerability] via GitHub's https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability[private reporting feature]. Requirements ------------ The following software and libraries are required to run qutebrowser: * https://www.python.org/[Python] 3.9 or newer * https://www.qt.io/[Qt], either 6.2.0 or newer, or 5.15.0 or newer, with the following modules: - QtCore / qtbase - QtQuick (part of qtbase or qtdeclarative in some distributions) - QtSQL (part of qtbase in some distributions) - QtDBus (part of qtbase in some distributions; note that a connection to DBus at runtime is optional) - QtOpenGL - QtWebEngine (if using Qt 5, 5.15.2 or newer), or - alternatively QtWebKit (5.212) - **This is not recommended** due to known security issues in QtWebKit, you most likely want to use qutebrowser with the default QtWebEngine backend (based on Chromium) instead. Quoting the https://github.com/qtwebkit/qtwebkit/releases[QtWebKit releases page]: _[The latest QtWebKit] release is based on [an] old WebKit revision with known unpatched vulnerabilities. Please use it carefully and avoid visiting untrusted websites and using it for transmission of sensitive data._ * https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 6.2.2 or newer (Qt 6) or 5.15.0 or newer (Qt 5) * https://palletsprojects.com/p/jinja/[jinja2] * https://github.com/yaml/pyyaml[PyYAML] On macOS, the following libraries are also required: * https://pyobjc.readthedocs.io/en/latest/[pyobjc-core and pyobjc-framework-Cocoa] The following libraries are optional: * https://pypi.org/project/adblock/[adblock] (for improved adblocking using ABP syntax) * https://pygments.org/[pygments] for syntax highlighting with `:view-source` on QtWebKit, or when using `:view-source --pygments` with the (default) QtWebEngine backend. * On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log output. * https://asciidoc.org/[asciidoc] to generate the documentation for the `:help` command, when using the git repository (rather than a release). See link:doc/install.asciidoc[the documentation] for directions on how to install qutebrowser and its dependencies. Donating -------- **qutebrowser's primary maintainer, The-Compiler, is currently working part-time on qutebrowser, funded by donations.** To sustain this for a long time, your help is needed! See the https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more information. Depending on your sign-up date and how long you keep a certain level, you can get qutebrowser t-shirts, stickers and more! GitHub Sponsors allows for one-time donations (using the buttons next to "Select a tier") as well as custom amounts. **For currencies other than Euro or Swiss Francs, this is the preferred donation method.** GitHub uses https://stripe.com/[Stripe] to accept payment via credit cards without any fees. Billing via PayPal is available as well, with less fees than a direct PayPal transaction. Alternatively, the following donation methods are available -- note that eligibility for swag (shirts/stickers/etc.) is handled on a case-by-case basis for those, please mailto:mail@qutebrowser.org[get in touch] for details. * https://liberapay.com/The-Compiler[Liberapay], which can handle payments via Credit Card, SEPA bank transfers, or Paypal. Payment fees are paid by me, but they are https://liberapay.com/about/faq#fees[relatively low]. * SEPA bank transfer inside Europe (**no fees**): - Account holder: Florian Bruhin - Country: Switzerland - IBAN (EUR): CH13 0900 0000 9160 4094 6 - IBAN (other): CH80 0900 0000 8711 8587 3 - Bank: PostFinance AG, Mingerstrasse 20, 3030 Bern, Switzerland (BIC: POFICHBEXXX) - If you need any other information: Contact me at mail@qutebrowser.org. - If possible, **please consider yearly or semi-yearly donations**, because of the additional overhead from many individual transactions for bookkeeping/tax purposes. * PayPal: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=CHF&source=url[CHF], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=EUR&source=url[EUR], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser¤cy_code=USD&source=url[USD]. **Note: Fees can be very high (around 5-40%, depending on the donated amounts)** - consider using GitHub Sponsors (credit card), Liberapay (credit cards, PayPal, or bank transfer) or SEPA bank transfers instead. * Cryptocurrencies: - Bitcoin: link:bitcoin:bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00[bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00] - Bitcoin Cash: link:bitcoincash:1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N[1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N] - Ethereum: link:ethereum:0x10c2425856F7a8799EBCaac4943026803b1089c6[0x10c2425856F7a8799EBCaac4943026803b1089c6] - Litecoin: link:litecoin:MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2[MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2] - Others: Please mailto:mail@qutebrowser.org[get in touch], I'd happily set up anything link:https://www.ledger.com/supported-crypto-assets[supported by Ledger Live] Sponsors -------- Thanks a lot to https://www.macstadium.com/[MacStadium] for supporting qutebrowser with a free hosted Mac Mini via their https://www.macstadium.com/opensource[Open Source Project]. (They don't require including this here - I've just been very happy with their offer, and without them, no macOS releases or tests would exist) Thanks to the https://www.hsr.ch/[HSR Hochschule für Technik Rapperswil], which made it possible to work on qutebrowser extensions as a student research project. image:doc/img/sponsors/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"] image:doc/img/sponsors/hsr.png["HSR Hochschule für Technik Rapperswil",link="https://www.hsr.ch/"] Authors ------- qutebrowser's primary author is Florian Bruhin (The Compiler), but qutebrowser wouldn't be what it is without the help of https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contributors]! Additionally, the following people have contributed graphics: * Jad/link:https://yelostudio.com[yelo] (new icon) * WOFall (original icon) * regines (key binding cheatsheet) Also, thanks to everyone who contributed to one of qutebrowser's link:doc/backers.asciidoc[crowdfunding campaigns]! Similar projects ---------------- Various projects with a similar goal like qutebrowser exist. Many of them were inspirations for qutebrowser in some way, thanks for that! Active ~~~~~~ * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2) * https://luakit.github.io/[luakit] (C/Lua, GTK+ with WebKit2) * https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebEngine or GTK+/WebKit2 - note there was a https://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution in 2019] which was handled quite badly) * https://vieb.dev/[Vieb] (JavaScript, Electron) * https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) * https://github.com/jun7/wyeb[wyeb] (C, GTK+ with WebKit2) * Chrome/Chromium addons: https://vimium.github.io/[Vimium] * Firefox addons (based on WebExtensions): https://tridactyl.xyz/[Tridactyl], https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] * Addons for Firefox and Chrome: https://github.com/brookhong/Surfingkeys[Surfingkeys] (https://github.com/brookhong/Surfingkeys/issues/1796[somewhat sketchy]...), https://lydell.github.io/LinkHints/[Link Hints] (hinting only), https://github.com/ueokande/vimmatic[Vimmatic] Inactive ~~~~~~~~ * https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1, https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] - main inspiration for qutebrowser) * https://github.com/parkouss/webmacs/[webmacs] (Python, Emacs-like with QtWebEngine, https://github.com/parkouss/webmacs/issues/137[unmaintained]) * https://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with WebKit1) * https://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1) * http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko) * https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2) * https://github.com/conformal/xombrero[xombrero] (C, GTK+ with WebKit1) * https://github.com/linkdd/cream-browser[Cream Browser] (C, GTK+ with WebKit1) * Firefox addons (not based on WebExtensions or no recent activity): http://www.vimperator.org/[Vimperator], http://bug.5digits.org/pentadactyl/index[Pentadactyl], https://github.com/akhodakivskiy/VimFx[VimFx] (seems to offer a https://gir.st/blog/legacyfox.htm[hack] to run on modern Firefox releases), https://github.com/shinglyu/QuantumVim[QuantumVim], https://github.com/ueokande/vim-vixen[Vim Vixen], https://github.com/amedama41/vvimpulation[VVimpulation], https://krabby.netlify.app/[Krabby] * Chrome/Chromium addons: https://github.com/k2nr/ViChrome/[ViChrome], https://github.com/jinzhu/vrome[Vrome], https://github.com/lusakasa/saka-key[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]), https://github.com/1995eaton/chromium-vim[cVim], https://github.com/dcchambers/vb4c[vb4c] (fork of cVim, https://github.com/dcchambers/vb4c/issues/23#issuecomment-810694017[unmaintained]), https://glee.github.io/[GleeBox] * Addons for Safari: https://televator.net/vimari/[Vimari] License ------- 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 . pdf.js ------ qutebrowser optionally uses https://github.com/mozilla/pdf.js/[pdf.js] to display PDF files in the browser. Windows releases come with a bundled pdf.js. pdf.js is distributed under the terms of the Apache License. You can find a copy of the license in `qutebrowser/3rdparty/pdfjs/LICENSE` (in the Windows release or after running `scripts/dev/update_3rdparty.py`), or online https://www.apache.org/licenses/LICENSE-2.0.html[here]. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183912.0 qutebrowser-3.6.1/qutebrowser.egg-info/SOURCES.txt0000644000175100017510000007764715102145350021532 0ustar00runnerrunnerLICENSE MANIFEST.in README.asciidoc pyproject.toml pytest.ini qutebrowser.py requirements.txt setup.py doc/changelog.asciidoc doc/qutebrowser.1 doc/qutebrowser.1.asciidoc doc/img/cheatsheet-big.png doc/img/cheatsheet-small.png doc/img/completion.png doc/img/downloads.png doc/img/hints.png doc/img/main.png doc/img/sponsors/hsr.png doc/img/sponsors/macstadium.png misc/Makefile misc/cheatsheet.svg misc/org.qutebrowser.qutebrowser.appdata.xml misc/org.qutebrowser.qutebrowser.desktop misc/apparmor/usr.bin.qutebrowser misc/requirements/README.md misc/requirements/requirements-dev.txt misc/requirements/requirements-dev.txt-raw misc/requirements/requirements-docs.txt misc/requirements/requirements-docs.txt-raw misc/requirements/requirements-flake8.txt misc/requirements/requirements-flake8.txt-raw misc/requirements/requirements-mypy.txt misc/requirements/requirements-mypy.txt-raw misc/requirements/requirements-pyinstaller.txt misc/requirements/requirements-pyinstaller.txt-raw misc/requirements/requirements-pylint.txt misc/requirements/requirements-pylint.txt-raw misc/requirements/requirements-pyqt-5.15.2.txt misc/requirements/requirements-pyqt-5.15.2.txt-raw misc/requirements/requirements-pyqt-5.15.txt misc/requirements/requirements-pyqt-5.15.txt-raw misc/requirements/requirements-pyqt-5.txt misc/requirements/requirements-pyqt-5.txt-raw misc/requirements/requirements-pyqt-6.10.txt misc/requirements/requirements-pyqt-6.10.txt-raw misc/requirements/requirements-pyqt-6.2.txt misc/requirements/requirements-pyqt-6.2.txt-raw misc/requirements/requirements-pyqt-6.3.txt misc/requirements/requirements-pyqt-6.3.txt-raw misc/requirements/requirements-pyqt-6.4.txt misc/requirements/requirements-pyqt-6.4.txt-raw misc/requirements/requirements-pyqt-6.5.txt misc/requirements/requirements-pyqt-6.5.txt-raw misc/requirements/requirements-pyqt-6.6.txt misc/requirements/requirements-pyqt-6.6.txt-raw misc/requirements/requirements-pyqt-6.7.txt misc/requirements/requirements-pyqt-6.7.txt-raw misc/requirements/requirements-pyqt-6.8.txt misc/requirements/requirements-pyqt-6.8.txt-raw misc/requirements/requirements-pyqt-6.9.txt misc/requirements/requirements-pyqt-6.9.txt-raw misc/requirements/requirements-pyqt-6.txt misc/requirements/requirements-pyqt-6.txt-raw misc/requirements/requirements-pyqt.txt misc/requirements/requirements-pyqt.txt-raw misc/requirements/requirements-pyroma.txt misc/requirements/requirements-pyroma.txt-raw misc/requirements/requirements-qutebrowser.txt-raw misc/requirements/requirements-sphinx.txt misc/requirements/requirements-sphinx.txt-raw misc/requirements/requirements-tests-bleeding.txt misc/requirements/requirements-tests.txt misc/requirements/requirements-tests.txt-raw misc/requirements/requirements-tox.txt misc/requirements/requirements-tox.txt-raw misc/requirements/requirements-vulture.txt misc/requirements/requirements-vulture.txt-raw misc/requirements/requirements-yamllint.txt misc/requirements/requirements-yamllint.txt-raw misc/userscripts/README.md misc/userscripts/add-nextcloud-bookmarks misc/userscripts/add-nextcloud-cookbook misc/userscripts/cast misc/userscripts/dmenu_qutebrowser misc/userscripts/format_json misc/userscripts/getbib misc/userscripts/kodi misc/userscripts/open_download misc/userscripts/openfeeds misc/userscripts/password_fill misc/userscripts/qr misc/userscripts/qute-1pass misc/userscripts/qute-bitwarden misc/userscripts/qute-keepass misc/userscripts/qute-keepassxc misc/userscripts/qute-lastpass misc/userscripts/qute-pass misc/userscripts/qutedmenu misc/userscripts/readability misc/userscripts/readability-js misc/userscripts/ripbang misc/userscripts/rss misc/userscripts/taskadd misc/userscripts/tor_identity misc/userscripts/view_in_mpv qutebrowser/__init__.py qutebrowser/__main__.py qutebrowser/app.py qutebrowser/git-commit-id qutebrowser/qutebrowser.py qutebrowser.egg-info/PKG-INFO qutebrowser.egg-info/SOURCES.txt qutebrowser.egg-info/dependency_links.txt qutebrowser.egg-info/entry_points.txt qutebrowser.egg-info/requires.txt qutebrowser.egg-info/top_level.txt qutebrowser.egg-info/zip-safe qutebrowser/api/__init__.py qutebrowser/api/apitypes.py qutebrowser/api/cmdutils.py qutebrowser/api/config.py qutebrowser/api/downloads.py qutebrowser/api/hook.py qutebrowser/api/interceptor.py qutebrowser/api/message.py qutebrowser/api/qtutils.py qutebrowser/browser/__init__.py qutebrowser/browser/browsertab.py qutebrowser/browser/commands.py qutebrowser/browser/downloads.py qutebrowser/browser/downloadview.py qutebrowser/browser/eventfilter.py qutebrowser/browser/greasemonkey.py qutebrowser/browser/hints.py qutebrowser/browser/history.py qutebrowser/browser/inspector.py qutebrowser/browser/navigate.py qutebrowser/browser/pdfjs.py qutebrowser/browser/qtnetworkdownloads.py qutebrowser/browser/qutescheme.py qutebrowser/browser/shared.py qutebrowser/browser/signalfilter.py qutebrowser/browser/urlmarks.py qutebrowser/browser/webelem.py qutebrowser/browser/network/__init__.py qutebrowser/browser/network/pac.py qutebrowser/browser/network/proxy.py qutebrowser/browser/webengine/__init__.py qutebrowser/browser/webengine/certificateerror.py qutebrowser/browser/webengine/cookies.py qutebrowser/browser/webengine/darkmode.py qutebrowser/browser/webengine/interceptor.py qutebrowser/browser/webengine/notification.py qutebrowser/browser/webengine/spell.py qutebrowser/browser/webengine/tabhistory.py qutebrowser/browser/webengine/webenginedownloads.py qutebrowser/browser/webengine/webengineelem.py qutebrowser/browser/webengine/webengineinspector.py qutebrowser/browser/webengine/webenginequtescheme.py qutebrowser/browser/webengine/webenginesettings.py qutebrowser/browser/webengine/webenginetab.py qutebrowser/browser/webengine/webview.py qutebrowser/browser/webkit/__init__.py qutebrowser/browser/webkit/cache.py qutebrowser/browser/webkit/certificateerror.py qutebrowser/browser/webkit/cookies.py qutebrowser/browser/webkit/httpheaders.py qutebrowser/browser/webkit/mhtml.py qutebrowser/browser/webkit/tabhistory.py qutebrowser/browser/webkit/webkitelem.py qutebrowser/browser/webkit/webkithistory.py qutebrowser/browser/webkit/webkitinspector.py qutebrowser/browser/webkit/webkitsettings.py qutebrowser/browser/webkit/webkittab.py qutebrowser/browser/webkit/webpage.py qutebrowser/browser/webkit/webview.py qutebrowser/browser/webkit/network/__init__.py qutebrowser/browser/webkit/network/filescheme.py qutebrowser/browser/webkit/network/networkmanager.py qutebrowser/browser/webkit/network/networkreply.py qutebrowser/browser/webkit/network/webkitqutescheme.py qutebrowser/commands/__init__.py qutebrowser/commands/argparser.py qutebrowser/commands/cmdexc.py qutebrowser/commands/command.py qutebrowser/commands/parser.py qutebrowser/commands/runners.py qutebrowser/commands/userscripts.py qutebrowser/completion/__init__.py qutebrowser/completion/completer.py qutebrowser/completion/completiondelegate.py qutebrowser/completion/completionwidget.py qutebrowser/completion/models/__init__.py qutebrowser/completion/models/completionmodel.py qutebrowser/completion/models/configmodel.py qutebrowser/completion/models/filepathcategory.py qutebrowser/completion/models/histcategory.py qutebrowser/completion/models/listcategory.py qutebrowser/completion/models/miscmodels.py qutebrowser/completion/models/urlmodel.py qutebrowser/completion/models/util.py qutebrowser/components/__init__.py qutebrowser/components/adblockcommands.py qutebrowser/components/braveadblock.py qutebrowser/components/caretcommands.py qutebrowser/components/hostblock.py qutebrowser/components/misccommands.py qutebrowser/components/readlinecommands.py qutebrowser/components/scrollcommands.py qutebrowser/components/zoomcommands.py qutebrowser/components/utils/__init__.py qutebrowser/components/utils/blockutils.py qutebrowser/config/__init__.py qutebrowser/config/config.py qutebrowser/config/configcache.py qutebrowser/config/configcommands.py qutebrowser/config/configdata.py qutebrowser/config/configdata.yml qutebrowser/config/configexc.py qutebrowser/config/configfiles.py qutebrowser/config/configinit.py qutebrowser/config/configtypes.py qutebrowser/config/configutils.py qutebrowser/config/qtargs.py qutebrowser/config/stylesheet.py qutebrowser/config/websettings.py qutebrowser/extensions/__init__.py qutebrowser/extensions/interceptors.py qutebrowser/extensions/loader.py qutebrowser/html/back.html qutebrowser/html/base.html qutebrowser/html/bindings.html qutebrowser/html/bookmarks.html qutebrowser/html/dirbrowser.html qutebrowser/html/error.html qutebrowser/html/history.html qutebrowser/html/license.html qutebrowser/html/log.html qutebrowser/html/no_pdfjs.html qutebrowser/html/pre.html qutebrowser/html/process.html qutebrowser/html/settings.html qutebrowser/html/startpage.html qutebrowser/html/styled.html qutebrowser/html/tabs.html qutebrowser/html/version.html qutebrowser/html/warning-qt5.html qutebrowser/html/warning-sessions.html qutebrowser/html/warning-webkit.html qutebrowser/html/doc/changelog.html qutebrowser/html/doc/commands.html qutebrowser/html/doc/configuring.html qutebrowser/html/doc/contributing.html qutebrowser/html/doc/faq.html qutebrowser/html/doc/index.html qutebrowser/html/doc/install.html qutebrowser/html/doc/quickstart.html qutebrowser/html/doc/settings.html qutebrowser/html/doc/stacktrace.html qutebrowser/html/doc/userscripts.html qutebrowser/html/doc/img/cheatsheet-big.png qutebrowser/html/doc/img/cheatsheet-small.png qutebrowser/icons/qutebrowser-128x128.png qutebrowser/icons/qutebrowser-16x16.png qutebrowser/icons/qutebrowser-24x24.png qutebrowser/icons/qutebrowser-256x256.png qutebrowser/icons/qutebrowser-32x32.png qutebrowser/icons/qutebrowser-48x48.png qutebrowser/icons/qutebrowser-512x512.png qutebrowser/icons/qutebrowser-64x64.png qutebrowser/icons/qutebrowser-96x96.png qutebrowser/icons/qutebrowser-all.svg qutebrowser/icons/qutebrowser-favicon.svg qutebrowser/icons/qutebrowser.icns qutebrowser/icons/qutebrowser.ico qutebrowser/icons/qutebrowser.svg qutebrowser/icons/qutebrowser.xpm qutebrowser/img/broken_qutebrowser_logo.png qutebrowser/img/file.svg qutebrowser/img/folder.svg qutebrowser/javascript/caret.js qutebrowser/javascript/global_wrapper.js qutebrowser/javascript/greasemonkey_wrapper.js qutebrowser/javascript/history.js qutebrowser/javascript/pac_utils.js qutebrowser/javascript/pdfjs_polyfills.js qutebrowser/javascript/position_caret.js qutebrowser/javascript/scroll.js qutebrowser/javascript/stylesheet.js qutebrowser/javascript/webelem.js qutebrowser/javascript/quirks/array_at.user.js qutebrowser/javascript/quirks/discord.user.js qutebrowser/javascript/quirks/googledocs.user.js qutebrowser/javascript/quirks/string_replaceall.user.js qutebrowser/javascript/quirks/whatsapp_web.user.js qutebrowser/keyinput/__init__.py qutebrowser/keyinput/basekeyparser.py qutebrowser/keyinput/eventfilter.py qutebrowser/keyinput/keyutils.py qutebrowser/keyinput/macros.py qutebrowser/keyinput/modeman.py qutebrowser/keyinput/modeparsers.py qutebrowser/mainwindow/__init__.py qutebrowser/mainwindow/mainwindow.py qutebrowser/mainwindow/messageview.py qutebrowser/mainwindow/prompt.py qutebrowser/mainwindow/tabbedbrowser.py qutebrowser/mainwindow/tabwidget.py qutebrowser/mainwindow/windowundo.py qutebrowser/mainwindow/statusbar/__init__.py qutebrowser/mainwindow/statusbar/backforward.py qutebrowser/mainwindow/statusbar/bar.py qutebrowser/mainwindow/statusbar/clock.py qutebrowser/mainwindow/statusbar/command.py qutebrowser/mainwindow/statusbar/keystring.py qutebrowser/mainwindow/statusbar/percentage.py qutebrowser/mainwindow/statusbar/progress.py qutebrowser/mainwindow/statusbar/searchmatch.py qutebrowser/mainwindow/statusbar/tabindex.py qutebrowser/mainwindow/statusbar/textbase.py qutebrowser/mainwindow/statusbar/url.py qutebrowser/misc/__init__.py qutebrowser/misc/autoupdate.py qutebrowser/misc/backendproblem.py qutebrowser/misc/binparsing.py qutebrowser/misc/checkpyver.py qutebrowser/misc/cmdhistory.py qutebrowser/misc/consolewidget.py qutebrowser/misc/crashdialog.py qutebrowser/misc/crashsignal.py qutebrowser/misc/debugcachestats.py qutebrowser/misc/earlyinit.py qutebrowser/misc/editor.py qutebrowser/misc/elf.py qutebrowser/misc/guiprocess.py qutebrowser/misc/httpclient.py qutebrowser/misc/ipc.py qutebrowser/misc/keyhintwidget.py qutebrowser/misc/lineparser.py qutebrowser/misc/miscwidgets.py qutebrowser/misc/msgbox.py qutebrowser/misc/nativeeventfilter.py qutebrowser/misc/objects.py qutebrowser/misc/pakjoy.py qutebrowser/misc/pastebin.py qutebrowser/misc/quitter.py qutebrowser/misc/savemanager.py qutebrowser/misc/sessions.py qutebrowser/misc/split.py qutebrowser/misc/sql.py qutebrowser/misc/throttle.py qutebrowser/misc/utilcmds.py qutebrowser/misc/wmname.py qutebrowser/qt/__init__.py qutebrowser/qt/_core_pyqtproperty.py qutebrowser/qt/core.py qutebrowser/qt/dbus.py qutebrowser/qt/gui.py qutebrowser/qt/machinery.py qutebrowser/qt/network.py qutebrowser/qt/opengl.py qutebrowser/qt/printsupport.py qutebrowser/qt/qml.py qutebrowser/qt/sip.py qutebrowser/qt/sql.py qutebrowser/qt/test.py qutebrowser/qt/webenginecore.py qutebrowser/qt/webenginewidgets.py qutebrowser/qt/webkit.py qutebrowser/qt/webkitwidgets.py qutebrowser/qt/widgets.py qutebrowser/utils/__init__.py qutebrowser/utils/debug.py qutebrowser/utils/docutils.py qutebrowser/utils/error.py qutebrowser/utils/javascript.py qutebrowser/utils/jinja.py qutebrowser/utils/log.py qutebrowser/utils/message.py qutebrowser/utils/objreg.py qutebrowser/utils/qtlog.py qutebrowser/utils/qtutils.py qutebrowser/utils/resources.py qutebrowser/utils/standarddir.py qutebrowser/utils/testfile qutebrowser/utils/urlmatch.py qutebrowser/utils/urlutils.py qutebrowser/utils/usertypes.py qutebrowser/utils/utils.py qutebrowser/utils/version.py scripts/__init__.py scripts/cycle-inputs.js scripts/dictcli.py scripts/hist_importer.py scripts/hostblock_blame.py scripts/importer.py scripts/keytester.py scripts/link_pyqt.py scripts/mkvenv.py scripts/open_url_in_instance.sh scripts/opengl_info.py scripts/setupcommon.py scripts/utils.py scripts/testbrowser/testbrowser_webengine.py scripts/testbrowser/testbrowser_webkit.py tests/conftest.py tests/test_conftest.py tests/end2end/conftest.py tests/end2end/test_adblock_e2e.py tests/end2end/test_dirbrowser.py tests/end2end/test_hints_html.py tests/end2end/test_insert_mode.py tests/end2end/test_invocations.py tests/end2end/test_mhtml_e2e.py tests/end2end/data/blocked-hosts.gz tests/end2end/data/caret.html tests/end2end/data/click_element.html tests/end2end/data/data_link.html tests/end2end/data/easylist.txt.gz tests/end2end/data/easyprivacy.txt.gz tests/end2end/data/editor.html tests/end2end/data/email_address.html tests/end2end/data/fileselect.html tests/end2end/data/hello.txt tests/end2end/data/hello2.txt tests/end2end/data/hello3.txt tests/end2end/data/hinting.txt tests/end2end/data/iframe_search.html tests/end2end/data/invalid_link.html tests/end2end/data/invalid_resource.html tests/end2end/data/issue2569.html tests/end2end/data/issue4011.html tests/end2end/data/l33t.txt tests/end2end/data/long_load.html tests/end2end/data/marks.html tests/end2end/data/paste_primary.html tests/end2end/data/prefers_reduced_motion.html tests/end2end/data/reload.txt tests/end2end/data/search.html tests/end2end/data/search_select.js tests/end2end/data/smart.txt tests/end2end/data/title with spaces.html tests/end2end/data/title.html tests/end2end/data/words.txt tests/end2end/data/äöü.html tests/end2end/data/backforward/1.txt tests/end2end/data/backforward/2.txt tests/end2end/data/backforward/3.txt tests/end2end/data/blocking/external_logo.html tests/end2end/data/blocking/qutebrowser-adblock tests/end2end/data/blocking/qutebrowser-hosts tests/end2end/data/brave-adblock/LICENSE tests/end2end/data/brave-adblock/README.md tests/end2end/data/brave-adblock/generate.py tests/end2end/data/brave-adblock/ublock-matches.tsv.gz tests/end2end/data/crashers/document_picture_in_picture.html tests/end2end/data/crashers/installedapp.html tests/end2end/data/crashers/webrtc.html tests/end2end/data/darkmode/blank.html tests/end2end/data/darkmode/mathml-display.html tests/end2end/data/darkmode/mathml-inline.html tests/end2end/data/darkmode/mathml.svg tests/end2end/data/darkmode/prefers-color-scheme.html tests/end2end/data/darkmode/yellow.html tests/end2end/data/downloads/download with no title.html tests/end2end/data/downloads/download with spaces.bin tests/end2end/data/downloads/download.bin tests/end2end/data/downloads/download2.bin tests/end2end/data/downloads/downloads.html tests/end2end/data/downloads/issue1243.html tests/end2end/data/downloads/issue1535.html tests/end2end/data/downloads/issue1725.html tests/end2end/data/downloads/issue2134.html tests/end2end/data/downloads/issue2298.html tests/end2end/data/downloads/issue889.html tests/end2end/data/downloads/qutebrowser.png tests/end2end/data/downloads/ä-issue908.bin tests/end2end/data/downloads/mhtml/complex/Background.png tests/end2end/data/downloads/mhtml/complex/Banner.png tests/end2end/data/downloads/mhtml/complex/DYK.png tests/end2end/data/downloads/mhtml/complex/Inline.png tests/end2end/data/downloads/mhtml/complex/base.css tests/end2end/data/downloads/mhtml/complex/complex.html tests/end2end/data/downloads/mhtml/complex/complex.mht tests/end2end/data/downloads/mhtml/complex/external-in-extern.css tests/end2end/data/downloads/mhtml/complex/favicon.png tests/end2end/data/downloads/mhtml/complex/not-css.qss tests/end2end/data/downloads/mhtml/complex/requests tests/end2end/data/downloads/mhtml/complex/script.js tests/end2end/data/downloads/mhtml/simple/requests tests/end2end/data/downloads/mhtml/simple/simple.html tests/end2end/data/downloads/mhtml/simple/simple.mht tests/end2end/data/hints/benchmark.html tests/end2end/data/hints/buttons.html tests/end2end/data/hints/custom_group.html tests/end2end/data/hints/iframe.html tests/end2end/data/hints/iframe_button.html tests/end2end/data/hints/iframe_input.html tests/end2end/data/hints/iframe_scroll.html tests/end2end/data/hints/iframe_target.html tests/end2end/data/hints/input.html tests/end2end/data/hints/issue1186.html tests/end2end/data/hints/issue1393.html tests/end2end/data/hints/issue3711.html tests/end2end/data/hints/issue3711_frame.html tests/end2end/data/hints/link_blank.html tests/end2end/data/hints/link_inject.html tests/end2end/data/hints/link_input.html tests/end2end/data/hints/link_span.html tests/end2end/data/hints/number.html tests/end2end/data/hints/rapid.html tests/end2end/data/hints/short_dict.html tests/end2end/data/hints/ace/ace.html tests/end2end/data/hints/ace/ace.js tests/end2end/data/hints/angular1/angular.min.js tests/end2end/data/hints/bootstrap/bootstrap.css tests/end2end/data/hints/bootstrap/checkbox.html tests/end2end/data/hints/html/README.md tests/end2end/data/hints/html/angular1.html tests/end2end/data/hints/html/click_handler.html tests/end2end/data/hints/html/invisible.html tests/end2end/data/hints/html/javascript.html tests/end2end/data/hints/html/nested_block_style.html tests/end2end/data/hints/html/nested_formatting_tags.html tests/end2end/data/hints/html/nested_table_style.html tests/end2end/data/hints/html/shadow_dom.html tests/end2end/data/hints/html/simple.html tests/end2end/data/hints/html/tabindex-negative.html tests/end2end/data/hints/html/target_blank_js.html tests/end2end/data/hints/html/with_spaces.html tests/end2end/data/hints/html/wrapped.html tests/end2end/data/hints/html/wrapped_button.html tests/end2end/data/hints/html/zoom_precision.html tests/end2end/data/insert_mode_settings/html/autofocus.html tests/end2end/data/insert_mode_settings/html/input.html tests/end2end/data/insert_mode_settings/html/textarea.html tests/end2end/data/javascript/consolelog.html tests/end2end/data/javascript/enabled.html tests/end2end/data/javascript/localstorage.html tests/end2end/data/javascript/notifications.html tests/end2end/data/javascript/window_open.html tests/end2end/data/javascript/windowsize.html tests/end2end/data/javascript/img/big.png tests/end2end/data/javascript/img/padded.png tests/end2end/data/javascript/img/padded2.png tests/end2end/data/javascript/img/rgb.png tests/end2end/data/javascript/img/rgba.png tests/end2end/data/keyinput/log.html tests/end2end/data/misc/hello.txt.html tests/end2end/data/misc/jseval.html tests/end2end/data/misc/jseval_file.js tests/end2end/data/misc/pyeval_file.py tests/end2end/data/misc/qutescheme_csrf.html tests/end2end/data/misc/test.pdf tests/end2end/data/misc/xhr_headers.html tests/end2end/data/navigate/index.html tests/end2end/data/navigate/multilinelinks.html tests/end2end/data/navigate/next.html tests/end2end/data/navigate/prev.html tests/end2end/data/navigate/rel.html tests/end2end/data/navigate/rel_nofollow.html tests/end2end/data/navigate/sub/index.html tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/10.txt tests/end2end/data/numbers/11.txt tests/end2end/data/numbers/12.txt tests/end2end/data/numbers/13.txt tests/end2end/data/numbers/14.txt tests/end2end/data/numbers/15.txt tests/end2end/data/numbers/16.txt tests/end2end/data/numbers/17.txt tests/end2end/data/numbers/18.txt tests/end2end/data/numbers/19.txt tests/end2end/data/numbers/2.txt tests/end2end/data/numbers/3.txt tests/end2end/data/numbers/4.txt tests/end2end/data/numbers/5.txt tests/end2end/data/numbers/6.txt tests/end2end/data/numbers/7.txt tests/end2end/data/numbers/8.txt tests/end2end/data/numbers/9.txt tests/end2end/data/prompt/clipboard.html tests/end2end/data/prompt/geolocation.html tests/end2end/data/prompt/jsalert.html tests/end2end/data/prompt/jsconfirm.html tests/end2end/data/prompt/jsprompt.html tests/end2end/data/prompt/notifications.html tests/end2end/data/prompt/script.js tests/end2end/data/scroll/no_doctype.html tests/end2end/data/scroll/position_absolute.html tests/end2end/data/scroll/simple.html tests/end2end/data/service-worker/data.json tests/end2end/data/service-worker/index.html tests/end2end/data/service-worker/worker.js tests/end2end/data/sessions/history_replace_state.html tests/end2end/data/sessions/snowman.html tests/end2end/data/ssl/cert.csr tests/end2end/data/ssl/cert.pem tests/end2end/data/ssl/key.pem tests/end2end/data/ssl/privkey.pem tests/end2end/data/userscripts/echo.bat tests/end2end/data/userscripts/echo_hint_text tests/end2end/data/userscripts/hello_if_count tests/end2end/data/userscripts/open_current_url tests/end2end/data/userscripts/open_current_url.bat tests/end2end/data/userscripts/stdinclose.py tests/end2end/features/backforward.feature tests/end2end/features/caret.feature tests/end2end/features/completion.feature tests/end2end/features/conftest.py tests/end2end/features/downloads.feature tests/end2end/features/editor.feature tests/end2end/features/hints.feature tests/end2end/features/history.feature tests/end2end/features/invoke.feature tests/end2end/features/javascript.feature tests/end2end/features/keyinput.feature tests/end2end/features/marks.feature tests/end2end/features/misc.feature tests/end2end/features/navigate.feature tests/end2end/features/notifications.feature tests/end2end/features/open.feature tests/end2end/features/private.feature tests/end2end/features/prompts.feature tests/end2end/features/qutescheme.feature tests/end2end/features/scroll.feature tests/end2end/features/search.feature tests/end2end/features/sessions.feature tests/end2end/features/spawn.feature tests/end2end/features/tabs.feature tests/end2end/features/test_backforward_bdd.py tests/end2end/features/test_caret_bdd.py tests/end2end/features/test_completion_bdd.py tests/end2end/features/test_downloads_bdd.py tests/end2end/features/test_editor_bdd.py tests/end2end/features/test_hints_bdd.py tests/end2end/features/test_history_bdd.py tests/end2end/features/test_invoke_bdd.py tests/end2end/features/test_javascript_bdd.py tests/end2end/features/test_keyinput_bdd.py tests/end2end/features/test_marks_bdd.py tests/end2end/features/test_misc_bdd.py tests/end2end/features/test_navigate_bdd.py tests/end2end/features/test_notifications_bdd.py tests/end2end/features/test_open_bdd.py tests/end2end/features/test_private_bdd.py tests/end2end/features/test_prompts_bdd.py tests/end2end/features/test_qutescheme_bdd.py tests/end2end/features/test_scroll_bdd.py tests/end2end/features/test_search_bdd.py tests/end2end/features/test_sessions_bdd.py tests/end2end/features/test_spawn_bdd.py tests/end2end/features/test_tabs_bdd.py tests/end2end/features/test_urlmarks_bdd.py tests/end2end/features/test_utilcmds_bdd.py tests/end2end/features/test_yankpaste_bdd.py tests/end2end/features/test_zoom_bdd.py tests/end2end/features/urlmarks.feature tests/end2end/features/utilcmds.feature tests/end2end/features/yankpaste.feature tests/end2end/features/zoom.feature tests/end2end/fixtures/notificationserver.py tests/end2end/fixtures/quteprocess.py tests/end2end/fixtures/test_quteprocess.py tests/end2end/fixtures/test_testprocess.py tests/end2end/fixtures/test_webserver.py tests/end2end/fixtures/testprocess.py tests/end2end/fixtures/webserver.py tests/end2end/fixtures/webserver_sub.py tests/end2end/fixtures/webserver_sub_ssl.py tests/end2end/misc/test_runners_e2e.py tests/end2end/templates/headers-link.html tests/end2end/templates/https-iframe.html tests/end2end/templates/https-script.html tests/helpers/fixtures.py tests/helpers/logfail.py tests/helpers/messagemock.py tests/helpers/stubs.py tests/helpers/test_helper_utils.py tests/helpers/test_logfail.py tests/helpers/test_stubs.py tests/helpers/testutils.py tests/manual/files.html tests/manual/mouse.html tests/manual/completion/changing_title.html tests/manual/hints/find_implementation.html tests/manual/hints/hide_unmatched_rapid_hints.html tests/manual/hints/issue824.html tests/manual/hints/issue925.html tests/manual/hints/other.html tests/manual/hints/zoom.html tests/manual/history/visited.html tests/manual/js/jsalert_multiline.html tests/manual/js/jsconfirm.html tests/manual/js/jsprompt.html tests/unit/test_app.py tests/unit/test_qt_machinery.py tests/unit/test_qutebrowser.py tests/unit/api/test_cmdutils.py tests/unit/browser/test_browsertab.py tests/unit/browser/test_caret.py tests/unit/browser/test_downloads.py tests/unit/browser/test_downloadview.py tests/unit/browser/test_hints.py tests/unit/browser/test_history.py tests/unit/browser/test_inspector.py tests/unit/browser/test_navigate.py tests/unit/browser/test_notification.py tests/unit/browser/test_pdfjs.py tests/unit/browser/test_qutescheme.py tests/unit/browser/test_shared.py tests/unit/browser/test_signalfilter.py tests/unit/browser/test_urlmarks.py tests/unit/browser/webengine/test_darkmode.py tests/unit/browser/webengine/test_spell.py tests/unit/browser/webengine/test_webengine_cookies.py tests/unit/browser/webengine/test_webenginedownloads.py tests/unit/browser/webengine/test_webengineinterceptor.py tests/unit/browser/webengine/test_webenginesettings.py tests/unit/browser/webengine/test_webenginetab.py tests/unit/browser/webengine/test_webview.py tests/unit/browser/webkit/test_cache.py tests/unit/browser/webkit/test_certificateerror.py tests/unit/browser/webkit/test_cookies.py tests/unit/browser/webkit/test_mhtml.py tests/unit/browser/webkit/test_tabhistory.py tests/unit/browser/webkit/test_webkit_view.py tests/unit/browser/webkit/test_webkitelem.py tests/unit/browser/webkit/test_webkitsettings.py tests/unit/browser/webkit/http/test_content_disposition.py tests/unit/browser/webkit/http/test_httpheaders.py tests/unit/browser/webkit/network/test_filescheme.py tests/unit/browser/webkit/network/test_networkmanager.py tests/unit/browser/webkit/network/test_networkreply.py tests/unit/browser/webkit/network/test_pac.py tests/unit/commands/test_argparser.py tests/unit/commands/test_cmdexc.py tests/unit/commands/test_parser.py tests/unit/commands/test_userscripts.py tests/unit/completion/test_completer.py tests/unit/completion/test_completiondelegate.py tests/unit/completion/test_completionmodel.py tests/unit/completion/test_completionwidget.py tests/unit/completion/test_histcategory.py tests/unit/completion/test_listcategory.py tests/unit/completion/test_models.py tests/unit/components/test_blockutils.py tests/unit/components/test_braveadblock.py tests/unit/components/test_hostblock.py tests/unit/components/test_misccommands.py tests/unit/components/test_readlinecommands.py tests/unit/config/test_config.py tests/unit/config/test_configcache.py tests/unit/config/test_configcommands.py tests/unit/config/test_configdata.py tests/unit/config/test_configexc.py tests/unit/config/test_configfiles.py tests/unit/config/test_configinit.py tests/unit/config/test_configtypes.py tests/unit/config/test_configutils.py tests/unit/config/test_qtargs.py tests/unit/config/test_qtargs_locale_workaround.py tests/unit/config/test_stylesheet.py tests/unit/config/test_websettings.py tests/unit/extensions/test_loader.py tests/unit/javascript/base.html tests/unit/javascript/conftest.py tests/unit/javascript/test_greasemonkey.py tests/unit/javascript/test_js_execution.py tests/unit/javascript/test_js_quirks.py tests/unit/javascript/position_caret/invisible.html tests/unit/javascript/position_caret/scrolled_down.html tests/unit/javascript/position_caret/scrolled_down_img.html tests/unit/javascript/position_caret/simple.html tests/unit/javascript/position_caret/test_position_caret.py tests/unit/javascript/stylesheet/green.css tests/unit/javascript/stylesheet/none.css tests/unit/javascript/stylesheet/simple.html tests/unit/javascript/stylesheet/simple.xml tests/unit/javascript/stylesheet/simple_bg_set_red.html tests/unit/javascript/stylesheet/test_appendchild.js tests/unit/javascript/stylesheet/test_stylesheet_js.py tests/unit/keyinput/conftest.py tests/unit/keyinput/key_data.py tests/unit/keyinput/test_basekeyparser.py tests/unit/keyinput/test_bindingtrie.py tests/unit/keyinput/test_keyutils.py tests/unit/keyinput/test_modeman.py tests/unit/keyinput/test_modeparsers.py tests/unit/mainwindow/test_messageview.py tests/unit/mainwindow/test_prompt.py tests/unit/mainwindow/test_tabbedbrowser.py tests/unit/mainwindow/test_tabwidget.py tests/unit/mainwindow/statusbar/test_backforward.py tests/unit/mainwindow/statusbar/test_percentage.py tests/unit/mainwindow/statusbar/test_progress.py tests/unit/mainwindow/statusbar/test_tabindex.py tests/unit/mainwindow/statusbar/test_textbase.py tests/unit/mainwindow/statusbar/test_url.py tests/unit/misc/test_autoupdate.py tests/unit/misc/test_checkpyver.py tests/unit/misc/test_cmdhistory.py tests/unit/misc/test_crashdialog.py tests/unit/misc/test_crashsignal.py tests/unit/misc/test_earlyinit.py tests/unit/misc/test_editor.py tests/unit/misc/test_elf.py tests/unit/misc/test_guiprocess.py tests/unit/misc/test_ipc.py tests/unit/misc/test_keyhints.py tests/unit/misc/test_lineparser.py tests/unit/misc/test_miscwidgets.py tests/unit/misc/test_msgbox.py tests/unit/misc/test_objects.py tests/unit/misc/test_pakjoy.py tests/unit/misc/test_pastebin.py tests/unit/misc/test_sessions.py tests/unit/misc/test_split.py tests/unit/misc/test_split_hypothesis.py tests/unit/misc/test_sql.py tests/unit/misc/test_throttle.py tests/unit/misc/test_utilcmds.py tests/unit/misc/test_wmname.py tests/unit/misc/userscripts/test_qute_lastpass.py tests/unit/scripts/test_dictcli.py tests/unit/scripts/test_importer.py tests/unit/scripts/test_problemmatchers.py tests/unit/scripts/importer_sample/chrome/bookmarks tests/unit/scripts/importer_sample/chrome/config_py tests/unit/scripts/importer_sample/chrome/quickmarks tests/unit/scripts/importer_sample/chrome/input/Bookmarks tests/unit/scripts/importer_sample/chrome/input/Web Data tests/unit/scripts/importer_sample/html/bookmarks tests/unit/scripts/importer_sample/html/config_py tests/unit/scripts/importer_sample/html/input tests/unit/scripts/importer_sample/html/quickmarks tests/unit/scripts/importer_sample/mozilla/bookmarks tests/unit/scripts/importer_sample/mozilla/config_py tests/unit/scripts/importer_sample/mozilla/quickmarks tests/unit/scripts/importer_sample/mozilla/input/places.sqlite tests/unit/utils/overflow_test_cases.py tests/unit/utils/test_debug.py tests/unit/utils/test_error.py tests/unit/utils/test_javascript.py tests/unit/utils/test_jinja.py tests/unit/utils/test_log.py tests/unit/utils/test_qtlog.py tests/unit/utils/test_qtutils.py tests/unit/utils/test_resources.py tests/unit/utils/test_standarddir.py tests/unit/utils/test_urlmatch.py tests/unit/utils/test_urlutils.py tests/unit/utils/test_utils.py tests/unit/utils/test_version.py tests/unit/utils/usertypes/test_misc.py tests/unit/utils/usertypes/test_neighborlist.py tests/unit/utils/usertypes/test_question.py tests/unit/utils/usertypes/test_timer.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183912.0 qutebrowser-3.6.1/qutebrowser.egg-info/dependency_links.txt0000644000175100017510000000000115102145350023667 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183912.0 qutebrowser-3.6.1/qutebrowser.egg-info/entry_points.txt0000644000175100017510000000007115102145350023115 0ustar00runnerrunner[gui_scripts] qutebrowser = qutebrowser.qutebrowser:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183912.0 qutebrowser-3.6.1/qutebrowser.egg-info/requires.txt0000644000175100017510000000001615102145350022216 0ustar00runnerrunnerjinja2 PyYAML ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183912.0 qutebrowser-3.6.1/qutebrowser.egg-info/top_level.txt0000644000175100017510000000001415102145350022346 0ustar00runnerrunnerqutebrowser ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183909.0 qutebrowser-3.6.1/qutebrowser.egg-info/zip-safe0000644000175100017510000000000115102145345021255 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/qutebrowser.py0000755000175100017510000000046315102145205016506 0ustar00runnerrunner#!/usr/bin/env python3 # SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Simple launcher for qutebrowser.""" import sys import qutebrowser.qutebrowser if __name__ == '__main__': sys.exit(qutebrowser.qutebrowser.main()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/requirements.txt0000644000175100017510000000047415102145205017035 0ustar00runnerrunner# This file is automatically generated by scripts/dev/recompile_requirements.py adblock==0.6.0 colorama==0.4.6 Jinja2==3.1.6 MarkupSafe==3.0.3 Pygments==2.19.2 PyYAML==6.0.3 # Unpinned due to recompile_requirements.py limitations pyobjc-core ; sys_platform=="darwin" pyobjc-framework-Cocoa ; sys_platform=="darwin" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5556388 qutebrowser-3.6.1/scripts/0000755000175100017510000000000015102145351015235 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/scripts/__init__.py0000644000175100017510000000003715102145205017344 0ustar00runnerrunner"""Various utility scripts.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/scripts/cycle-inputs.js0000644000175100017510000000244715102145205020217 0ustar00runnerrunner/* Cycle text boxes. * works with the types defined in 'types'. * Note: Does not work for ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/email_address.html0000644000175100017510000000030415102145205022615 0ustar00runnerrunner Email address Email address ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/fileselect.html0000644000175100017510000000231015102145205022137 0ustar00runnerrunner Testing fileselection
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hello.txt0000644000175100017510000000001515102145205020776 0ustar00runnerrunnerHello World! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hello2.txt0000644000175100017510000000001715102145205021062 0ustar00runnerrunnerHello World 2! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hello3.txt0000644000175100017510000000001715102145205021063 0ustar00runnerrunnerHello World 3! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hinting.txt0000644000175100017510000000001015102145205021326 0ustar00runnerrunnerhinting ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5756385 qutebrowser-3.6.1/tests/end2end/data/hints/0000755000175100017510000000000015102145351020265 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5756385 qutebrowser-3.6.1/tests/end2end/data/hints/ace/0000755000175100017510000000000015102145351021015 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/ace/ace.html0000644000175100017510000000077515102145205022442 0ustar00runnerrunner ACE editor







././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0
qutebrowser-3.6.1/tests/end2end/data/hints/ace/ace.js0000644000175100017510000241327315102145205022115 0ustar00runnerrunner/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

/**
 * Define a module along with a payload
 * @param module a name for the payload
 * @param payload a function to call with (require, exports, module) params
 */

(function() {

var ACE_NAMESPACE = "";

var global = (function() { return this; })();
if (!global && typeof window != "undefined") global = window; // strict mode


if (!ACE_NAMESPACE && typeof requirejs !== "undefined")
    return;


var define = function(module, deps, payload) {
    if (typeof module !== "string") {
        if (define.original)
            define.original.apply(this, arguments);
        else {
            console.error("dropping module because define wasn\'t a string.");
            console.trace();
        }
        return;
    }
    if (arguments.length == 2)
        payload = deps;
    if (!define.modules[module]) {
        define.payloads[module] = payload;
        define.modules[module] = null;
    }
};

define.modules = {};
define.payloads = {};

/**
 * Get at functionality define()ed using the function above
 */
var _require = function(parentId, module, callback) {
    if (typeof module === "string") {
        var payload = lookup(parentId, module);
        if (payload != undefined) {
            callback && callback();
            return payload;
        }
    } else if (Object.prototype.toString.call(module) === "[object Array]") {
        var params = [];
        for (var i = 0, l = module.length; i < l; ++i) {
            var dep = lookup(parentId, module[i]);
            if (dep == undefined && require.original)
                return;
            params.push(dep);
        }
        return callback && callback.apply(null, params) || true;
    }
};

var require = function(module, callback) {
    var packagedModule = _require("", module, callback);
    if (packagedModule == undefined && require.original)
        return require.original.apply(this, arguments);
    return packagedModule;
};

var normalizeModule = function(parentId, moduleName) {
    // normalize plugin requires
    if (moduleName.indexOf("!") !== -1) {
        var chunks = moduleName.split("!");
        return normalizeModule(parentId, chunks[0]) + "!" + normalizeModule(parentId, chunks[1]);
    }
    // normalize relative requires
    if (moduleName.charAt(0) == ".") {
        var base = parentId.split("/").slice(0, -1).join("/");
        moduleName = base + "/" + moduleName;

        while(moduleName.indexOf(".") !== -1 && previous != moduleName) {
            var previous = moduleName;
            moduleName = moduleName.replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "");
        }
    }
    return moduleName;
};

/**
 * Internal function to lookup moduleNames and resolve them by calling the
 * definition function if needed.
 */
var lookup = function(parentId, moduleName) {
    moduleName = normalizeModule(parentId, moduleName);

    var module = define.modules[moduleName];
    if (!module) {
        module = define.payloads[moduleName];
        if (typeof module === 'function') {
            var exports = {};
            var mod = {
                id: moduleName,
                uri: '',
                exports: exports,
                packaged: true
            };

            var req = function(module, callback) {
                return _require(moduleName, module, callback);
            };

            var returnValue = module(req, exports, mod);
            exports = returnValue || mod.exports;
            define.modules[moduleName] = exports;
            delete define.payloads[moduleName];
        }
        module = define.modules[moduleName] = exports || module;
    }
    return module;
};

function exportAce(ns) {
    var root = global;
    if (ns) {
        if (!global[ns])
            global[ns] = {};
        root = global[ns];
    }

    if (!root.define || !root.define.packaged) {
        define.original = root.define;
        root.define = define;
        root.define.packaged = true;
    }

    if (!root.require || !root.require.packaged) {
        require.original = root.require;
        root.require = require;
        root.require.packaged = true;
    }
}

exportAce(ACE_NAMESPACE);

})();

define("ace/lib/regexp",["require","exports","module"], function(require, exports, module) {
"use strict";

    var real = {
            exec: RegExp.prototype.exec,
            test: RegExp.prototype.test,
            match: String.prototype.match,
            replace: String.prototype.replace,
            split: String.prototype.split
        },
        compliantExecNpcg = real.exec.call(/()??/, "")[1] === undefined, // check `exec` handling of nonparticipating capturing groups
        compliantLastIndexIncrement = function () {
            var x = /^/g;
            real.test.call(x, "");
            return !x.lastIndex;
        }();

    if (compliantLastIndexIncrement && compliantExecNpcg)
        return;
    RegExp.prototype.exec = function (str) {
        var match = real.exec.apply(this, arguments),
            name, r2;
        if ( typeof(str) == 'string' && match) {
            if (!compliantExecNpcg && match.length > 1 && indexOf(match, "") > -1) {
                r2 = RegExp(this.source, real.replace.call(getNativeFlags(this), "g", ""));
                real.replace.call(str.slice(match.index), r2, function () {
                    for (var i = 1; i < arguments.length - 2; i++) {
                        if (arguments[i] === undefined)
                            match[i] = undefined;
                    }
                });
            }
            if (this._xregexp && this._xregexp.captureNames) {
                for (var i = 1; i < match.length; i++) {
                    name = this._xregexp.captureNames[i - 1];
                    if (name)
                       match[name] = match[i];
                }
            }
            if (!compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index))
                this.lastIndex--;
        }
        return match;
    };
    if (!compliantLastIndexIncrement) {
        RegExp.prototype.test = function (str) {
            var match = real.exec.call(this, str);
            if (match && this.global && !match[0].length && (this.lastIndex > match.index))
                this.lastIndex--;
            return !!match;
        };
    }

    function getNativeFlags (regex) {
        return (regex.global     ? "g" : "") +
               (regex.ignoreCase ? "i" : "") +
               (regex.multiline  ? "m" : "") +
               (regex.extended   ? "x" : "") + // Proposed for ES4; included in AS3
               (regex.sticky     ? "y" : "");
    }

    function indexOf (array, item, from) {
        if (Array.prototype.indexOf) // Use the native array method if available
            return array.indexOf(item, from);
        for (var i = from || 0; i < array.length; i++) {
            if (array[i] === item)
                return i;
        }
        return -1;
    }

});

define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) {

function Empty() {}

if (!Function.prototype.bind) {
    Function.prototype.bind = function bind(that) { // .length is 1
        var target = this;
        if (typeof target != "function") {
            throw new TypeError("Function.prototype.bind called on incompatible " + target);
        }
        var args = slice.call(arguments, 1); // for normal call
        var bound = function () {

            if (this instanceof bound) {

                var result = target.apply(
                    this,
                    args.concat(slice.call(arguments))
                );
                if (Object(result) === result) {
                    return result;
                }
                return this;

            } else {
                return target.apply(
                    that,
                    args.concat(slice.call(arguments))
                );

            }

        };
        if(target.prototype) {
            Empty.prototype = target.prototype;
            bound.prototype = new Empty();
            Empty.prototype = null;
        }
        return bound;
    };
}
var call = Function.prototype.call;
var prototypeOfArray = Array.prototype;
var prototypeOfObject = Object.prototype;
var slice = prototypeOfArray.slice;
var _toString = call.bind(prototypeOfObject.toString);
var owns = call.bind(prototypeOfObject.hasOwnProperty);
var defineGetter;
var defineSetter;
var lookupGetter;
var lookupSetter;
var supportsAccessors;
if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) {
    defineGetter = call.bind(prototypeOfObject.__defineGetter__);
    defineSetter = call.bind(prototypeOfObject.__defineSetter__);
    lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);
    lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);
}
if ([1,2].splice(0).length != 2) {
    if(function() { // test IE < 9 to splice bug - see issue #138
        function makeArray(l) {
            var a = new Array(l+2);
            a[0] = a[1] = 0;
            return a;
        }
        var array = [], lengthBefore;
        
        array.splice.apply(array, makeArray(20));
        array.splice.apply(array, makeArray(26));

        lengthBefore = array.length; //46
        array.splice(5, 0, "XXX"); // add one element

        lengthBefore + 1 == array.length

        if (lengthBefore + 1 == array.length) {
            return true;// has right splice implementation without bugs
        }
    }()) {//IE 6/7
        var array_splice = Array.prototype.splice;
        Array.prototype.splice = function(start, deleteCount) {
            if (!arguments.length) {
                return [];
            } else {
                return array_splice.apply(this, [
                    start === void 0 ? 0 : start,
                    deleteCount === void 0 ? (this.length - start) : deleteCount
                ].concat(slice.call(arguments, 2)))
            }
        };
    } else {//IE8
        Array.prototype.splice = function(pos, removeCount){
            var length = this.length;
            if (pos > 0) {
                if (pos > length)
                    pos = length;
            } else if (pos == void 0) {
                pos = 0;
            } else if (pos < 0) {
                pos = Math.max(length + pos, 0);
            }

            if (!(pos+removeCount < length))
                removeCount = length - pos;

            var removed = this.slice(pos, pos+removeCount);
            var insert = slice.call(arguments, 2);
            var add = insert.length;            
            if (pos === length) {
                if (add) {
                    this.push.apply(this, insert);
                }
            } else {
                var remove = Math.min(removeCount, length - pos);
                var tailOldPos = pos + remove;
                var tailNewPos = tailOldPos + add - remove;
                var tailCount = length - tailOldPos;
                var lengthAfterRemove = length - remove;

                if (tailNewPos < tailOldPos) { // case A
                    for (var i = 0; i < tailCount; ++i) {
                        this[tailNewPos+i] = this[tailOldPos+i];
                    }
                } else if (tailNewPos > tailOldPos) { // case B
                    for (i = tailCount; i--; ) {
                        this[tailNewPos+i] = this[tailOldPos+i];
                    }
                } // else, add == remove (nothing to do)

                if (add && pos === lengthAfterRemove) {
                    this.length = lengthAfterRemove; // truncate array
                    this.push.apply(this, insert);
                } else {
                    this.length = lengthAfterRemove + add; // reserves space
                    for (i = 0; i < add; ++i) {
                        this[pos+i] = insert[i];
                    }
                }
            }
            return removed;
        };
    }
}
if (!Array.isArray) {
    Array.isArray = function isArray(obj) {
        return _toString(obj) == "[object Array]";
    };
}
var boxedString = Object("a"),
    splitString = boxedString[0] != "a" || !(0 in boxedString);

if (!Array.prototype.forEach) {
    Array.prototype.forEach = function forEach(fun /*, thisp*/) {
        var object = toObject(this),
            self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                object,
            thisp = arguments[1],
            i = -1,
            length = self.length >>> 0;
        if (_toString(fun) != "[object Function]") {
            throw new TypeError(); // TODO message
        }

        while (++i < length) {
            if (i in self) {
                fun.call(thisp, self[i], i, object);
            }
        }
    };
}
if (!Array.prototype.map) {
    Array.prototype.map = function map(fun /*, thisp*/) {
        var object = toObject(this),
            self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                object,
            length = self.length >>> 0,
            result = Array(length),
            thisp = arguments[1];
        if (_toString(fun) != "[object Function]") {
            throw new TypeError(fun + " is not a function");
        }

        for (var i = 0; i < length; i++) {
            if (i in self)
                result[i] = fun.call(thisp, self[i], i, object);
        }
        return result;
    };
}
if (!Array.prototype.filter) {
    Array.prototype.filter = function filter(fun /*, thisp */) {
        var object = toObject(this),
            self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                    object,
            length = self.length >>> 0,
            result = [],
            value,
            thisp = arguments[1];
        if (_toString(fun) != "[object Function]") {
            throw new TypeError(fun + " is not a function");
        }

        for (var i = 0; i < length; i++) {
            if (i in self) {
                value = self[i];
                if (fun.call(thisp, value, i, object)) {
                    result.push(value);
                }
            }
        }
        return result;
    };
}
if (!Array.prototype.every) {
    Array.prototype.every = function every(fun /*, thisp */) {
        var object = toObject(this),
            self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                object,
            length = self.length >>> 0,
            thisp = arguments[1];
        if (_toString(fun) != "[object Function]") {
            throw new TypeError(fun + " is not a function");
        }

        for (var i = 0; i < length; i++) {
            if (i in self && !fun.call(thisp, self[i], i, object)) {
                return false;
            }
        }
        return true;
    };
}
if (!Array.prototype.some) {
    Array.prototype.some = function some(fun /*, thisp */) {
        var object = toObject(this),
            self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                object,
            length = self.length >>> 0,
            thisp = arguments[1];
        if (_toString(fun) != "[object Function]") {
            throw new TypeError(fun + " is not a function");
        }

        for (var i = 0; i < length; i++) {
            if (i in self && fun.call(thisp, self[i], i, object)) {
                return true;
            }
        }
        return false;
    };
}
if (!Array.prototype.reduce) {
    Array.prototype.reduce = function reduce(fun /*, initial*/) {
        var object = toObject(this),
            self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                object,
            length = self.length >>> 0;
        if (_toString(fun) != "[object Function]") {
            throw new TypeError(fun + " is not a function");
        }
        if (!length && arguments.length == 1) {
            throw new TypeError("reduce of empty array with no initial value");
        }

        var i = 0;
        var result;
        if (arguments.length >= 2) {
            result = arguments[1];
        } else {
            do {
                if (i in self) {
                    result = self[i++];
                    break;
                }
                if (++i >= length) {
                    throw new TypeError("reduce of empty array with no initial value");
                }
            } while (true);
        }

        for (; i < length; i++) {
            if (i in self) {
                result = fun.call(void 0, result, self[i], i, object);
            }
        }

        return result;
    };
}
if (!Array.prototype.reduceRight) {
    Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) {
        var object = toObject(this),
            self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                object,
            length = self.length >>> 0;
        if (_toString(fun) != "[object Function]") {
            throw new TypeError(fun + " is not a function");
        }
        if (!length && arguments.length == 1) {
            throw new TypeError("reduceRight of empty array with no initial value");
        }

        var result, i = length - 1;
        if (arguments.length >= 2) {
            result = arguments[1];
        } else {
            do {
                if (i in self) {
                    result = self[i--];
                    break;
                }
                if (--i < 0) {
                    throw new TypeError("reduceRight of empty array with no initial value");
                }
            } while (true);
        }

        do {
            if (i in this) {
                result = fun.call(void 0, result, self[i], i, object);
            }
        } while (i--);

        return result;
    };
}
if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) {
    Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) {
        var self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                toObject(this),
            length = self.length >>> 0;

        if (!length) {
            return -1;
        }

        var i = 0;
        if (arguments.length > 1) {
            i = toInteger(arguments[1]);
        }
        i = i >= 0 ? i : Math.max(0, length + i);
        for (; i < length; i++) {
            if (i in self && self[i] === sought) {
                return i;
            }
        }
        return -1;
    };
}
if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) {
    Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) {
        var self = splitString && _toString(this) == "[object String]" ?
                this.split("") :
                toObject(this),
            length = self.length >>> 0;

        if (!length) {
            return -1;
        }
        var i = length - 1;
        if (arguments.length > 1) {
            i = Math.min(i, toInteger(arguments[1]));
        }
        i = i >= 0 ? i : length - Math.abs(i);
        for (; i >= 0; i--) {
            if (i in self && sought === self[i]) {
                return i;
            }
        }
        return -1;
    };
}
if (!Object.getPrototypeOf) {
    Object.getPrototypeOf = function getPrototypeOf(object) {
        return object.__proto__ || (
            object.constructor ?
            object.constructor.prototype :
            prototypeOfObject
        );
    };
}
if (!Object.getOwnPropertyDescriptor) {
    var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " +
                         "non-object: ";
    Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) {
        if ((typeof object != "object" && typeof object != "function") || object === null)
            throw new TypeError(ERR_NON_OBJECT + object);
        if (!owns(object, property))
            return;

        var descriptor, getter, setter;
        descriptor =  { enumerable: true, configurable: true };
        if (supportsAccessors) {
            var prototype = object.__proto__;
            object.__proto__ = prototypeOfObject;

            var getter = lookupGetter(object, property);
            var setter = lookupSetter(object, property);
            object.__proto__ = prototype;

            if (getter || setter) {
                if (getter) descriptor.get = getter;
                if (setter) descriptor.set = setter;
                return descriptor;
            }
        }
        descriptor.value = object[property];
        return descriptor;
    };
}
if (!Object.getOwnPropertyNames) {
    Object.getOwnPropertyNames = function getOwnPropertyNames(object) {
        return Object.keys(object);
    };
}
if (!Object.create) {
    var createEmpty;
    if (Object.prototype.__proto__ === null) {
        createEmpty = function () {
            return { "__proto__": null };
        };
    } else {
        createEmpty = function () {
            var empty = {};
            for (var i in empty)
                empty[i] = null;
            empty.constructor =
            empty.hasOwnProperty =
            empty.propertyIsEnumerable =
            empty.isPrototypeOf =
            empty.toLocaleString =
            empty.toString =
            empty.valueOf =
            empty.__proto__ = null;
            return empty;
        }
    }

    Object.create = function create(prototype, properties) {
        var object;
        if (prototype === null) {
            object = createEmpty();
        } else {
            if (typeof prototype != "object")
                throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'");
            var Type = function () {};
            Type.prototype = prototype;
            object = new Type();
            object.__proto__ = prototype;
        }
        if (properties !== void 0)
            Object.defineProperties(object, properties);
        return object;
    };
}

function doesDefinePropertyWork(object) {
    try {
        Object.defineProperty(object, "sentinel", {});
        return "sentinel" in object;
    } catch (exception) {
    }
}
if (Object.defineProperty) {
    var definePropertyWorksOnObject = doesDefinePropertyWork({});
    var definePropertyWorksOnDom = typeof document == "undefined" ||
        doesDefinePropertyWork(document.createElement("div"));
    if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) {
        var definePropertyFallback = Object.defineProperty;
    }
}

if (!Object.defineProperty || definePropertyFallback) {
    var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ";
    var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: "
    var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " +
                                      "on this javascript engine";

    Object.defineProperty = function defineProperty(object, property, descriptor) {
        if ((typeof object != "object" && typeof object != "function") || object === null)
            throw new TypeError(ERR_NON_OBJECT_TARGET + object);
        if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null)
            throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);
        if (definePropertyFallback) {
            try {
                return definePropertyFallback.call(Object, object, property, descriptor);
            } catch (exception) {
            }
        }
        if (owns(descriptor, "value")) {

            if (supportsAccessors && (lookupGetter(object, property) ||
                                      lookupSetter(object, property)))
            {
                var prototype = object.__proto__;
                object.__proto__ = prototypeOfObject;
                delete object[property];
                object[property] = descriptor.value;
                object.__proto__ = prototype;
            } else {
                object[property] = descriptor.value;
            }
        } else {
            if (!supportsAccessors)
                throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);
            if (owns(descriptor, "get"))
                defineGetter(object, property, descriptor.get);
            if (owns(descriptor, "set"))
                defineSetter(object, property, descriptor.set);
        }

        return object;
    };
}
if (!Object.defineProperties) {
    Object.defineProperties = function defineProperties(object, properties) {
        for (var property in properties) {
            if (owns(properties, property))
                Object.defineProperty(object, property, properties[property]);
        }
        return object;
    };
}
if (!Object.seal) {
    Object.seal = function seal(object) {
        return object;
    };
}
if (!Object.freeze) {
    Object.freeze = function freeze(object) {
        return object;
    };
}
try {
    Object.freeze(function () {});
} catch (exception) {
    Object.freeze = (function freeze(freezeObject) {
        return function freeze(object) {
            if (typeof object == "function") {
                return object;
            } else {
                return freezeObject(object);
            }
        };
    })(Object.freeze);
}
if (!Object.preventExtensions) {
    Object.preventExtensions = function preventExtensions(object) {
        return object;
    };
}
if (!Object.isSealed) {
    Object.isSealed = function isSealed(object) {
        return false;
    };
}
if (!Object.isFrozen) {
    Object.isFrozen = function isFrozen(object) {
        return false;
    };
}
if (!Object.isExtensible) {
    Object.isExtensible = function isExtensible(object) {
        if (Object(object) === object) {
            throw new TypeError(); // TODO message
        }
        var name = '';
        while (owns(object, name)) {
            name += '?';
        }
        object[name] = true;
        var returnValue = owns(object, name);
        delete object[name];
        return returnValue;
    };
}
if (!Object.keys) {
    var hasDontEnumBug = true,
        dontEnums = [
            "toString",
            "toLocaleString",
            "valueOf",
            "hasOwnProperty",
            "isPrototypeOf",
            "propertyIsEnumerable",
            "constructor"
        ],
        dontEnumsLength = dontEnums.length;

    for (var key in {"toString": null}) {
        hasDontEnumBug = false;
    }

    Object.keys = function keys(object) {

        if (
            (typeof object != "object" && typeof object != "function") ||
            object === null
        ) {
            throw new TypeError("Object.keys called on a non-object");
        }

        var keys = [];
        for (var name in object) {
            if (owns(object, name)) {
                keys.push(name);
            }
        }

        if (hasDontEnumBug) {
            for (var i = 0, ii = dontEnumsLength; i < ii; i++) {
                var dontEnum = dontEnums[i];
                if (owns(object, dontEnum)) {
                    keys.push(dontEnum);
                }
            }
        }
        return keys;
    };

}
if (!Date.now) {
    Date.now = function now() {
        return new Date().getTime();
    };
}
var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
    "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
    "\u2029\uFEFF";
if (!String.prototype.trim || ws.trim()) {
    ws = "[" + ws + "]";
    var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
        trimEndRegexp = new RegExp(ws + ws + "*$");
    String.prototype.trim = function trim() {
        return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, "");
    };
}

function toInteger(n) {
    n = +n;
    if (n !== n) { // isNaN
        n = 0;
    } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) {
        n = (n > 0 || -1) * Math.floor(Math.abs(n));
    }
    return n;
}

function isPrimitive(input) {
    var type = typeof input;
    return (
        input === null ||
        type === "undefined" ||
        type === "boolean" ||
        type === "number" ||
        type === "string"
    );
}

function toPrimitive(input) {
    var val, valueOf, toString;
    if (isPrimitive(input)) {
        return input;
    }
    valueOf = input.valueOf;
    if (typeof valueOf === "function") {
        val = valueOf.call(input);
        if (isPrimitive(val)) {
            return val;
        }
    }
    toString = input.toString;
    if (typeof toString === "function") {
        val = toString.call(input);
        if (isPrimitive(val)) {
            return val;
        }
    }
    throw new TypeError();
}
var toObject = function (o) {
    if (o == null) { // this matches both null and undefined
        throw new TypeError("can't convert "+o+" to object");
    }
    return Object(o);
};

});

define("ace/lib/fixoldbrowsers",["require","exports","module","ace/lib/regexp","ace/lib/es5-shim"], function(require, exports, module) {
"use strict";

require("./regexp");
require("./es5-shim");

});

define("ace/lib/dom",["require","exports","module"], function(require, exports, module) {
"use strict";

var XHTML_NS = "http://www.w3.org/1999/xhtml";

exports.getDocumentHead = function(doc) {
    if (!doc)
        doc = document;
    return doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement;
}

exports.createElement = function(tag, ns) {
    return document.createElementNS ?
           document.createElementNS(ns || XHTML_NS, tag) :
           document.createElement(tag);
};

exports.hasCssClass = function(el, name) {
    var classes = (el.className + "").split(/\s+/g);
    return classes.indexOf(name) !== -1;
};
exports.addCssClass = function(el, name) {
    if (!exports.hasCssClass(el, name)) {
        el.className += " " + name;
    }
};
exports.removeCssClass = function(el, name) {
    var classes = el.className.split(/\s+/g);
    while (true) {
        var index = classes.indexOf(name);
        if (index == -1) {
            break;
        }
        classes.splice(index, 1);
    }
    el.className = classes.join(" ");
};

exports.toggleCssClass = function(el, name) {
    var classes = el.className.split(/\s+/g), add = true;
    while (true) {
        var index = classes.indexOf(name);
        if (index == -1) {
            break;
        }
        add = false;
        classes.splice(index, 1);
    }
    if (add)
        classes.push(name);

    el.className = classes.join(" ");
    return add;
};
exports.setCssClass = function(node, className, include) {
    if (include) {
        exports.addCssClass(node, className);
    } else {
        exports.removeCssClass(node, className);
    }
};

exports.hasCssString = function(id, doc) {
    var index = 0, sheets;
    doc = doc || document;

    if (doc.createStyleSheet && (sheets = doc.styleSheets)) {
        while (index < sheets.length)
            if (sheets[index++].owningElement.id === id) return true;
    } else if ((sheets = doc.getElementsByTagName("style"))) {
        while (index < sheets.length)
            if (sheets[index++].id === id) return true;
    }

    return false;
};

exports.importCssString = function importCssString(cssText, id, doc) {
    doc = doc || document;
    if (id && exports.hasCssString(id, doc))
        return null;
    
    var style;
    
    if (id)
        cssText += "\n/*# sourceURL=ace/css/" + id + " */";
    
    if (doc.createStyleSheet) {
        style = doc.createStyleSheet();
        style.cssText = cssText;
        if (id)
            style.owningElement.id = id;
    } else {
        style = exports.createElement("style");
        style.appendChild(doc.createTextNode(cssText));
        if (id)
            style.id = id;

        exports.getDocumentHead(doc).appendChild(style);
    }
};

exports.importCssStylsheet = function(uri, doc) {
    if (doc.createStyleSheet) {
        doc.createStyleSheet(uri);
    } else {
        var link = exports.createElement('link');
        link.rel = 'stylesheet';
        link.href = uri;

        exports.getDocumentHead(doc).appendChild(link);
    }
};

exports.getInnerWidth = function(element) {
    return (
        parseInt(exports.computedStyle(element, "paddingLeft"), 10) +
        parseInt(exports.computedStyle(element, "paddingRight"), 10) + 
        element.clientWidth
    );
};

exports.getInnerHeight = function(element) {
    return (
        parseInt(exports.computedStyle(element, "paddingTop"), 10) +
        parseInt(exports.computedStyle(element, "paddingBottom"), 10) +
        element.clientHeight
    );
};

exports.scrollbarWidth = function(document) {
    var inner = exports.createElement("ace_inner");
    inner.style.width = "100%";
    inner.style.minWidth = "0px";
    inner.style.height = "200px";
    inner.style.display = "block";

    var outer = exports.createElement("ace_outer");
    var style = outer.style;

    style.position = "absolute";
    style.left = "-10000px";
    style.overflow = "hidden";
    style.width = "200px";
    style.minWidth = "0px";
    style.height = "150px";
    style.display = "block";

    outer.appendChild(inner);

    var body = document.documentElement;
    body.appendChild(outer);

    var noScrollbar = inner.offsetWidth;

    style.overflow = "scroll";
    var withScrollbar = inner.offsetWidth;

    if (noScrollbar == withScrollbar) {
        withScrollbar = outer.clientWidth;
    }

    body.removeChild(outer);

    return noScrollbar-withScrollbar;
};

if (typeof document == "undefined") {
    exports.importCssString = function() {};
    return;
}

if (window.pageYOffset !== undefined) {
    exports.getPageScrollTop = function() {
        return window.pageYOffset;
    };

    exports.getPageScrollLeft = function() {
        return window.pageXOffset;
    };
}
else {
    exports.getPageScrollTop = function() {
        return document.body.scrollTop;
    };

    exports.getPageScrollLeft = function() {
        return document.body.scrollLeft;
    };
}

if (window.getComputedStyle)
    exports.computedStyle = function(element, style) {
        if (style)
            return (window.getComputedStyle(element, "") || {})[style] || "";
        return window.getComputedStyle(element, "") || {};
    };
else
    exports.computedStyle = function(element, style) {
        if (style)
            return element.currentStyle[style];
        return element.currentStyle;
    };
exports.setInnerHtml = function(el, innerHtml) {
    var element = el.cloneNode(false);//document.createElement("div");
    element.innerHTML = innerHtml;
    el.parentNode.replaceChild(element, el);
    return element;
};

if ("textContent" in document.documentElement) {
    exports.setInnerText = function(el, innerText) {
        el.textContent = innerText;
    };

    exports.getInnerText = function(el) {
        return el.textContent;
    };
}
else {
    exports.setInnerText = function(el, innerText) {
        el.innerText = innerText;
    };

    exports.getInnerText = function(el) {
        return el.innerText;
    };
}

exports.getParentWindow = function(document) {
    return document.defaultView || document.parentWindow;
};

});

define("ace/lib/oop",["require","exports","module"], function(require, exports, module) {
"use strict";

exports.inherits = function(ctor, superCtor) {
    ctor.super_ = superCtor;
    ctor.prototype = Object.create(superCtor.prototype, {
        constructor: {
            value: ctor,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
};

exports.mixin = function(obj, mixin) {
    for (var key in mixin) {
        obj[key] = mixin[key];
    }
    return obj;
};

exports.implement = function(proto, mixin) {
    exports.mixin(proto, mixin);
};

});

define("ace/lib/keys",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/oop"], function(require, exports, module) {
"use strict";

require("./fixoldbrowsers");

var oop = require("./oop");
var Keys = (function() {
    var ret = {
        MODIFIER_KEYS: {
            16: 'Shift', 17: 'Ctrl', 18: 'Alt', 224: 'Meta'
        },

        KEY_MODS: {
            "ctrl": 1, "alt": 2, "option" : 2, "shift": 4,
            "super": 8, "meta": 8, "command": 8, "cmd": 8
        },

        FUNCTION_KEYS : {
            8  : "Backspace",
            9  : "Tab",
            13 : "Return",
            19 : "Pause",
            27 : "Esc",
            32 : "Space",
            33 : "PageUp",
            34 : "PageDown",
            35 : "End",
            36 : "Home",
            37 : "Left",
            38 : "Up",
            39 : "Right",
            40 : "Down",
            44 : "Print",
            45 : "Insert",
            46 : "Delete",
            96 : "Numpad0",
            97 : "Numpad1",
            98 : "Numpad2",
            99 : "Numpad3",
            100: "Numpad4",
            101: "Numpad5",
            102: "Numpad6",
            103: "Numpad7",
            104: "Numpad8",
            105: "Numpad9",
            '-13': "NumpadEnter",
            112: "F1",
            113: "F2",
            114: "F3",
            115: "F4",
            116: "F5",
            117: "F6",
            118: "F7",
            119: "F8",
            120: "F9",
            121: "F10",
            122: "F11",
            123: "F12",
            144: "Numlock",
            145: "Scrolllock"
        },

        PRINTABLE_KEYS: {
           32: ' ',  48: '0',  49: '1',  50: '2',  51: '3',  52: '4', 53:  '5',
           54: '6',  55: '7',  56: '8',  57: '9',  59: ';',  61: '=', 65:  'a',
           66: 'b',  67: 'c',  68: 'd',  69: 'e',  70: 'f',  71: 'g', 72:  'h',
           73: 'i',  74: 'j',  75: 'k',  76: 'l',  77: 'm',  78: 'n', 79:  'o',
           80: 'p',  81: 'q',  82: 'r',  83: 's',  84: 't',  85: 'u', 86:  'v',
           87: 'w',  88: 'x',  89: 'y',  90: 'z', 107: '+', 109: '-', 110: '.',
          186: ';', 187: '=', 188: ',', 189: '-', 190: '.', 191: '/', 192: '`',
          219: '[', 220: '\\',221: ']', 222: "'", 111: '/', 106: '*'
        }
    };
    var name, i;
    for (i in ret.FUNCTION_KEYS) {
        name = ret.FUNCTION_KEYS[i].toLowerCase();
        ret[name] = parseInt(i, 10);
    }
    for (i in ret.PRINTABLE_KEYS) {
        name = ret.PRINTABLE_KEYS[i].toLowerCase();
        ret[name] = parseInt(i, 10);
    }
    oop.mixin(ret, ret.MODIFIER_KEYS);
    oop.mixin(ret, ret.PRINTABLE_KEYS);
    oop.mixin(ret, ret.FUNCTION_KEYS);
    ret.enter = ret["return"];
    ret.escape = ret.esc;
    ret.del = ret["delete"];
    ret[173] = '-';
    
    (function() {
        var mods = ["cmd", "ctrl", "alt", "shift"];
        for (var i = Math.pow(2, mods.length); i--;) {            
            ret.KEY_MODS[i] = mods.filter(function(x) {
                return i & ret.KEY_MODS[x];
            }).join("-") + "-";
        }
    })();

    ret.KEY_MODS[0] = "";
    ret.KEY_MODS[-1] = "input-";

    return ret;
})();
oop.mixin(exports, Keys);

exports.keyCodeToString = function(keyCode) {
    var keyString = Keys[keyCode];
    if (typeof keyString != "string")
        keyString = String.fromCharCode(keyCode);
    return keyString.toLowerCase();
};

});

define("ace/lib/useragent",["require","exports","module"], function(require, exports, module) {
"use strict";
exports.OS = {
    LINUX: "LINUX",
    MAC: "MAC",
    WINDOWS: "WINDOWS"
};
exports.getOS = function() {
    if (exports.isMac) {
        return exports.OS.MAC;
    } else if (exports.isLinux) {
        return exports.OS.LINUX;
    } else {
        return exports.OS.WINDOWS;
    }
};
if (typeof navigator != "object")
    return;

var os = (navigator.platform.match(/mac|win|linux/i) || ["other"])[0].toLowerCase();
var ua = navigator.userAgent;
exports.isWin = (os == "win");
exports.isMac = (os == "mac");
exports.isLinux = (os == "linux");
exports.isIE = 
    (navigator.appName == "Microsoft Internet Explorer" || navigator.appName.indexOf("MSAppHost") >= 0)
    ? parseFloat((ua.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1])
    : parseFloat((ua.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]); // for ie
    
exports.isOldIE = exports.isIE && exports.isIE < 9;
exports.isGecko = exports.isMozilla = (window.Controllers || window.controllers) && window.navigator.product === "Gecko";
exports.isOldGecko = exports.isGecko && parseInt((ua.match(/rv:(\d+)/)||[])[1], 10) < 4;
exports.isOpera = window.opera && Object.prototype.toString.call(window.opera) == "[object Opera]";
exports.isWebKit = parseFloat(ua.split("WebKit/")[1]) || undefined;

exports.isChrome = parseFloat(ua.split(" Chrome/")[1]) || undefined;

exports.isAIR = ua.indexOf("AdobeAIR") >= 0;

exports.isIPad = ua.indexOf("iPad") >= 0;

exports.isTouchPad = ua.indexOf("TouchPad") >= 0;

exports.isChromeOS = ua.indexOf(" CrOS ") >= 0;

});

define("ace/lib/event",["require","exports","module","ace/lib/keys","ace/lib/useragent"], function(require, exports, module) {
"use strict";

var keys = require("./keys");
var useragent = require("./useragent");

var pressedKeys = null;
var ts = 0;

exports.addListener = function(elem, type, callback) {
    if (elem.addEventListener) {
        return elem.addEventListener(type, callback, false);
    }
    if (elem.attachEvent) {
        var wrapper = function() {
            callback.call(elem, window.event);
        };
        callback._wrapper = wrapper;
        elem.attachEvent("on" + type, wrapper);
    }
};

exports.removeListener = function(elem, type, callback) {
    if (elem.removeEventListener) {
        return elem.removeEventListener(type, callback, false);
    }
    if (elem.detachEvent) {
        elem.detachEvent("on" + type, callback._wrapper || callback);
    }
};
exports.stopEvent = function(e) {
    exports.stopPropagation(e);
    exports.preventDefault(e);
    return false;
};

exports.stopPropagation = function(e) {
    if (e.stopPropagation)
        e.stopPropagation();
    else
        e.cancelBubble = true;
};

exports.preventDefault = function(e) {
    if (e.preventDefault)
        e.preventDefault();
    else
        e.returnValue = false;
};
exports.getButton = function(e) {
    if (e.type == "dblclick")
        return 0;
    if (e.type == "contextmenu" || (useragent.isMac && (e.ctrlKey && !e.altKey && !e.shiftKey)))
        return 2;
    if (e.preventDefault) {
        return e.button;
    }
    else {
        return {1:0, 2:2, 4:1}[e.button];
    }
};

exports.capture = function(el, eventHandler, releaseCaptureHandler) {
    function onMouseUp(e) {
        eventHandler && eventHandler(e);
        releaseCaptureHandler && releaseCaptureHandler(e);

        exports.removeListener(document, "mousemove", eventHandler, true);
        exports.removeListener(document, "mouseup", onMouseUp, true);
        exports.removeListener(document, "dragstart", onMouseUp, true);
    }

    exports.addListener(document, "mousemove", eventHandler, true);
    exports.addListener(document, "mouseup", onMouseUp, true);
    exports.addListener(document, "dragstart", onMouseUp, true);
    
    return onMouseUp;
};

exports.addTouchMoveListener = function (el, callback) {
    if ("ontouchmove" in el) {
        var startx, starty;
        exports.addListener(el, "touchstart", function (e) {
            var touchObj = e.changedTouches[0];
            startx = touchObj.clientX;
            starty = touchObj.clientY;
        });
        exports.addListener(el, "touchmove", function (e) {
            var factor = 1,
            touchObj = e.changedTouches[0];

            e.wheelX = -(touchObj.clientX - startx) / factor;
            e.wheelY = -(touchObj.clientY - starty) / factor;

            startx = touchObj.clientX;
            starty = touchObj.clientY;

            callback(e);
        });
    } 
};

exports.addMouseWheelListener = function(el, callback) {
    if ("onmousewheel" in el) {
        exports.addListener(el, "mousewheel", function(e) {
            var factor = 8;
            if (e.wheelDeltaX !== undefined) {
                e.wheelX = -e.wheelDeltaX / factor;
                e.wheelY = -e.wheelDeltaY / factor;
            } else {
                e.wheelX = 0;
                e.wheelY = -e.wheelDelta / factor;
            }
            callback(e);
        });
    } else if ("onwheel" in el) {
        exports.addListener(el, "wheel",  function(e) {
            var factor = 0.35;
            switch (e.deltaMode) {
                case e.DOM_DELTA_PIXEL:
                    e.wheelX = e.deltaX * factor || 0;
                    e.wheelY = e.deltaY * factor || 0;
                    break;
                case e.DOM_DELTA_LINE:
                case e.DOM_DELTA_PAGE:
                    e.wheelX = (e.deltaX || 0) * 5;
                    e.wheelY = (e.deltaY || 0) * 5;
                    break;
            }
            
            callback(e);
        });
    } else {
        exports.addListener(el, "DOMMouseScroll", function(e) {
            if (e.axis && e.axis == e.HORIZONTAL_AXIS) {
                e.wheelX = (e.detail || 0) * 5;
                e.wheelY = 0;
            } else {
                e.wheelX = 0;
                e.wheelY = (e.detail || 0) * 5;
            }
            callback(e);
        });
    }
};

exports.addMultiMouseDownListener = function(elements, timeouts, eventHandler, callbackName) {
    var clicks = 0;
    var startX, startY, timer; 
    var eventNames = {
        2: "dblclick",
        3: "tripleclick",
        4: "quadclick"
    };

    function onMousedown(e) {
        if (exports.getButton(e) !== 0) {
            clicks = 0;
        } else if (e.detail > 1) {
            clicks++;
            if (clicks > 4)
                clicks = 1;
        } else {
            clicks = 1;
        }
        if (useragent.isIE) {
            var isNewClick = Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5;
            if (!timer || isNewClick)
                clicks = 1;
            if (timer)
                clearTimeout(timer);
            timer = setTimeout(function() {timer = null}, timeouts[clicks - 1] || 600);

            if (clicks == 1) {
                startX = e.clientX;
                startY = e.clientY;
            }
        }
        
        e._clicks = clicks;

        eventHandler[callbackName]("mousedown", e);

        if (clicks > 4)
            clicks = 0;
        else if (clicks > 1)
            return eventHandler[callbackName](eventNames[clicks], e);
    }
    function onDblclick(e) {
        clicks = 2;
        if (timer)
            clearTimeout(timer);
        timer = setTimeout(function() {timer = null}, timeouts[clicks - 1] || 600);
        eventHandler[callbackName]("mousedown", e);
        eventHandler[callbackName](eventNames[clicks], e);
    }
    if (!Array.isArray(elements))
        elements = [elements];
    elements.forEach(function(el) {
        exports.addListener(el, "mousedown", onMousedown);
        if (useragent.isOldIE)
            exports.addListener(el, "dblclick", onDblclick);
    });
};

var getModifierHash = useragent.isMac && useragent.isOpera && !("KeyboardEvent" in window)
    ? function(e) {
        return 0 | (e.metaKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.ctrlKey ? 8 : 0);
    }
    : function(e) {
        return 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0);
    };

exports.getModifierString = function(e) {
    return keys.KEY_MODS[getModifierHash(e)];
};

function normalizeCommandKeys(callback, e, keyCode) {
    var hashId = getModifierHash(e);

    if (!useragent.isMac && pressedKeys) {
        if (e.getModifierState && (e.getModifierState("OS") || e.getModifierState("Win")))
            hashId |= 8;
        if (pressedKeys.altGr) {
            if ((3 & hashId) != 3)
                pressedKeys.altGr = 0;
            else
                return;
        }
        if (keyCode === 18 || keyCode === 17) {
            var location = "location" in e ? e.location : e.keyLocation;
            if (keyCode === 17 && location === 1) {
                if (pressedKeys[keyCode] == 1)
                    ts = e.timeStamp;
            } else if (keyCode === 18 && hashId === 3 && location === 2) {
                var dt = e.timeStamp - ts;
                if (dt < 50)
                    pressedKeys.altGr = true;
            }
        }
    }
    
    if (keyCode in keys.MODIFIER_KEYS) {
        keyCode = -1;
    }
    if (hashId & 8 && (keyCode >= 91 && keyCode <= 93)) {
        keyCode = -1;
    }
    
    if (!hashId && keyCode === 13) {
        var location = "location" in e ? e.location : e.keyLocation;
        if (location === 3) {
            callback(e, hashId, -keyCode);
            if (e.defaultPrevented)
                return;
        }
    }
    
    if (useragent.isChromeOS && hashId & 8) {
        callback(e, hashId, keyCode);
        if (e.defaultPrevented)
            return;
        else
            hashId &= ~8;
    }
    if (!hashId && !(keyCode in keys.FUNCTION_KEYS) && !(keyCode in keys.PRINTABLE_KEYS)) {
        return false;
    }
    
    return callback(e, hashId, keyCode);
}


exports.addCommandKeyListener = function(el, callback) {
    var addListener = exports.addListener;
    if (useragent.isOldGecko || (useragent.isOpera && !("KeyboardEvent" in window))) {
        var lastKeyDownKeyCode = null;
        addListener(el, "keydown", function(e) {
            lastKeyDownKeyCode = e.keyCode;
        });
        addListener(el, "keypress", function(e) {
            return normalizeCommandKeys(callback, e, lastKeyDownKeyCode);
        });
    } else {
        var lastDefaultPrevented = null;

        addListener(el, "keydown", function(e) {
            pressedKeys[e.keyCode] = (pressedKeys[e.keyCode] || 0) + 1;
            var result = normalizeCommandKeys(callback, e, e.keyCode);
            lastDefaultPrevented = e.defaultPrevented;
            return result;
        });

        addListener(el, "keypress", function(e) {
            if (lastDefaultPrevented && (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey)) {
                exports.stopEvent(e);
                lastDefaultPrevented = null;
            }
        });

        addListener(el, "keyup", function(e) {
            pressedKeys[e.keyCode] = null;
        });

        if (!pressedKeys) {
            resetPressedKeys();
            addListener(window, "focus", resetPressedKeys);
        }
    }
};
function resetPressedKeys() {
    pressedKeys = Object.create(null);
}

if (typeof window == "object" && window.postMessage && !useragent.isOldIE) {
    var postMessageId = 1;
    exports.nextTick = function(callback, win) {
        win = win || window;
        var messageName = "zero-timeout-message-" + postMessageId;
        exports.addListener(win, "message", function listener(e) {
            if (e.data == messageName) {
                exports.stopPropagation(e);
                exports.removeListener(win, "message", listener);
                callback();
            }
        });
        win.postMessage(messageName, "*");
    };
}


exports.nextFrame = typeof window == "object" && (window.requestAnimationFrame
    || window.mozRequestAnimationFrame
    || window.webkitRequestAnimationFrame
    || window.msRequestAnimationFrame
    || window.oRequestAnimationFrame);

if (exports.nextFrame)
    exports.nextFrame = exports.nextFrame.bind(window);
else
    exports.nextFrame = function(callback) {
        setTimeout(callback, 17);
    };
});

define("ace/lib/lang",["require","exports","module"], function(require, exports, module) {
"use strict";

exports.last = function(a) {
    return a[a.length - 1];
};

exports.stringReverse = function(string) {
    return string.split("").reverse().join("");
};

exports.stringRepeat = function (string, count) {
    var result = '';
    while (count > 0) {
        if (count & 1)
            result += string;

        if (count >>= 1)
            string += string;
    }
    return result;
};

var trimBeginRegexp = /^\s\s*/;
var trimEndRegexp = /\s\s*$/;

exports.stringTrimLeft = function (string) {
    return string.replace(trimBeginRegexp, '');
};

exports.stringTrimRight = function (string) {
    return string.replace(trimEndRegexp, '');
};

exports.copyObject = function(obj) {
    var copy = {};
    for (var key in obj) {
        copy[key] = obj[key];
    }
    return copy;
};

exports.copyArray = function(array){
    var copy = [];
    for (var i=0, l=array.length; i= 53) {
          onInput();
        }
    };
    
    

    var syncComposition = lang.delayedCall(onCompositionUpdate, 50);

    event.addListener(text, "compositionstart", onCompositionStart);
    if (useragent.isGecko) {
        event.addListener(text, "text", function(){syncComposition.schedule()});
    } else {
        event.addListener(text, "keyup", function(){syncComposition.schedule()});
        event.addListener(text, "keydown", function(){syncComposition.schedule()});
    }
    event.addListener(text, "compositionend", onCompositionEnd);

    this.getElement = function() {
        return text;
    };

    this.setReadOnly = function(readOnly) {
       text.readOnly = readOnly;
    };

    this.onContextMenu = function(e) {
        afterContextMenu = true;
        resetSelection(host.selection.isEmpty());
        host._emit("nativecontextmenu", {target: host, domEvent: e});
        this.moveToMouse(e, true);
    };
    
    this.moveToMouse = function(e, bringToFront) {
        if (!bringToFront && useragent.isOldIE)
            return;
        if (!tempStyle)
            tempStyle = text.style.cssText;
        text.style.cssText = (bringToFront ? "z-index:100000;" : "")
            + "height:" + text.style.height + ";"
            + (useragent.isIE ? "opacity:0.1;" : "");

        var rect = host.container.getBoundingClientRect();
        var style = dom.computedStyle(host.container);
        var top = rect.top + (parseInt(style.borderTopWidth) || 0);
        var left = rect.left + (parseInt(rect.borderLeftWidth) || 0);
        var maxTop = rect.bottom - top - text.clientHeight -2;
        var move = function(e) {
            text.style.left = e.clientX - left - 2 + "px";
            text.style.top = Math.min(e.clientY - top - 2, maxTop) + "px";
        }; 
        move(e);

        if (e.type != "mousedown")
            return;

        if (host.renderer.$keepTextAreaAtCursor)
            host.renderer.$keepTextAreaAtCursor = null;

        clearTimeout(closeTimeout);
        if (useragent.isWin && !useragent.isOldIE)
            event.capture(host.container, move, onContextMenuClose);
    };

    this.onContextMenuClose = onContextMenuClose;
    var closeTimeout;
    function onContextMenuClose() {
        clearTimeout(closeTimeout);
        closeTimeout = setTimeout(function () {
            if (tempStyle) {
                text.style.cssText = tempStyle;
                tempStyle = '';
            }
            if (host.renderer.$keepTextAreaAtCursor == null) {
                host.renderer.$keepTextAreaAtCursor = true;
                host.renderer.$moveTextAreaToCursor();
            }
        }, useragent.isOldIE ? 200 : 0);
    }

    var onContextMenu = function(e) {
        host.textInput.onContextMenu(e);
        onContextMenuClose();
    };
    event.addListener(text, "mouseup", onContextMenu);
    event.addListener(text, "mousedown", function(e) {
        e.preventDefault();
        onContextMenuClose();
    });
    event.addListener(host.renderer.scroller, "contextmenu", onContextMenu);
    event.addListener(text, "contextmenu", onContextMenu);
};

exports.TextInput = TextInput;
});

define("ace/mouse/default_handlers",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"], function(require, exports, module) {
"use strict";

var dom = require("../lib/dom");
var event = require("../lib/event");
var useragent = require("../lib/useragent");

var DRAG_OFFSET = 0; // pixels

function DefaultHandlers(mouseHandler) {
    mouseHandler.$clickSelection = null;

    var editor = mouseHandler.editor;
    editor.setDefaultHandler("mousedown", this.onMouseDown.bind(mouseHandler));
    editor.setDefaultHandler("dblclick", this.onDoubleClick.bind(mouseHandler));
    editor.setDefaultHandler("tripleclick", this.onTripleClick.bind(mouseHandler));
    editor.setDefaultHandler("quadclick", this.onQuadClick.bind(mouseHandler));
    editor.setDefaultHandler("mousewheel", this.onMouseWheel.bind(mouseHandler));
    editor.setDefaultHandler("touchmove", this.onTouchMove.bind(mouseHandler));

    var exports = ["select", "startSelect", "selectEnd", "selectAllEnd", "selectByWordsEnd",
        "selectByLinesEnd", "dragWait", "dragWaitEnd", "focusWait"];

    exports.forEach(function(x) {
        mouseHandler[x] = this[x];
    }, this);

    mouseHandler.selectByLines = this.extendSelectionBy.bind(mouseHandler, "getLineRange");
    mouseHandler.selectByWords = this.extendSelectionBy.bind(mouseHandler, "getWordRange");
}

(function() {

    this.onMouseDown = function(ev) {
        var inSelection = ev.inSelection();
        var pos = ev.getDocumentPosition();
        this.mousedownEvent = ev;
        var editor = this.editor;

        var button = ev.getButton();
        if (button !== 0) {
            var selectionRange = editor.getSelectionRange();
            var selectionEmpty = selectionRange.isEmpty();
            editor.$blockScrolling++;
            if (selectionEmpty || button == 1)
                editor.selection.moveToPosition(pos);
            editor.$blockScrolling--;
            if (button == 2)
                editor.textInput.onContextMenu(ev.domEvent);
            return; // stopping event here breaks contextmenu on ff mac
        }

        this.mousedownEvent.time = Date.now();
        if (inSelection && !editor.isFocused()) {
            editor.focus();
            if (this.$focusTimout && !this.$clickSelection && !editor.inMultiSelectMode) {
                this.setState("focusWait");
                this.captureMouse(ev);
                return;
            }
        }

        this.captureMouse(ev);
        this.startSelect(pos, ev.domEvent._clicks > 1);
        return ev.preventDefault();
    };

    this.startSelect = function(pos, waitForClickSelection) {
        pos = pos || this.editor.renderer.screenToTextCoordinates(this.x, this.y);
        var editor = this.editor;
        editor.$blockScrolling++;
        if (this.mousedownEvent.getShiftKey())
            editor.selection.selectToPosition(pos);
        else if (!waitForClickSelection)
            editor.selection.moveToPosition(pos);
        if (!waitForClickSelection)
            this.select();
        if (editor.renderer.scroller.setCapture) {
            editor.renderer.scroller.setCapture();
        }
        editor.setStyle("ace_selecting");
        this.setState("select");
        editor.$blockScrolling--;
    };

    this.select = function() {
        var anchor, editor = this.editor;
        var cursor = editor.renderer.screenToTextCoordinates(this.x, this.y);
        editor.$blockScrolling++;
        if (this.$clickSelection) {
            var cmp = this.$clickSelection.comparePoint(cursor);

            if (cmp == -1) {
                anchor = this.$clickSelection.end;
            } else if (cmp == 1) {
                anchor = this.$clickSelection.start;
            } else {
                var orientedRange = calcRangeOrientation(this.$clickSelection, cursor);
                cursor = orientedRange.cursor;
                anchor = orientedRange.anchor;
            }
            editor.selection.setSelectionAnchor(anchor.row, anchor.column);
        }
        editor.selection.selectToPosition(cursor);
        editor.$blockScrolling--;
        editor.renderer.scrollCursorIntoView();
    };

    this.extendSelectionBy = function(unitName) {
        var anchor, editor = this.editor;
        var cursor = editor.renderer.screenToTextCoordinates(this.x, this.y);
        var range = editor.selection[unitName](cursor.row, cursor.column);
        editor.$blockScrolling++;
        if (this.$clickSelection) {
            var cmpStart = this.$clickSelection.comparePoint(range.start);
            var cmpEnd = this.$clickSelection.comparePoint(range.end);

            if (cmpStart == -1 && cmpEnd <= 0) {
                anchor = this.$clickSelection.end;
                if (range.end.row != cursor.row || range.end.column != cursor.column)
                    cursor = range.start;
            } else if (cmpEnd == 1 && cmpStart >= 0) {
                anchor = this.$clickSelection.start;
                if (range.start.row != cursor.row || range.start.column != cursor.column)
                    cursor = range.end;
            } else if (cmpStart == -1 && cmpEnd == 1) {
                cursor = range.end;
                anchor = range.start;
            } else {
                var orientedRange = calcRangeOrientation(this.$clickSelection, cursor);
                cursor = orientedRange.cursor;
                anchor = orientedRange.anchor;
            }
            editor.selection.setSelectionAnchor(anchor.row, anchor.column);
        }
        editor.selection.selectToPosition(cursor);
        editor.$blockScrolling--;
        editor.renderer.scrollCursorIntoView();
    };

    this.selectEnd =
    this.selectAllEnd =
    this.selectByWordsEnd =
    this.selectByLinesEnd = function() {
        this.$clickSelection = null;
        this.editor.unsetStyle("ace_selecting");
        if (this.editor.renderer.scroller.releaseCapture) {
            this.editor.renderer.scroller.releaseCapture();
        }
    };

    this.focusWait = function() {
        var distance = calcDistance(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y);
        var time = Date.now();

        if (distance > DRAG_OFFSET || time - this.mousedownEvent.time > this.$focusTimout)
            this.startSelect(this.mousedownEvent.getDocumentPosition());
    };

    this.onDoubleClick = function(ev) {
        var pos = ev.getDocumentPosition();
        var editor = this.editor;
        var session = editor.session;

        var range = session.getBracketRange(pos);
        if (range) {
            if (range.isEmpty()) {
                range.start.column--;
                range.end.column++;
            }
            this.setState("select");
        } else {
            range = editor.selection.getWordRange(pos.row, pos.column);
            this.setState("selectByWords");
        }
        this.$clickSelection = range;
        this.select();
    };

    this.onTripleClick = function(ev) {
        var pos = ev.getDocumentPosition();
        var editor = this.editor;

        this.setState("selectByLines");
        var range = editor.getSelectionRange();
        if (range.isMultiLine() && range.contains(pos.row, pos.column)) {
            this.$clickSelection = editor.selection.getLineRange(range.start.row);
            this.$clickSelection.end = editor.selection.getLineRange(range.end.row).end;
        } else {
            this.$clickSelection = editor.selection.getLineRange(pos.row);
        }
        this.select();
    };

    this.onQuadClick = function(ev) {
        var editor = this.editor;

        editor.selectAll();
        this.$clickSelection = editor.getSelectionRange();
        this.setState("selectAll");
    };

    this.onMouseWheel = function(ev) {
        if (ev.getAccelKey())
            return;
        if (ev.getShiftKey() && ev.wheelY && !ev.wheelX) {
            ev.wheelX = ev.wheelY;
            ev.wheelY = 0;
        }

        var t = ev.domEvent.timeStamp;
        var dt = t - (this.$lastScrollTime||0);
        
        var editor = this.editor;
        var isScrolable = editor.renderer.isScrollableBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed);
        if (isScrolable || dt < 200) {
            this.$lastScrollTime = t;
            editor.renderer.scrollBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed);
            return ev.stop();
        }
    };
    
    this.onTouchMove = function (ev) {
        var t = ev.domEvent.timeStamp;
        var dt = t - (this.$lastScrollTime || 0);

        var editor = this.editor;
        var isScrolable = editor.renderer.isScrollableBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed);
        if (isScrolable || dt < 200) {
            this.$lastScrollTime = t;
            editor.renderer.scrollBy(ev.wheelX * ev.speed, ev.wheelY * ev.speed);
            return ev.stop();
        }
    };

}).call(DefaultHandlers.prototype);

exports.DefaultHandlers = DefaultHandlers;

function calcDistance(ax, ay, bx, by) {
    return Math.sqrt(Math.pow(bx - ax, 2) + Math.pow(by - ay, 2));
}

function calcRangeOrientation(range, cursor) {
    if (range.start.row == range.end.row)
        var cmp = 2 * cursor.column - range.start.column - range.end.column;
    else if (range.start.row == range.end.row - 1 && !range.start.column && !range.end.column)
        var cmp = cursor.column - 4;
    else
        var cmp = 2 * cursor.row - range.start.row - range.end.row;

    if (cmp < 0)
        return {cursor: range.start, anchor: range.end};
    else
        return {cursor: range.end, anchor: range.start};
}

});

define("ace/tooltip",["require","exports","module","ace/lib/oop","ace/lib/dom"], function(require, exports, module) {
"use strict";

var oop = require("./lib/oop");
var dom = require("./lib/dom");
function Tooltip (parentNode) {
    this.isOpen = false;
    this.$element = null;
    this.$parentNode = parentNode;
}

(function() {
    this.$init = function() {
        this.$element = dom.createElement("div");
        this.$element.className = "ace_tooltip";
        this.$element.style.display = "none";
        this.$parentNode.appendChild(this.$element);
        return this.$element;
    };
    this.getElement = function() {
        return this.$element || this.$init();
    };
    this.setText = function(text) {
        dom.setInnerText(this.getElement(), text);
    };
    this.setHtml = function(html) {
        this.getElement().innerHTML = html;
    };
    this.setPosition = function(x, y) {
        this.getElement().style.left = x + "px";
        this.getElement().style.top = y + "px";
    };
    this.setClassName = function(className) {
        dom.addCssClass(this.getElement(), className);
    };
    this.show = function(text, x, y) {
        if (text != null)
            this.setText(text);
        if (x != null && y != null)
            this.setPosition(x, y);
        if (!this.isOpen) {
            this.getElement().style.display = "block";
            this.isOpen = true;
        }
    };

    this.hide = function() {
        if (this.isOpen) {
            this.getElement().style.display = "none";
            this.isOpen = false;
        }
    };
    this.getHeight = function() {
        return this.getElement().offsetHeight;
    };
    this.getWidth = function() {
        return this.getElement().offsetWidth;
    };

}).call(Tooltip.prototype);

exports.Tooltip = Tooltip;
});

define("ace/mouse/default_gutter_handler",["require","exports","module","ace/lib/dom","ace/lib/oop","ace/lib/event","ace/tooltip"], function(require, exports, module) {
"use strict";
var dom = require("../lib/dom");
var oop = require("../lib/oop");
var event = require("../lib/event");
var Tooltip = require("../tooltip").Tooltip;

function GutterHandler(mouseHandler) {
    var editor = mouseHandler.editor;
    var gutter = editor.renderer.$gutterLayer;
    var tooltip = new GutterTooltip(editor.container);

    mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) {
        if (!editor.isFocused() || e.getButton() != 0)
            return;
        var gutterRegion = gutter.getRegion(e);

        if (gutterRegion == "foldWidgets")
            return;

        var row = e.getDocumentPosition().row;
        var selection = editor.session.selection;

        if (e.getShiftKey())
            selection.selectTo(row, 0);
        else {
            if (e.domEvent.detail == 2) {
                editor.selectAll();
                return e.preventDefault();
            }
            mouseHandler.$clickSelection = editor.selection.getLineRange(row);
        }
        mouseHandler.setState("selectByLines");
        mouseHandler.captureMouse(e);
        return e.preventDefault();
    });


    var tooltipTimeout, mouseEvent, tooltipAnnotation;

    function showTooltip() {
        var row = mouseEvent.getDocumentPosition().row;
        var annotation = gutter.$annotations[row];
        if (!annotation)
            return hideTooltip();

        var maxRow = editor.session.getLength();
        if (row == maxRow) {
            var screenRow = editor.renderer.pixelToScreenCoordinates(0, mouseEvent.y).row;
            var pos = mouseEvent.$pos;
            if (screenRow > editor.session.documentToScreenRow(pos.row, pos.column))
                return hideTooltip();
        }

        if (tooltipAnnotation == annotation)
            return;
        tooltipAnnotation = annotation.text.join("
"); tooltip.setHtml(tooltipAnnotation); tooltip.show(); editor._signal("showGutterTooltip", tooltip); editor.on("mousewheel", hideTooltip); if (mouseHandler.$tooltipFollowsMouse) { moveTooltip(mouseEvent); } else { var gutterElement = mouseEvent.domEvent.target; var rect = gutterElement.getBoundingClientRect(); var style = tooltip.getElement().style; style.left = rect.right + "px"; style.top = rect.bottom + "px"; } } function hideTooltip() { if (tooltipTimeout) tooltipTimeout = clearTimeout(tooltipTimeout); if (tooltipAnnotation) { tooltip.hide(); tooltipAnnotation = null; editor._signal("hideGutterTooltip", tooltip); editor.removeEventListener("mousewheel", hideTooltip); } } function moveTooltip(e) { tooltip.setPosition(e.x, e.y); } mouseHandler.editor.setDefaultHandler("guttermousemove", function(e) { var target = e.domEvent.target || e.domEvent.srcElement; if (dom.hasCssClass(target, "ace_fold-widget")) return hideTooltip(); if (tooltipAnnotation && mouseHandler.$tooltipFollowsMouse) moveTooltip(e); mouseEvent = e; if (tooltipTimeout) return; tooltipTimeout = setTimeout(function() { tooltipTimeout = null; if (mouseEvent && !mouseHandler.isMousePressed) showTooltip(); else hideTooltip(); }, 50); }); event.addListener(editor.renderer.$gutter, "mouseout", function(e) { mouseEvent = null; if (!tooltipAnnotation || tooltipTimeout) return; tooltipTimeout = setTimeout(function() { tooltipTimeout = null; hideTooltip(); }, 50); }); editor.on("changeSession", hideTooltip); } function GutterTooltip(parentNode) { Tooltip.call(this, parentNode); } oop.inherits(GutterTooltip, Tooltip); (function(){ this.setPosition = function(x, y) { var windowWidth = window.innerWidth || document.documentElement.clientWidth; var windowHeight = window.innerHeight || document.documentElement.clientHeight; var width = this.getWidth(); var height = this.getHeight(); x += 15; y += 15; if (x + width > windowWidth) { x -= (x + width) - windowWidth; } if (y + height > windowHeight) { y -= 20 + height; } Tooltip.prototype.setPosition.call(this, x, y); }; }).call(GutterTooltip.prototype); exports.GutterHandler = GutterHandler; }); define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"], function(require, exports, module) { "use strict"; var event = require("../lib/event"); var useragent = require("../lib/useragent"); var MouseEvent = exports.MouseEvent = function(domEvent, editor) { this.domEvent = domEvent; this.editor = editor; this.x = this.clientX = domEvent.clientX; this.y = this.clientY = domEvent.clientY; this.$pos = null; this.$inSelection = null; this.propagationStopped = false; this.defaultPrevented = false; }; (function() { this.stopPropagation = function() { event.stopPropagation(this.domEvent); this.propagationStopped = true; }; this.preventDefault = function() { event.preventDefault(this.domEvent); this.defaultPrevented = true; }; this.stop = function() { this.stopPropagation(); this.preventDefault(); }; this.getDocumentPosition = function() { if (this.$pos) return this.$pos; this.$pos = this.editor.renderer.screenToTextCoordinates(this.clientX, this.clientY); return this.$pos; }; this.inSelection = function() { if (this.$inSelection !== null) return this.$inSelection; var editor = this.editor; var selectionRange = editor.getSelectionRange(); if (selectionRange.isEmpty()) this.$inSelection = false; else { var pos = this.getDocumentPosition(); this.$inSelection = selectionRange.contains(pos.row, pos.column); } return this.$inSelection; }; this.getButton = function() { return event.getButton(this.domEvent); }; this.getShiftKey = function() { return this.domEvent.shiftKey; }; this.getAccelKey = useragent.isMac ? function() { return this.domEvent.metaKey; } : function() { return this.domEvent.ctrlKey; }; }).call(MouseEvent.prototype); }); define("ace/mouse/dragdrop_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"], function(require, exports, module) { "use strict"; var dom = require("../lib/dom"); var event = require("../lib/event"); var useragent = require("../lib/useragent"); var AUTOSCROLL_DELAY = 200; var SCROLL_CURSOR_DELAY = 200; var SCROLL_CURSOR_HYSTERESIS = 5; function DragdropHandler(mouseHandler) { var editor = mouseHandler.editor; var blankImage = dom.createElement("img"); blankImage.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; if (useragent.isOpera) blankImage.style.cssText = "width:1px;height:1px;position:fixed;top:0;left:0;z-index:2147483647;opacity:0;"; var exports = ["dragWait", "dragWaitEnd", "startDrag", "dragReadyEnd", "onMouseDrag"]; exports.forEach(function(x) { mouseHandler[x] = this[x]; }, this); editor.addEventListener("mousedown", this.onMouseDown.bind(mouseHandler)); var mouseTarget = editor.container; var dragSelectionMarker, x, y; var timerId, range; var dragCursor, counter = 0; var dragOperation; var isInternal; var autoScrollStartTime; var cursorMovedTime; var cursorPointOnCaretMoved; this.onDragStart = function(e) { if (this.cancelDrag || !mouseTarget.draggable) { var self = this; setTimeout(function(){ self.startSelect(); self.captureMouse(e); }, 0); return e.preventDefault(); } range = editor.getSelectionRange(); var dataTransfer = e.dataTransfer; dataTransfer.effectAllowed = editor.getReadOnly() ? "copy" : "copyMove"; if (useragent.isOpera) { editor.container.appendChild(blankImage); blankImage.scrollTop = 0; } dataTransfer.setDragImage && dataTransfer.setDragImage(blankImage, 0, 0); if (useragent.isOpera) { editor.container.removeChild(blankImage); } dataTransfer.clearData(); dataTransfer.setData("Text", editor.session.getTextRange()); isInternal = true; this.setState("drag"); }; this.onDragEnd = function(e) { mouseTarget.draggable = false; isInternal = false; this.setState(null); if (!editor.getReadOnly()) { var dropEffect = e.dataTransfer.dropEffect; if (!dragOperation && dropEffect == "move") editor.session.remove(editor.getSelectionRange()); editor.renderer.$cursorLayer.setBlinking(true); } this.editor.unsetStyle("ace_dragging"); this.editor.renderer.setCursorStyle(""); }; this.onDragEnter = function(e) { if (editor.getReadOnly() || !canAccept(e.dataTransfer)) return; x = e.clientX; y = e.clientY; if (!dragSelectionMarker) addDragMarker(); counter++; e.dataTransfer.dropEffect = dragOperation = getDropEffect(e); return event.preventDefault(e); }; this.onDragOver = function(e) { if (editor.getReadOnly() || !canAccept(e.dataTransfer)) return; x = e.clientX; y = e.clientY; if (!dragSelectionMarker) { addDragMarker(); counter++; } if (onMouseMoveTimer !== null) onMouseMoveTimer = null; e.dataTransfer.dropEffect = dragOperation = getDropEffect(e); return event.preventDefault(e); }; this.onDragLeave = function(e) { counter--; if (counter <= 0 && dragSelectionMarker) { clearDragMarker(); dragOperation = null; return event.preventDefault(e); } }; this.onDrop = function(e) { if (!dragCursor) return; var dataTransfer = e.dataTransfer; if (isInternal) { switch (dragOperation) { case "move": if (range.contains(dragCursor.row, dragCursor.column)) { range = { start: dragCursor, end: dragCursor }; } else { range = editor.moveText(range, dragCursor); } break; case "copy": range = editor.moveText(range, dragCursor, true); break; } } else { var dropData = dataTransfer.getData('Text'); range = { start: dragCursor, end: editor.session.insert(dragCursor, dropData) }; editor.focus(); dragOperation = null; } clearDragMarker(); return event.preventDefault(e); }; event.addListener(mouseTarget, "dragstart", this.onDragStart.bind(mouseHandler)); event.addListener(mouseTarget, "dragend", this.onDragEnd.bind(mouseHandler)); event.addListener(mouseTarget, "dragenter", this.onDragEnter.bind(mouseHandler)); event.addListener(mouseTarget, "dragover", this.onDragOver.bind(mouseHandler)); event.addListener(mouseTarget, "dragleave", this.onDragLeave.bind(mouseHandler)); event.addListener(mouseTarget, "drop", this.onDrop.bind(mouseHandler)); function scrollCursorIntoView(cursor, prevCursor) { var now = Date.now(); var vMovement = !prevCursor || cursor.row != prevCursor.row; var hMovement = !prevCursor || cursor.column != prevCursor.column; if (!cursorMovedTime || vMovement || hMovement) { editor.$blockScrolling += 1; editor.moveCursorToPosition(cursor); editor.$blockScrolling -= 1; cursorMovedTime = now; cursorPointOnCaretMoved = {x: x, y: y}; } else { var distance = calcDistance(cursorPointOnCaretMoved.x, cursorPointOnCaretMoved.y, x, y); if (distance > SCROLL_CURSOR_HYSTERESIS) { cursorMovedTime = null; } else if (now - cursorMovedTime >= SCROLL_CURSOR_DELAY) { editor.renderer.scrollCursorIntoView(); cursorMovedTime = null; } } } function autoScroll(cursor, prevCursor) { var now = Date.now(); var lineHeight = editor.renderer.layerConfig.lineHeight; var characterWidth = editor.renderer.layerConfig.characterWidth; var editorRect = editor.renderer.scroller.getBoundingClientRect(); var offsets = { x: { left: x - editorRect.left, right: editorRect.right - x }, y: { top: y - editorRect.top, bottom: editorRect.bottom - y } }; var nearestXOffset = Math.min(offsets.x.left, offsets.x.right); var nearestYOffset = Math.min(offsets.y.top, offsets.y.bottom); var scrollCursor = {row: cursor.row, column: cursor.column}; if (nearestXOffset / characterWidth <= 2) { scrollCursor.column += (offsets.x.left < offsets.x.right ? -3 : +2); } if (nearestYOffset / lineHeight <= 1) { scrollCursor.row += (offsets.y.top < offsets.y.bottom ? -1 : +1); } var vScroll = cursor.row != scrollCursor.row; var hScroll = cursor.column != scrollCursor.column; var vMovement = !prevCursor || cursor.row != prevCursor.row; if (vScroll || (hScroll && !vMovement)) { if (!autoScrollStartTime) autoScrollStartTime = now; else if (now - autoScrollStartTime >= AUTOSCROLL_DELAY) editor.renderer.scrollCursorIntoView(scrollCursor); } else { autoScrollStartTime = null; } } function onDragInterval() { var prevCursor = dragCursor; dragCursor = editor.renderer.screenToTextCoordinates(x, y); scrollCursorIntoView(dragCursor, prevCursor); autoScroll(dragCursor, prevCursor); } function addDragMarker() { range = editor.selection.toOrientedRange(); dragSelectionMarker = editor.session.addMarker(range, "ace_selection", editor.getSelectionStyle()); editor.clearSelection(); if (editor.isFocused()) editor.renderer.$cursorLayer.setBlinking(false); clearInterval(timerId); onDragInterval(); timerId = setInterval(onDragInterval, 20); counter = 0; event.addListener(document, "mousemove", onMouseMove); } function clearDragMarker() { clearInterval(timerId); editor.session.removeMarker(dragSelectionMarker); dragSelectionMarker = null; editor.$blockScrolling += 1; editor.selection.fromOrientedRange(range); editor.$blockScrolling -= 1; if (editor.isFocused() && !isInternal) editor.renderer.$cursorLayer.setBlinking(!editor.getReadOnly()); range = null; dragCursor = null; counter = 0; autoScrollStartTime = null; cursorMovedTime = null; event.removeListener(document, "mousemove", onMouseMove); } var onMouseMoveTimer = null; function onMouseMove() { if (onMouseMoveTimer == null) { onMouseMoveTimer = setTimeout(function() { if (onMouseMoveTimer != null && dragSelectionMarker) clearDragMarker(); }, 20); } } function canAccept(dataTransfer) { var types = dataTransfer.types; return !types || Array.prototype.some.call(types, function(type) { return type == 'text/plain' || type == 'Text'; }); } function getDropEffect(e) { var copyAllowed = ['copy', 'copymove', 'all', 'uninitialized']; var moveAllowed = ['move', 'copymove', 'linkmove', 'all', 'uninitialized']; var copyModifierState = useragent.isMac ? e.altKey : e.ctrlKey; var effectAllowed = "uninitialized"; try { effectAllowed = e.dataTransfer.effectAllowed.toLowerCase(); } catch (e) {} var dropEffect = "none"; if (copyModifierState && copyAllowed.indexOf(effectAllowed) >= 0) dropEffect = "copy"; else if (moveAllowed.indexOf(effectAllowed) >= 0) dropEffect = "move"; else if (copyAllowed.indexOf(effectAllowed) >= 0) dropEffect = "copy"; return dropEffect; } } (function() { this.dragWait = function() { var interval = Date.now() - this.mousedownEvent.time; if (interval > this.editor.getDragDelay()) this.startDrag(); }; this.dragWaitEnd = function() { var target = this.editor.container; target.draggable = false; this.startSelect(this.mousedownEvent.getDocumentPosition()); this.selectEnd(); }; this.dragReadyEnd = function(e) { this.editor.renderer.$cursorLayer.setBlinking(!this.editor.getReadOnly()); this.editor.unsetStyle("ace_dragging"); this.editor.renderer.setCursorStyle(""); this.dragWaitEnd(); }; this.startDrag = function(){ this.cancelDrag = false; var editor = this.editor; var target = editor.container; target.draggable = true; editor.renderer.$cursorLayer.setBlinking(false); editor.setStyle("ace_dragging"); var cursorStyle = useragent.isWin ? "default" : "move"; editor.renderer.setCursorStyle(cursorStyle); this.setState("dragReady"); }; this.onMouseDrag = function(e) { var target = this.editor.container; if (useragent.isIE && this.state == "dragReady") { var distance = calcDistance(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y); if (distance > 3) target.dragDrop(); } if (this.state === "dragWait") { var distance = calcDistance(this.mousedownEvent.x, this.mousedownEvent.y, this.x, this.y); if (distance > 0) { target.draggable = false; this.startSelect(this.mousedownEvent.getDocumentPosition()); } } }; this.onMouseDown = function(e) { if (!this.$dragEnabled) return; this.mousedownEvent = e; var editor = this.editor; var inSelection = e.inSelection(); var button = e.getButton(); var clickCount = e.domEvent.detail || 1; if (clickCount === 1 && button === 0 && inSelection) { if (e.editor.inMultiSelectMode && (e.getAccelKey() || e.getShiftKey())) return; this.mousedownEvent.time = Date.now(); var eventTarget = e.domEvent.target || e.domEvent.srcElement; if ("unselectable" in eventTarget) eventTarget.unselectable = "on"; if (editor.getDragDelay()) { if (useragent.isWebKit) { this.cancelDrag = true; var mouseTarget = editor.container; mouseTarget.draggable = true; } this.setState("dragWait"); } else { this.startDrag(); } this.captureMouse(e, this.onMouseDrag.bind(this)); e.defaultPrevented = true; } }; }).call(DragdropHandler.prototype); function calcDistance(ax, ay, bx, by) { return Math.sqrt(Math.pow(bx - ax, 2) + Math.pow(by - ay, 2)); } exports.DragdropHandler = DragdropHandler; }); define("ace/lib/net",["require","exports","module","ace/lib/dom"], function(require, exports, module) { "use strict"; var dom = require("./dom"); exports.get = function (url, callback) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { callback(xhr.responseText); } }; xhr.send(null); }; exports.loadScript = function(path, callback) { var head = dom.getDocumentHead(); var s = document.createElement('script'); s.src = path; head.appendChild(s); s.onload = s.onreadystatechange = function(_, isAbort) { if (isAbort || !s.readyState || s.readyState == "loaded" || s.readyState == "complete") { s = s.onload = s.onreadystatechange = null; if (!isAbort) callback(); } }; }; exports.qualifyURL = function(url) { var a = document.createElement('a'); a.href = url; return a.href; } }); define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) { "use strict"; var EventEmitter = {}; var stopPropagation = function() { this.propagationStopped = true; }; var preventDefault = function() { this.defaultPrevented = true; }; EventEmitter._emit = EventEmitter._dispatchEvent = function(eventName, e) { this._eventRegistry || (this._eventRegistry = {}); this._defaultHandlers || (this._defaultHandlers = {}); var listeners = this._eventRegistry[eventName] || []; var defaultHandler = this._defaultHandlers[eventName]; if (!listeners.length && !defaultHandler) return; if (typeof e != "object" || !e) e = {}; if (!e.type) e.type = eventName; if (!e.stopPropagation) e.stopPropagation = stopPropagation; if (!e.preventDefault) e.preventDefault = preventDefault; listeners = listeners.slice(); for (var i=0; i 1) base = parts[parts.length - 2]; var path = options[component + "Path"]; if (path == null) { path = options.basePath; } else if (sep == "/") { component = sep = ""; } if (path && path.slice(-1) != "/") path += "/"; return path + component + sep + base + this.get("suffix"); }; exports.setModuleUrl = function(name, subst) { return options.$moduleUrls[name] = subst; }; exports.$loading = {}; exports.loadModule = function(moduleName, onLoad) { var module, moduleType; if (Array.isArray(moduleName)) { moduleType = moduleName[0]; moduleName = moduleName[1]; } try { module = require(moduleName); } catch (e) {} if (module && !exports.$loading[moduleName]) return onLoad && onLoad(module); if (!exports.$loading[moduleName]) exports.$loading[moduleName] = []; exports.$loading[moduleName].push(onLoad); if (exports.$loading[moduleName].length > 1) return; var afterLoad = function() { require([moduleName], function(module) { exports._emit("load.module", {name: moduleName, module: module}); var listeners = exports.$loading[moduleName]; exports.$loading[moduleName] = null; listeners.forEach(function(onLoad) { onLoad && onLoad(module); }); }); }; if (!exports.get("packaged")) return afterLoad(); net.loadScript(exports.moduleUrl(moduleName, moduleType), afterLoad); }; init(true);function init(packaged) { if (!global || !global.document) return; options.packaged = packaged || require.packaged || module.packaged || (global.define && define.packaged); var scriptOptions = {}; var scriptUrl = ""; var currentScript = (document.currentScript || document._currentScript ); // native or polyfill var currentDocument = currentScript && currentScript.ownerDocument || document; var scripts = currentDocument.getElementsByTagName("script"); for (var i=0; i [" + this.end.row + "/" + this.end.column + "]"); }; this.contains = function(row, column) { return this.compare(row, column) == 0; }; this.compareRange = function(range) { var cmp, end = range.end, start = range.start; cmp = this.compare(end.row, end.column); if (cmp == 1) { cmp = this.compare(start.row, start.column); if (cmp == 1) { return 2; } else if (cmp == 0) { return 1; } else { return 0; } } else if (cmp == -1) { return -2; } else { cmp = this.compare(start.row, start.column); if (cmp == -1) { return -1; } else if (cmp == 1) { return 42; } else { return 0; } } }; this.comparePoint = function(p) { return this.compare(p.row, p.column); }; this.containsRange = function(range) { return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0; }; this.intersects = function(range) { var cmp = this.compareRange(range); return (cmp == -1 || cmp == 0 || cmp == 1); }; this.isEnd = function(row, column) { return this.end.row == row && this.end.column == column; }; this.isStart = function(row, column) { return this.start.row == row && this.start.column == column; }; this.setStart = function(row, column) { if (typeof row == "object") { this.start.column = row.column; this.start.row = row.row; } else { this.start.row = row; this.start.column = column; } }; this.setEnd = function(row, column) { if (typeof row == "object") { this.end.column = row.column; this.end.row = row.row; } else { this.end.row = row; this.end.column = column; } }; this.inside = function(row, column) { if (this.compare(row, column) == 0) { if (this.isEnd(row, column) || this.isStart(row, column)) { return false; } else { return true; } } return false; }; this.insideStart = function(row, column) { if (this.compare(row, column) == 0) { if (this.isEnd(row, column)) { return false; } else { return true; } } return false; }; this.insideEnd = function(row, column) { if (this.compare(row, column) == 0) { if (this.isStart(row, column)) { return false; } else { return true; } } return false; }; this.compare = function(row, column) { if (!this.isMultiLine()) { if (row === this.start.row) { return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); } } if (row < this.start.row) return -1; if (row > this.end.row) return 1; if (this.start.row === row) return column >= this.start.column ? 0 : -1; if (this.end.row === row) return column <= this.end.column ? 0 : 1; return 0; }; this.compareStart = function(row, column) { if (this.start.row == row && this.start.column == column) { return -1; } else { return this.compare(row, column); } }; this.compareEnd = function(row, column) { if (this.end.row == row && this.end.column == column) { return 1; } else { return this.compare(row, column); } }; this.compareInside = function(row, column) { if (this.end.row == row && this.end.column == column) { return 1; } else if (this.start.row == row && this.start.column == column) { return -1; } else { return this.compare(row, column); } }; this.clipRows = function(firstRow, lastRow) { if (this.end.row > lastRow) var end = {row: lastRow + 1, column: 0}; else if (this.end.row < firstRow) var end = {row: firstRow, column: 0}; if (this.start.row > lastRow) var start = {row: lastRow + 1, column: 0}; else if (this.start.row < firstRow) var start = {row: firstRow, column: 0}; return Range.fromPoints(start || this.start, end || this.end); }; this.extend = function(row, column) { var cmp = this.compare(row, column); if (cmp == 0) return this; else if (cmp == -1) var start = {row: row, column: column}; else var end = {row: row, column: column}; return Range.fromPoints(start || this.start, end || this.end); }; this.isEmpty = function() { return (this.start.row === this.end.row && this.start.column === this.end.column); }; this.isMultiLine = function() { return (this.start.row !== this.end.row); }; this.clone = function() { return Range.fromPoints(this.start, this.end); }; this.collapseRows = function() { if (this.end.column == 0) return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0) else return new Range(this.start.row, 0, this.end.row, 0) }; this.toScreenRange = function(session) { var screenPosStart = session.documentToScreenPosition(this.start); var screenPosEnd = session.documentToScreenPosition(this.end); return new Range( screenPosStart.row, screenPosStart.column, screenPosEnd.row, screenPosEnd.column ); }; this.moveBy = function(row, column) { this.start.row += row; this.start.column += column; this.end.row += row; this.end.column += column; }; }).call(Range.prototype); Range.fromPoints = function(start, end) { return new Range(start.row, start.column, end.row, end.column); }; Range.comparePoints = comparePoints; Range.comparePoints = function(p1, p2) { return p1.row - p2.row || p1.column - p2.column; }; exports.Range = Range; }); define("ace/selection",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter","ace/range"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var lang = require("./lib/lang"); var EventEmitter = require("./lib/event_emitter").EventEmitter; var Range = require("./range").Range; var Selection = function(session) { this.session = session; this.doc = session.getDocument(); this.clearSelection(); this.lead = this.selectionLead = this.doc.createAnchor(0, 0); this.anchor = this.selectionAnchor = this.doc.createAnchor(0, 0); var self = this; this.lead.on("change", function(e) { self._emit("changeCursor"); if (!self.$isEmpty) self._emit("changeSelection"); if (!self.$keepDesiredColumnOnChange && e.old.column != e.value.column) self.$desiredColumn = null; }); this.selectionAnchor.on("change", function() { if (!self.$isEmpty) self._emit("changeSelection"); }); }; (function() { oop.implement(this, EventEmitter); this.isEmpty = function() { return (this.$isEmpty || ( this.anchor.row == this.lead.row && this.anchor.column == this.lead.column )); }; this.isMultiLine = function() { if (this.isEmpty()) { return false; } return this.getRange().isMultiLine(); }; this.getCursor = function() { return this.lead.getPosition(); }; this.setSelectionAnchor = function(row, column) { this.anchor.setPosition(row, column); if (this.$isEmpty) { this.$isEmpty = false; this._emit("changeSelection"); } }; this.getSelectionAnchor = function() { if (this.$isEmpty) return this.getSelectionLead(); else return this.anchor.getPosition(); }; this.getSelectionLead = function() { return this.lead.getPosition(); }; this.shiftSelection = function(columns) { if (this.$isEmpty) { this.moveCursorTo(this.lead.row, this.lead.column + columns); return; } var anchor = this.getSelectionAnchor(); var lead = this.getSelectionLead(); var isBackwards = this.isBackwards(); if (!isBackwards || anchor.column !== 0) this.setSelectionAnchor(anchor.row, anchor.column + columns); if (isBackwards || lead.column !== 0) { this.$moveSelection(function() { this.moveCursorTo(lead.row, lead.column + columns); }); } }; this.isBackwards = function() { var anchor = this.anchor; var lead = this.lead; return (anchor.row > lead.row || (anchor.row == lead.row && anchor.column > lead.column)); }; this.getRange = function() { var anchor = this.anchor; var lead = this.lead; if (this.isEmpty()) return Range.fromPoints(lead, lead); if (this.isBackwards()) { return Range.fromPoints(lead, anchor); } else { return Range.fromPoints(anchor, lead); } }; this.clearSelection = function() { if (!this.$isEmpty) { this.$isEmpty = true; this._emit("changeSelection"); } }; this.selectAll = function() { var lastRow = this.doc.getLength() - 1; this.setSelectionAnchor(0, 0); this.moveCursorTo(lastRow, this.doc.getLine(lastRow).length); }; this.setRange = this.setSelectionRange = function(range, reverse) { if (reverse) { this.setSelectionAnchor(range.end.row, range.end.column); this.selectTo(range.start.row, range.start.column); } else { this.setSelectionAnchor(range.start.row, range.start.column); this.selectTo(range.end.row, range.end.column); } if (this.getRange().isEmpty()) this.$isEmpty = true; this.$desiredColumn = null; }; this.$moveSelection = function(mover) { var lead = this.lead; if (this.$isEmpty) this.setSelectionAnchor(lead.row, lead.column); mover.call(this); }; this.selectTo = function(row, column) { this.$moveSelection(function() { this.moveCursorTo(row, column); }); }; this.selectToPosition = function(pos) { this.$moveSelection(function() { this.moveCursorToPosition(pos); }); }; this.moveTo = function(row, column) { this.clearSelection(); this.moveCursorTo(row, column); }; this.moveToPosition = function(pos) { this.clearSelection(); this.moveCursorToPosition(pos); }; this.selectUp = function() { this.$moveSelection(this.moveCursorUp); }; this.selectDown = function() { this.$moveSelection(this.moveCursorDown); }; this.selectRight = function() { this.$moveSelection(this.moveCursorRight); }; this.selectLeft = function() { this.$moveSelection(this.moveCursorLeft); }; this.selectLineStart = function() { this.$moveSelection(this.moveCursorLineStart); }; this.selectLineEnd = function() { this.$moveSelection(this.moveCursorLineEnd); }; this.selectFileEnd = function() { this.$moveSelection(this.moveCursorFileEnd); }; this.selectFileStart = function() { this.$moveSelection(this.moveCursorFileStart); }; this.selectWordRight = function() { this.$moveSelection(this.moveCursorWordRight); }; this.selectWordLeft = function() { this.$moveSelection(this.moveCursorWordLeft); }; this.getWordRange = function(row, column) { if (typeof column == "undefined") { var cursor = row || this.lead; row = cursor.row; column = cursor.column; } return this.session.getWordRange(row, column); }; this.selectWord = function() { this.setSelectionRange(this.getWordRange()); }; this.selectAWord = function() { var cursor = this.getCursor(); var range = this.session.getAWordRange(cursor.row, cursor.column); this.setSelectionRange(range); }; this.getLineRange = function(row, excludeLastChar) { var rowStart = typeof row == "number" ? row : this.lead.row; var rowEnd; var foldLine = this.session.getFoldLine(rowStart); if (foldLine) { rowStart = foldLine.start.row; rowEnd = foldLine.end.row; } else { rowEnd = rowStart; } if (excludeLastChar === true) return new Range(rowStart, 0, rowEnd, this.session.getLine(rowEnd).length); else return new Range(rowStart, 0, rowEnd + 1, 0); }; this.selectLine = function() { this.setSelectionRange(this.getLineRange()); }; this.moveCursorUp = function() { this.moveCursorBy(-1, 0); }; this.moveCursorDown = function() { this.moveCursorBy(1, 0); }; this.moveCursorLeft = function() { var cursor = this.lead.getPosition(), fold; if (fold = this.session.getFoldAt(cursor.row, cursor.column, -1)) { this.moveCursorTo(fold.start.row, fold.start.column); } else if (cursor.column === 0) { if (cursor.row > 0) { this.moveCursorTo(cursor.row - 1, this.doc.getLine(cursor.row - 1).length); } } else { var tabSize = this.session.getTabSize(); if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column-tabSize, cursor.column).split(" ").length-1 == tabSize) this.moveCursorBy(0, -tabSize); else this.moveCursorBy(0, -1); } }; this.moveCursorRight = function() { var cursor = this.lead.getPosition(), fold; if (fold = this.session.getFoldAt(cursor.row, cursor.column, 1)) { this.moveCursorTo(fold.end.row, fold.end.column); } else if (this.lead.column == this.doc.getLine(this.lead.row).length) { if (this.lead.row < this.doc.getLength() - 1) { this.moveCursorTo(this.lead.row + 1, 0); } } else { var tabSize = this.session.getTabSize(); var cursor = this.lead; if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column, cursor.column+tabSize).split(" ").length-1 == tabSize) this.moveCursorBy(0, tabSize); else this.moveCursorBy(0, 1); } }; this.moveCursorLineStart = function() { var row = this.lead.row; var column = this.lead.column; var screenRow = this.session.documentToScreenRow(row, column); var firstColumnPosition = this.session.screenToDocumentPosition(screenRow, 0); var beforeCursor = this.session.getDisplayLine( row, null, firstColumnPosition.row, firstColumnPosition.column ); var leadingSpace = beforeCursor.match(/^\s*/); if (leadingSpace[0].length != column && !this.session.$useEmacsStyleLineStart) firstColumnPosition.column += leadingSpace[0].length; this.moveCursorToPosition(firstColumnPosition); }; this.moveCursorLineEnd = function() { var lead = this.lead; var lineEnd = this.session.getDocumentLastRowColumnPosition(lead.row, lead.column); if (this.lead.column == lineEnd.column) { var line = this.session.getLine(lineEnd.row); if (lineEnd.column == line.length) { var textEnd = line.search(/\s+$/); if (textEnd > 0) lineEnd.column = textEnd; } } this.moveCursorTo(lineEnd.row, lineEnd.column); }; this.moveCursorFileEnd = function() { var row = this.doc.getLength() - 1; var column = this.doc.getLine(row).length; this.moveCursorTo(row, column); }; this.moveCursorFileStart = function() { this.moveCursorTo(0, 0); }; this.moveCursorLongWordRight = function() { var row = this.lead.row; var column = this.lead.column; var line = this.doc.getLine(row); var rightOfCursor = line.substring(column); var match; this.session.nonTokenRe.lastIndex = 0; this.session.tokenRe.lastIndex = 0; var fold = this.session.getFoldAt(row, column, 1); if (fold) { this.moveCursorTo(fold.end.row, fold.end.column); return; } if (match = this.session.nonTokenRe.exec(rightOfCursor)) { column += this.session.nonTokenRe.lastIndex; this.session.nonTokenRe.lastIndex = 0; rightOfCursor = line.substring(column); } if (column >= line.length) { this.moveCursorTo(row, line.length); this.moveCursorRight(); if (row < this.doc.getLength() - 1) this.moveCursorWordRight(); return; } if (match = this.session.tokenRe.exec(rightOfCursor)) { column += this.session.tokenRe.lastIndex; this.session.tokenRe.lastIndex = 0; } this.moveCursorTo(row, column); }; this.moveCursorLongWordLeft = function() { var row = this.lead.row; var column = this.lead.column; var fold; if (fold = this.session.getFoldAt(row, column, -1)) { this.moveCursorTo(fold.start.row, fold.start.column); return; } var str = this.session.getFoldStringAt(row, column, -1); if (str == null) { str = this.doc.getLine(row).substring(0, column); } var leftOfCursor = lang.stringReverse(str); var match; this.session.nonTokenRe.lastIndex = 0; this.session.tokenRe.lastIndex = 0; if (match = this.session.nonTokenRe.exec(leftOfCursor)) { column -= this.session.nonTokenRe.lastIndex; leftOfCursor = leftOfCursor.slice(this.session.nonTokenRe.lastIndex); this.session.nonTokenRe.lastIndex = 0; } if (column <= 0) { this.moveCursorTo(row, 0); this.moveCursorLeft(); if (row > 0) this.moveCursorWordLeft(); return; } if (match = this.session.tokenRe.exec(leftOfCursor)) { column -= this.session.tokenRe.lastIndex; this.session.tokenRe.lastIndex = 0; } this.moveCursorTo(row, column); }; this.$shortWordEndIndex = function(rightOfCursor) { var match, index = 0, ch; var whitespaceRe = /\s/; var tokenRe = this.session.tokenRe; tokenRe.lastIndex = 0; if (match = this.session.tokenRe.exec(rightOfCursor)) { index = this.session.tokenRe.lastIndex; } else { while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch)) index ++; if (index < 1) { tokenRe.lastIndex = 0; while ((ch = rightOfCursor[index]) && !tokenRe.test(ch)) { tokenRe.lastIndex = 0; index ++; if (whitespaceRe.test(ch)) { if (index > 2) { index--; break; } else { while ((ch = rightOfCursor[index]) && whitespaceRe.test(ch)) index ++; if (index > 2) break; } } } } } tokenRe.lastIndex = 0; return index; }; this.moveCursorShortWordRight = function() { var row = this.lead.row; var column = this.lead.column; var line = this.doc.getLine(row); var rightOfCursor = line.substring(column); var fold = this.session.getFoldAt(row, column, 1); if (fold) return this.moveCursorTo(fold.end.row, fold.end.column); if (column == line.length) { var l = this.doc.getLength(); do { row++; rightOfCursor = this.doc.getLine(row); } while (row < l && /^\s*$/.test(rightOfCursor)); if (!/^\s+/.test(rightOfCursor)) rightOfCursor = ""; column = 0; } var index = this.$shortWordEndIndex(rightOfCursor); this.moveCursorTo(row, column + index); }; this.moveCursorShortWordLeft = function() { var row = this.lead.row; var column = this.lead.column; var fold; if (fold = this.session.getFoldAt(row, column, -1)) return this.moveCursorTo(fold.start.row, fold.start.column); var line = this.session.getLine(row).substring(0, column); if (column === 0) { do { row--; line = this.doc.getLine(row); } while (row > 0 && /^\s*$/.test(line)); column = line.length; if (!/\s+$/.test(line)) line = ""; } var leftOfCursor = lang.stringReverse(line); var index = this.$shortWordEndIndex(leftOfCursor); return this.moveCursorTo(row, column - index); }; this.moveCursorWordRight = function() { if (this.session.$selectLongWords) this.moveCursorLongWordRight(); else this.moveCursorShortWordRight(); }; this.moveCursorWordLeft = function() { if (this.session.$selectLongWords) this.moveCursorLongWordLeft(); else this.moveCursorShortWordLeft(); }; this.moveCursorBy = function(rows, chars) { var screenPos = this.session.documentToScreenPosition( this.lead.row, this.lead.column ); if (chars === 0) { if (this.$desiredColumn) screenPos.column = this.$desiredColumn; else this.$desiredColumn = screenPos.column; } var docPos = this.session.screenToDocumentPosition(screenPos.row + rows, screenPos.column); if (rows !== 0 && chars === 0 && docPos.row === this.lead.row && docPos.column === this.lead.column) { if (this.session.lineWidgets && this.session.lineWidgets[docPos.row]) { if (docPos.row > 0 || rows > 0) docPos.row++; } } this.moveCursorTo(docPos.row, docPos.column + chars, chars === 0); }; this.moveCursorToPosition = function(position) { this.moveCursorTo(position.row, position.column); }; this.moveCursorTo = function(row, column, keepDesiredColumn) { var fold = this.session.getFoldAt(row, column, 1); if (fold) { row = fold.start.row; column = fold.start.column; } this.$keepDesiredColumnOnChange = true; this.lead.setPosition(row, column); this.$keepDesiredColumnOnChange = false; if (!keepDesiredColumn) this.$desiredColumn = null; }; this.moveCursorToScreen = function(row, column, keepDesiredColumn) { var pos = this.session.screenToDocumentPosition(row, column); this.moveCursorTo(pos.row, pos.column, keepDesiredColumn); }; this.detach = function() { this.lead.detach(); this.anchor.detach(); this.session = this.doc = null; }; this.fromOrientedRange = function(range) { this.setSelectionRange(range, range.cursor == range.start); this.$desiredColumn = range.desiredColumn || this.$desiredColumn; }; this.toOrientedRange = function(range) { var r = this.getRange(); if (range) { range.start.column = r.start.column; range.start.row = r.start.row; range.end.column = r.end.column; range.end.row = r.end.row; } else { range = r; } range.cursor = this.isBackwards() ? range.start : range.end; range.desiredColumn = this.$desiredColumn; return range; }; this.getRangeOfMovements = function(func) { var start = this.getCursor(); try { func(this); var end = this.getCursor(); return Range.fromPoints(start,end); } catch(e) { return Range.fromPoints(start,start); } finally { this.moveCursorToPosition(start); } }; this.toJSON = function() { if (this.rangeCount) { var data = this.ranges.map(function(r) { var r1 = r.clone(); r1.isBackwards = r.cursor == r.start; return r1; }); } else { var data = this.getRange(); data.isBackwards = this.isBackwards(); } return data; }; this.fromJSON = function(data) { if (data.start == undefined) { if (this.rangeList) { this.toSingleRange(data[0]); for (var i = data.length; i--; ) { var r = Range.fromPoints(data[i].start, data[i].end); if (data[i].isBackwards) r.cursor = r.start; this.addRange(r, true); } return; } else data = data[0]; } if (this.rangeList) this.toSingleRange(data); this.setSelectionRange(data, data.isBackwards); }; this.isEqual = function(data) { if ((data.length || this.rangeCount) && data.length != this.rangeCount) return false; if (!data.length || !this.ranges) return this.getRange().isEqual(data); for (var i = this.ranges.length; i--; ) { if (!this.ranges[i].isEqual(data[i])) return false; } return true; }; }).call(Selection.prototype); exports.Selection = Selection; }); define("ace/tokenizer",["require","exports","module","ace/config"], function(require, exports, module) { "use strict"; var config = require("./config"); var MAX_TOKEN_COUNT = 2000; var Tokenizer = function(rules) { this.states = rules; this.regExps = {}; this.matchMappings = {}; for (var key in this.states) { var state = this.states[key]; var ruleRegExps = []; var matchTotal = 0; var mapping = this.matchMappings[key] = {defaultToken: "text"}; var flag = "g"; var splitterRurles = []; for (var i = 0; i < state.length; i++) { var rule = state[i]; if (rule.defaultToken) mapping.defaultToken = rule.defaultToken; if (rule.caseInsensitive) flag = "gi"; if (rule.regex == null) continue; if (rule.regex instanceof RegExp) rule.regex = rule.regex.toString().slice(1, -1); var adjustedregex = rule.regex; var matchcount = new RegExp("(?:(" + adjustedregex + ")|(.))").exec("a").length - 2; if (Array.isArray(rule.token)) { if (rule.token.length == 1 || matchcount == 1) { rule.token = rule.token[0]; } else if (matchcount - 1 != rule.token.length) { this.reportError("number of classes and regexp groups doesn't match", { rule: rule, groupCount: matchcount - 1 }); rule.token = rule.token[0]; } else { rule.tokenArray = rule.token; rule.token = null; rule.onMatch = this.$arrayTokens; } } else if (typeof rule.token == "function" && !rule.onMatch) { if (matchcount > 1) rule.onMatch = this.$applyToken; else rule.onMatch = rule.token; } if (matchcount > 1) { if (/\\\d/.test(rule.regex)) { adjustedregex = rule.regex.replace(/\\([0-9]+)/g, function(match, digit) { return "\\" + (parseInt(digit, 10) + matchTotal + 1); }); } else { matchcount = 1; adjustedregex = this.removeCapturingGroups(rule.regex); } if (!rule.splitRegex && typeof rule.token != "string") splitterRurles.push(rule); // flag will be known only at the very end } mapping[matchTotal] = i; matchTotal += matchcount; ruleRegExps.push(adjustedregex); if (!rule.onMatch) rule.onMatch = null; } if (!ruleRegExps.length) { mapping[0] = 0; ruleRegExps.push("$"); } splitterRurles.forEach(function(rule) { rule.splitRegex = this.createSplitterRegexp(rule.regex, flag); }, this); this.regExps[key] = new RegExp("(" + ruleRegExps.join(")|(") + ")|($)", flag); } }; (function() { this.$setMaxTokenCount = function(m) { MAX_TOKEN_COUNT = m | 0; }; this.$applyToken = function(str) { var values = this.splitRegex.exec(str).slice(1); var types = this.token.apply(this, values); if (typeof types === "string") return [{type: types, value: str}]; var tokens = []; for (var i = 0, l = types.length; i < l; i++) { if (values[i]) tokens[tokens.length] = { type: types[i], value: values[i] }; } return tokens; }; this.$arrayTokens = function(str) { if (!str) return []; var values = this.splitRegex.exec(str); if (!values) return "text"; var tokens = []; var types = this.tokenArray; for (var i = 0, l = types.length; i < l; i++) { if (values[i + 1]) tokens[tokens.length] = { type: types[i], value: values[i + 1] }; } return tokens; }; this.removeCapturingGroups = function(src) { var r = src.replace( /\[(?:\\.|[^\]])*?\]|\\.|\(\?[:=!]|(\()/g, function(x, y) {return y ? "(?:" : x;} ); return r; }; this.createSplitterRegexp = function(src, flag) { if (src.indexOf("(?=") != -1) { var stack = 0; var inChClass = false; var lastCapture = {}; src.replace(/(\\.)|(\((?:\?[=!])?)|(\))|([\[\]])/g, function( m, esc, parenOpen, parenClose, square, index ) { if (inChClass) { inChClass = square != "]"; } else if (square) { inChClass = true; } else if (parenClose) { if (stack == lastCapture.stack) { lastCapture.end = index+1; lastCapture.stack = -1; } stack--; } else if (parenOpen) { stack++; if (parenOpen.length != 1) { lastCapture.stack = stack lastCapture.start = index; } } return m; }); if (lastCapture.end != null && /^\)*$/.test(src.substr(lastCapture.end))) src = src.substring(0, lastCapture.start) + src.substr(lastCapture.end); } if (src.charAt(0) != "^") src = "^" + src; if (src.charAt(src.length - 1) != "$") src += "$"; return new RegExp(src, (flag||"").replace("g", "")); }; this.getLineTokens = function(line, startState) { if (startState && typeof startState != "string") { var stack = startState.slice(0); startState = stack[0]; if (startState === "#tmp") { stack.shift() startState = stack.shift() } } else var stack = []; var currentState = startState || "start"; var state = this.states[currentState]; if (!state) { currentState = "start"; state = this.states[currentState]; } var mapping = this.matchMappings[currentState]; var re = this.regExps[currentState]; re.lastIndex = 0; var match, tokens = []; var lastIndex = 0; var matchAttempts = 0; var token = {type: null, value: ""}; while (match = re.exec(line)) { var type = mapping.defaultToken; var rule = null; var value = match[0]; var index = re.lastIndex; if (index - value.length > lastIndex) { var skipped = line.substring(lastIndex, index - value.length); if (token.type == type) { token.value += skipped; } else { if (token.type) tokens.push(token); token = {type: type, value: skipped}; } } for (var i = 0; i < match.length-2; i++) { if (match[i + 1] === undefined) continue; rule = state[mapping[i]]; if (rule.onMatch) type = rule.onMatch(value, currentState, stack); else type = rule.token; if (rule.next) { if (typeof rule.next == "string") { currentState = rule.next; } else { currentState = rule.next(currentState, stack); } state = this.states[currentState]; if (!state) { this.reportError("state doesn't exist", currentState); currentState = "start"; state = this.states[currentState]; } mapping = this.matchMappings[currentState]; lastIndex = index; re = this.regExps[currentState]; re.lastIndex = index; } break; } if (value) { if (typeof type === "string") { if ((!rule || rule.merge !== false) && token.type === type) { token.value += value; } else { if (token.type) tokens.push(token); token = {type: type, value: value}; } } else if (type) { if (token.type) tokens.push(token); token = {type: null, value: ""}; for (var i = 0; i < type.length; i++) tokens.push(type[i]); } } if (lastIndex == line.length) break; lastIndex = index; if (matchAttempts++ > MAX_TOKEN_COUNT) { if (matchAttempts > 2 * line.length) { this.reportError("infinite loop with in ace tokenizer", { startState: startState, line: line }); } while (lastIndex < line.length) { if (token.type) tokens.push(token); token = { value: line.substring(lastIndex, lastIndex += 2000), type: "overflow" }; } currentState = "start"; stack = []; break; } } if (token.type) tokens.push(token); if (stack.length > 1) { if (stack[0] !== currentState) stack.unshift("#tmp", currentState); } return { tokens : tokens, state : stack.length ? stack : currentState }; }; this.reportError = config.reportError; }).call(Tokenizer.prototype); exports.Tokenizer = Tokenizer; }); define("ace/mode/text_highlight_rules",["require","exports","module","ace/lib/lang"], function(require, exports, module) { "use strict"; var lang = require("../lib/lang"); var TextHighlightRules = function() { this.$rules = { "start" : [{ token : "empty_line", regex : '^$' }, { defaultToken : "text" }] }; }; (function() { this.addRules = function(rules, prefix) { if (!prefix) { for (var key in rules) this.$rules[key] = rules[key]; return; } for (var key in rules) { var state = rules[key]; for (var i = 0; i < state.length; i++) { var rule = state[i]; if (rule.next || rule.onMatch) { if (typeof rule.next == "string") { if (rule.next.indexOf(prefix) !== 0) rule.next = prefix + rule.next; } if (rule.nextState && rule.nextState.indexOf(prefix) !== 0) rule.nextState = prefix + rule.nextState; } } this.$rules[prefix + key] = state; } }; this.getRules = function() { return this.$rules; }; this.embedRules = function (HighlightRules, prefix, escapeRules, states, append) { var embedRules = typeof HighlightRules == "function" ? new HighlightRules().getRules() : HighlightRules; if (states) { for (var i = 0; i < states.length; i++) states[i] = prefix + states[i]; } else { states = []; for (var key in embedRules) states.push(prefix + key); } this.addRules(embedRules, prefix); if (escapeRules) { var addRules = Array.prototype[append ? "push" : "unshift"]; for (var i = 0; i < states.length; i++) addRules.apply(this.$rules[states[i]], lang.deepCopy(escapeRules)); } if (!this.$embeds) this.$embeds = []; this.$embeds.push(prefix); }; this.getEmbeds = function() { return this.$embeds; }; var pushState = function(currentState, stack) { if (currentState != "start" || stack.length) stack.unshift(this.nextState, currentState); return this.nextState; }; var popState = function(currentState, stack) { stack.shift(); return stack.shift() || "start"; }; this.normalizeRules = function() { var id = 0; var rules = this.$rules; function processState(key) { var state = rules[key]; state.processed = true; for (var i = 0; i < state.length; i++) { var rule = state[i]; var toInsert = null; if (Array.isArray(rule)) { toInsert = rule; rule = {}; } if (!rule.regex && rule.start) { rule.regex = rule.start; if (!rule.next) rule.next = []; rule.next.push({ defaultToken: rule.token }, { token: rule.token + ".end", regex: rule.end || rule.start, next: "pop" }); rule.token = rule.token + ".start"; rule.push = true; } var next = rule.next || rule.push; if (next && Array.isArray(next)) { var stateName = rule.stateName; if (!stateName) { stateName = rule.token; if (typeof stateName != "string") stateName = stateName[0] || ""; if (rules[stateName]) stateName += id++; } rules[stateName] = next; rule.next = stateName; processState(stateName); } else if (next == "pop") { rule.next = popState; } if (rule.push) { rule.nextState = rule.next || rule.push; rule.next = pushState; delete rule.push; } if (rule.rules) { for (var r in rule.rules) { if (rules[r]) { if (rules[r].push) rules[r].push.apply(rules[r], rule.rules[r]); } else { rules[r] = rule.rules[r]; } } } var includeName = typeof rule == "string" ? rule : typeof rule.include == "string" ? rule.include : ""; if (includeName) { toInsert = rules[includeName]; } if (toInsert) { var args = [i, 1].concat(toInsert); if (rule.noEscape) args = args.filter(function(x) {return !x.next;}); state.splice.apply(state, args); i--; } if (rule.keywordMap) { rule.token = this.createKeywordMapper( rule.keywordMap, rule.defaultToken || "text", rule.caseInsensitive ); delete rule.defaultToken; } } } Object.keys(rules).forEach(processState, this); }; this.createKeywordMapper = function(map, defaultToken, ignoreCase, splitChar) { var keywords = Object.create(null); Object.keys(map).forEach(function(className) { var a = map[className]; if (ignoreCase) a = a.toLowerCase(); var list = a.split(splitChar || "|"); for (var i = list.length; i--; ) keywords[list[i]] = className; }); if (Object.getPrototypeOf(keywords)) { keywords.__proto__ = null; } this.$keywordList = Object.keys(keywords); map = null; return ignoreCase ? function(value) {return keywords[value.toLowerCase()] || defaultToken } : function(value) {return keywords[value] || defaultToken }; }; this.getKeywords = function() { return this.$keywords; }; }).call(TextHighlightRules.prototype); exports.TextHighlightRules = TextHighlightRules; }); define("ace/mode/behaviour",["require","exports","module"], function(require, exports, module) { "use strict"; var Behaviour = function() { this.$behaviours = {}; }; (function () { this.add = function (name, action, callback) { switch (undefined) { case this.$behaviours: this.$behaviours = {}; case this.$behaviours[name]: this.$behaviours[name] = {}; } this.$behaviours[name][action] = callback; } this.addBehaviours = function (behaviours) { for (var key in behaviours) { for (var action in behaviours[key]) { this.add(key, action, behaviours[key][action]); } } } this.remove = function (name) { if (this.$behaviours && this.$behaviours[name]) { delete this.$behaviours[name]; } } this.inherit = function (mode, filter) { if (typeof mode === "function") { var behaviours = new mode().getBehaviours(filter); } else { var behaviours = mode.getBehaviours(filter); } this.addBehaviours(behaviours); } this.getBehaviours = function (filter) { if (!filter) { return this.$behaviours; } else { var ret = {} for (var i = 0; i < filter.length; i++) { if (this.$behaviours[filter[i]]) { ret[filter[i]] = this.$behaviours[filter[i]]; } } return ret; } } }).call(Behaviour.prototype); exports.Behaviour = Behaviour; }); define("ace/token_iterator",["require","exports","module"], function(require, exports, module) { "use strict"; var TokenIterator = function(session, initialRow, initialColumn) { this.$session = session; this.$row = initialRow; this.$rowTokens = session.getTokens(initialRow); var token = session.getTokenAt(initialRow, initialColumn); this.$tokenIndex = token ? token.index : -1; }; (function() { this.stepBackward = function() { this.$tokenIndex -= 1; while (this.$tokenIndex < 0) { this.$row -= 1; if (this.$row < 0) { this.$row = 0; return null; } this.$rowTokens = this.$session.getTokens(this.$row); this.$tokenIndex = this.$rowTokens.length - 1; } return this.$rowTokens[this.$tokenIndex]; }; this.stepForward = function() { this.$tokenIndex += 1; var rowCount; while (this.$tokenIndex >= this.$rowTokens.length) { this.$row += 1; if (!rowCount) rowCount = this.$session.getLength(); if (this.$row >= rowCount) { this.$row = rowCount - 1; return null; } this.$rowTokens = this.$session.getTokens(this.$row); this.$tokenIndex = 0; } return this.$rowTokens[this.$tokenIndex]; }; this.getCurrentToken = function () { return this.$rowTokens[this.$tokenIndex]; }; this.getCurrentTokenRow = function () { return this.$row; }; this.getCurrentTokenColumn = function() { var rowTokens = this.$rowTokens; var tokenIndex = this.$tokenIndex; var column = rowTokens[tokenIndex].start; if (column !== undefined) return column; column = 0; while (tokenIndex > 0) { tokenIndex -= 1; column += rowTokens[tokenIndex].value.length; } return column; }; this.getCurrentTokenPosition = function() { return {row: this.$row, column: this.getCurrentTokenColumn()}; }; }).call(TokenIterator.prototype); exports.TokenIterator = TokenIterator; }); define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"], function(require, exports, module) { "use strict"; var oop = require("../../lib/oop"); var Behaviour = require("../behaviour").Behaviour; var TokenIterator = require("../../token_iterator").TokenIterator; var lang = require("../../lib/lang"); var SAFE_INSERT_IN_TOKENS = ["text", "paren.rparen", "punctuation.operator"]; var SAFE_INSERT_BEFORE_TOKENS = ["text", "paren.rparen", "punctuation.operator", "comment"]; var context; var contextCache = {}; var initContext = function(editor) { var id = -1; if (editor.multiSelect) { id = editor.selection.index; if (contextCache.rangeCount != editor.multiSelect.rangeCount) contextCache = {rangeCount: editor.multiSelect.rangeCount}; } if (contextCache[id]) return context = contextCache[id]; context = contextCache[id] = { autoInsertedBrackets: 0, autoInsertedRow: -1, autoInsertedLineEnd: "", maybeInsertedBrackets: 0, maybeInsertedRow: -1, maybeInsertedLineStart: "", maybeInsertedLineEnd: "" }; }; var getWrapped = function(selection, selected, opening, closing) { var rowDiff = selection.end.row - selection.start.row; return { text: opening + selected + closing, selection: [ 0, selection.start.column + 1, rowDiff, selection.end.column + (rowDiff ? 0 : 1) ] }; }; var CstyleBehaviour = function() { this.add("braces", "insertion", function(state, action, editor, session, text) { var cursor = editor.getCursorPosition(); var line = session.doc.getLine(cursor.row); if (text == '{') { initContext(editor); var selection = editor.getSelectionRange(); var selected = session.doc.getTextRange(selection); if (selected !== "" && selected !== "{" && editor.getWrapBehavioursEnabled()) { return getWrapped(selection, selected, '{', '}'); } else if (CstyleBehaviour.isSaneInsertion(editor, session)) { if (/[\]\}\)]/.test(line[cursor.column]) || editor.inMultiSelectMode) { CstyleBehaviour.recordAutoInsert(editor, session, "}"); return { text: '{}', selection: [1, 1] }; } else { CstyleBehaviour.recordMaybeInsert(editor, session, "{"); return { text: '{', selection: [1, 1] }; } } } else if (text == '}') { initContext(editor); var rightChar = line.substring(cursor.column, cursor.column + 1); if (rightChar == '}') { var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) { CstyleBehaviour.popAutoInsertedClosing(); return { text: '', selection: [1, 1] }; } } } else if (text == "\n" || text == "\r\n") { initContext(editor); var closing = ""; if (CstyleBehaviour.isMaybeInsertedClosing(cursor, line)) { closing = lang.stringRepeat("}", context.maybeInsertedBrackets); CstyleBehaviour.clearMaybeInsertedClosing(); } var rightChar = line.substring(cursor.column, cursor.column + 1); if (rightChar === '}') { var openBracePos = session.findMatchingBracket({row: cursor.row, column: cursor.column+1}, '}'); if (!openBracePos) return null; var next_indent = this.$getIndent(session.getLine(openBracePos.row)); } else if (closing) { var next_indent = this.$getIndent(line); } else { CstyleBehaviour.clearMaybeInsertedClosing(); return; } var indent = next_indent + session.getTabString(); return { text: '\n' + indent + '\n' + next_indent + closing, selection: [1, indent.length, 1, indent.length] }; } else { CstyleBehaviour.clearMaybeInsertedClosing(); } }); this.add("braces", "deletion", function(state, action, editor, session, range) { var selected = session.doc.getTextRange(range); if (!range.isMultiLine() && selected == '{') { initContext(editor); var line = session.doc.getLine(range.start.row); var rightChar = line.substring(range.end.column, range.end.column + 1); if (rightChar == '}') { range.end.column++; return range; } else { context.maybeInsertedBrackets--; } } }); this.add("parens", "insertion", function(state, action, editor, session, text) { if (text == '(') { initContext(editor); var selection = editor.getSelectionRange(); var selected = session.doc.getTextRange(selection); if (selected !== "" && editor.getWrapBehavioursEnabled()) { return getWrapped(selection, selected, '(', ')'); } else if (CstyleBehaviour.isSaneInsertion(editor, session)) { CstyleBehaviour.recordAutoInsert(editor, session, ")"); return { text: '()', selection: [1, 1] }; } } else if (text == ')') { initContext(editor); var cursor = editor.getCursorPosition(); var line = session.doc.getLine(cursor.row); var rightChar = line.substring(cursor.column, cursor.column + 1); if (rightChar == ')') { var matching = session.$findOpeningBracket(')', {column: cursor.column + 1, row: cursor.row}); if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) { CstyleBehaviour.popAutoInsertedClosing(); return { text: '', selection: [1, 1] }; } } } }); this.add("parens", "deletion", function(state, action, editor, session, range) { var selected = session.doc.getTextRange(range); if (!range.isMultiLine() && selected == '(') { initContext(editor); var line = session.doc.getLine(range.start.row); var rightChar = line.substring(range.start.column + 1, range.start.column + 2); if (rightChar == ')') { range.end.column++; return range; } } }); this.add("brackets", "insertion", function(state, action, editor, session, text) { if (text == '[') { initContext(editor); var selection = editor.getSelectionRange(); var selected = session.doc.getTextRange(selection); if (selected !== "" && editor.getWrapBehavioursEnabled()) { return getWrapped(selection, selected, '[', ']'); } else if (CstyleBehaviour.isSaneInsertion(editor, session)) { CstyleBehaviour.recordAutoInsert(editor, session, "]"); return { text: '[]', selection: [1, 1] }; } } else if (text == ']') { initContext(editor); var cursor = editor.getCursorPosition(); var line = session.doc.getLine(cursor.row); var rightChar = line.substring(cursor.column, cursor.column + 1); if (rightChar == ']') { var matching = session.$findOpeningBracket(']', {column: cursor.column + 1, row: cursor.row}); if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) { CstyleBehaviour.popAutoInsertedClosing(); return { text: '', selection: [1, 1] }; } } } }); this.add("brackets", "deletion", function(state, action, editor, session, range) { var selected = session.doc.getTextRange(range); if (!range.isMultiLine() && selected == '[') { initContext(editor); var line = session.doc.getLine(range.start.row); var rightChar = line.substring(range.start.column + 1, range.start.column + 2); if (rightChar == ']') { range.end.column++; return range; } } }); this.add("string_dquotes", "insertion", function(state, action, editor, session, text) { if (text == '"' || text == "'") { if (this.lineCommentStart && this.lineCommentStart.indexOf(text) != -1) return; initContext(editor); var quote = text; var selection = editor.getSelectionRange(); var selected = session.doc.getTextRange(selection); if (selected !== "" && selected !== "'" && selected != '"' && editor.getWrapBehavioursEnabled()) { return getWrapped(selection, selected, quote, quote); } else if (!selected) { var cursor = editor.getCursorPosition(); var line = session.doc.getLine(cursor.row); var leftChar = line.substring(cursor.column-1, cursor.column); var rightChar = line.substring(cursor.column, cursor.column + 1); var token = session.getTokenAt(cursor.row, cursor.column); var rightToken = session.getTokenAt(cursor.row, cursor.column + 1); if (leftChar == "\\" && token && /escape/.test(token.type)) return null; var stringBefore = token && /string|escape/.test(token.type); var stringAfter = !rightToken || /string|escape/.test(rightToken.type); var pair; if (rightChar == quote) { pair = stringBefore !== stringAfter; if (pair && /string\.end/.test(rightToken.type)) pair = false; } else { if (stringBefore && !stringAfter) return null; // wrap string with different quote if (stringBefore && stringAfter) return null; // do not pair quotes inside strings var wordRe = session.$mode.tokenRe; wordRe.lastIndex = 0; var isWordBefore = wordRe.test(leftChar); wordRe.lastIndex = 0; var isWordAfter = wordRe.test(leftChar); if (isWordBefore || isWordAfter) return null; // before or after alphanumeric if (rightChar && !/[\s;,.})\]\\]/.test(rightChar)) return null; // there is rightChar and it isn't closing pair = true; } return { text: pair ? quote + quote : "", selection: [1,1] }; } } }); this.add("string_dquotes", "deletion", function(state, action, editor, session, range) { var selected = session.doc.getTextRange(range); if (!range.isMultiLine() && (selected == '"' || selected == "'")) { initContext(editor); var line = session.doc.getLine(range.start.row); var rightChar = line.substring(range.start.column + 1, range.start.column + 2); if (rightChar == selected) { range.end.column++; return range; } } }); }; CstyleBehaviour.isSaneInsertion = function(editor, session) { var cursor = editor.getCursorPosition(); var iterator = new TokenIterator(session, cursor.row, cursor.column); if (!this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) { var iterator2 = new TokenIterator(session, cursor.row, cursor.column + 1); if (!this.$matchTokenType(iterator2.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) return false; } iterator.stepForward(); return iterator.getCurrentTokenRow() !== cursor.row || this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_BEFORE_TOKENS); }; CstyleBehaviour.$matchTokenType = function(token, types) { return types.indexOf(token.type || token) > -1; }; CstyleBehaviour.recordAutoInsert = function(editor, session, bracket) { var cursor = editor.getCursorPosition(); var line = session.doc.getLine(cursor.row); if (!this.isAutoInsertedClosing(cursor, line, context.autoInsertedLineEnd[0])) context.autoInsertedBrackets = 0; context.autoInsertedRow = cursor.row; context.autoInsertedLineEnd = bracket + line.substr(cursor.column); context.autoInsertedBrackets++; }; CstyleBehaviour.recordMaybeInsert = function(editor, session, bracket) { var cursor = editor.getCursorPosition(); var line = session.doc.getLine(cursor.row); if (!this.isMaybeInsertedClosing(cursor, line)) context.maybeInsertedBrackets = 0; context.maybeInsertedRow = cursor.row; context.maybeInsertedLineStart = line.substr(0, cursor.column) + bracket; context.maybeInsertedLineEnd = line.substr(cursor.column); context.maybeInsertedBrackets++; }; CstyleBehaviour.isAutoInsertedClosing = function(cursor, line, bracket) { return context.autoInsertedBrackets > 0 && cursor.row === context.autoInsertedRow && bracket === context.autoInsertedLineEnd[0] && line.substr(cursor.column) === context.autoInsertedLineEnd; }; CstyleBehaviour.isMaybeInsertedClosing = function(cursor, line) { return context.maybeInsertedBrackets > 0 && cursor.row === context.maybeInsertedRow && line.substr(cursor.column) === context.maybeInsertedLineEnd && line.substr(0, cursor.column) == context.maybeInsertedLineStart; }; CstyleBehaviour.popAutoInsertedClosing = function() { context.autoInsertedLineEnd = context.autoInsertedLineEnd.substr(1); context.autoInsertedBrackets--; }; CstyleBehaviour.clearMaybeInsertedClosing = function() { if (context) { context.maybeInsertedBrackets = 0; context.maybeInsertedRow = -1; } }; oop.inherits(CstyleBehaviour, Behaviour); exports.CstyleBehaviour = CstyleBehaviour; }); define("ace/unicode",["require","exports","module"], function(require, exports, module) { "use strict"; exports.packages = {}; addUnicodePackage({ L: "0041-005A0061-007A00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05250531-055605590561-058705D0-05EA05F0-05F20621-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280904-0939093D09500958-0961097109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510D0-10FA10FC1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209421022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2D00-2D252D30-2D652D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A65FA662-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78BA78CA7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC", Ll: "0061-007A00AA00B500BA00DF-00F600F8-00FF01010103010501070109010B010D010F01110113011501170119011B011D011F01210123012501270129012B012D012F01310133013501370138013A013C013E014001420144014601480149014B014D014F01510153015501570159015B015D015F01610163016501670169016B016D016F0171017301750177017A017C017E-0180018301850188018C018D019201950199-019B019E01A101A301A501A801AA01AB01AD01B001B401B601B901BA01BD-01BF01C601C901CC01CE01D001D201D401D601D801DA01DC01DD01DF01E101E301E501E701E901EB01ED01EF01F001F301F501F901FB01FD01FF02010203020502070209020B020D020F02110213021502170219021B021D021F02210223022502270229022B022D022F02310233-0239023C023F0240024202470249024B024D024F-02930295-02AF037103730377037B-037D039003AC-03CE03D003D103D5-03D703D903DB03DD03DF03E103E303E503E703E903EB03ED03EF-03F303F503F803FB03FC0430-045F04610463046504670469046B046D046F04710473047504770479047B047D047F0481048B048D048F04910493049504970499049B049D049F04A104A304A504A704A904AB04AD04AF04B104B304B504B704B904BB04BD04BF04C204C404C604C804CA04CC04CE04CF04D104D304D504D704D904DB04DD04DF04E104E304E504E704E904EB04ED04EF04F104F304F504F704F904FB04FD04FF05010503050505070509050B050D050F05110513051505170519051B051D051F0521052305250561-05871D00-1D2B1D62-1D771D79-1D9A1E011E031E051E071E091E0B1E0D1E0F1E111E131E151E171E191E1B1E1D1E1F1E211E231E251E271E291E2B1E2D1E2F1E311E331E351E371E391E3B1E3D1E3F1E411E431E451E471E491E4B1E4D1E4F1E511E531E551E571E591E5B1E5D1E5F1E611E631E651E671E691E6B1E6D1E6F1E711E731E751E771E791E7B1E7D1E7F1E811E831E851E871E891E8B1E8D1E8F1E911E931E95-1E9D1E9F1EA11EA31EA51EA71EA91EAB1EAD1EAF1EB11EB31EB51EB71EB91EBB1EBD1EBF1EC11EC31EC51EC71EC91ECB1ECD1ECF1ED11ED31ED51ED71ED91EDB1EDD1EDF1EE11EE31EE51EE71EE91EEB1EED1EEF1EF11EF31EF51EF71EF91EFB1EFD1EFF-1F071F10-1F151F20-1F271F30-1F371F40-1F451F50-1F571F60-1F671F70-1F7D1F80-1F871F90-1F971FA0-1FA71FB0-1FB41FB61FB71FBE1FC2-1FC41FC61FC71FD0-1FD31FD61FD71FE0-1FE71FF2-1FF41FF61FF7210A210E210F2113212F21342139213C213D2146-2149214E21842C30-2C5E2C612C652C662C682C6A2C6C2C712C732C742C76-2C7C2C812C832C852C872C892C8B2C8D2C8F2C912C932C952C972C992C9B2C9D2C9F2CA12CA32CA52CA72CA92CAB2CAD2CAF2CB12CB32CB52CB72CB92CBB2CBD2CBF2CC12CC32CC52CC72CC92CCB2CCD2CCF2CD12CD32CD52CD72CD92CDB2CDD2CDF2CE12CE32CE42CEC2CEE2D00-2D25A641A643A645A647A649A64BA64DA64FA651A653A655A657A659A65BA65DA65FA663A665A667A669A66BA66DA681A683A685A687A689A68BA68DA68FA691A693A695A697A723A725A727A729A72BA72DA72F-A731A733A735A737A739A73BA73DA73FA741A743A745A747A749A74BA74DA74FA751A753A755A757A759A75BA75DA75FA761A763A765A767A769A76BA76DA76FA771-A778A77AA77CA77FA781A783A785A787A78CFB00-FB06FB13-FB17FF41-FF5A", Lu: "0041-005A00C0-00D600D8-00DE01000102010401060108010A010C010E01100112011401160118011A011C011E01200122012401260128012A012C012E01300132013401360139013B013D013F0141014301450147014A014C014E01500152015401560158015A015C015E01600162016401660168016A016C016E017001720174017601780179017B017D018101820184018601870189-018B018E-0191019301940196-0198019C019D019F01A001A201A401A601A701A901AC01AE01AF01B1-01B301B501B701B801BC01C401C701CA01CD01CF01D101D301D501D701D901DB01DE01E001E201E401E601E801EA01EC01EE01F101F401F6-01F801FA01FC01FE02000202020402060208020A020C020E02100212021402160218021A021C021E02200222022402260228022A022C022E02300232023A023B023D023E02410243-02460248024A024C024E03700372037603860388-038A038C038E038F0391-03A103A3-03AB03CF03D2-03D403D803DA03DC03DE03E003E203E403E603E803EA03EC03EE03F403F703F903FA03FD-042F04600462046404660468046A046C046E04700472047404760478047A047C047E0480048A048C048E04900492049404960498049A049C049E04A004A204A404A604A804AA04AC04AE04B004B204B404B604B804BA04BC04BE04C004C104C304C504C704C904CB04CD04D004D204D404D604D804DA04DC04DE04E004E204E404E604E804EA04EC04EE04F004F204F404F604F804FA04FC04FE05000502050405060508050A050C050E05100512051405160518051A051C051E0520052205240531-055610A0-10C51E001E021E041E061E081E0A1E0C1E0E1E101E121E141E161E181E1A1E1C1E1E1E201E221E241E261E281E2A1E2C1E2E1E301E321E341E361E381E3A1E3C1E3E1E401E421E441E461E481E4A1E4C1E4E1E501E521E541E561E581E5A1E5C1E5E1E601E621E641E661E681E6A1E6C1E6E1E701E721E741E761E781E7A1E7C1E7E1E801E821E841E861E881E8A1E8C1E8E1E901E921E941E9E1EA01EA21EA41EA61EA81EAA1EAC1EAE1EB01EB21EB41EB61EB81EBA1EBC1EBE1EC01EC21EC41EC61EC81ECA1ECC1ECE1ED01ED21ED41ED61ED81EDA1EDC1EDE1EE01EE21EE41EE61EE81EEA1EEC1EEE1EF01EF21EF41EF61EF81EFA1EFC1EFE1F08-1F0F1F18-1F1D1F28-1F2F1F38-1F3F1F48-1F4D1F591F5B1F5D1F5F1F68-1F6F1FB8-1FBB1FC8-1FCB1FD8-1FDB1FE8-1FEC1FF8-1FFB21022107210B-210D2110-211221152119-211D212421262128212A-212D2130-2133213E213F214521832C00-2C2E2C602C62-2C642C672C692C6B2C6D-2C702C722C752C7E-2C802C822C842C862C882C8A2C8C2C8E2C902C922C942C962C982C9A2C9C2C9E2CA02CA22CA42CA62CA82CAA2CAC2CAE2CB02CB22CB42CB62CB82CBA2CBC2CBE2CC02CC22CC42CC62CC82CCA2CCC2CCE2CD02CD22CD42CD62CD82CDA2CDC2CDE2CE02CE22CEB2CEDA640A642A644A646A648A64AA64CA64EA650A652A654A656A658A65AA65CA65EA662A664A666A668A66AA66CA680A682A684A686A688A68AA68CA68EA690A692A694A696A722A724A726A728A72AA72CA72EA732A734A736A738A73AA73CA73EA740A742A744A746A748A74AA74CA74EA750A752A754A756A758A75AA75CA75EA760A762A764A766A768A76AA76CA76EA779A77BA77DA77EA780A782A784A786A78BFF21-FF3A", Lt: "01C501C801CB01F21F88-1F8F1F98-1F9F1FA8-1FAF1FBC1FCC1FFC", Lm: "02B0-02C102C6-02D102E0-02E402EC02EE0374037A0559064006E506E607F407F507FA081A0824082809710E460EC610FC17D718431AA71C78-1C7D1D2C-1D611D781D9B-1DBF2071207F2090-20942C7D2D6F2E2F30053031-3035303B309D309E30FC-30FEA015A4F8-A4FDA60CA67FA717-A71FA770A788A9CFAA70AADDFF70FF9EFF9F", Lo: "01BB01C0-01C3029405D0-05EA05F0-05F20621-063F0641-064A066E066F0671-06D306D506EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA0800-08150904-0939093D09500958-096109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E450E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10D0-10FA1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317DC1820-18421844-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C771CE9-1CEC1CEE-1CF12135-21382D30-2D652D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE3006303C3041-3096309F30A1-30FA30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A014A016-A48CA4D0-A4F7A500-A60BA610-A61FA62AA62BA66EA6A0-A6E5A7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2AA00-AA28AA40-AA42AA44-AA4BAA60-AA6FAA71-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADBAADCABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF66-FF6FFF71-FF9DFFA0-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC", M: "0300-036F0483-04890591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DE-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0903093C093E-094E0951-0955096209630981-098309BC09BE-09C409C709C809CB-09CD09D709E209E30A01-0A030A3C0A3E-0A420A470A480A4B-0A4D0A510A700A710A750A81-0A830ABC0ABE-0AC50AC7-0AC90ACB-0ACD0AE20AE30B01-0B030B3C0B3E-0B440B470B480B4B-0B4D0B560B570B620B630B820BBE-0BC20BC6-0BC80BCA-0BCD0BD70C01-0C030C3E-0C440C46-0C480C4A-0C4D0C550C560C620C630C820C830CBC0CBE-0CC40CC6-0CC80CCA-0CCD0CD50CD60CE20CE30D020D030D3E-0D440D46-0D480D4A-0D4D0D570D620D630D820D830DCA0DCF-0DD40DD60DD8-0DDF0DF20DF30E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F3E0F3F0F71-0F840F860F870F90-0F970F99-0FBC0FC6102B-103E1056-1059105E-10601062-10641067-106D1071-10741082-108D108F109A-109D135F1712-17141732-1734175217531772177317B6-17D317DD180B-180D18A91920-192B1930-193B19B0-19C019C819C91A17-1A1B1A55-1A5E1A60-1A7C1A7F1B00-1B041B34-1B441B6B-1B731B80-1B821BA1-1BAA1C24-1C371CD0-1CD21CD4-1CE81CED1CF21DC0-1DE61DFD-1DFF20D0-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66F-A672A67CA67DA6F0A6F1A802A806A80BA823-A827A880A881A8B4-A8C4A8E0-A8F1A926-A92DA947-A953A980-A983A9B3-A9C0AA29-AA36AA43AA4CAA4DAA7BAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE3-ABEAABECABEDFB1EFE00-FE0FFE20-FE26", Mn: "0300-036F0483-04870591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DF-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0902093C0941-0948094D0951-095509620963098109BC09C1-09C409CD09E209E30A010A020A3C0A410A420A470A480A4B-0A4D0A510A700A710A750A810A820ABC0AC1-0AC50AC70AC80ACD0AE20AE30B010B3C0B3F0B41-0B440B4D0B560B620B630B820BC00BCD0C3E-0C400C46-0C480C4A-0C4D0C550C560C620C630CBC0CBF0CC60CCC0CCD0CE20CE30D41-0D440D4D0D620D630DCA0DD2-0DD40DD60E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F71-0F7E0F80-0F840F860F870F90-0F970F99-0FBC0FC6102D-10301032-10371039103A103D103E10581059105E-10601071-1074108210851086108D109D135F1712-17141732-1734175217531772177317B7-17BD17C617C9-17D317DD180B-180D18A91920-19221927192819321939-193B1A171A181A561A58-1A5E1A601A621A65-1A6C1A73-1A7C1A7F1B00-1B031B341B36-1B3A1B3C1B421B6B-1B731B801B811BA2-1BA51BA81BA91C2C-1C331C361C371CD0-1CD21CD4-1CE01CE2-1CE81CED1DC0-1DE61DFD-1DFF20D0-20DC20E120E5-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66FA67CA67DA6F0A6F1A802A806A80BA825A826A8C4A8E0-A8F1A926-A92DA947-A951A980-A982A9B3A9B6-A9B9A9BCAA29-AA2EAA31AA32AA35AA36AA43AA4CAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE5ABE8ABEDFB1EFE00-FE0FFE20-FE26", Mc: "0903093E-09400949-094C094E0982098309BE-09C009C709C809CB09CC09D70A030A3E-0A400A830ABE-0AC00AC90ACB0ACC0B020B030B3E0B400B470B480B4B0B4C0B570BBE0BBF0BC10BC20BC6-0BC80BCA-0BCC0BD70C01-0C030C41-0C440C820C830CBE0CC0-0CC40CC70CC80CCA0CCB0CD50CD60D020D030D3E-0D400D46-0D480D4A-0D4C0D570D820D830DCF-0DD10DD8-0DDF0DF20DF30F3E0F3F0F7F102B102C10311038103B103C105610571062-10641067-106D108310841087-108C108F109A-109C17B617BE-17C517C717C81923-19261929-192B193019311933-193819B0-19C019C819C91A19-1A1B1A551A571A611A631A641A6D-1A721B041B351B3B1B3D-1B411B431B441B821BA11BA61BA71BAA1C24-1C2B1C341C351CE11CF2A823A824A827A880A881A8B4-A8C3A952A953A983A9B4A9B5A9BAA9BBA9BD-A9C0AA2FAA30AA33AA34AA4DAA7BABE3ABE4ABE6ABE7ABE9ABEAABEC", Me: "0488048906DE20DD-20E020E2-20E4A670-A672", N: "0030-003900B200B300B900BC-00BE0660-066906F0-06F907C0-07C90966-096F09E6-09EF09F4-09F90A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BF20C66-0C6F0C78-0C7E0CE6-0CEF0D66-0D750E50-0E590ED0-0ED90F20-0F331040-10491090-10991369-137C16EE-16F017E0-17E917F0-17F91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C5920702074-20792080-20892150-21822185-21892460-249B24EA-24FF2776-27932CFD30073021-30293038-303A3192-31953220-32293251-325F3280-328932B1-32BFA620-A629A6E6-A6EFA830-A835A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19", Nd: "0030-00390660-066906F0-06F907C0-07C90966-096F09E6-09EF0A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BEF0C66-0C6F0CE6-0CEF0D66-0D6F0E50-0E590ED0-0ED90F20-0F291040-10491090-109917E0-17E91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C59A620-A629A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19", Nl: "16EE-16F02160-21822185-218830073021-30293038-303AA6E6-A6EF", No: "00B200B300B900BC-00BE09F4-09F90BF0-0BF20C78-0C7E0D70-0D750F2A-0F331369-137C17F0-17F920702074-20792080-20892150-215F21892460-249B24EA-24FF2776-27932CFD3192-31953220-32293251-325F3280-328932B1-32BFA830-A835", P: "0021-00230025-002A002C-002F003A003B003F0040005B-005D005F007B007D00A100AB00B700BB00BF037E0387055A-055F0589058A05BE05C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F3A-0F3D0F850FD0-0FD4104A-104F10FB1361-13681400166D166E169B169C16EB-16ED1735173617D4-17D617D8-17DA1800-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD32010-20272030-20432045-20512053-205E207D207E208D208E2329232A2768-277527C527C627E6-27EF2983-299829D8-29DB29FC29FD2CF9-2CFC2CFE2CFF2E00-2E2E2E302E313001-30033008-30113014-301F3030303D30A030FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFD3EFD3FFE10-FE19FE30-FE52FE54-FE61FE63FE68FE6AFE6BFF01-FF03FF05-FF0AFF0C-FF0FFF1AFF1BFF1FFF20FF3B-FF3DFF3FFF5BFF5DFF5F-FF65", Pd: "002D058A05BE140018062010-20152E172E1A301C303030A0FE31FE32FE58FE63FF0D", Ps: "0028005B007B0F3A0F3C169B201A201E2045207D208D23292768276A276C276E27702772277427C527E627E827EA27EC27EE2983298529872989298B298D298F299129932995299729D829DA29FC2E222E242E262E283008300A300C300E3010301430163018301A301DFD3EFE17FE35FE37FE39FE3BFE3DFE3FFE41FE43FE47FE59FE5BFE5DFF08FF3BFF5BFF5FFF62", Pe: "0029005D007D0F3B0F3D169C2046207E208E232A2769276B276D276F27712773277527C627E727E927EB27ED27EF298429862988298A298C298E2990299229942996299829D929DB29FD2E232E252E272E293009300B300D300F3011301530173019301B301E301FFD3FFE18FE36FE38FE3AFE3CFE3EFE40FE42FE44FE48FE5AFE5CFE5EFF09FF3DFF5DFF60FF63", Pi: "00AB2018201B201C201F20392E022E042E092E0C2E1C2E20", Pf: "00BB2019201D203A2E032E052E0A2E0D2E1D2E21", Pc: "005F203F20402054FE33FE34FE4D-FE4FFF3F", Po: "0021-00230025-0027002A002C002E002F003A003B003F0040005C00A100B700BF037E0387055A-055F058905C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F850FD0-0FD4104A-104F10FB1361-1368166D166E16EB-16ED1735173617D4-17D617D8-17DA1800-18051807-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD3201620172020-20272030-2038203B-203E2041-20432047-205120532055-205E2CF9-2CFC2CFE2CFF2E002E012E06-2E082E0B2E0E-2E162E182E192E1B2E1E2E1F2E2A-2E2E2E302E313001-3003303D30FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFE10-FE16FE19FE30FE45FE46FE49-FE4CFE50-FE52FE54-FE57FE5F-FE61FE68FE6AFE6BFF01-FF03FF05-FF07FF0AFF0CFF0EFF0FFF1AFF1BFF1FFF20FF3CFF61FF64FF65", S: "0024002B003C-003E005E0060007C007E00A2-00A900AC00AE-00B100B400B600B800D700F702C2-02C502D2-02DF02E5-02EB02ED02EF-02FF03750384038503F604820606-0608060B060E060F06E906FD06FE07F609F209F309FA09FB0AF10B700BF3-0BFA0C7F0CF10CF20D790E3F0F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-139917DB194019E0-19FF1B61-1B6A1B74-1B7C1FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE20442052207A-207C208A-208C20A0-20B8210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B2140-2144214A-214D214F2190-2328232B-23E82400-24262440-244A249C-24E92500-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE27C0-27C427C7-27CA27CC27D0-27E527F0-29822999-29D729DC-29FB29FE-2B4C2B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F309B309C319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A700-A716A720A721A789A78AA828-A82BA836-A839AA77-AA79FB29FDFCFDFDFE62FE64-FE66FE69FF04FF0BFF1C-FF1EFF3EFF40FF5CFF5EFFE0-FFE6FFE8-FFEEFFFCFFFD", Sm: "002B003C-003E007C007E00AC00B100D700F703F60606-060820442052207A-207C208A-208C2140-2144214B2190-2194219A219B21A021A321A621AE21CE21CF21D221D421F4-22FF2308-230B23202321237C239B-23B323DC-23E125B725C125F8-25FF266F27C0-27C427C7-27CA27CC27D0-27E527F0-27FF2900-29822999-29D729DC-29FB29FE-2AFF2B30-2B442B47-2B4CFB29FE62FE64-FE66FF0BFF1C-FF1EFF5CFF5EFFE2FFE9-FFEC", Sc: "002400A2-00A5060B09F209F309FB0AF10BF90E3F17DB20A0-20B8A838FDFCFE69FF04FFE0FFE1FFE5FFE6", Sk: "005E006000A800AF00B400B802C2-02C502D2-02DF02E5-02EB02ED02EF-02FF0375038403851FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE309B309CA700-A716A720A721A789A78AFF3EFF40FFE3", So: "00A600A700A900AE00B000B60482060E060F06E906FD06FE07F609FA0B700BF3-0BF80BFA0C7F0CF10CF20D790F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-1399194019E0-19FF1B61-1B6A1B74-1B7C210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B214A214C214D214F2195-2199219C-219F21A121A221A421A521A7-21AD21AF-21CD21D021D121D321D5-21F32300-2307230C-231F2322-2328232B-237B237D-239A23B4-23DB23E2-23E82400-24262440-244A249C-24E92500-25B625B8-25C025C2-25F72600-266E2670-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE2800-28FF2B00-2B2F2B452B462B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A828-A82BA836A837A839AA77-AA79FDFDFFE4FFE8FFEDFFEEFFFCFFFD", Z: "002000A01680180E2000-200A20282029202F205F3000", Zs: "002000A01680180E2000-200A202F205F3000", Zl: "2028", Zp: "2029", C: "0000-001F007F-009F00AD03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-0605061C061D0620065F06DD070E070F074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17B417B517DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF200B-200F202A-202E2060-206F20722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-F8FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFD-FF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFFBFFFEFFFF", Cc: "0000-001F007F-009F", Cf: "00AD0600-060306DD070F17B417B5200B-200F202A-202E2060-2064206A-206FFEFFFFF9-FFFB", Co: "E000-F8FF", Cs: "D800-DFFF", Cn: "03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-05FF06040605061C061D0620065F070E074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF2065-206920722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-D7FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFDFEFEFF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFF8FFFEFFFF" }); function addUnicodePackage (pack) { var codePoint = /\w{4}/g; for (var name in pack) exports.packages[name] = pack[name].replace(codePoint, "\\u$&"); } }); define("ace/mode/text",["require","exports","module","ace/tokenizer","ace/mode/text_highlight_rules","ace/mode/behaviour/cstyle","ace/unicode","ace/lib/lang","ace/token_iterator","ace/range"], function(require, exports, module) { "use strict"; var Tokenizer = require("../tokenizer").Tokenizer; var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; var CstyleBehaviour = require("./behaviour/cstyle").CstyleBehaviour; var unicode = require("../unicode"); var lang = require("../lib/lang"); var TokenIterator = require("../token_iterator").TokenIterator; var Range = require("../range").Range; var Mode = function() { this.HighlightRules = TextHighlightRules; }; (function() { this.$defaultBehaviour = new CstyleBehaviour(); this.tokenRe = new RegExp("^[" + unicode.packages.L + unicode.packages.Mn + unicode.packages.Mc + unicode.packages.Nd + unicode.packages.Pc + "\\$_]+", "g" ); this.nonTokenRe = new RegExp("^(?:[^" + unicode.packages.L + unicode.packages.Mn + unicode.packages.Mc + unicode.packages.Nd + unicode.packages.Pc + "\\$_]|\\s])+", "g" ); this.getTokenizer = function() { if (!this.$tokenizer) { this.$highlightRules = this.$highlightRules || new this.HighlightRules(this.$highlightRuleConfig); this.$tokenizer = new Tokenizer(this.$highlightRules.getRules()); } return this.$tokenizer; }; this.lineCommentStart = ""; this.blockComment = ""; this.toggleCommentLines = function(state, session, startRow, endRow) { var doc = session.doc; var ignoreBlankLines = true; var shouldRemove = true; var minIndent = Infinity; var tabSize = session.getTabSize(); var insertAtTabStop = false; if (!this.lineCommentStart) { if (!this.blockComment) return false; var lineCommentStart = this.blockComment.start; var lineCommentEnd = this.blockComment.end; var regexpStart = new RegExp("^(\\s*)(?:" + lang.escapeRegExp(lineCommentStart) + ")"); var regexpEnd = new RegExp("(?:" + lang.escapeRegExp(lineCommentEnd) + ")\\s*$"); var comment = function(line, i) { if (testRemove(line, i)) return; if (!ignoreBlankLines || /\S/.test(line)) { doc.insertInLine({row: i, column: line.length}, lineCommentEnd); doc.insertInLine({row: i, column: minIndent}, lineCommentStart); } }; var uncomment = function(line, i) { var m; if (m = line.match(regexpEnd)) doc.removeInLine(i, line.length - m[0].length, line.length); if (m = line.match(regexpStart)) doc.removeInLine(i, m[1].length, m[0].length); }; var testRemove = function(line, row) { if (regexpStart.test(line)) return true; var tokens = session.getTokens(row); for (var i = 0; i < tokens.length; i++) { if (tokens[i].type === "comment") return true; } }; } else { if (Array.isArray(this.lineCommentStart)) { var regexpStart = this.lineCommentStart.map(lang.escapeRegExp).join("|"); var lineCommentStart = this.lineCommentStart[0]; } else { var regexpStart = lang.escapeRegExp(this.lineCommentStart); var lineCommentStart = this.lineCommentStart; } regexpStart = new RegExp("^(\\s*)(?:" + regexpStart + ") ?"); insertAtTabStop = session.getUseSoftTabs(); var uncomment = function(line, i) { var m = line.match(regexpStart); if (!m) return; var start = m[1].length, end = m[0].length; if (!shouldInsertSpace(line, start, end) && m[0][end - 1] == " ") end--; doc.removeInLine(i, start, end); }; var commentWithSpace = lineCommentStart + " "; var comment = function(line, i) { if (!ignoreBlankLines || /\S/.test(line)) { if (shouldInsertSpace(line, minIndent, minIndent)) doc.insertInLine({row: i, column: minIndent}, commentWithSpace); else doc.insertInLine({row: i, column: minIndent}, lineCommentStart); } }; var testRemove = function(line, i) { return regexpStart.test(line); }; var shouldInsertSpace = function(line, before, after) { var spaces = 0; while (before-- && line.charAt(before) == " ") spaces++; if (spaces % tabSize != 0) return false; var spaces = 0; while (line.charAt(after++) == " ") spaces++; if (tabSize > 2) return spaces % tabSize != tabSize - 1; else return spaces % tabSize == 0; return true; }; } function iter(fun) { for (var i = startRow; i <= endRow; i++) fun(doc.getLine(i), i); } var minEmptyLength = Infinity; iter(function(line, i) { var indent = line.search(/\S/); if (indent !== -1) { if (indent < minIndent) minIndent = indent; if (shouldRemove && !testRemove(line, i)) shouldRemove = false; } else if (minEmptyLength > line.length) { minEmptyLength = line.length; } }); if (minIndent == Infinity) { minIndent = minEmptyLength; ignoreBlankLines = false; shouldRemove = false; } if (insertAtTabStop && minIndent % tabSize != 0) minIndent = Math.floor(minIndent / tabSize) * tabSize; iter(shouldRemove ? uncomment : comment); }; this.toggleBlockComment = function(state, session, range, cursor) { var comment = this.blockComment; if (!comment) return; if (!comment.start && comment[0]) comment = comment[0]; var iterator = new TokenIterator(session, cursor.row, cursor.column); var token = iterator.getCurrentToken(); var sel = session.selection; var initialRange = session.selection.toOrientedRange(); var startRow, colDiff; if (token && /comment/.test(token.type)) { var startRange, endRange; while (token && /comment/.test(token.type)) { var i = token.value.indexOf(comment.start); if (i != -1) { var row = iterator.getCurrentTokenRow(); var column = iterator.getCurrentTokenColumn() + i; startRange = new Range(row, column, row, column + comment.start.length); break; } token = iterator.stepBackward(); } var iterator = new TokenIterator(session, cursor.row, cursor.column); var token = iterator.getCurrentToken(); while (token && /comment/.test(token.type)) { var i = token.value.indexOf(comment.end); if (i != -1) { var row = iterator.getCurrentTokenRow(); var column = iterator.getCurrentTokenColumn() + i; endRange = new Range(row, column, row, column + comment.end.length); break; } token = iterator.stepForward(); } if (endRange) session.remove(endRange); if (startRange) { session.remove(startRange); startRow = startRange.start.row; colDiff = -comment.start.length; } } else { colDiff = comment.start.length; startRow = range.start.row; session.insert(range.end, comment.end); session.insert(range.start, comment.start); } if (initialRange.start.row == startRow) initialRange.start.column += colDiff; if (initialRange.end.row == startRow) initialRange.end.column += colDiff; session.selection.fromOrientedRange(initialRange); }; this.getNextLineIndent = function(state, line, tab) { return this.$getIndent(line); }; this.checkOutdent = function(state, line, input) { return false; }; this.autoOutdent = function(state, doc, row) { }; this.$getIndent = function(line) { return line.match(/^\s*/)[0]; }; this.createWorker = function(session) { return null; }; this.createModeDelegates = function (mapping) { this.$embeds = []; this.$modes = {}; for (var i in mapping) { if (mapping[i]) { this.$embeds.push(i); this.$modes[i] = new mapping[i](); } } var delegations = ["toggleBlockComment", "toggleCommentLines", "getNextLineIndent", "checkOutdent", "autoOutdent", "transformAction", "getCompletions"]; for (var i = 0; i < delegations.length; i++) { (function(scope) { var functionName = delegations[i]; var defaultHandler = scope[functionName]; scope[delegations[i]] = function() { return this.$delegator(functionName, arguments, defaultHandler); }; }(this)); } }; this.$delegator = function(method, args, defaultHandler) { var state = args[0]; if (typeof state != "string") state = state[0]; for (var i = 0; i < this.$embeds.length; i++) { if (!this.$modes[this.$embeds[i]]) continue; var split = state.split(this.$embeds[i]); if (!split[0] && split[1]) { args[0] = split[1]; var mode = this.$modes[this.$embeds[i]]; return mode[method].apply(mode, args); } } var ret = defaultHandler.apply(this, args); return defaultHandler ? ret : undefined; }; this.transformAction = function(state, action, editor, session, param) { if (this.$behaviour) { var behaviours = this.$behaviour.getBehaviours(); for (var key in behaviours) { if (behaviours[key][action]) { var ret = behaviours[key][action].apply(this, arguments); if (ret) { return ret; } } } } }; this.getKeywords = function(append) { if (!this.completionKeywords) { var rules = this.$tokenizer.rules; var completionKeywords = []; for (var rule in rules) { var ruleItr = rules[rule]; for (var r = 0, l = ruleItr.length; r < l; r++) { if (typeof ruleItr[r].token === "string") { if (/keyword|support|storage/.test(ruleItr[r].token)) completionKeywords.push(ruleItr[r].regex); } else if (typeof ruleItr[r].token === "object") { for (var a = 0, aLength = ruleItr[r].token.length; a < aLength; a++) { if (/keyword|support|storage/.test(ruleItr[r].token[a])) { var rule = ruleItr[r].regex.match(/\(.+?\)/g)[a]; completionKeywords.push(rule.substr(1, rule.length - 2)); } } } } } this.completionKeywords = completionKeywords; } if (!append) return this.$keywordList; return completionKeywords.concat(this.$keywordList || []); }; this.$createKeywordList = function() { if (!this.$highlightRules) this.getTokenizer(); return this.$keywordList = this.$highlightRules.$keywordList || []; }; this.getCompletions = function(state, session, pos, prefix) { var keywords = this.$keywordList || this.$createKeywordList(); return keywords.map(function(word) { return { name: word, value: word, score: 0, meta: "keyword" }; }); }; this.$id = "ace/mode/text"; }).call(Mode.prototype); exports.Mode = Mode; }); define("ace/apply_delta",["require","exports","module"], function(require, exports, module) { "use strict"; function throwDeltaError(delta, errorText){ console.log("Invalid Delta:", delta); throw "Invalid Delta: " + errorText; } function positionInDocument(docLines, position) { return position.row >= 0 && position.row < docLines.length && position.column >= 0 && position.column <= docLines[position.row].length; } function validateDelta(docLines, delta) { if (delta.action != "insert" && delta.action != "remove") throwDeltaError(delta, "delta.action must be 'insert' or 'remove'"); if (!(delta.lines instanceof Array)) throwDeltaError(delta, "delta.lines must be an Array"); if (!delta.start || !delta.end) throwDeltaError(delta, "delta.start/end must be an present"); var start = delta.start; if (!positionInDocument(docLines, delta.start)) throwDeltaError(delta, "delta.start must be contained in document"); var end = delta.end; if (delta.action == "remove" && !positionInDocument(docLines, end)) throwDeltaError(delta, "delta.end must contained in document for 'remove' actions"); var numRangeRows = end.row - start.row; var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0)); if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars) throwDeltaError(delta, "delta.range must match delta lines"); } exports.applyDelta = function(docLines, delta, doNotValidate) { var row = delta.start.row; var startColumn = delta.start.column; var line = docLines[row] || ""; switch (delta.action) { case "insert": var lines = delta.lines; if (lines.length === 1) { docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); } else { var args = [row, 1].concat(delta.lines); docLines.splice.apply(docLines, args); docLines[row] = line.substring(0, startColumn) + docLines[row]; docLines[row + delta.lines.length - 1] += line.substring(startColumn); } break; case "remove": var endColumn = delta.end.column; var endRow = delta.end.row; if (row === endRow) { docLines[row] = line.substring(0, startColumn) + line.substring(endColumn); } else { docLines.splice( row, endRow - row + 1, line.substring(0, startColumn) + docLines[endRow].substring(endColumn) ); } break; } } }); define("ace/anchor",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var EventEmitter = require("./lib/event_emitter").EventEmitter; var Anchor = exports.Anchor = function(doc, row, column) { this.$onChange = this.onChange.bind(this); this.attach(doc); if (typeof column == "undefined") this.setPosition(row.row, row.column); else this.setPosition(row, column); }; (function() { oop.implement(this, EventEmitter); this.getPosition = function() { return this.$clipPositionToDocument(this.row, this.column); }; this.getDocument = function() { return this.document; }; this.$insertRight = false; this.onChange = function(delta) { if (delta.start.row == delta.end.row && delta.start.row != this.row) return; if (delta.start.row > this.row) return; var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); this.setPosition(point.row, point.column, true); }; function $pointsInOrder(point1, point2, equalPointsInOrder) { var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); } function $getTransformedPoint(delta, point, moveIfEqual) { var deltaIsInsert = delta.action == "insert"; var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column); var deltaStart = delta.start; var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range. if ($pointsInOrder(point, deltaStart, moveIfEqual)) { return { row: point.row, column: point.column }; } if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) { return { row: point.row + deltaRowShift, column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) }; } return { row: deltaStart.row, column: deltaStart.column }; } this.setPosition = function(row, column, noClip) { var pos; if (noClip) { pos = { row: row, column: column }; } else { pos = this.$clipPositionToDocument(row, column); } if (this.row == pos.row && this.column == pos.column) return; var old = { row: this.row, column: this.column }; this.row = pos.row; this.column = pos.column; this._signal("change", { old: old, value: pos }); }; this.detach = function() { this.document.removeEventListener("change", this.$onChange); }; this.attach = function(doc) { this.document = doc || this.document; this.document.on("change", this.$onChange); }; this.$clipPositionToDocument = function(row, column) { var pos = {}; if (row >= this.document.getLength()) { pos.row = Math.max(0, this.document.getLength() - 1); pos.column = this.document.getLine(pos.row).length; } else if (row < 0) { pos.row = 0; pos.column = 0; } else { pos.row = row; pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column)); } if (column < 0) pos.column = 0; return pos; }; }).call(Anchor.prototype); }); define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var applyDelta = require("./apply_delta").applyDelta; var EventEmitter = require("./lib/event_emitter").EventEmitter; var Range = require("./range").Range; var Anchor = require("./anchor").Anchor; var Document = function(textOrLines) { this.$lines = [""]; if (textOrLines.length === 0) { this.$lines = [""]; } else if (Array.isArray(textOrLines)) { this.insertMergedLines({row: 0, column: 0}, textOrLines); } else { this.insert({row: 0, column:0}, textOrLines); } }; (function() { oop.implement(this, EventEmitter); this.setValue = function(text) { var len = this.getLength() - 1; this.remove(new Range(0, 0, len, this.getLine(len).length)); this.insert({row: 0, column: 0}, text); }; this.getValue = function() { return this.getAllLines().join(this.getNewLineCharacter()); }; this.createAnchor = function(row, column) { return new Anchor(this, row, column); }; if ("aaa".split(/a/).length === 0) { this.$split = function(text) { return text.replace(/\r\n|\r/g, "\n").split("\n"); }; } else { this.$split = function(text) { return text.split(/\r\n|\r|\n/); }; } this.$detectNewLine = function(text) { var match = text.match(/^.*?(\r\n|\r|\n)/m); this.$autoNewLine = match ? match[1] : "\n"; this._signal("changeNewLineMode"); }; this.getNewLineCharacter = function() { switch (this.$newLineMode) { case "windows": return "\r\n"; case "unix": return "\n"; default: return this.$autoNewLine || "\n"; } }; this.$autoNewLine = ""; this.$newLineMode = "auto"; this.setNewLineMode = function(newLineMode) { if (this.$newLineMode === newLineMode) return; this.$newLineMode = newLineMode; this._signal("changeNewLineMode"); }; this.getNewLineMode = function() { return this.$newLineMode; }; this.isNewLine = function(text) { return (text == "\r\n" || text == "\r" || text == "\n"); }; this.getLine = function(row) { return this.$lines[row] || ""; }; this.getLines = function(firstRow, lastRow) { return this.$lines.slice(firstRow, lastRow + 1); }; this.getAllLines = function() { return this.getLines(0, this.getLength()); }; this.getLength = function() { return this.$lines.length; }; this.getTextRange = function(range) { return this.getLinesForRange(range).join(this.getNewLineCharacter()); }; this.getLinesForRange = function(range) { var lines; if (range.start.row === range.end.row) { lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; } else { lines = this.getLines(range.start.row, range.end.row); lines[0] = (lines[0] || "").substring(range.start.column); var l = lines.length - 1; if (range.end.row - range.start.row == l) lines[l] = lines[l].substring(0, range.end.column); } return lines; }; this.insertLines = function(row, lines) { console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."); return this.insertFullLines(row, lines); }; this.removeLines = function(firstRow, lastRow) { console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."); return this.removeFullLines(firstRow, lastRow); }; this.insertNewLine = function(position) { console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."); return this.insertMergedLines(position, ["", ""]); }; this.insert = function(position, text) { if (this.getLength() <= 1) this.$detectNewLine(text); return this.insertMergedLines(position, this.$split(text)); }; this.insertInLine = function(position, text) { var start = this.clippedPos(position.row, position.column); var end = this.pos(position.row, position.column + text.length); this.applyDelta({ start: start, end: end, action: "insert", lines: [text] }, true); return this.clonePos(end); }; this.clippedPos = function(row, column) { var length = this.getLength(); if (row === undefined) { row = length; } else if (row < 0) { row = 0; } else if (row >= length) { row = length - 1; column = undefined; } var line = this.getLine(row); if (column == undefined) column = line.length; column = Math.min(Math.max(column, 0), line.length); return {row: row, column: column}; }; this.clonePos = function(pos) { return {row: pos.row, column: pos.column}; }; this.pos = function(row, column) { return {row: row, column: column}; }; this.$clipPosition = function(position) { var length = this.getLength(); if (position.row >= length) { position.row = Math.max(0, length - 1); position.column = this.getLine(length - 1).length; } else { position.row = Math.max(0, position.row); position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length); } return position; }; this.insertFullLines = function(row, lines) { row = Math.min(Math.max(row, 0), this.getLength()); var column = 0; if (row < this.getLength()) { lines = lines.concat([""]); column = 0; } else { lines = [""].concat(lines); row--; column = this.$lines[row].length; } this.insertMergedLines({row: row, column: column}, lines); }; this.insertMergedLines = function(position, lines) { var start = this.clippedPos(position.row, position.column); var end = { row: start.row + lines.length - 1, column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length }; this.applyDelta({ start: start, end: end, action: "insert", lines: lines }); return this.clonePos(end); }; this.remove = function(range) { var start = this.clippedPos(range.start.row, range.start.column); var end = this.clippedPos(range.end.row, range.end.column); this.applyDelta({ start: start, end: end, action: "remove", lines: this.getLinesForRange({start: start, end: end}) }); return this.clonePos(start); }; this.removeInLine = function(row, startColumn, endColumn) { var start = this.clippedPos(row, startColumn); var end = this.clippedPos(row, endColumn); this.applyDelta({ start: start, end: end, action: "remove", lines: this.getLinesForRange({start: start, end: end}) }, true); return this.clonePos(start); }; this.removeFullLines = function(firstRow, lastRow) { firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1); lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1); var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0; var deleteLastNewLine = lastRow < this.getLength() - 1; var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); var range = new Range(startRow, startCol, endRow, endCol); var deletedLines = this.$lines.slice(firstRow, lastRow + 1); this.applyDelta({ start: range.start, end: range.end, action: "remove", lines: this.getLinesForRange(range) }); return deletedLines; }; this.removeNewLine = function(row) { if (row < this.getLength() - 1 && row >= 0) { this.applyDelta({ start: this.pos(row, this.getLine(row).length), end: this.pos(row + 1, 0), action: "remove", lines: ["", ""] }); } }; this.replace = function(range, text) { if (!(range instanceof Range)) range = Range.fromPoints(range.start, range.end); if (text.length === 0 && range.isEmpty()) return range.start; if (text == this.getTextRange(range)) return range.end; this.remove(range); var end; if (text) { end = this.insert(range.start, text); } else { end = range.start; } return end; }; this.applyDeltas = function(deltas) { for (var i=0; i=0; i--) { this.revertDelta(deltas[i]); } }; this.applyDelta = function(delta, doNotValidate) { var isInsert = delta.action == "insert"; if (isInsert ? delta.lines.length <= 1 && !delta.lines[0] : !Range.comparePoints(delta.start, delta.end)) { return; } if (isInsert && delta.lines.length > 20000) this.$splitAndapplyLargeDelta(delta, 20000); applyDelta(this.$lines, delta, doNotValidate); this._signal("change", delta); }; this.$splitAndapplyLargeDelta = function(delta, MAX) { var lines = delta.lines; var l = lines.length; var row = delta.start.row; var column = delta.start.column; var from = 0, to = 0; do { from = to; to += MAX - 1; var chunk = lines.slice(from, to); if (to > l) { delta.lines = chunk; delta.start.row = row + from; delta.start.column = column; break; } chunk.push(""); this.applyDelta({ start: this.pos(row + from, column), end: this.pos(row + to, column = 0), action: delta.action, lines: chunk }, true); } while(true); }; this.revertDelta = function(delta) { this.applyDelta({ start: this.clonePos(delta.start), end: this.clonePos(delta.end), action: (delta.action == "insert" ? "remove" : "insert"), lines: delta.lines.slice() }); }; this.indexToPosition = function(index, startRow) { var lines = this.$lines || this.getAllLines(); var newlineLength = this.getNewLineCharacter().length; for (var i = startRow || 0, l = lines.length; i < l; i++) { index -= lines[i].length + newlineLength; if (index < 0) return {row: i, column: index + lines[i].length + newlineLength}; } return {row: l-1, column: lines[l-1].length}; }; this.positionToIndex = function(pos, startRow) { var lines = this.$lines || this.getAllLines(); var newlineLength = this.getNewLineCharacter().length; var index = 0; var row = Math.min(pos.row, lines.length); for (var i = startRow || 0; i < row; ++i) index += lines[i].length + newlineLength; return index + pos.column; }; }).call(Document.prototype); exports.Document = Document; }); define("ace/background_tokenizer",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var EventEmitter = require("./lib/event_emitter").EventEmitter; var BackgroundTokenizer = function(tokenizer, editor) { this.running = false; this.lines = []; this.states = []; this.currentLine = 0; this.tokenizer = tokenizer; var self = this; this.$worker = function() { if (!self.running) { return; } var workerStart = new Date(); var currentLine = self.currentLine; var endLine = -1; var doc = self.doc; var startLine = currentLine; while (self.lines[currentLine]) currentLine++; var len = doc.getLength(); var processedLines = 0; self.running = false; while (currentLine < len) { self.$tokenizeRow(currentLine); endLine = currentLine; do { currentLine++; } while (self.lines[currentLine]); processedLines ++; if ((processedLines % 5 === 0) && (new Date() - workerStart) > 20) { self.running = setTimeout(self.$worker, 20); break; } } self.currentLine = currentLine; if (startLine <= endLine) self.fireUpdateEvent(startLine, endLine); }; }; (function(){ oop.implement(this, EventEmitter); this.setTokenizer = function(tokenizer) { this.tokenizer = tokenizer; this.lines = []; this.states = []; this.start(0); }; this.setDocument = function(doc) { this.doc = doc; this.lines = []; this.states = []; this.stop(); }; this.fireUpdateEvent = function(firstRow, lastRow) { var data = { first: firstRow, last: lastRow }; this._signal("update", {data: data}); }; this.start = function(startRow) { this.currentLine = Math.min(startRow || 0, this.currentLine, this.doc.getLength()); this.lines.splice(this.currentLine, this.lines.length); this.states.splice(this.currentLine, this.states.length); this.stop(); this.running = setTimeout(this.$worker, 700); }; this.scheduleStart = function() { if (!this.running) this.running = setTimeout(this.$worker, 700); } this.$updateOnChange = function(delta) { var startRow = delta.start.row; var len = delta.end.row - startRow; if (len === 0) { this.lines[startRow] = null; } else if (delta.action == "remove") { this.lines.splice(startRow, len + 1, null); this.states.splice(startRow, len + 1, null); } else { var args = Array(len + 1); args.unshift(startRow, 1); this.lines.splice.apply(this.lines, args); this.states.splice.apply(this.states, args); } this.currentLine = Math.min(startRow, this.currentLine, this.doc.getLength()); this.stop(); }; this.stop = function() { if (this.running) clearTimeout(this.running); this.running = false; }; this.getTokens = function(row) { return this.lines[row] || this.$tokenizeRow(row); }; this.getState = function(row) { if (this.currentLine == row) this.$tokenizeRow(row); return this.states[row] || "start"; }; this.$tokenizeRow = function(row) { var line = this.doc.getLine(row); var state = this.states[row - 1]; var data = this.tokenizer.getLineTokens(line, state, row); if (this.states[row] + "" !== data.state + "") { this.states[row] = data.state; this.lines[row + 1] = null; if (this.currentLine > row + 1) this.currentLine = row + 1; } else if (this.currentLine == row) { this.currentLine = row + 1; } return this.lines[row] = data.tokens; }; }).call(BackgroundTokenizer.prototype); exports.BackgroundTokenizer = BackgroundTokenizer; }); define("ace/search_highlight",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"], function(require, exports, module) { "use strict"; var lang = require("./lib/lang"); var oop = require("./lib/oop"); var Range = require("./range").Range; var SearchHighlight = function(regExp, clazz, type) { this.setRegexp(regExp); this.clazz = clazz; this.type = type || "text"; }; (function() { this.MAX_RANGES = 500; this.setRegexp = function(regExp) { if (this.regExp+"" == regExp+"") return; this.regExp = regExp; this.cache = []; }; this.update = function(html, markerLayer, session, config) { if (!this.regExp) return; var start = config.firstRow, end = config.lastRow; for (var i = start; i <= end; i++) { var ranges = this.cache[i]; if (ranges == null) { ranges = lang.getMatchOffsets(session.getLine(i), this.regExp); if (ranges.length > this.MAX_RANGES) ranges = ranges.slice(0, this.MAX_RANGES); ranges = ranges.map(function(match) { return new Range(i, match.offset, i, match.offset + match.length); }); this.cache[i] = ranges.length ? ranges : ""; } for (var j = ranges.length; j --; ) { markerLayer.drawSingleLineMarker( html, ranges[j].toScreenRange(session), this.clazz, config); } } }; }).call(SearchHighlight.prototype); exports.SearchHighlight = SearchHighlight; }); define("ace/edit_session/fold_line",["require","exports","module","ace/range"], function(require, exports, module) { "use strict"; var Range = require("../range").Range; function FoldLine(foldData, folds) { this.foldData = foldData; if (Array.isArray(folds)) { this.folds = folds; } else { folds = this.folds = [ folds ]; } var last = folds[folds.length - 1]; this.range = new Range(folds[0].start.row, folds[0].start.column, last.end.row, last.end.column); this.start = this.range.start; this.end = this.range.end; this.folds.forEach(function(fold) { fold.setFoldLine(this); }, this); } (function() { this.shiftRow = function(shift) { this.start.row += shift; this.end.row += shift; this.folds.forEach(function(fold) { fold.start.row += shift; fold.end.row += shift; }); }; this.addFold = function(fold) { if (fold.sameRow) { if (fold.start.row < this.startRow || fold.endRow > this.endRow) { throw new Error("Can't add a fold to this FoldLine as it has no connection"); } this.folds.push(fold); this.folds.sort(function(a, b) { return -a.range.compareEnd(b.start.row, b.start.column); }); if (this.range.compareEnd(fold.start.row, fold.start.column) > 0) { this.end.row = fold.end.row; this.end.column = fold.end.column; } else if (this.range.compareStart(fold.end.row, fold.end.column) < 0) { this.start.row = fold.start.row; this.start.column = fold.start.column; } } else if (fold.start.row == this.end.row) { this.folds.push(fold); this.end.row = fold.end.row; this.end.column = fold.end.column; } else if (fold.end.row == this.start.row) { this.folds.unshift(fold); this.start.row = fold.start.row; this.start.column = fold.start.column; } else { throw new Error("Trying to add fold to FoldRow that doesn't have a matching row"); } fold.foldLine = this; }; this.containsRow = function(row) { return row >= this.start.row && row <= this.end.row; }; this.walk = function(callback, endRow, endColumn) { var lastEnd = 0, folds = this.folds, fold, cmp, stop, isNewRow = true; if (endRow == null) { endRow = this.end.row; endColumn = this.end.column; } for (var i = 0; i < folds.length; i++) { fold = folds[i]; cmp = fold.range.compareStart(endRow, endColumn); if (cmp == -1) { callback(null, endRow, endColumn, lastEnd, isNewRow); return; } stop = callback(null, fold.start.row, fold.start.column, lastEnd, isNewRow); stop = !stop && callback(fold.placeholder, fold.start.row, fold.start.column, lastEnd); if (stop || cmp === 0) { return; } isNewRow = !fold.sameRow; lastEnd = fold.end.column; } callback(null, endRow, endColumn, lastEnd, isNewRow); }; this.getNextFoldTo = function(row, column) { var fold, cmp; for (var i = 0; i < this.folds.length; i++) { fold = this.folds[i]; cmp = fold.range.compareEnd(row, column); if (cmp == -1) { return { fold: fold, kind: "after" }; } else if (cmp === 0) { return { fold: fold, kind: "inside" }; } } return null; }; this.addRemoveChars = function(row, column, len) { var ret = this.getNextFoldTo(row, column), fold, folds; if (ret) { fold = ret.fold; if (ret.kind == "inside" && fold.start.column != column && fold.start.row != row) { window.console && window.console.log(row, column, fold); } else if (fold.start.row == row) { folds = this.folds; var i = folds.indexOf(fold); if (i === 0) { this.start.column += len; } for (i; i < folds.length; i++) { fold = folds[i]; fold.start.column += len; if (!fold.sameRow) { return; } fold.end.column += len; } this.end.column += len; } } }; this.split = function(row, column) { var pos = this.getNextFoldTo(row, column); if (!pos || pos.kind == "inside") return null; var fold = pos.fold; var folds = this.folds; var foldData = this.foldData; var i = folds.indexOf(fold); var foldBefore = folds[i - 1]; this.end.row = foldBefore.end.row; this.end.column = foldBefore.end.column; folds = folds.splice(i, folds.length - i); var newFoldLine = new FoldLine(foldData, folds); foldData.splice(foldData.indexOf(this) + 1, 0, newFoldLine); return newFoldLine; }; this.merge = function(foldLineNext) { var folds = foldLineNext.folds; for (var i = 0; i < folds.length; i++) { this.addFold(folds[i]); } var foldData = this.foldData; foldData.splice(foldData.indexOf(foldLineNext), 1); }; this.toString = function() { var ret = [this.range.toString() + ": [" ]; this.folds.forEach(function(fold) { ret.push(" " + fold.toString()); }); ret.push("]"); return ret.join("\n"); }; this.idxToPosition = function(idx) { var lastFoldEndColumn = 0; for (var i = 0; i < this.folds.length; i++) { var fold = this.folds[i]; idx -= fold.start.column - lastFoldEndColumn; if (idx < 0) { return { row: fold.start.row, column: fold.start.column + idx }; } idx -= fold.placeholder.length; if (idx < 0) { return fold.start; } lastFoldEndColumn = fold.end.column; } return { row: this.end.row, column: this.end.column + idx }; }; }).call(FoldLine.prototype); exports.FoldLine = FoldLine; }); define("ace/range_list",["require","exports","module","ace/range"], function(require, exports, module) { "use strict"; var Range = require("./range").Range; var comparePoints = Range.comparePoints; var RangeList = function() { this.ranges = []; }; (function() { this.comparePoints = comparePoints; this.pointIndex = function(pos, excludeEdges, startIndex) { var list = this.ranges; for (var i = startIndex || 0; i < list.length; i++) { var range = list[i]; var cmpEnd = comparePoints(pos, range.end); if (cmpEnd > 0) continue; var cmpStart = comparePoints(pos, range.start); if (cmpEnd === 0) return excludeEdges && cmpStart !== 0 ? -i-2 : i; if (cmpStart > 0 || (cmpStart === 0 && !excludeEdges)) return i; return -i-1; } return -i - 1; }; this.add = function(range) { var excludeEdges = !range.isEmpty(); var startIndex = this.pointIndex(range.start, excludeEdges); if (startIndex < 0) startIndex = -startIndex - 1; var endIndex = this.pointIndex(range.end, excludeEdges, startIndex); if (endIndex < 0) endIndex = -endIndex - 1; else endIndex++; return this.ranges.splice(startIndex, endIndex - startIndex, range); }; this.addList = function(list) { var removed = []; for (var i = list.length; i--; ) { removed.push.apply(removed, this.add(list[i])); } return removed; }; this.substractPoint = function(pos) { var i = this.pointIndex(pos); if (i >= 0) return this.ranges.splice(i, 1); }; this.merge = function() { var removed = []; var list = this.ranges; list = list.sort(function(a, b) { return comparePoints(a.start, b.start); }); var next = list[0], range; for (var i = 1; i < list.length; i++) { range = next; next = list[i]; var cmp = comparePoints(range.end, next.start); if (cmp < 0) continue; if (cmp == 0 && !range.isEmpty() && !next.isEmpty()) continue; if (comparePoints(range.end, next.end) < 0) { range.end.row = next.end.row; range.end.column = next.end.column; } list.splice(i, 1); removed.push(next); next = range; i--; } this.ranges = list; return removed; }; this.contains = function(row, column) { return this.pointIndex({row: row, column: column}) >= 0; }; this.containsPoint = function(pos) { return this.pointIndex(pos) >= 0; }; this.rangeAtPoint = function(pos) { var i = this.pointIndex(pos); if (i >= 0) return this.ranges[i]; }; this.clipRows = function(startRow, endRow) { var list = this.ranges; if (list[0].start.row > endRow || list[list.length - 1].start.row < startRow) return []; var startIndex = this.pointIndex({row: startRow, column: 0}); if (startIndex < 0) startIndex = -startIndex - 1; var endIndex = this.pointIndex({row: endRow, column: 0}, startIndex); if (endIndex < 0) endIndex = -endIndex - 1; var clipped = []; for (var i = startIndex; i < endIndex; i++) { clipped.push(list[i]); } return clipped; }; this.removeAll = function() { return this.ranges.splice(0, this.ranges.length); }; this.attach = function(session) { if (this.session) this.detach(); this.session = session; this.onChange = this.$onChange.bind(this); this.session.on('change', this.onChange); }; this.detach = function() { if (!this.session) return; this.session.removeListener('change', this.onChange); this.session = null; }; this.$onChange = function(delta) { if (delta.action == "insert"){ var start = delta.start; var end = delta.end; } else { var end = delta.start; var start = delta.end; } var startRow = start.row; var endRow = end.row; var lineDif = endRow - startRow; var colDiff = -start.column + end.column; var ranges = this.ranges; for (var i = 0, n = ranges.length; i < n; i++) { var r = ranges[i]; if (r.end.row < startRow) continue; if (r.start.row > startRow) break; if (r.start.row == startRow && r.start.column >= start.column ) { if (r.start.column == start.column && this.$insertRight) { } else { r.start.column += colDiff; r.start.row += lineDif; } } if (r.end.row == startRow && r.end.column >= start.column) { if (r.end.column == start.column && this.$insertRight) { continue; } if (r.end.column == start.column && colDiff > 0 && i < n - 1) { if (r.end.column > r.start.column && r.end.column == ranges[i+1].start.column) r.end.column -= colDiff; } r.end.column += colDiff; r.end.row += lineDif; } } if (lineDif != 0 && i < n) { for (; i < n; i++) { var r = ranges[i]; r.start.row += lineDif; r.end.row += lineDif; } } }; }).call(RangeList.prototype); exports.RangeList = RangeList; }); define("ace/edit_session/fold",["require","exports","module","ace/range","ace/range_list","ace/lib/oop"], function(require, exports, module) { "use strict"; var Range = require("../range").Range; var RangeList = require("../range_list").RangeList; var oop = require("../lib/oop") var Fold = exports.Fold = function(range, placeholder) { this.foldLine = null; this.placeholder = placeholder; this.range = range; this.start = range.start; this.end = range.end; this.sameRow = range.start.row == range.end.row; this.subFolds = this.ranges = []; }; oop.inherits(Fold, RangeList); (function() { this.toString = function() { return '"' + this.placeholder + '" ' + this.range.toString(); }; this.setFoldLine = function(foldLine) { this.foldLine = foldLine; this.subFolds.forEach(function(fold) { fold.setFoldLine(foldLine); }); }; this.clone = function() { var range = this.range.clone(); var fold = new Fold(range, this.placeholder); this.subFolds.forEach(function(subFold) { fold.subFolds.push(subFold.clone()); }); fold.collapseChildren = this.collapseChildren; return fold; }; this.addSubFold = function(fold) { if (this.range.isEqual(fold)) return; if (!this.range.containsRange(fold)) throw new Error("A fold can't intersect already existing fold" + fold.range + this.range); consumeRange(fold, this.start); var row = fold.start.row, column = fold.start.column; for (var i = 0, cmp = -1; i < this.subFolds.length; i++) { cmp = this.subFolds[i].range.compare(row, column); if (cmp != 1) break; } var afterStart = this.subFolds[i]; if (cmp == 0) return afterStart.addSubFold(fold); var row = fold.range.end.row, column = fold.range.end.column; for (var j = i, cmp = -1; j < this.subFolds.length; j++) { cmp = this.subFolds[j].range.compare(row, column); if (cmp != 1) break; } var afterEnd = this.subFolds[j]; if (cmp == 0) throw new Error("A fold can't intersect already existing fold" + fold.range + this.range); var consumedFolds = this.subFolds.splice(i, j - i, fold); fold.setFoldLine(this.foldLine); return fold; }; this.restoreRange = function(range) { return restoreRange(range, this.start); }; }).call(Fold.prototype); function consumePoint(point, anchor) { point.row -= anchor.row; if (point.row == 0) point.column -= anchor.column; } function consumeRange(range, anchor) { consumePoint(range.start, anchor); consumePoint(range.end, anchor); } function restorePoint(point, anchor) { if (point.row == 0) point.column += anchor.column; point.row += anchor.row; } function restoreRange(range, anchor) { restorePoint(range.start, anchor); restorePoint(range.end, anchor); } }); define("ace/edit_session/folding",["require","exports","module","ace/range","ace/edit_session/fold_line","ace/edit_session/fold","ace/token_iterator"], function(require, exports, module) { "use strict"; var Range = require("../range").Range; var FoldLine = require("./fold_line").FoldLine; var Fold = require("./fold").Fold; var TokenIterator = require("../token_iterator").TokenIterator; function Folding() { this.getFoldAt = function(row, column, side) { var foldLine = this.getFoldLine(row); if (!foldLine) return null; var folds = foldLine.folds; for (var i = 0; i < folds.length; i++) { var fold = folds[i]; if (fold.range.contains(row, column)) { if (side == 1 && fold.range.isEnd(row, column)) { continue; } else if (side == -1 && fold.range.isStart(row, column)) { continue; } return fold; } } }; this.getFoldsInRange = function(range) { var start = range.start; var end = range.end; var foldLines = this.$foldData; var foundFolds = []; start.column += 1; end.column -= 1; for (var i = 0; i < foldLines.length; i++) { var cmp = foldLines[i].range.compareRange(range); if (cmp == 2) { continue; } else if (cmp == -2) { break; } var folds = foldLines[i].folds; for (var j = 0; j < folds.length; j++) { var fold = folds[j]; cmp = fold.range.compareRange(range); if (cmp == -2) { break; } else if (cmp == 2) { continue; } else if (cmp == 42) { break; } foundFolds.push(fold); } } start.column -= 1; end.column += 1; return foundFolds; }; this.getFoldsInRangeList = function(ranges) { if (Array.isArray(ranges)) { var folds = []; ranges.forEach(function(range) { folds = folds.concat(this.getFoldsInRange(range)); }, this); } else { var folds = this.getFoldsInRange(ranges); } return folds; }; this.getAllFolds = function() { var folds = []; var foldLines = this.$foldData; for (var i = 0; i < foldLines.length; i++) for (var j = 0; j < foldLines[i].folds.length; j++) folds.push(foldLines[i].folds[j]); return folds; }; this.getFoldStringAt = function(row, column, trim, foldLine) { foldLine = foldLine || this.getFoldLine(row); if (!foldLine) return null; var lastFold = { end: { column: 0 } }; var str, fold; for (var i = 0; i < foldLine.folds.length; i++) { fold = foldLine.folds[i]; var cmp = fold.range.compareEnd(row, column); if (cmp == -1) { str = this .getLine(fold.start.row) .substring(lastFold.end.column, fold.start.column); break; } else if (cmp === 0) { return null; } lastFold = fold; } if (!str) str = this.getLine(fold.start.row).substring(lastFold.end.column); if (trim == -1) return str.substring(0, column - lastFold.end.column); else if (trim == 1) return str.substring(column - lastFold.end.column); else return str; }; this.getFoldLine = function(docRow, startFoldLine) { var foldData = this.$foldData; var i = 0; if (startFoldLine) i = foldData.indexOf(startFoldLine); if (i == -1) i = 0; for (i; i < foldData.length; i++) { var foldLine = foldData[i]; if (foldLine.start.row <= docRow && foldLine.end.row >= docRow) { return foldLine; } else if (foldLine.end.row > docRow) { return null; } } return null; }; this.getNextFoldLine = function(docRow, startFoldLine) { var foldData = this.$foldData; var i = 0; if (startFoldLine) i = foldData.indexOf(startFoldLine); if (i == -1) i = 0; for (i; i < foldData.length; i++) { var foldLine = foldData[i]; if (foldLine.end.row >= docRow) { return foldLine; } } return null; }; this.getFoldedRowCount = function(first, last) { var foldData = this.$foldData, rowCount = last-first+1; for (var i = 0; i < foldData.length; i++) { var foldLine = foldData[i], end = foldLine.end.row, start = foldLine.start.row; if (end >= last) { if (start < last) { if (start >= first) rowCount -= last-start; else rowCount = 0; // in one fold } break; } else if (end >= first){ if (start >= first) // fold inside range rowCount -= end-start; else rowCount -= end-first+1; } } return rowCount; }; this.$addFoldLine = function(foldLine) { this.$foldData.push(foldLine); this.$foldData.sort(function(a, b) { return a.start.row - b.start.row; }); return foldLine; }; this.addFold = function(placeholder, range) { var foldData = this.$foldData; var added = false; var fold; if (placeholder instanceof Fold) fold = placeholder; else { fold = new Fold(range, placeholder); fold.collapseChildren = range.collapseChildren; } this.$clipRangeToDocument(fold.range); var startRow = fold.start.row; var startColumn = fold.start.column; var endRow = fold.end.row; var endColumn = fold.end.column; if (!(startRow < endRow || startRow == endRow && startColumn <= endColumn - 2)) throw new Error("The range has to be at least 2 characters width"); var startFold = this.getFoldAt(startRow, startColumn, 1); var endFold = this.getFoldAt(endRow, endColumn, -1); if (startFold && endFold == startFold) return startFold.addSubFold(fold); if (startFold && !startFold.range.isStart(startRow, startColumn)) this.removeFold(startFold); if (endFold && !endFold.range.isEnd(endRow, endColumn)) this.removeFold(endFold); var folds = this.getFoldsInRange(fold.range); if (folds.length > 0) { this.removeFolds(folds); folds.forEach(function(subFold) { fold.addSubFold(subFold); }); } for (var i = 0; i < foldData.length; i++) { var foldLine = foldData[i]; if (endRow == foldLine.start.row) { foldLine.addFold(fold); added = true; break; } else if (startRow == foldLine.end.row) { foldLine.addFold(fold); added = true; if (!fold.sameRow) { var foldLineNext = foldData[i + 1]; if (foldLineNext && foldLineNext.start.row == endRow) { foldLine.merge(foldLineNext); break; } } break; } else if (endRow <= foldLine.start.row) { break; } } if (!added) foldLine = this.$addFoldLine(new FoldLine(this.$foldData, fold)); if (this.$useWrapMode) this.$updateWrapData(foldLine.start.row, foldLine.start.row); else this.$updateRowLengthCache(foldLine.start.row, foldLine.start.row); this.$modified = true; this._signal("changeFold", { data: fold, action: "add" }); return fold; }; this.addFolds = function(folds) { folds.forEach(function(fold) { this.addFold(fold); }, this); }; this.removeFold = function(fold) { var foldLine = fold.foldLine; var startRow = foldLine.start.row; var endRow = foldLine.end.row; var foldLines = this.$foldData; var folds = foldLine.folds; if (folds.length == 1) { foldLines.splice(foldLines.indexOf(foldLine), 1); } else if (foldLine.range.isEnd(fold.end.row, fold.end.column)) { folds.pop(); foldLine.end.row = folds[folds.length - 1].end.row; foldLine.end.column = folds[folds.length - 1].end.column; } else if (foldLine.range.isStart(fold.start.row, fold.start.column)) { folds.shift(); foldLine.start.row = folds[0].start.row; foldLine.start.column = folds[0].start.column; } else if (fold.sameRow) { folds.splice(folds.indexOf(fold), 1); } else { var newFoldLine = foldLine.split(fold.start.row, fold.start.column); folds = newFoldLine.folds; folds.shift(); newFoldLine.start.row = folds[0].start.row; newFoldLine.start.column = folds[0].start.column; } if (!this.$updating) { if (this.$useWrapMode) this.$updateWrapData(startRow, endRow); else this.$updateRowLengthCache(startRow, endRow); } this.$modified = true; this._signal("changeFold", { data: fold, action: "remove" }); }; this.removeFolds = function(folds) { var cloneFolds = []; for (var i = 0; i < folds.length; i++) { cloneFolds.push(folds[i]); } cloneFolds.forEach(function(fold) { this.removeFold(fold); }, this); this.$modified = true; }; this.expandFold = function(fold) { this.removeFold(fold); fold.subFolds.forEach(function(subFold) { fold.restoreRange(subFold); this.addFold(subFold); }, this); if (fold.collapseChildren > 0) { this.foldAll(fold.start.row+1, fold.end.row, fold.collapseChildren-1); } fold.subFolds = []; }; this.expandFolds = function(folds) { folds.forEach(function(fold) { this.expandFold(fold); }, this); }; this.unfold = function(location, expandInner) { var range, folds; if (location == null) { range = new Range(0, 0, this.getLength(), 0); expandInner = true; } else if (typeof location == "number") range = new Range(location, 0, location, this.getLine(location).length); else if ("row" in location) range = Range.fromPoints(location, location); else range = location; folds = this.getFoldsInRangeList(range); if (expandInner) { this.removeFolds(folds); } else { var subFolds = folds; while (subFolds.length) { this.expandFolds(subFolds); subFolds = this.getFoldsInRangeList(range); } } if (folds.length) return folds; }; this.isRowFolded = function(docRow, startFoldRow) { return !!this.getFoldLine(docRow, startFoldRow); }; this.getRowFoldEnd = function(docRow, startFoldRow) { var foldLine = this.getFoldLine(docRow, startFoldRow); return foldLine ? foldLine.end.row : docRow; }; this.getRowFoldStart = function(docRow, startFoldRow) { var foldLine = this.getFoldLine(docRow, startFoldRow); return foldLine ? foldLine.start.row : docRow; }; this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) { if (startRow == null) startRow = foldLine.start.row; if (startColumn == null) startColumn = 0; if (endRow == null) endRow = foldLine.end.row; if (endColumn == null) endColumn = this.getLine(endRow).length; var doc = this.doc; var textLine = ""; foldLine.walk(function(placeholder, row, column, lastColumn) { if (row < startRow) return; if (row == startRow) { if (column < startColumn) return; lastColumn = Math.max(startColumn, lastColumn); } if (placeholder != null) { textLine += placeholder; } else { textLine += doc.getLine(row).substring(lastColumn, column); } }, endRow, endColumn); return textLine; }; this.getDisplayLine = function(row, endColumn, startRow, startColumn) { var foldLine = this.getFoldLine(row); if (!foldLine) { var line; line = this.doc.getLine(row); return line.substring(startColumn || 0, endColumn || line.length); } else { return this.getFoldDisplayLine( foldLine, row, endColumn, startRow, startColumn); } }; this.$cloneFoldData = function() { var fd = []; fd = this.$foldData.map(function(foldLine) { var folds = foldLine.folds.map(function(fold) { return fold.clone(); }); return new FoldLine(fd, folds); }); return fd; }; this.toggleFold = function(tryToUnfold) { var selection = this.selection; var range = selection.getRange(); var fold; var bracketPos; if (range.isEmpty()) { var cursor = range.start; fold = this.getFoldAt(cursor.row, cursor.column); if (fold) { this.expandFold(fold); return; } else if (bracketPos = this.findMatchingBracket(cursor)) { if (range.comparePoint(bracketPos) == 1) { range.end = bracketPos; } else { range.start = bracketPos; range.start.column++; range.end.column--; } } else if (bracketPos = this.findMatchingBracket({row: cursor.row, column: cursor.column + 1})) { if (range.comparePoint(bracketPos) == 1) range.end = bracketPos; else range.start = bracketPos; range.start.column++; } else { range = this.getCommentFoldRange(cursor.row, cursor.column) || range; } } else { var folds = this.getFoldsInRange(range); if (tryToUnfold && folds.length) { this.expandFolds(folds); return; } else if (folds.length == 1 ) { fold = folds[0]; } } if (!fold) fold = this.getFoldAt(range.start.row, range.start.column); if (fold && fold.range.toString() == range.toString()) { this.expandFold(fold); return; } var placeholder = "..."; if (!range.isMultiLine()) { placeholder = this.getTextRange(range); if (placeholder.length < 4) return; placeholder = placeholder.trim().substring(0, 2) + ".."; } this.addFold(placeholder, range); }; this.getCommentFoldRange = function(row, column, dir) { var iterator = new TokenIterator(this, row, column); var token = iterator.getCurrentToken(); if (token && /^comment|string/.test(token.type)) { var range = new Range(); var re = new RegExp(token.type.replace(/\..*/, "\\.")); if (dir != 1) { do { token = iterator.stepBackward(); } while (token && re.test(token.type)); iterator.stepForward(); } range.start.row = iterator.getCurrentTokenRow(); range.start.column = iterator.getCurrentTokenColumn() + 2; iterator = new TokenIterator(this, row, column); if (dir != -1) { do { token = iterator.stepForward(); } while (token && re.test(token.type)); token = iterator.stepBackward(); } else token = iterator.getCurrentToken(); range.end.row = iterator.getCurrentTokenRow(); range.end.column = iterator.getCurrentTokenColumn() + token.value.length - 2; return range; } }; this.foldAll = function(startRow, endRow, depth) { if (depth == undefined) depth = 100000; // JSON.stringify doesn't hanle Infinity var foldWidgets = this.foldWidgets; if (!foldWidgets) return; // mode doesn't support folding endRow = endRow || this.getLength(); startRow = startRow || 0; for (var row = startRow; row < endRow; row++) { if (foldWidgets[row] == null) foldWidgets[row] = this.getFoldWidget(row); if (foldWidgets[row] != "start") continue; var range = this.getFoldWidgetRange(row); if (range && range.isMultiLine() && range.end.row <= endRow && range.start.row >= startRow ) { row = range.end.row; try { var fold = this.addFold("...", range); if (fold) fold.collapseChildren = depth; } catch(e) {} } } }; this.$foldStyles = { "manual": 1, "markbegin": 1, "markbeginend": 1 }; this.$foldStyle = "markbegin"; this.setFoldStyle = function(style) { if (!this.$foldStyles[style]) throw new Error("invalid fold style: " + style + "[" + Object.keys(this.$foldStyles).join(", ") + "]"); if (this.$foldStyle == style) return; this.$foldStyle = style; if (style == "manual") this.unfold(); var mode = this.$foldMode; this.$setFolding(null); this.$setFolding(mode); }; this.$setFolding = function(foldMode) { if (this.$foldMode == foldMode) return; this.$foldMode = foldMode; this.off('change', this.$updateFoldWidgets); this.off('tokenizerUpdate', this.$tokenizerUpdateFoldWidgets); this._signal("changeAnnotation"); if (!foldMode || this.$foldStyle == "manual") { this.foldWidgets = null; return; } this.foldWidgets = []; this.getFoldWidget = foldMode.getFoldWidget.bind(foldMode, this, this.$foldStyle); this.getFoldWidgetRange = foldMode.getFoldWidgetRange.bind(foldMode, this, this.$foldStyle); this.$updateFoldWidgets = this.updateFoldWidgets.bind(this); this.$tokenizerUpdateFoldWidgets = this.tokenizerUpdateFoldWidgets.bind(this); this.on('change', this.$updateFoldWidgets); this.on('tokenizerUpdate', this.$tokenizerUpdateFoldWidgets); }; this.getParentFoldRangeData = function (row, ignoreCurrent) { var fw = this.foldWidgets; if (!fw || (ignoreCurrent && fw[row])) return {}; var i = row - 1, firstRange; while (i >= 0) { var c = fw[i]; if (c == null) c = fw[i] = this.getFoldWidget(i); if (c == "start") { var range = this.getFoldWidgetRange(i); if (!firstRange) firstRange = range; if (range && range.end.row >= row) break; } i--; } return { range: i !== -1 && range, firstRange: firstRange }; }; this.onFoldWidgetClick = function(row, e) { e = e.domEvent; var options = { children: e.shiftKey, all: e.ctrlKey || e.metaKey, siblings: e.altKey }; var range = this.$toggleFoldWidget(row, options); if (!range) { var el = (e.target || e.srcElement); if (el && /ace_fold-widget/.test(el.className)) el.className += " ace_invalid"; } }; this.$toggleFoldWidget = function(row, options) { if (!this.getFoldWidget) return; var type = this.getFoldWidget(row); var line = this.getLine(row); var dir = type === "end" ? -1 : 1; var fold = this.getFoldAt(row, dir === -1 ? 0 : line.length, dir); if (fold) { if (options.children || options.all) this.removeFold(fold); else this.expandFold(fold); return fold; } var range = this.getFoldWidgetRange(row, true); if (range && !range.isMultiLine()) { fold = this.getFoldAt(range.start.row, range.start.column, 1); if (fold && range.isEqual(fold.range)) { this.removeFold(fold); return fold; } } if (options.siblings) { var data = this.getParentFoldRangeData(row); if (data.range) { var startRow = data.range.start.row + 1; var endRow = data.range.end.row; } this.foldAll(startRow, endRow, options.all ? 10000 : 0); } else if (options.children) { endRow = range ? range.end.row : this.getLength(); this.foldAll(row + 1, endRow, options.all ? 10000 : 0); } else if (range) { if (options.all) range.collapseChildren = 10000; this.addFold("...", range); } return range; }; this.toggleFoldWidget = function(toggleParent) { var row = this.selection.getCursor().row; row = this.getRowFoldStart(row); var range = this.$toggleFoldWidget(row, {}); if (range) return; var data = this.getParentFoldRangeData(row, true); range = data.range || data.firstRange; if (range) { row = range.start.row; var fold = this.getFoldAt(row, this.getLine(row).length, 1); if (fold) { this.removeFold(fold); } else { this.addFold("...", range); } } }; this.updateFoldWidgets = function(delta) { var firstRow = delta.start.row; var len = delta.end.row - firstRow; if (len === 0) { this.foldWidgets[firstRow] = null; } else if (delta.action == 'remove') { this.foldWidgets.splice(firstRow, len + 1, null); } else { var args = Array(len + 1); args.unshift(firstRow, 1); this.foldWidgets.splice.apply(this.foldWidgets, args); } }; this.tokenizerUpdateFoldWidgets = function(e) { var rows = e.data; if (rows.first != rows.last) { if (this.foldWidgets.length > rows.first) this.foldWidgets.splice(rows.first, this.foldWidgets.length); } }; } exports.Folding = Folding; }); define("ace/edit_session/bracket_match",["require","exports","module","ace/token_iterator","ace/range"], function(require, exports, module) { "use strict"; var TokenIterator = require("../token_iterator").TokenIterator; var Range = require("../range").Range; function BracketMatch() { this.findMatchingBracket = function(position, chr) { if (position.column == 0) return null; var charBeforeCursor = chr || this.getLine(position.row).charAt(position.column-1); if (charBeforeCursor == "") return null; var match = charBeforeCursor.match(/([\(\[\{])|([\)\]\}])/); if (!match) return null; if (match[1]) return this.$findClosingBracket(match[1], position); else return this.$findOpeningBracket(match[2], position); }; this.getBracketRange = function(pos) { var line = this.getLine(pos.row); var before = true, range; var chr = line.charAt(pos.column-1); var match = chr && chr.match(/([\(\[\{])|([\)\]\}])/); if (!match) { chr = line.charAt(pos.column); pos = {row: pos.row, column: pos.column + 1}; match = chr && chr.match(/([\(\[\{])|([\)\]\}])/); before = false; } if (!match) return null; if (match[1]) { var bracketPos = this.$findClosingBracket(match[1], pos); if (!bracketPos) return null; range = Range.fromPoints(pos, bracketPos); if (!before) { range.end.column++; range.start.column--; } range.cursor = range.end; } else { var bracketPos = this.$findOpeningBracket(match[2], pos); if (!bracketPos) return null; range = Range.fromPoints(bracketPos, pos); if (!before) { range.start.column++; range.end.column--; } range.cursor = range.start; } return range; }; this.$brackets = { ")": "(", "(": ")", "]": "[", "[": "]", "{": "}", "}": "{" }; this.$findOpeningBracket = function(bracket, position, typeRe) { var openBracket = this.$brackets[bracket]; var depth = 1; var iterator = new TokenIterator(this, position.row, position.column); var token = iterator.getCurrentToken(); if (!token) token = iterator.stepForward(); if (!token) return; if (!typeRe){ typeRe = new RegExp( "(\\.?" + token.type.replace(".", "\\.").replace("rparen", ".paren") .replace(/\b(?:end)\b/, "(?:start|begin|end)") + ")+" ); } var valueIndex = position.column - iterator.getCurrentTokenColumn() - 2; var value = token.value; while (true) { while (valueIndex >= 0) { var chr = value.charAt(valueIndex); if (chr == openBracket) { depth -= 1; if (depth == 0) { return {row: iterator.getCurrentTokenRow(), column: valueIndex + iterator.getCurrentTokenColumn()}; } } else if (chr == bracket) { depth += 1; } valueIndex -= 1; } do { token = iterator.stepBackward(); } while (token && !typeRe.test(token.type)); if (token == null) break; value = token.value; valueIndex = value.length - 1; } return null; }; this.$findClosingBracket = function(bracket, position, typeRe) { var closingBracket = this.$brackets[bracket]; var depth = 1; var iterator = new TokenIterator(this, position.row, position.column); var token = iterator.getCurrentToken(); if (!token) token = iterator.stepForward(); if (!token) return; if (!typeRe){ typeRe = new RegExp( "(\\.?" + token.type.replace(".", "\\.").replace("lparen", ".paren") .replace(/\b(?:start|begin)\b/, "(?:start|begin|end)") + ")+" ); } var valueIndex = position.column - iterator.getCurrentTokenColumn(); while (true) { var value = token.value; var valueLength = value.length; while (valueIndex < valueLength) { var chr = value.charAt(valueIndex); if (chr == closingBracket) { depth -= 1; if (depth == 0) { return {row: iterator.getCurrentTokenRow(), column: valueIndex + iterator.getCurrentTokenColumn()}; } } else if (chr == bracket) { depth += 1; } valueIndex += 1; } do { token = iterator.stepForward(); } while (token && !typeRe.test(token.type)); if (token == null) break; valueIndex = 0; } return null; }; } exports.BracketMatch = BracketMatch; }); define("ace/edit_session",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/config","ace/lib/event_emitter","ace/selection","ace/mode/text","ace/range","ace/document","ace/background_tokenizer","ace/search_highlight","ace/edit_session/folding","ace/edit_session/bracket_match"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var lang = require("./lib/lang"); var config = require("./config"); var EventEmitter = require("./lib/event_emitter").EventEmitter; var Selection = require("./selection").Selection; var TextMode = require("./mode/text").Mode; var Range = require("./range").Range; var Document = require("./document").Document; var BackgroundTokenizer = require("./background_tokenizer").BackgroundTokenizer; var SearchHighlight = require("./search_highlight").SearchHighlight; var EditSession = function(text, mode) { this.$breakpoints = []; this.$decorations = []; this.$frontMarkers = {}; this.$backMarkers = {}; this.$markerId = 1; this.$undoSelect = true; this.$foldData = []; this.id = "session" + (++EditSession.$uid); this.$foldData.toString = function() { return this.join("\n"); }; this.on("changeFold", this.onChangeFold.bind(this)); this.$onChange = this.onChange.bind(this); if (typeof text != "object" || !text.getLine) text = new Document(text); this.setDocument(text); this.selection = new Selection(this); config.resetOptions(this); this.setMode(mode); config._signal("session", this); }; (function() { oop.implement(this, EventEmitter); this.setDocument = function(doc) { if (this.doc) this.doc.removeListener("change", this.$onChange); this.doc = doc; doc.on("change", this.$onChange); if (this.bgTokenizer) this.bgTokenizer.setDocument(this.getDocument()); this.resetCaches(); }; this.getDocument = function() { return this.doc; }; this.$resetRowCache = function(docRow) { if (!docRow) { this.$docRowCache = []; this.$screenRowCache = []; return; } var l = this.$docRowCache.length; var i = this.$getRowCacheIndex(this.$docRowCache, docRow) + 1; if (l > i) { this.$docRowCache.splice(i, l); this.$screenRowCache.splice(i, l); } }; this.$getRowCacheIndex = function(cacheArray, val) { var low = 0; var hi = cacheArray.length - 1; while (low <= hi) { var mid = (low + hi) >> 1; var c = cacheArray[mid]; if (val > c) low = mid + 1; else if (val < c) hi = mid - 1; else return mid; } return low -1; }; this.resetCaches = function() { this.$modified = true; this.$wrapData = []; this.$rowLengthCache = []; this.$resetRowCache(0); if (this.bgTokenizer) this.bgTokenizer.start(0); }; this.onChangeFold = function(e) { var fold = e.data; this.$resetRowCache(fold.start.row); }; this.onChange = function(delta) { this.$modified = true; this.$resetRowCache(delta.start.row); var removedFolds = this.$updateInternalDataOnChange(delta); if (!this.$fromUndo && this.$undoManager && !delta.ignore) { this.$deltasDoc.push(delta); if (removedFolds && removedFolds.length != 0) { this.$deltasFold.push({ action: "removeFolds", folds: removedFolds }); } this.$informUndoManager.schedule(); } this.bgTokenizer && this.bgTokenizer.$updateOnChange(delta); this._signal("change", delta); }; this.setValue = function(text) { this.doc.setValue(text); this.selection.moveTo(0, 0); this.$resetRowCache(0); this.$deltas = []; this.$deltasDoc = []; this.$deltasFold = []; this.setUndoManager(this.$undoManager); this.getUndoManager().reset(); }; this.getValue = this.toString = function() { return this.doc.getValue(); }; this.getSelection = function() { return this.selection; }; this.getState = function(row) { return this.bgTokenizer.getState(row); }; this.getTokens = function(row) { return this.bgTokenizer.getTokens(row); }; this.getTokenAt = function(row, column) { var tokens = this.bgTokenizer.getTokens(row); var token, c = 0; if (column == null) { i = tokens.length - 1; c = this.getLine(row).length; } else { for (var i = 0; i < tokens.length; i++) { c += tokens[i].value.length; if (c >= column) break; } } token = tokens[i]; if (!token) return null; token.index = i; token.start = c - token.value.length; return token; }; this.setUndoManager = function(undoManager) { this.$undoManager = undoManager; this.$deltas = []; this.$deltasDoc = []; this.$deltasFold = []; if (this.$informUndoManager) this.$informUndoManager.cancel(); if (undoManager) { var self = this; this.$syncInformUndoManager = function() { self.$informUndoManager.cancel(); if (self.$deltasFold.length) { self.$deltas.push({ group: "fold", deltas: self.$deltasFold }); self.$deltasFold = []; } if (self.$deltasDoc.length) { self.$deltas.push({ group: "doc", deltas: self.$deltasDoc }); self.$deltasDoc = []; } if (self.$deltas.length > 0) { undoManager.execute({ action: "aceupdate", args: [self.$deltas, self], merge: self.mergeUndoDeltas }); } self.mergeUndoDeltas = false; self.$deltas = []; }; this.$informUndoManager = lang.delayedCall(this.$syncInformUndoManager); } }; this.markUndoGroup = function() { if (this.$syncInformUndoManager) this.$syncInformUndoManager(); }; this.$defaultUndoManager = { undo: function() {}, redo: function() {}, reset: function() {} }; this.getUndoManager = function() { return this.$undoManager || this.$defaultUndoManager; }; this.getTabString = function() { if (this.getUseSoftTabs()) { return lang.stringRepeat(" ", this.getTabSize()); } else { return "\t"; } }; this.setUseSoftTabs = function(val) { this.setOption("useSoftTabs", val); }; this.getUseSoftTabs = function() { return this.$useSoftTabs && !this.$mode.$indentWithTabs; }; this.setTabSize = function(tabSize) { this.setOption("tabSize", tabSize); }; this.getTabSize = function() { return this.$tabSize; }; this.isTabStop = function(position) { return this.$useSoftTabs && (position.column % this.$tabSize === 0); }; this.$overwrite = false; this.setOverwrite = function(overwrite) { this.setOption("overwrite", overwrite); }; this.getOverwrite = function() { return this.$overwrite; }; this.toggleOverwrite = function() { this.setOverwrite(!this.$overwrite); }; this.addGutterDecoration = function(row, className) { if (!this.$decorations[row]) this.$decorations[row] = ""; this.$decorations[row] += " " + className; this._signal("changeBreakpoint", {}); }; this.removeGutterDecoration = function(row, className) { this.$decorations[row] = (this.$decorations[row] || "").replace(" " + className, ""); this._signal("changeBreakpoint", {}); }; this.getBreakpoints = function() { return this.$breakpoints; }; this.setBreakpoints = function(rows) { this.$breakpoints = []; for (var i=0; i 0) inToken = !!line.charAt(column - 1).match(this.tokenRe); if (!inToken) inToken = !!line.charAt(column).match(this.tokenRe); if (inToken) var re = this.tokenRe; else if (/^\s+$/.test(line.slice(column-1, column+1))) var re = /\s/; else var re = this.nonTokenRe; var start = column; if (start > 0) { do { start--; } while (start >= 0 && line.charAt(start).match(re)); start++; } var end = column; while (end < line.length && line.charAt(end).match(re)) { end++; } return new Range(row, start, row, end); }; this.getAWordRange = function(row, column) { var wordRange = this.getWordRange(row, column); var line = this.getLine(wordRange.end.row); while (line.charAt(wordRange.end.column).match(/[ \t]/)) { wordRange.end.column += 1; } return wordRange; }; this.setNewLineMode = function(newLineMode) { this.doc.setNewLineMode(newLineMode); }; this.getNewLineMode = function() { return this.doc.getNewLineMode(); }; this.setUseWorker = function(useWorker) { this.setOption("useWorker", useWorker); }; this.getUseWorker = function() { return this.$useWorker; }; this.onReloadTokenizer = function(e) { var rows = e.data; this.bgTokenizer.start(rows.first); this._signal("tokenizerUpdate", e); }; this.$modes = {}; this.$mode = null; this.$modeId = null; this.setMode = function(mode, cb) { if (mode && typeof mode === "object") { if (mode.getTokenizer) return this.$onChangeMode(mode); var options = mode; var path = options.path; } else { path = mode || "ace/mode/text"; } if (!this.$modes["ace/mode/text"]) this.$modes["ace/mode/text"] = new TextMode(); if (this.$modes[path] && !options) { this.$onChangeMode(this.$modes[path]); cb && cb(); return; } this.$modeId = path; config.loadModule(["mode", path], function(m) { if (this.$modeId !== path) return cb && cb(); if (this.$modes[path] && !options) { this.$onChangeMode(this.$modes[path]); } else if (m && m.Mode) { m = new m.Mode(options); if (!options) { this.$modes[path] = m; m.$id = path; } this.$onChangeMode(m); } cb && cb(); }.bind(this)); if (!this.$mode) this.$onChangeMode(this.$modes["ace/mode/text"], true); }; this.$onChangeMode = function(mode, $isPlaceholder) { if (!$isPlaceholder) this.$modeId = mode.$id; if (this.$mode === mode) return; this.$mode = mode; this.$stopWorker(); if (this.$useWorker) this.$startWorker(); var tokenizer = mode.getTokenizer(); if(tokenizer.addEventListener !== undefined) { var onReloadTokenizer = this.onReloadTokenizer.bind(this); tokenizer.addEventListener("update", onReloadTokenizer); } if (!this.bgTokenizer) { this.bgTokenizer = new BackgroundTokenizer(tokenizer); var _self = this; this.bgTokenizer.addEventListener("update", function(e) { _self._signal("tokenizerUpdate", e); }); } else { this.bgTokenizer.setTokenizer(tokenizer); } this.bgTokenizer.setDocument(this.getDocument()); this.tokenRe = mode.tokenRe; this.nonTokenRe = mode.nonTokenRe; if (!$isPlaceholder) { if (mode.attachToSession) mode.attachToSession(this); this.$options.wrapMethod.set.call(this, this.$wrapMethod); this.$setFolding(mode.foldingRules); this.bgTokenizer.start(0); this._emit("changeMode"); } }; this.$stopWorker = function() { if (this.$worker) { this.$worker.terminate(); this.$worker = null; } }; this.$startWorker = function() { try { this.$worker = this.$mode.createWorker(this); } catch (e) { config.warn("Could not load worker", e); this.$worker = null; } }; this.getMode = function() { return this.$mode; }; this.$scrollTop = 0; this.setScrollTop = function(scrollTop) { if (this.$scrollTop === scrollTop || isNaN(scrollTop)) return; this.$scrollTop = scrollTop; this._signal("changeScrollTop", scrollTop); }; this.getScrollTop = function() { return this.$scrollTop; }; this.$scrollLeft = 0; this.setScrollLeft = function(scrollLeft) { if (this.$scrollLeft === scrollLeft || isNaN(scrollLeft)) return; this.$scrollLeft = scrollLeft; this._signal("changeScrollLeft", scrollLeft); }; this.getScrollLeft = function() { return this.$scrollLeft; }; this.getScreenWidth = function() { this.$computeWidth(); if (this.lineWidgets) return Math.max(this.getLineWidgetMaxWidth(), this.screenWidth); return this.screenWidth; }; this.getLineWidgetMaxWidth = function() { if (this.lineWidgetsWidth != null) return this.lineWidgetsWidth; var width = 0; this.lineWidgets.forEach(function(w) { if (w && w.screenWidth > width) width = w.screenWidth; }); return this.lineWidgetWidth = width; }; this.$computeWidth = function(force) { if (this.$modified || force) { this.$modified = false; if (this.$useWrapMode) return this.screenWidth = this.$wrapLimit; var lines = this.doc.getAllLines(); var cache = this.$rowLengthCache; var longestScreenLine = 0; var foldIndex = 0; var foldLine = this.$foldData[foldIndex]; var foldStart = foldLine ? foldLine.start.row : Infinity; var len = lines.length; for (var i = 0; i < len; i++) { if (i > foldStart) { i = foldLine.end.row + 1; if (i >= len) break; foldLine = this.$foldData[foldIndex++]; foldStart = foldLine ? foldLine.start.row : Infinity; } if (cache[i] == null) cache[i] = this.$getStringScreenWidth(lines[i])[0]; if (cache[i] > longestScreenLine) longestScreenLine = cache[i]; } this.screenWidth = longestScreenLine; } }; this.getLine = function(row) { return this.doc.getLine(row); }; this.getLines = function(firstRow, lastRow) { return this.doc.getLines(firstRow, lastRow); }; this.getLength = function() { return this.doc.getLength(); }; this.getTextRange = function(range) { return this.doc.getTextRange(range || this.selection.getRange()); }; this.insert = function(position, text) { return this.doc.insert(position, text); }; this.remove = function(range) { return this.doc.remove(range); }; this.removeFullLines = function(firstRow, lastRow){ return this.doc.removeFullLines(firstRow, lastRow); }; this.undoChanges = function(deltas, dontSelect) { if (!deltas.length) return; this.$fromUndo = true; var lastUndoRange = null; for (var i = deltas.length - 1; i != -1; i--) { var delta = deltas[i]; if (delta.group == "doc") { this.doc.revertDeltas(delta.deltas); lastUndoRange = this.$getUndoSelection(delta.deltas, true, lastUndoRange); } else { delta.deltas.forEach(function(foldDelta) { this.addFolds(foldDelta.folds); }, this); } } this.$fromUndo = false; lastUndoRange && this.$undoSelect && !dontSelect && this.selection.setSelectionRange(lastUndoRange); return lastUndoRange; }; this.redoChanges = function(deltas, dontSelect) { if (!deltas.length) return; this.$fromUndo = true; var lastUndoRange = null; for (var i = 0; i < deltas.length; i++) { var delta = deltas[i]; if (delta.group == "doc") { this.doc.applyDeltas(delta.deltas); lastUndoRange = this.$getUndoSelection(delta.deltas, false, lastUndoRange); } } this.$fromUndo = false; lastUndoRange && this.$undoSelect && !dontSelect && this.selection.setSelectionRange(lastUndoRange); return lastUndoRange; }; this.setUndoSelect = function(enable) { this.$undoSelect = enable; }; this.$getUndoSelection = function(deltas, isUndo, lastUndoRange) { function isInsert(delta) { return isUndo ? delta.action !== "insert" : delta.action === "insert"; } var delta = deltas[0]; var range, point; var lastDeltaIsInsert = false; if (isInsert(delta)) { range = Range.fromPoints(delta.start, delta.end); lastDeltaIsInsert = true; } else { range = Range.fromPoints(delta.start, delta.start); lastDeltaIsInsert = false; } for (var i = 1; i < deltas.length; i++) { delta = deltas[i]; if (isInsert(delta)) { point = delta.start; if (range.compare(point.row, point.column) == -1) { range.setStart(point); } point = delta.end; if (range.compare(point.row, point.column) == 1) { range.setEnd(point); } lastDeltaIsInsert = true; } else { point = delta.start; if (range.compare(point.row, point.column) == -1) { range = Range.fromPoints(delta.start, delta.start); } lastDeltaIsInsert = false; } } if (lastUndoRange != null) { if (Range.comparePoints(lastUndoRange.start, range.start) === 0) { lastUndoRange.start.column += range.end.column - range.start.column; lastUndoRange.end.column += range.end.column - range.start.column; } var cmp = lastUndoRange.compareRange(range); if (cmp == 1) { range.setStart(lastUndoRange.start); } else if (cmp == -1) { range.setEnd(lastUndoRange.end); } } return range; }; this.replace = function(range, text) { return this.doc.replace(range, text); }; this.moveText = function(fromRange, toPosition, copy) { var text = this.getTextRange(fromRange); var folds = this.getFoldsInRange(fromRange); var toRange = Range.fromPoints(toPosition, toPosition); if (!copy) { this.remove(fromRange); var rowDiff = fromRange.start.row - fromRange.end.row; var collDiff = rowDiff ? -fromRange.end.column : fromRange.start.column - fromRange.end.column; if (collDiff) { if (toRange.start.row == fromRange.end.row && toRange.start.column > fromRange.end.column) toRange.start.column += collDiff; if (toRange.end.row == fromRange.end.row && toRange.end.column > fromRange.end.column) toRange.end.column += collDiff; } if (rowDiff && toRange.start.row >= fromRange.end.row) { toRange.start.row += rowDiff; toRange.end.row += rowDiff; } } toRange.end = this.insert(toRange.start, text); if (folds.length) { var oldStart = fromRange.start; var newStart = toRange.start; var rowDiff = newStart.row - oldStart.row; var collDiff = newStart.column - oldStart.column; this.addFolds(folds.map(function(x) { x = x.clone(); if (x.start.row == oldStart.row) x.start.column += collDiff; if (x.end.row == oldStart.row) x.end.column += collDiff; x.start.row += rowDiff; x.end.row += rowDiff; return x; })); } return toRange; }; this.indentRows = function(startRow, endRow, indentString) { indentString = indentString.replace(/\t/g, this.getTabString()); for (var row=startRow; row<=endRow; row++) this.doc.insertInLine({row: row, column: 0}, indentString); }; this.outdentRows = function (range) { var rowRange = range.collapseRows(); var deleteRange = new Range(0, 0, 0, 0); var size = this.getTabSize(); for (var i = rowRange.start.row; i <= rowRange.end.row; ++i) { var line = this.getLine(i); deleteRange.start.row = i; deleteRange.end.row = i; for (var j = 0; j < size; ++j) if (line.charAt(j) != ' ') break; if (j < size && line.charAt(j) == '\t') { deleteRange.start.column = j; deleteRange.end.column = j + 1; } else { deleteRange.start.column = 0; deleteRange.end.column = j; } this.remove(deleteRange); } }; this.$moveLines = function(firstRow, lastRow, dir) { firstRow = this.getRowFoldStart(firstRow); lastRow = this.getRowFoldEnd(lastRow); if (dir < 0) { var row = this.getRowFoldStart(firstRow + dir); if (row < 0) return 0; var diff = row-firstRow; } else if (dir > 0) { var row = this.getRowFoldEnd(lastRow + dir); if (row > this.doc.getLength()-1) return 0; var diff = row-lastRow; } else { firstRow = this.$clipRowToDocument(firstRow); lastRow = this.$clipRowToDocument(lastRow); var diff = lastRow - firstRow + 1; } var range = new Range(firstRow, 0, lastRow, Number.MAX_VALUE); var folds = this.getFoldsInRange(range).map(function(x){ x = x.clone(); x.start.row += diff; x.end.row += diff; return x; }); var lines = dir == 0 ? this.doc.getLines(firstRow, lastRow) : this.doc.removeFullLines(firstRow, lastRow); this.doc.insertFullLines(firstRow+diff, lines); folds.length && this.addFolds(folds); return diff; }; this.moveLinesUp = function(firstRow, lastRow) { return this.$moveLines(firstRow, lastRow, -1); }; this.moveLinesDown = function(firstRow, lastRow) { return this.$moveLines(firstRow, lastRow, 1); }; this.duplicateLines = function(firstRow, lastRow) { return this.$moveLines(firstRow, lastRow, 0); }; this.$clipRowToDocument = function(row) { return Math.max(0, Math.min(row, this.doc.getLength()-1)); }; this.$clipColumnToRow = function(row, column) { if (column < 0) return 0; return Math.min(this.doc.getLine(row).length, column); }; this.$clipPositionToDocument = function(row, column) { column = Math.max(0, column); if (row < 0) { row = 0; column = 0; } else { var len = this.doc.getLength(); if (row >= len) { row = len - 1; column = this.doc.getLine(len-1).length; } else { column = Math.min(this.doc.getLine(row).length, column); } } return { row: row, column: column }; }; this.$clipRangeToDocument = function(range) { if (range.start.row < 0) { range.start.row = 0; range.start.column = 0; } else { range.start.column = this.$clipColumnToRow( range.start.row, range.start.column ); } var len = this.doc.getLength() - 1; if (range.end.row > len) { range.end.row = len; range.end.column = this.doc.getLine(len).length; } else { range.end.column = this.$clipColumnToRow( range.end.row, range.end.column ); } return range; }; this.$wrapLimit = 80; this.$useWrapMode = false; this.$wrapLimitRange = { min : null, max : null }; this.setUseWrapMode = function(useWrapMode) { if (useWrapMode != this.$useWrapMode) { this.$useWrapMode = useWrapMode; this.$modified = true; this.$resetRowCache(0); if (useWrapMode) { var len = this.getLength(); this.$wrapData = Array(len); this.$updateWrapData(0, len - 1); } this._signal("changeWrapMode"); } }; this.getUseWrapMode = function() { return this.$useWrapMode; }; this.setWrapLimitRange = function(min, max) { if (this.$wrapLimitRange.min !== min || this.$wrapLimitRange.max !== max) { this.$wrapLimitRange = { min: min, max: max }; this.$modified = true; if (this.$useWrapMode) this._signal("changeWrapMode"); } }; this.adjustWrapLimit = function(desiredLimit, $printMargin) { var limits = this.$wrapLimitRange; if (limits.max < 0) limits = {min: $printMargin, max: $printMargin}; var wrapLimit = this.$constrainWrapLimit(desiredLimit, limits.min, limits.max); if (wrapLimit != this.$wrapLimit && wrapLimit > 1) { this.$wrapLimit = wrapLimit; this.$modified = true; if (this.$useWrapMode) { this.$updateWrapData(0, this.getLength() - 1); this.$resetRowCache(0); this._signal("changeWrapLimit"); } return true; } return false; }; this.$constrainWrapLimit = function(wrapLimit, min, max) { if (min) wrapLimit = Math.max(min, wrapLimit); if (max) wrapLimit = Math.min(max, wrapLimit); return wrapLimit; }; this.getWrapLimit = function() { return this.$wrapLimit; }; this.setWrapLimit = function (limit) { this.setWrapLimitRange(limit, limit); }; this.getWrapLimitRange = function() { return { min : this.$wrapLimitRange.min, max : this.$wrapLimitRange.max }; }; this.$updateInternalDataOnChange = function(delta) { var useWrapMode = this.$useWrapMode; var action = delta.action; var start = delta.start; var end = delta.end; var firstRow = start.row; var lastRow = end.row; var len = lastRow - firstRow; var removedFolds = null; this.$updating = true; if (len != 0) { if (action === "remove") { this[useWrapMode ? "$wrapData" : "$rowLengthCache"].splice(firstRow, len); var foldLines = this.$foldData; removedFolds = this.getFoldsInRange(delta); this.removeFolds(removedFolds); var foldLine = this.getFoldLine(end.row); var idx = 0; if (foldLine) { foldLine.addRemoveChars(end.row, end.column, start.column - end.column); foldLine.shiftRow(-len); var foldLineBefore = this.getFoldLine(firstRow); if (foldLineBefore && foldLineBefore !== foldLine) { foldLineBefore.merge(foldLine); foldLine = foldLineBefore; } idx = foldLines.indexOf(foldLine) + 1; } for (idx; idx < foldLines.length; idx++) { var foldLine = foldLines[idx]; if (foldLine.start.row >= end.row) { foldLine.shiftRow(-len); } } lastRow = firstRow; } else { var args = Array(len); args.unshift(firstRow, 0); var arr = useWrapMode ? this.$wrapData : this.$rowLengthCache arr.splice.apply(arr, args); var foldLines = this.$foldData; var foldLine = this.getFoldLine(firstRow); var idx = 0; if (foldLine) { var cmp = foldLine.range.compareInside(start.row, start.column); if (cmp == 0) { foldLine = foldLine.split(start.row, start.column); if (foldLine) { foldLine.shiftRow(len); foldLine.addRemoveChars(lastRow, 0, end.column - start.column); } } else if (cmp == -1) { foldLine.addRemoveChars(firstRow, 0, end.column - start.column); foldLine.shiftRow(len); } idx = foldLines.indexOf(foldLine) + 1; } for (idx; idx < foldLines.length; idx++) { var foldLine = foldLines[idx]; if (foldLine.start.row >= firstRow) { foldLine.shiftRow(len); } } } } else { len = Math.abs(delta.start.column - delta.end.column); if (action === "remove") { removedFolds = this.getFoldsInRange(delta); this.removeFolds(removedFolds); len = -len; } var foldLine = this.getFoldLine(firstRow); if (foldLine) { foldLine.addRemoveChars(firstRow, start.column, len); } } if (useWrapMode && this.$wrapData.length != this.doc.getLength()) { console.error("doc.getLength() and $wrapData.length have to be the same!"); } this.$updating = false; if (useWrapMode) this.$updateWrapData(firstRow, lastRow); else this.$updateRowLengthCache(firstRow, lastRow); return removedFolds; }; this.$updateRowLengthCache = function(firstRow, lastRow, b) { this.$rowLengthCache[firstRow] = null; this.$rowLengthCache[lastRow] = null; }; this.$updateWrapData = function(firstRow, lastRow) { var lines = this.doc.getAllLines(); var tabSize = this.getTabSize(); var wrapData = this.$wrapData; var wrapLimit = this.$wrapLimit; var tokens; var foldLine; var row = firstRow; lastRow = Math.min(lastRow, lines.length - 1); while (row <= lastRow) { foldLine = this.getFoldLine(row, foldLine); if (!foldLine) { tokens = this.$getDisplayTokens(lines[row]); wrapData[row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize); row ++; } else { tokens = []; foldLine.walk(function(placeholder, row, column, lastColumn) { var walkTokens; if (placeholder != null) { walkTokens = this.$getDisplayTokens( placeholder, tokens.length); walkTokens[0] = PLACEHOLDER_START; for (var i = 1; i < walkTokens.length; i++) { walkTokens[i] = PLACEHOLDER_BODY; } } else { walkTokens = this.$getDisplayTokens( lines[row].substring(lastColumn, column), tokens.length); } tokens = tokens.concat(walkTokens); }.bind(this), foldLine.end.row, lines[foldLine.end.row].length + 1 ); wrapData[foldLine.start.row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize); row = foldLine.end.row + 1; } } }; var CHAR = 1, CHAR_EXT = 2, PLACEHOLDER_START = 3, PLACEHOLDER_BODY = 4, PUNCTUATION = 9, SPACE = 10, TAB = 11, TAB_SPACE = 12; this.$computeWrapSplits = function(tokens, wrapLimit, tabSize) { if (tokens.length == 0) { return []; } var splits = []; var displayLength = tokens.length; var lastSplit = 0, lastDocSplit = 0; var isCode = this.$wrapAsCode; var indentedSoftWrap = this.$indentedSoftWrap; var maxIndent = wrapLimit <= Math.max(2 * tabSize, 8) || indentedSoftWrap === false ? 0 : Math.floor(wrapLimit / 2); function getWrapIndent() { var indentation = 0; if (maxIndent === 0) return indentation; if (indentedSoftWrap) { for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; if (token == SPACE) indentation += 1; else if (token == TAB) indentation += tabSize; else if (token == TAB_SPACE) continue; else break; } } if (isCode && indentedSoftWrap !== false) indentation += tabSize; return Math.min(indentation, maxIndent); } function addSplit(screenPos) { var displayed = tokens.slice(lastSplit, screenPos); var len = displayed.length; displayed.join("") .replace(/12/g, function() { len -= 1; }) .replace(/2/g, function() { len -= 1; }); if (!splits.length) { indent = getWrapIndent(); splits.indent = indent; } lastDocSplit += len; splits.push(lastDocSplit); lastSplit = screenPos; } var indent = 0; while (displayLength - lastSplit > wrapLimit - indent) { var split = lastSplit + wrapLimit - indent; if (tokens[split - 1] >= SPACE && tokens[split] >= SPACE) { addSplit(split); continue; } if (tokens[split] == PLACEHOLDER_START || tokens[split] == PLACEHOLDER_BODY) { for (split; split != lastSplit - 1; split--) { if (tokens[split] == PLACEHOLDER_START) { break; } } if (split > lastSplit) { addSplit(split); continue; } split = lastSplit + wrapLimit; for (split; split < tokens.length; split++) { if (tokens[split] != PLACEHOLDER_BODY) { break; } } if (split == tokens.length) { break; // Breaks the while-loop. } addSplit(split); continue; } var minSplit = Math.max(split - (wrapLimit -(wrapLimit>>2)), lastSplit - 1); while (split > minSplit && tokens[split] < PLACEHOLDER_START) { split --; } if (isCode) { while (split > minSplit && tokens[split] < PLACEHOLDER_START) { split --; } while (split > minSplit && tokens[split] == PUNCTUATION) { split --; } } else { while (split > minSplit && tokens[split] < SPACE) { split --; } } if (split > minSplit) { addSplit(++split); continue; } split = lastSplit + wrapLimit; if (tokens[split] == CHAR_EXT) split--; addSplit(split - indent); } return splits; }; this.$getDisplayTokens = function(str, offset) { var arr = []; var tabSize; offset = offset || 0; for (var i = 0; i < str.length; i++) { var c = str.charCodeAt(i); if (c == 9) { tabSize = this.getScreenTabSize(arr.length + offset); arr.push(TAB); for (var n = 1; n < tabSize; n++) { arr.push(TAB_SPACE); } } else if (c == 32) { arr.push(SPACE); } else if((c > 39 && c < 48) || (c > 57 && c < 64)) { arr.push(PUNCTUATION); } else if (c >= 0x1100 && isFullWidth(c)) { arr.push(CHAR, CHAR_EXT); } else { arr.push(CHAR); } } return arr; }; this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) { if (maxScreenColumn == 0) return [0, 0]; if (maxScreenColumn == null) maxScreenColumn = Infinity; screenColumn = screenColumn || 0; var c, column; for (column = 0; column < str.length; column++) { c = str.charCodeAt(column); if (c == 9) { screenColumn += this.getScreenTabSize(screenColumn); } else if (c >= 0x1100 && isFullWidth(c)) { screenColumn += 2; } else { screenColumn += 1; } if (screenColumn > maxScreenColumn) { break; } } return [screenColumn, column]; }; this.lineWidgets = null; this.getRowLength = function(row) { if (this.lineWidgets) var h = this.lineWidgets[row] && this.lineWidgets[row].rowCount || 0; else h = 0 if (!this.$useWrapMode || !this.$wrapData[row]) { return 1 + h; } else { return this.$wrapData[row].length + 1 + h; } }; this.getRowLineCount = function(row) { if (!this.$useWrapMode || !this.$wrapData[row]) { return 1; } else { return this.$wrapData[row].length + 1; } }; this.getRowWrapIndent = function(screenRow) { if (this.$useWrapMode) { var pos = this.screenToDocumentPosition(screenRow, Number.MAX_VALUE); var splits = this.$wrapData[pos.row]; return splits.length && splits[0] < pos.column ? splits.indent : 0; } else { return 0; } } this.getScreenLastRowColumn = function(screenRow) { var pos = this.screenToDocumentPosition(screenRow, Number.MAX_VALUE); return this.documentToScreenColumn(pos.row, pos.column); }; this.getDocumentLastRowColumn = function(docRow, docColumn) { var screenRow = this.documentToScreenRow(docRow, docColumn); return this.getScreenLastRowColumn(screenRow); }; this.getDocumentLastRowColumnPosition = function(docRow, docColumn) { var screenRow = this.documentToScreenRow(docRow, docColumn); return this.screenToDocumentPosition(screenRow, Number.MAX_VALUE / 10); }; this.getRowSplitData = function(row) { if (!this.$useWrapMode) { return undefined; } else { return this.$wrapData[row]; } }; this.getScreenTabSize = function(screenColumn) { return this.$tabSize - screenColumn % this.$tabSize; }; this.screenToDocumentRow = function(screenRow, screenColumn) { return this.screenToDocumentPosition(screenRow, screenColumn).row; }; this.screenToDocumentColumn = function(screenRow, screenColumn) { return this.screenToDocumentPosition(screenRow, screenColumn).column; }; this.screenToDocumentPosition = function(screenRow, screenColumn) { if (screenRow < 0) return {row: 0, column: 0}; var line; var docRow = 0; var docColumn = 0; var column; var row = 0; var rowLength = 0; var rowCache = this.$screenRowCache; var i = this.$getRowCacheIndex(rowCache, screenRow); var l = rowCache.length; if (l && i >= 0) { var row = rowCache[i]; var docRow = this.$docRowCache[i]; var doCache = screenRow > rowCache[l - 1]; } else { var doCache = !l; } var maxRow = this.getLength() - 1; var foldLine = this.getNextFoldLine(docRow); var foldStart = foldLine ? foldLine.start.row : Infinity; while (row <= screenRow) { rowLength = this.getRowLength(docRow); if (row + rowLength > screenRow || docRow >= maxRow) { break; } else { row += rowLength; docRow++; if (docRow > foldStart) { docRow = foldLine.end.row+1; foldLine = this.getNextFoldLine(docRow, foldLine); foldStart = foldLine ? foldLine.start.row : Infinity; } } if (doCache) { this.$docRowCache.push(docRow); this.$screenRowCache.push(row); } } if (foldLine && foldLine.start.row <= docRow) { line = this.getFoldDisplayLine(foldLine); docRow = foldLine.start.row; } else if (row + rowLength <= screenRow || docRow > maxRow) { return { row: maxRow, column: this.getLine(maxRow).length }; } else { line = this.getLine(docRow); foldLine = null; } var wrapIndent = 0; if (this.$useWrapMode) { var splits = this.$wrapData[docRow]; if (splits) { var splitIndex = Math.floor(screenRow - row); column = splits[splitIndex]; if(splitIndex > 0 && splits.length) { wrapIndent = splits.indent; docColumn = splits[splitIndex - 1] || splits[splits.length - 1]; line = line.substring(docColumn); } } } docColumn += this.$getStringScreenWidth(line, screenColumn - wrapIndent)[1]; if (this.$useWrapMode && docColumn >= column) docColumn = column - 1; if (foldLine) return foldLine.idxToPosition(docColumn); return {row: docRow, column: docColumn}; }; this.documentToScreenPosition = function(docRow, docColumn) { if (typeof docColumn === "undefined") var pos = this.$clipPositionToDocument(docRow.row, docRow.column); else pos = this.$clipPositionToDocument(docRow, docColumn); docRow = pos.row; docColumn = pos.column; var screenRow = 0; var foldStartRow = null; var fold = null; fold = this.getFoldAt(docRow, docColumn, 1); if (fold) { docRow = fold.start.row; docColumn = fold.start.column; } var rowEnd, row = 0; var rowCache = this.$docRowCache; var i = this.$getRowCacheIndex(rowCache, docRow); var l = rowCache.length; if (l && i >= 0) { var row = rowCache[i]; var screenRow = this.$screenRowCache[i]; var doCache = docRow > rowCache[l - 1]; } else { var doCache = !l; } var foldLine = this.getNextFoldLine(row); var foldStart = foldLine ?foldLine.start.row :Infinity; while (row < docRow) { if (row >= foldStart) { rowEnd = foldLine.end.row + 1; if (rowEnd > docRow) break; foldLine = this.getNextFoldLine(rowEnd, foldLine); foldStart = foldLine ?foldLine.start.row :Infinity; } else { rowEnd = row + 1; } screenRow += this.getRowLength(row); row = rowEnd; if (doCache) { this.$docRowCache.push(row); this.$screenRowCache.push(screenRow); } } var textLine = ""; if (foldLine && row >= foldStart) { textLine = this.getFoldDisplayLine(foldLine, docRow, docColumn); foldStartRow = foldLine.start.row; } else { textLine = this.getLine(docRow).substring(0, docColumn); foldStartRow = docRow; } var wrapIndent = 0; if (this.$useWrapMode) { var wrapRow = this.$wrapData[foldStartRow]; if (wrapRow) { var screenRowOffset = 0; while (textLine.length >= wrapRow[screenRowOffset]) { screenRow ++; screenRowOffset++; } textLine = textLine.substring( wrapRow[screenRowOffset - 1] || 0, textLine.length ); wrapIndent = screenRowOffset > 0 ? wrapRow.indent : 0; } } return { row: screenRow, column: wrapIndent + this.$getStringScreenWidth(textLine)[0] }; }; this.documentToScreenColumn = function(row, docColumn) { return this.documentToScreenPosition(row, docColumn).column; }; this.documentToScreenRow = function(docRow, docColumn) { return this.documentToScreenPosition(docRow, docColumn).row; }; this.getScreenLength = function() { var screenRows = 0; var fold = null; if (!this.$useWrapMode) { screenRows = this.getLength(); var foldData = this.$foldData; for (var i = 0; i < foldData.length; i++) { fold = foldData[i]; screenRows -= fold.end.row - fold.start.row; } } else { var lastRow = this.$wrapData.length; var row = 0, i = 0; var fold = this.$foldData[i++]; var foldStart = fold ? fold.start.row :Infinity; while (row < lastRow) { var splits = this.$wrapData[row]; screenRows += splits ? splits.length + 1 : 1; row ++; if (row > foldStart) { row = fold.end.row+1; fold = this.$foldData[i++]; foldStart = fold ?fold.start.row :Infinity; } } } if (this.lineWidgets) screenRows += this.$getWidgetScreenLength(); return screenRows; }; this.$setFontMetrics = function(fm) { if (!this.$enableVarChar) return; this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) { if (maxScreenColumn === 0) return [0, 0]; if (!maxScreenColumn) maxScreenColumn = Infinity; screenColumn = screenColumn || 0; var c, column; for (column = 0; column < str.length; column++) { c = str.charAt(column); if (c === "\t") { screenColumn += this.getScreenTabSize(screenColumn); } else { screenColumn += fm.getCharacterWidth(c); } if (screenColumn > maxScreenColumn) { break; } } return [screenColumn, column]; }; }; this.destroy = function() { if (this.bgTokenizer) { this.bgTokenizer.setDocument(null); this.bgTokenizer = null; } this.$stopWorker(); }; function isFullWidth(c) { if (c < 0x1100) return false; return c >= 0x1100 && c <= 0x115F || c >= 0x11A3 && c <= 0x11A7 || c >= 0x11FA && c <= 0x11FF || c >= 0x2329 && c <= 0x232A || c >= 0x2E80 && c <= 0x2E99 || c >= 0x2E9B && c <= 0x2EF3 || c >= 0x2F00 && c <= 0x2FD5 || c >= 0x2FF0 && c <= 0x2FFB || c >= 0x3000 && c <= 0x303E || c >= 0x3041 && c <= 0x3096 || c >= 0x3099 && c <= 0x30FF || c >= 0x3105 && c <= 0x312D || c >= 0x3131 && c <= 0x318E || c >= 0x3190 && c <= 0x31BA || c >= 0x31C0 && c <= 0x31E3 || c >= 0x31F0 && c <= 0x321E || c >= 0x3220 && c <= 0x3247 || c >= 0x3250 && c <= 0x32FE || c >= 0x3300 && c <= 0x4DBF || c >= 0x4E00 && c <= 0xA48C || c >= 0xA490 && c <= 0xA4C6 || c >= 0xA960 && c <= 0xA97C || c >= 0xAC00 && c <= 0xD7A3 || c >= 0xD7B0 && c <= 0xD7C6 || c >= 0xD7CB && c <= 0xD7FB || c >= 0xF900 && c <= 0xFAFF || c >= 0xFE10 && c <= 0xFE19 || c >= 0xFE30 && c <= 0xFE52 || c >= 0xFE54 && c <= 0xFE66 || c >= 0xFE68 && c <= 0xFE6B || c >= 0xFF01 && c <= 0xFF60 || c >= 0xFFE0 && c <= 0xFFE6; } }).call(EditSession.prototype); require("./edit_session/folding").Folding.call(EditSession.prototype); require("./edit_session/bracket_match").BracketMatch.call(EditSession.prototype); config.defineOptions(EditSession.prototype, "session", { wrap: { set: function(value) { if (!value || value == "off") value = false; else if (value == "free") value = true; else if (value == "printMargin") value = -1; else if (typeof value == "string") value = parseInt(value, 10) || false; if (this.$wrap == value) return; this.$wrap = value; if (!value) { this.setUseWrapMode(false); } else { var col = typeof value == "number" ? value : null; this.setWrapLimitRange(col, col); this.setUseWrapMode(true); } }, get: function() { if (this.getUseWrapMode()) { if (this.$wrap == -1) return "printMargin"; if (!this.getWrapLimitRange().min) return "free"; return this.$wrap; } return "off"; }, handlesSet: true }, wrapMethod: { set: function(val) { val = val == "auto" ? this.$mode.type != "text" : val != "text"; if (val != this.$wrapAsCode) { this.$wrapAsCode = val; if (this.$useWrapMode) { this.$modified = true; this.$resetRowCache(0); this.$updateWrapData(0, this.getLength() - 1); } } }, initialValue: "auto" }, indentedSoftWrap: { initialValue: true }, firstLineNumber: { set: function() {this._signal("changeBreakpoint");}, initialValue: 1 }, useWorker: { set: function(useWorker) { this.$useWorker = useWorker; this.$stopWorker(); if (useWorker) this.$startWorker(); }, initialValue: true }, useSoftTabs: {initialValue: true}, tabSize: { set: function(tabSize) { if (isNaN(tabSize) || this.$tabSize === tabSize) return; this.$modified = true; this.$rowLengthCache = []; this.$tabSize = tabSize; this._signal("changeTabSize"); }, initialValue: 4, handlesSet: true }, overwrite: { set: function(val) {this._signal("changeOverwrite");}, initialValue: false }, newLineMode: { set: function(val) {this.doc.setNewLineMode(val)}, get: function() {return this.doc.getNewLineMode()}, handlesSet: true }, mode: { set: function(val) { this.setMode(val) }, get: function() { return this.$modeId } } }); exports.EditSession = EditSession; }); define("ace/search",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"], function(require, exports, module) { "use strict"; var lang = require("./lib/lang"); var oop = require("./lib/oop"); var Range = require("./range").Range; var Search = function() { this.$options = {}; }; (function() { this.set = function(options) { oop.mixin(this.$options, options); return this; }; this.getOptions = function() { return lang.copyObject(this.$options); }; this.setOptions = function(options) { this.$options = options; }; this.find = function(session) { var options = this.$options; var iterator = this.$matchIterator(session, options); if (!iterator) return false; var firstRange = null; iterator.forEach(function(range, row, offset) { if (!range.start) { var column = range.offset + (offset || 0); firstRange = new Range(row, column, row, column + range.length); if (!range.length && options.start && options.start.start && options.skipCurrent != false && firstRange.isEqual(options.start) ) { firstRange = null; return false; } } else firstRange = range; return true; }); return firstRange; }; this.findAll = function(session) { var options = this.$options; if (!options.needle) return []; this.$assembleRegExp(options); var range = options.range; var lines = range ? session.getLines(range.start.row, range.end.row) : session.doc.getAllLines(); var ranges = []; var re = options.re; if (options.$isMultiLine) { var len = re.length; var maxRow = lines.length - len; var prevRange; outer: for (var row = re.offset || 0; row <= maxRow; row++) { for (var j = 0; j < len; j++) if (lines[row + j].search(re[j]) == -1) continue outer; var startLine = lines[row]; var line = lines[row + len - 1]; var startIndex = startLine.length - startLine.match(re[0])[0].length; var endIndex = line.match(re[len - 1])[0].length; if (prevRange && prevRange.end.row === row && prevRange.end.column > startIndex ) { continue; } ranges.push(prevRange = new Range( row, startIndex, row + len - 1, endIndex )); if (len > 2) row = row + len - 2; } } else { for (var i = 0; i < lines.length; i++) { var matches = lang.getMatchOffsets(lines[i], re); for (var j = 0; j < matches.length; j++) { var match = matches[j]; ranges.push(new Range(i, match.offset, i, match.offset + match.length)); } } } if (range) { var startColumn = range.start.column; var endColumn = range.start.column; var i = 0, j = ranges.length - 1; while (i < j && ranges[i].start.column < startColumn && ranges[i].start.row == range.start.row) i++; while (i < j && ranges[j].end.column > endColumn && ranges[j].end.row == range.end.row) j--; ranges = ranges.slice(i, j + 1); for (i = 0, j = ranges.length; i < j; i++) { ranges[i].start.row += range.start.row; ranges[i].end.row += range.start.row; } } return ranges; }; this.replace = function(input, replacement) { var options = this.$options; var re = this.$assembleRegExp(options); if (options.$isMultiLine) return replacement; if (!re) return; var match = re.exec(input); if (!match || match[0].length != input.length) return null; replacement = input.replace(re, replacement); if (options.preserveCase) { replacement = replacement.split(""); for (var i = Math.min(input.length, input.length); i--; ) { var ch = input[i]; if (ch && ch.toLowerCase() != ch) replacement[i] = replacement[i].toUpperCase(); else replacement[i] = replacement[i].toLowerCase(); } replacement = replacement.join(""); } return replacement; }; this.$matchIterator = function(session, options) { var re = this.$assembleRegExp(options); if (!re) return false; var callback; if (options.$isMultiLine) { var len = re.length; var matchIterator = function(line, row, offset) { var startIndex = line.search(re[0]); if (startIndex == -1) return; for (var i = 1; i < len; i++) { line = session.getLine(row + i); if (line.search(re[i]) == -1) return; } var endIndex = line.match(re[len - 1])[0].length; var range = new Range(row, startIndex, row + len - 1, endIndex); if (re.offset == 1) { range.start.row--; range.start.column = Number.MAX_VALUE; } else if (offset) range.start.column += offset; if (callback(range)) return true; }; } else if (options.backwards) { var matchIterator = function(line, row, startIndex) { var matches = lang.getMatchOffsets(line, re); for (var i = matches.length-1; i >= 0; i--) if (callback(matches[i], row, startIndex)) return true; }; } else { var matchIterator = function(line, row, startIndex) { var matches = lang.getMatchOffsets(line, re); for (var i = 0; i < matches.length; i++) if (callback(matches[i], row, startIndex)) return true; }; } var lineIterator = this.$lineIterator(session, options); return { forEach: function(_callback) { callback = _callback; lineIterator.forEach(matchIterator); } }; }; this.$assembleRegExp = function(options, $disableFakeMultiline) { if (options.needle instanceof RegExp) return options.re = options.needle; var needle = options.needle; if (!options.needle) return options.re = false; if (!options.regExp) needle = lang.escapeRegExp(needle); if (options.wholeWord) needle = addWordBoundary(needle, options); var modifier = options.caseSensitive ? "gm" : "gmi"; options.$isMultiLine = !$disableFakeMultiline && /[\n\r]/.test(needle); if (options.$isMultiLine) return options.re = this.$assembleMultilineRegExp(needle, modifier); try { var re = new RegExp(needle, modifier); } catch(e) { re = false; } return options.re = re; }; this.$assembleMultilineRegExp = function(needle, modifier) { var parts = needle.replace(/\r\n|\r|\n/g, "$\n^").split("\n"); var re = []; for (var i = 0; i < parts.length; i++) try { re.push(new RegExp(parts[i], modifier)); } catch(e) { return false; } if (parts[0] == "") { re.shift(); re.offset = 1; } else { re.offset = 0; } return re; }; this.$lineIterator = function(session, options) { var backwards = options.backwards == true; var skipCurrent = options.skipCurrent != false; var range = options.range; var start = options.start; if (!start) start = range ? range[backwards ? "end" : "start"] : session.selection.getRange(); if (start.start) start = start[skipCurrent != backwards ? "end" : "start"]; var firstRow = range ? range.start.row : 0; var lastRow = range ? range.end.row : session.getLength() - 1; var forEach = backwards ? function(callback) { var row = start.row; var line = session.getLine(row).substring(0, start.column); if (callback(line, row)) return; for (row--; row >= firstRow; row--) if (callback(session.getLine(row), row)) return; if (options.wrap == false) return; for (row = lastRow, firstRow = start.row; row >= firstRow; row--) if (callback(session.getLine(row), row)) return; } : function(callback) { var row = start.row; var line = session.getLine(row).substr(start.column); if (callback(line, row, start.column)) return; for (row = row+1; row <= lastRow; row++) if (callback(session.getLine(row), row)) return; if (options.wrap == false) return; for (row = firstRow, lastRow = start.row; row <= lastRow; row++) if (callback(session.getLine(row), row)) return; }; return {forEach: forEach}; }; }).call(Search.prototype); function addWordBoundary(needle, options) { function wordBoundary(c) { if (/\w/.test(c) || options.regExp) return "\\b"; return ""; } return wordBoundary(needle[0]) + needle + wordBoundary(needle[needle.length - 1]); } exports.Search = Search; }); define("ace/keyboard/hash_handler",["require","exports","module","ace/lib/keys","ace/lib/useragent"], function(require, exports, module) { "use strict"; var keyUtil = require("../lib/keys"); var useragent = require("../lib/useragent"); var KEY_MODS = keyUtil.KEY_MODS; function HashHandler(config, platform) { this.platform = platform || (useragent.isMac ? "mac" : "win"); this.commands = {}; this.commandKeyBinding = {}; this.addCommands(config); this.$singleCommand = true; } function MultiHashHandler(config, platform) { HashHandler.call(this, config, platform); this.$singleCommand = false; } MultiHashHandler.prototype = HashHandler.prototype; (function() { this.addCommand = function(command) { if (this.commands[command.name]) this.removeCommand(command); this.commands[command.name] = command; if (command.bindKey) this._buildKeyHash(command); }; this.removeCommand = function(command, keepCommand) { var name = command && (typeof command === 'string' ? command : command.name); command = this.commands[name]; if (!keepCommand) delete this.commands[name]; var ckb = this.commandKeyBinding; for (var keyId in ckb) { var cmdGroup = ckb[keyId]; if (cmdGroup == command) { delete ckb[keyId]; } else if (Array.isArray(cmdGroup)) { var i = cmdGroup.indexOf(command); if (i != -1) { cmdGroup.splice(i, 1); if (cmdGroup.length == 1) ckb[keyId] = cmdGroup[0]; } } } }; this.bindKey = function(key, command, position) { if (typeof key == "object" && key) { if (position == undefined) position = key.position; key = key[this.platform]; } if (!key) return; if (typeof command == "function") return this.addCommand({exec: command, bindKey: key, name: command.name || key}); key.split("|").forEach(function(keyPart) { var chain = ""; if (keyPart.indexOf(" ") != -1) { var parts = keyPart.split(/\s+/); keyPart = parts.pop(); parts.forEach(function(keyPart) { var binding = this.parseKeys(keyPart); var id = KEY_MODS[binding.hashId] + binding.key; chain += (chain ? " " : "") + id; this._addCommandToBinding(chain, "chainKeys"); }, this); chain += " "; } var binding = this.parseKeys(keyPart); var id = KEY_MODS[binding.hashId] + binding.key; this._addCommandToBinding(chain + id, command, position); }, this); }; function getPosition(command) { return typeof command == "object" && command.bindKey && command.bindKey.position || 0; } this._addCommandToBinding = function(keyId, command, position) { var ckb = this.commandKeyBinding, i; if (!command) { delete ckb[keyId]; } else if (!ckb[keyId] || this.$singleCommand) { ckb[keyId] = command; } else { if (!Array.isArray(ckb[keyId])) { ckb[keyId] = [ckb[keyId]]; } else if ((i = ckb[keyId].indexOf(command)) != -1) { ckb[keyId].splice(i, 1); } if (typeof position != "number") { if (position || command.isDefault) position = -100; else position = getPosition(command); } var commands = ckb[keyId]; for (i = 0; i < commands.length; i++) { var other = commands[i]; var otherPos = getPosition(other); if (otherPos > position) break; } commands.splice(i, 0, command); } }; this.addCommands = function(commands) { commands && Object.keys(commands).forEach(function(name) { var command = commands[name]; if (!command) return; if (typeof command === "string") return this.bindKey(command, name); if (typeof command === "function") command = { exec: command }; if (typeof command !== "object") return; if (!command.name) command.name = name; this.addCommand(command); }, this); }; this.removeCommands = function(commands) { Object.keys(commands).forEach(function(name) { this.removeCommand(commands[name]); }, this); }; this.bindKeys = function(keyList) { Object.keys(keyList).forEach(function(key) { this.bindKey(key, keyList[key]); }, this); }; this._buildKeyHash = function(command) { this.bindKey(command.bindKey, command); }; this.parseKeys = function(keys) { var parts = keys.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(x){return x}); var key = parts.pop(); var keyCode = keyUtil[key]; if (keyUtil.FUNCTION_KEYS[keyCode]) key = keyUtil.FUNCTION_KEYS[keyCode].toLowerCase(); else if (!parts.length) return {key: key, hashId: -1}; else if (parts.length == 1 && parts[0] == "shift") return {key: key.toUpperCase(), hashId: -1}; var hashId = 0; for (var i = parts.length; i--;) { var modifier = keyUtil.KEY_MODS[parts[i]]; if (modifier == null) { if (typeof console != "undefined") console.error("invalid modifier " + parts[i] + " in " + keys); return false; } hashId |= modifier; } return {key: key, hashId: hashId}; }; this.findKeyCommand = function findKeyCommand(hashId, keyString) { var key = KEY_MODS[hashId] + keyString; return this.commandKeyBinding[key]; }; this.handleKeyboard = function(data, hashId, keyString, keyCode) { if (keyCode < 0) return; var key = KEY_MODS[hashId] + keyString; var command = this.commandKeyBinding[key]; if (data.$keyChain) { data.$keyChain += " " + key; command = this.commandKeyBinding[data.$keyChain] || command; } if (command) { if (command == "chainKeys" || command[command.length - 1] == "chainKeys") { data.$keyChain = data.$keyChain || key; return {command: "null"}; } } if (data.$keyChain) { if ((!hashId || hashId == 4) && keyString.length == 1) data.$keyChain = data.$keyChain.slice(0, -key.length - 1); // wait for input else if (hashId == -1 || keyCode > 0) data.$keyChain = ""; // reset keyChain } return {command: command}; }; this.getStatusText = function(editor, data) { return data.$keyChain || ""; }; }).call(HashHandler.prototype); exports.HashHandler = HashHandler; exports.MultiHashHandler = MultiHashHandler; }); define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"], function(require, exports, module) { "use strict"; var oop = require("../lib/oop"); var MultiHashHandler = require("../keyboard/hash_handler").MultiHashHandler; var EventEmitter = require("../lib/event_emitter").EventEmitter; var CommandManager = function(platform, commands) { MultiHashHandler.call(this, commands, platform); this.byName = this.commands; this.setDefaultHandler("exec", function(e) { return e.command.exec(e.editor, e.args || {}); }); }; oop.inherits(CommandManager, MultiHashHandler); (function() { oop.implement(this, EventEmitter); this.exec = function(command, editor, args) { if (Array.isArray(command)) { for (var i = command.length; i--; ) { if (this.exec(command[i], editor, args)) return true; } return false; } if (typeof command === "string") command = this.commands[command]; if (!command) return false; if (editor && editor.$readOnly && !command.readOnly) return false; var e = {editor: editor, command: command, args: args}; e.returnValue = this._emit("exec", e); this._signal("afterExec", e); return e.returnValue === false ? false : true; }; this.toggleRecording = function(editor) { if (this.$inReplay) return; editor && editor._emit("changeStatus"); if (this.recording) { this.macro.pop(); this.removeEventListener("exec", this.$addCommandToMacro); if (!this.macro.length) this.macro = this.oldMacro; return this.recording = false; } if (!this.$addCommandToMacro) { this.$addCommandToMacro = function(e) { this.macro.push([e.command, e.args]); }.bind(this); } this.oldMacro = this.macro; this.macro = []; this.on("exec", this.$addCommandToMacro); return this.recording = true; }; this.replay = function(editor) { if (this.$inReplay || !this.macro) return; if (this.recording) return this.toggleRecording(editor); try { this.$inReplay = true; this.macro.forEach(function(x) { if (typeof x == "string") this.exec(x, editor); else this.exec(x[0], editor, x[1]); }, this); } finally { this.$inReplay = false; } }; this.trimMacro = function(m) { return m.map(function(x){ if (typeof x[0] != "string") x[0] = x[0].name; if (!x[1]) x = x[0]; return x; }); }; }).call(CommandManager.prototype); exports.CommandManager = CommandManager; }); define("ace/commands/default_commands",["require","exports","module","ace/lib/lang","ace/config","ace/range"], function(require, exports, module) { "use strict"; var lang = require("../lib/lang"); var config = require("../config"); var Range = require("../range").Range; function bindKey(win, mac) { return {win: win, mac: mac}; } exports.commands = [{ name: "showSettingsMenu", bindKey: bindKey("Ctrl-,", "Command-,"), exec: function(editor) { config.loadModule("ace/ext/settings_menu", function(module) { module.init(editor); editor.showSettingsMenu(); }); }, readOnly: true }, { name: "goToNextError", bindKey: bindKey("Alt-E", "F4"), exec: function(editor) { config.loadModule("ace/ext/error_marker", function(module) { module.showErrorMarker(editor, 1); }); }, scrollIntoView: "animate", readOnly: true }, { name: "goToPreviousError", bindKey: bindKey("Alt-Shift-E", "Shift-F4"), exec: function(editor) { config.loadModule("ace/ext/error_marker", function(module) { module.showErrorMarker(editor, -1); }); }, scrollIntoView: "animate", readOnly: true }, { name: "selectall", bindKey: bindKey("Ctrl-A", "Command-A"), exec: function(editor) { editor.selectAll(); }, readOnly: true }, { name: "centerselection", bindKey: bindKey(null, "Ctrl-L"), exec: function(editor) { editor.centerSelection(); }, readOnly: true }, { name: "gotoline", bindKey: bindKey("Ctrl-L", "Command-L"), exec: function(editor) { var line = parseInt(prompt("Enter line number:"), 10); if (!isNaN(line)) { editor.gotoLine(line); } }, readOnly: true }, { name: "fold", bindKey: bindKey("Alt-L|Ctrl-F1", "Command-Alt-L|Command-F1"), exec: function(editor) { editor.session.toggleFold(false); }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: true }, { name: "unfold", bindKey: bindKey("Alt-Shift-L|Ctrl-Shift-F1", "Command-Alt-Shift-L|Command-Shift-F1"), exec: function(editor) { editor.session.toggleFold(true); }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: true }, { name: "toggleFoldWidget", bindKey: bindKey("F2", "F2"), exec: function(editor) { editor.session.toggleFoldWidget(); }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: true }, { name: "toggleParentFoldWidget", bindKey: bindKey("Alt-F2", "Alt-F2"), exec: function(editor) { editor.session.toggleFoldWidget(true); }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: true }, { name: "foldall", bindKey: bindKey(null, "Ctrl-Command-Option-0"), exec: function(editor) { editor.session.foldAll(); }, scrollIntoView: "center", readOnly: true }, { name: "foldOther", bindKey: bindKey("Alt-0", "Command-Option-0"), exec: function(editor) { editor.session.foldAll(); editor.session.unfold(editor.selection.getAllRanges()); }, scrollIntoView: "center", readOnly: true }, { name: "unfoldall", bindKey: bindKey("Alt-Shift-0", "Command-Option-Shift-0"), exec: function(editor) { editor.session.unfold(); }, scrollIntoView: "center", readOnly: true }, { name: "findnext", bindKey: bindKey("Ctrl-K", "Command-G"), exec: function(editor) { editor.findNext(); }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: true }, { name: "findprevious", bindKey: bindKey("Ctrl-Shift-K", "Command-Shift-G"), exec: function(editor) { editor.findPrevious(); }, multiSelectAction: "forEach", scrollIntoView: "center", readOnly: true }, { name: "selectOrFindNext", bindKey: bindKey("Alt-K", "Ctrl-G"), exec: function(editor) { if (editor.selection.isEmpty()) editor.selection.selectWord(); else editor.findNext(); }, readOnly: true }, { name: "selectOrFindPrevious", bindKey: bindKey("Alt-Shift-K", "Ctrl-Shift-G"), exec: function(editor) { if (editor.selection.isEmpty()) editor.selection.selectWord(); else editor.findPrevious(); }, readOnly: true }, { name: "find", bindKey: bindKey("Ctrl-F", "Command-F"), exec: function(editor) { config.loadModule("ace/ext/searchbox", function(e) {e.Search(editor)}); }, readOnly: true }, { name: "overwrite", bindKey: "Insert", exec: function(editor) { editor.toggleOverwrite(); }, readOnly: true }, { name: "selecttostart", bindKey: bindKey("Ctrl-Shift-Home", "Command-Shift-Home|Command-Shift-Up"), exec: function(editor) { editor.getSelection().selectFileStart(); }, multiSelectAction: "forEach", readOnly: true, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "gotostart", bindKey: bindKey("Ctrl-Home", "Command-Home|Command-Up"), exec: function(editor) { editor.navigateFileStart(); }, multiSelectAction: "forEach", readOnly: true, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "selectup", bindKey: bindKey("Shift-Up", "Shift-Up|Ctrl-Shift-P"), exec: function(editor) { editor.getSelection().selectUp(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "golineup", bindKey: bindKey("Up", "Up|Ctrl-P"), exec: function(editor, args) { editor.navigateUp(args.times); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selecttoend", bindKey: bindKey("Ctrl-Shift-End", "Command-Shift-End|Command-Shift-Down"), exec: function(editor) { editor.getSelection().selectFileEnd(); }, multiSelectAction: "forEach", readOnly: true, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "gotoend", bindKey: bindKey("Ctrl-End", "Command-End|Command-Down"), exec: function(editor) { editor.navigateFileEnd(); }, multiSelectAction: "forEach", readOnly: true, scrollIntoView: "animate", aceCommandGroup: "fileJump" }, { name: "selectdown", bindKey: bindKey("Shift-Down", "Shift-Down|Ctrl-Shift-N"), exec: function(editor) { editor.getSelection().selectDown(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "golinedown", bindKey: bindKey("Down", "Down|Ctrl-N"), exec: function(editor, args) { editor.navigateDown(args.times); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selectwordleft", bindKey: bindKey("Ctrl-Shift-Left", "Option-Shift-Left"), exec: function(editor) { editor.getSelection().selectWordLeft(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "gotowordleft", bindKey: bindKey("Ctrl-Left", "Option-Left"), exec: function(editor) { editor.navigateWordLeft(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selecttolinestart", bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left|Ctrl-Shift-A"), exec: function(editor) { editor.getSelection().selectLineStart(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "gotolinestart", bindKey: bindKey("Alt-Left|Home", "Command-Left|Home|Ctrl-A"), exec: function(editor) { editor.navigateLineStart(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selectleft", bindKey: bindKey("Shift-Left", "Shift-Left|Ctrl-Shift-B"), exec: function(editor) { editor.getSelection().selectLeft(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "gotoleft", bindKey: bindKey("Left", "Left|Ctrl-B"), exec: function(editor, args) { editor.navigateLeft(args.times); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selectwordright", bindKey: bindKey("Ctrl-Shift-Right", "Option-Shift-Right"), exec: function(editor) { editor.getSelection().selectWordRight(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "gotowordright", bindKey: bindKey("Ctrl-Right", "Option-Right"), exec: function(editor) { editor.navigateWordRight(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selecttolineend", bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right|Shift-End|Ctrl-Shift-E"), exec: function(editor) { editor.getSelection().selectLineEnd(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "gotolineend", bindKey: bindKey("Alt-Right|End", "Command-Right|End|Ctrl-E"), exec: function(editor) { editor.navigateLineEnd(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selectright", bindKey: bindKey("Shift-Right", "Shift-Right"), exec: function(editor) { editor.getSelection().selectRight(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "gotoright", bindKey: bindKey("Right", "Right|Ctrl-F"), exec: function(editor, args) { editor.navigateRight(args.times); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selectpagedown", bindKey: "Shift-PageDown", exec: function(editor) { editor.selectPageDown(); }, readOnly: true }, { name: "pagedown", bindKey: bindKey(null, "Option-PageDown"), exec: function(editor) { editor.scrollPageDown(); }, readOnly: true }, { name: "gotopagedown", bindKey: bindKey("PageDown", "PageDown|Ctrl-V"), exec: function(editor) { editor.gotoPageDown(); }, readOnly: true }, { name: "selectpageup", bindKey: "Shift-PageUp", exec: function(editor) { editor.selectPageUp(); }, readOnly: true }, { name: "pageup", bindKey: bindKey(null, "Option-PageUp"), exec: function(editor) { editor.scrollPageUp(); }, readOnly: true }, { name: "gotopageup", bindKey: "PageUp", exec: function(editor) { editor.gotoPageUp(); }, readOnly: true }, { name: "scrollup", bindKey: bindKey("Ctrl-Up", null), exec: function(e) { e.renderer.scrollBy(0, -2 * e.renderer.layerConfig.lineHeight); }, readOnly: true }, { name: "scrolldown", bindKey: bindKey("Ctrl-Down", null), exec: function(e) { e.renderer.scrollBy(0, 2 * e.renderer.layerConfig.lineHeight); }, readOnly: true }, { name: "selectlinestart", bindKey: "Shift-Home", exec: function(editor) { editor.getSelection().selectLineStart(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "selectlineend", bindKey: "Shift-End", exec: function(editor) { editor.getSelection().selectLineEnd(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "togglerecording", bindKey: bindKey("Ctrl-Alt-E", "Command-Option-E"), exec: function(editor) { editor.commands.toggleRecording(editor); }, readOnly: true }, { name: "replaymacro", bindKey: bindKey("Ctrl-Shift-E", "Command-Shift-E"), exec: function(editor) { editor.commands.replay(editor); }, readOnly: true }, { name: "jumptomatching", bindKey: bindKey("Ctrl-P", "Ctrl-P"), exec: function(editor) { editor.jumpToMatching(); }, multiSelectAction: "forEach", scrollIntoView: "animate", readOnly: true }, { name: "selecttomatching", bindKey: bindKey("Ctrl-Shift-P", "Ctrl-Shift-P"), exec: function(editor) { editor.jumpToMatching(true); }, multiSelectAction: "forEach", scrollIntoView: "animate", readOnly: true }, { name: "expandToMatching", bindKey: bindKey("Ctrl-Shift-M", "Ctrl-Shift-M"), exec: function(editor) { editor.jumpToMatching(true, true); }, multiSelectAction: "forEach", scrollIntoView: "animate", readOnly: true }, { name: "passKeysToBrowser", bindKey: bindKey(null, null), exec: function() {}, passEvent: true, readOnly: true }, { name: "copy", exec: function(editor) { }, readOnly: true }, { name: "cut", exec: function(editor) { var range = editor.getSelectionRange(); editor._emit("cut", range); if (!editor.selection.isEmpty()) { editor.session.remove(range); editor.clearSelection(); } }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "paste", exec: function(editor, args) { editor.$handlePaste(args); }, scrollIntoView: "cursor" }, { name: "removeline", bindKey: bindKey("Ctrl-D", "Command-D"), exec: function(editor) { editor.removeLines(); }, scrollIntoView: "cursor", multiSelectAction: "forEachLine" }, { name: "duplicateSelection", bindKey: bindKey("Ctrl-Shift-D", "Command-Shift-D"), exec: function(editor) { editor.duplicateSelection(); }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "sortlines", bindKey: bindKey("Ctrl-Alt-S", "Command-Alt-S"), exec: function(editor) { editor.sortLines(); }, scrollIntoView: "selection", multiSelectAction: "forEachLine" }, { name: "togglecomment", bindKey: bindKey("Ctrl-/", "Command-/"), exec: function(editor) { editor.toggleCommentLines(); }, multiSelectAction: "forEachLine", scrollIntoView: "selectionPart" }, { name: "toggleBlockComment", bindKey: bindKey("Ctrl-Shift-/", "Command-Shift-/"), exec: function(editor) { editor.toggleBlockComment(); }, multiSelectAction: "forEach", scrollIntoView: "selectionPart" }, { name: "modifyNumberUp", bindKey: bindKey("Ctrl-Shift-Up", "Alt-Shift-Up"), exec: function(editor) { editor.modifyNumber(1); }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "modifyNumberDown", bindKey: bindKey("Ctrl-Shift-Down", "Alt-Shift-Down"), exec: function(editor) { editor.modifyNumber(-1); }, scrollIntoView: "cursor", multiSelectAction: "forEach" }, { name: "replace", bindKey: bindKey("Ctrl-H", "Command-Option-F"), exec: function(editor) { config.loadModule("ace/ext/searchbox", function(e) {e.Search(editor, true)}); } }, { name: "undo", bindKey: bindKey("Ctrl-Z", "Command-Z"), exec: function(editor) { editor.undo(); } }, { name: "redo", bindKey: bindKey("Ctrl-Shift-Z|Ctrl-Y", "Command-Shift-Z|Command-Y"), exec: function(editor) { editor.redo(); } }, { name: "copylinesup", bindKey: bindKey("Alt-Shift-Up", "Command-Option-Up"), exec: function(editor) { editor.copyLinesUp(); }, scrollIntoView: "cursor" }, { name: "movelinesup", bindKey: bindKey("Alt-Up", "Option-Up"), exec: function(editor) { editor.moveLinesUp(); }, scrollIntoView: "cursor" }, { name: "copylinesdown", bindKey: bindKey("Alt-Shift-Down", "Command-Option-Down"), exec: function(editor) { editor.copyLinesDown(); }, scrollIntoView: "cursor" }, { name: "movelinesdown", bindKey: bindKey("Alt-Down", "Option-Down"), exec: function(editor) { editor.moveLinesDown(); }, scrollIntoView: "cursor" }, { name: "del", bindKey: bindKey("Delete", "Delete|Ctrl-D|Shift-Delete"), exec: function(editor) { editor.remove("right"); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "backspace", bindKey: bindKey( "Shift-Backspace|Backspace", "Ctrl-Backspace|Shift-Backspace|Backspace|Ctrl-H" ), exec: function(editor) { editor.remove("left"); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "cut_or_delete", bindKey: bindKey("Shift-Delete", null), exec: function(editor) { if (editor.selection.isEmpty()) { editor.remove("left"); } else { return false; } }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removetolinestart", bindKey: bindKey("Alt-Backspace", "Command-Backspace"), exec: function(editor) { editor.removeToLineStart(); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removetolineend", bindKey: bindKey("Alt-Delete", "Ctrl-K"), exec: function(editor) { editor.removeToLineEnd(); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removewordleft", bindKey: bindKey("Ctrl-Backspace", "Alt-Backspace|Ctrl-Alt-Backspace"), exec: function(editor) { editor.removeWordLeft(); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "removewordright", bindKey: bindKey("Ctrl-Delete", "Alt-Delete"), exec: function(editor) { editor.removeWordRight(); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "outdent", bindKey: bindKey("Shift-Tab", "Shift-Tab"), exec: function(editor) { editor.blockOutdent(); }, multiSelectAction: "forEach", scrollIntoView: "selectionPart" }, { name: "indent", bindKey: bindKey("Tab", "Tab"), exec: function(editor) { editor.indent(); }, multiSelectAction: "forEach", scrollIntoView: "selectionPart" }, { name: "blockoutdent", bindKey: bindKey("Ctrl-[", "Ctrl-["), exec: function(editor) { editor.blockOutdent(); }, multiSelectAction: "forEachLine", scrollIntoView: "selectionPart" }, { name: "blockindent", bindKey: bindKey("Ctrl-]", "Ctrl-]"), exec: function(editor) { editor.blockIndent(); }, multiSelectAction: "forEachLine", scrollIntoView: "selectionPart" }, { name: "insertstring", exec: function(editor, str) { editor.insert(str); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "inserttext", exec: function(editor, args) { editor.insert(lang.stringRepeat(args.text || "", args.times || 1)); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "splitline", bindKey: bindKey(null, "Ctrl-O"), exec: function(editor) { editor.splitLine(); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "transposeletters", bindKey: bindKey("Ctrl-T", "Ctrl-T"), exec: function(editor) { editor.transposeLetters(); }, multiSelectAction: function(editor) {editor.transposeSelections(1); }, scrollIntoView: "cursor" }, { name: "touppercase", bindKey: bindKey("Ctrl-U", "Ctrl-U"), exec: function(editor) { editor.toUpperCase(); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "tolowercase", bindKey: bindKey("Ctrl-Shift-U", "Ctrl-Shift-U"), exec: function(editor) { editor.toLowerCase(); }, multiSelectAction: "forEach", scrollIntoView: "cursor" }, { name: "expandtoline", bindKey: bindKey("Ctrl-Shift-L", "Command-Shift-L"), exec: function(editor) { var range = editor.selection.getRange(); range.start.column = range.end.column = 0; range.end.row++; editor.selection.setRange(range, false); }, multiSelectAction: "forEach", scrollIntoView: "cursor", readOnly: true }, { name: "joinlines", bindKey: bindKey(null, null), exec: function(editor) { var isBackwards = editor.selection.isBackwards(); var selectionStart = isBackwards ? editor.selection.getSelectionLead() : editor.selection.getSelectionAnchor(); var selectionEnd = isBackwards ? editor.selection.getSelectionAnchor() : editor.selection.getSelectionLead(); var firstLineEndCol = editor.session.doc.getLine(selectionStart.row).length; var selectedText = editor.session.doc.getTextRange(editor.selection.getRange()); var selectedCount = selectedText.replace(/\n\s*/, " ").length; var insertLine = editor.session.doc.getLine(selectionStart.row); for (var i = selectionStart.row + 1; i <= selectionEnd.row + 1; i++) { var curLine = lang.stringTrimLeft(lang.stringTrimRight(editor.session.doc.getLine(i))); if (curLine.length !== 0) { curLine = " " + curLine; } insertLine += curLine; } if (selectionEnd.row + 1 < (editor.session.doc.getLength() - 1)) { insertLine += editor.session.doc.getNewLineCharacter(); } editor.clearSelection(); editor.session.doc.replace(new Range(selectionStart.row, 0, selectionEnd.row + 2, 0), insertLine); if (selectedCount > 0) { editor.selection.moveCursorTo(selectionStart.row, selectionStart.column); editor.selection.selectTo(selectionStart.row, selectionStart.column + selectedCount); } else { firstLineEndCol = editor.session.doc.getLine(selectionStart.row).length > firstLineEndCol ? (firstLineEndCol + 1) : firstLineEndCol; editor.selection.moveCursorTo(selectionStart.row, firstLineEndCol); } }, multiSelectAction: "forEach", readOnly: true }, { name: "invertSelection", bindKey: bindKey(null, null), exec: function(editor) { var endRow = editor.session.doc.getLength() - 1; var endCol = editor.session.doc.getLine(endRow).length; var ranges = editor.selection.rangeList.ranges; var newRanges = []; if (ranges.length < 1) { ranges = [editor.selection.getRange()]; } for (var i = 0; i < ranges.length; i++) { if (i == (ranges.length - 1)) { if (!(ranges[i].end.row === endRow && ranges[i].end.column === endCol)) { newRanges.push(new Range(ranges[i].end.row, ranges[i].end.column, endRow, endCol)); } } if (i === 0) { if (!(ranges[i].start.row === 0 && ranges[i].start.column === 0)) { newRanges.push(new Range(0, 0, ranges[i].start.row, ranges[i].start.column)); } } else { newRanges.push(new Range(ranges[i-1].end.row, ranges[i-1].end.column, ranges[i].start.row, ranges[i].start.column)); } } editor.exitMultiSelectMode(); editor.clearSelection(); for(var i = 0; i < newRanges.length; i++) { editor.selection.addRange(newRanges[i], false); } }, readOnly: true, scrollIntoView: "none" }]; }); define("ace/editor",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/keyboard/textinput","ace/mouse/mouse_handler","ace/mouse/fold_handler","ace/keyboard/keybinding","ace/edit_session","ace/search","ace/range","ace/lib/event_emitter","ace/commands/command_manager","ace/commands/default_commands","ace/config","ace/token_iterator"], function(require, exports, module) { "use strict"; require("./lib/fixoldbrowsers"); var oop = require("./lib/oop"); var dom = require("./lib/dom"); var lang = require("./lib/lang"); var useragent = require("./lib/useragent"); var TextInput = require("./keyboard/textinput").TextInput; var MouseHandler = require("./mouse/mouse_handler").MouseHandler; var FoldHandler = require("./mouse/fold_handler").FoldHandler; var KeyBinding = require("./keyboard/keybinding").KeyBinding; var EditSession = require("./edit_session").EditSession; var Search = require("./search").Search; var Range = require("./range").Range; var EventEmitter = require("./lib/event_emitter").EventEmitter; var CommandManager = require("./commands/command_manager").CommandManager; var defaultCommands = require("./commands/default_commands").commands; var config = require("./config"); var TokenIterator = require("./token_iterator").TokenIterator; var Editor = function(renderer, session) { var container = renderer.getContainerElement(); this.container = container; this.renderer = renderer; this.commands = new CommandManager(useragent.isMac ? "mac" : "win", defaultCommands); this.textInput = new TextInput(renderer.getTextAreaContainer(), this); this.renderer.textarea = this.textInput.getElement(); this.keyBinding = new KeyBinding(this); this.$mouseHandler = new MouseHandler(this); new FoldHandler(this); this.$blockScrolling = 0; this.$search = new Search().set({ wrap: true }); this.$historyTracker = this.$historyTracker.bind(this); this.commands.on("exec", this.$historyTracker); this.$initOperationListeners(); this._$emitInputEvent = lang.delayedCall(function() { this._signal("input", {}); if (this.session && this.session.bgTokenizer) this.session.bgTokenizer.scheduleStart(); }.bind(this)); this.on("change", function(_, _self) { _self._$emitInputEvent.schedule(31); }); this.setSession(session || new EditSession("")); config.resetOptions(this); config._signal("editor", this); }; (function(){ oop.implement(this, EventEmitter); this.$initOperationListeners = function() { function last(a) {return a[a.length - 1]} this.selections = []; this.commands.on("exec", this.startOperation.bind(this), true); this.commands.on("afterExec", this.endOperation.bind(this), true); this.$opResetTimer = lang.delayedCall(this.endOperation.bind(this)); this.on("change", function() { this.curOp || this.startOperation(); this.curOp.docChanged = true; }.bind(this), true); this.on("changeSelection", function() { this.curOp || this.startOperation(); this.curOp.selectionChanged = true; }.bind(this), true); }; this.curOp = null; this.prevOp = {}; this.startOperation = function(commadEvent) { if (this.curOp) { if (!commadEvent || this.curOp.command) return; this.prevOp = this.curOp; } if (!commadEvent) { this.previousCommand = null; commadEvent = {}; } this.$opResetTimer.schedule(); this.curOp = { command: commadEvent.command || {}, args: commadEvent.args, scrollTop: this.renderer.scrollTop }; if (this.curOp.command.name && this.curOp.command.scrollIntoView !== undefined) this.$blockScrolling++; }; this.endOperation = function(e) { if (this.curOp) { if (e && e.returnValue === false) return this.curOp = null; this._signal("beforeEndOperation"); var command = this.curOp.command; if (command.name && this.$blockScrolling > 0) this.$blockScrolling--; var scrollIntoView = command && command.scrollIntoView; if (scrollIntoView) { switch (scrollIntoView) { case "center-animate": scrollIntoView = "animate"; case "center": this.renderer.scrollCursorIntoView(null, 0.5); break; case "animate": case "cursor": this.renderer.scrollCursorIntoView(); break; case "selectionPart": var range = this.selection.getRange(); var config = this.renderer.layerConfig; if (range.start.row >= config.lastRow || range.end.row <= config.firstRow) { this.renderer.scrollSelectionIntoView(this.selection.anchor, this.selection.lead); } break; default: break; } if (scrollIntoView == "animate") this.renderer.animateScrolling(this.curOp.scrollTop); } this.prevOp = this.curOp; this.curOp = null; } }; this.$mergeableCommands = ["backspace", "del", "insertstring"]; this.$historyTracker = function(e) { if (!this.$mergeUndoDeltas) return; var prev = this.prevOp; var mergeableCommands = this.$mergeableCommands; var shouldMerge = prev.command && (e.command.name == prev.command.name); if (e.command.name == "insertstring") { var text = e.args; if (this.mergeNextCommand === undefined) this.mergeNextCommand = true; shouldMerge = shouldMerge && this.mergeNextCommand // previous command allows to coalesce with && (!/\s/.test(text) || /\s/.test(prev.args)); // previous insertion was of same type this.mergeNextCommand = true; } else { shouldMerge = shouldMerge && mergeableCommands.indexOf(e.command.name) !== -1; // the command is mergeable } if ( this.$mergeUndoDeltas != "always" && Date.now() - this.sequenceStartTime > 2000 ) { shouldMerge = false; // the sequence is too long } if (shouldMerge) this.session.mergeUndoDeltas = true; else if (mergeableCommands.indexOf(e.command.name) !== -1) this.sequenceStartTime = Date.now(); }; this.setKeyboardHandler = function(keyboardHandler, cb) { if (keyboardHandler && typeof keyboardHandler === "string") { this.$keybindingId = keyboardHandler; var _self = this; config.loadModule(["keybinding", keyboardHandler], function(module) { if (_self.$keybindingId == keyboardHandler) _self.keyBinding.setKeyboardHandler(module && module.handler); cb && cb(); }); } else { this.$keybindingId = null; this.keyBinding.setKeyboardHandler(keyboardHandler); cb && cb(); } }; this.getKeyboardHandler = function() { return this.keyBinding.getKeyboardHandler(); }; this.setSession = function(session) { if (this.session == session) return; if (this.curOp) this.endOperation(); this.curOp = {}; var oldSession = this.session; if (oldSession) { this.session.off("change", this.$onDocumentChange); this.session.off("changeMode", this.$onChangeMode); this.session.off("tokenizerUpdate", this.$onTokenizerUpdate); this.session.off("changeTabSize", this.$onChangeTabSize); this.session.off("changeWrapLimit", this.$onChangeWrapLimit); this.session.off("changeWrapMode", this.$onChangeWrapMode); this.session.off("changeFold", this.$onChangeFold); this.session.off("changeFrontMarker", this.$onChangeFrontMarker); this.session.off("changeBackMarker", this.$onChangeBackMarker); this.session.off("changeBreakpoint", this.$onChangeBreakpoint); this.session.off("changeAnnotation", this.$onChangeAnnotation); this.session.off("changeOverwrite", this.$onCursorChange); this.session.off("changeScrollTop", this.$onScrollTopChange); this.session.off("changeScrollLeft", this.$onScrollLeftChange); var selection = this.session.getSelection(); selection.off("changeCursor", this.$onCursorChange); selection.off("changeSelection", this.$onSelectionChange); } this.session = session; if (session) { this.$onDocumentChange = this.onDocumentChange.bind(this); session.on("change", this.$onDocumentChange); this.renderer.setSession(session); this.$onChangeMode = this.onChangeMode.bind(this); session.on("changeMode", this.$onChangeMode); this.$onTokenizerUpdate = this.onTokenizerUpdate.bind(this); session.on("tokenizerUpdate", this.$onTokenizerUpdate); this.$onChangeTabSize = this.renderer.onChangeTabSize.bind(this.renderer); session.on("changeTabSize", this.$onChangeTabSize); this.$onChangeWrapLimit = this.onChangeWrapLimit.bind(this); session.on("changeWrapLimit", this.$onChangeWrapLimit); this.$onChangeWrapMode = this.onChangeWrapMode.bind(this); session.on("changeWrapMode", this.$onChangeWrapMode); this.$onChangeFold = this.onChangeFold.bind(this); session.on("changeFold", this.$onChangeFold); this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this); this.session.on("changeFrontMarker", this.$onChangeFrontMarker); this.$onChangeBackMarker = this.onChangeBackMarker.bind(this); this.session.on("changeBackMarker", this.$onChangeBackMarker); this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this); this.session.on("changeBreakpoint", this.$onChangeBreakpoint); this.$onChangeAnnotation = this.onChangeAnnotation.bind(this); this.session.on("changeAnnotation", this.$onChangeAnnotation); this.$onCursorChange = this.onCursorChange.bind(this); this.session.on("changeOverwrite", this.$onCursorChange); this.$onScrollTopChange = this.onScrollTopChange.bind(this); this.session.on("changeScrollTop", this.$onScrollTopChange); this.$onScrollLeftChange = this.onScrollLeftChange.bind(this); this.session.on("changeScrollLeft", this.$onScrollLeftChange); this.selection = session.getSelection(); this.selection.on("changeCursor", this.$onCursorChange); this.$onSelectionChange = this.onSelectionChange.bind(this); this.selection.on("changeSelection", this.$onSelectionChange); this.onChangeMode(); this.$blockScrolling += 1; this.onCursorChange(); this.$blockScrolling -= 1; this.onScrollTopChange(); this.onScrollLeftChange(); this.onSelectionChange(); this.onChangeFrontMarker(); this.onChangeBackMarker(); this.onChangeBreakpoint(); this.onChangeAnnotation(); this.session.getUseWrapMode() && this.renderer.adjustWrapLimit(); this.renderer.updateFull(); } else { this.selection = null; this.renderer.setSession(session); } this._signal("changeSession", { session: session, oldSession: oldSession }); this.curOp = null; oldSession && oldSession._signal("changeEditor", {oldEditor: this}); session && session._signal("changeEditor", {editor: this}); }; this.getSession = function() { return this.session; }; this.setValue = function(val, cursorPos) { this.session.doc.setValue(val); if (!cursorPos) this.selectAll(); else if (cursorPos == 1) this.navigateFileEnd(); else if (cursorPos == -1) this.navigateFileStart(); return val; }; this.getValue = function() { return this.session.getValue(); }; this.getSelection = function() { return this.selection; }; this.resize = function(force) { this.renderer.onResize(force); }; this.setTheme = function(theme, cb) { this.renderer.setTheme(theme, cb); }; this.getTheme = function() { return this.renderer.getTheme(); }; this.setStyle = function(style) { this.renderer.setStyle(style); }; this.unsetStyle = function(style) { this.renderer.unsetStyle(style); }; this.getFontSize = function () { return this.getOption("fontSize") || dom.computedStyle(this.container, "fontSize"); }; this.setFontSize = function(size) { this.setOption("fontSize", size); }; this.$highlightBrackets = function() { if (this.session.$bracketHighlight) { this.session.removeMarker(this.session.$bracketHighlight); this.session.$bracketHighlight = null; } if (this.$highlightPending) { return; } var self = this; this.$highlightPending = true; setTimeout(function() { self.$highlightPending = false; var session = self.session; if (!session || !session.bgTokenizer) return; var pos = session.findMatchingBracket(self.getCursorPosition()); if (pos) { var range = new Range(pos.row, pos.column, pos.row, pos.column + 1); } else if (session.$mode.getMatching) { var range = session.$mode.getMatching(self.session); } if (range) session.$bracketHighlight = session.addMarker(range, "ace_bracket", "text"); }, 50); }; this.$highlightTags = function() { if (this.$highlightTagPending) return; var self = this; this.$highlightTagPending = true; setTimeout(function() { self.$highlightTagPending = false; var session = self.session; if (!session || !session.bgTokenizer) return; var pos = self.getCursorPosition(); var iterator = new TokenIterator(self.session, pos.row, pos.column); var token = iterator.getCurrentToken(); if (!token || !/\b(?:tag-open|tag-name)/.test(token.type)) { session.removeMarker(session.$tagHighlight); session.$tagHighlight = null; return; } if (token.type.indexOf("tag-open") != -1) { token = iterator.stepForward(); if (!token) return; } var tag = token.value; var depth = 0; var prevToken = iterator.stepBackward(); if (prevToken.value == '<'){ do { prevToken = token; token = iterator.stepForward(); if (token && token.value === tag && token.type.indexOf('tag-name') !== -1) { if (prevToken.value === '<'){ depth++; } else if (prevToken.value === '= 0); } else { do { token = prevToken; prevToken = iterator.stepBackward(); if (token && token.value === tag && token.type.indexOf('tag-name') !== -1) { if (prevToken.value === '<') { depth++; } else if (prevToken.value === ' 1)) highlight = false; } if (session.$highlightLineMarker && !highlight) { session.removeMarker(session.$highlightLineMarker.id); session.$highlightLineMarker = null; } else if (!session.$highlightLineMarker && highlight) { var range = new Range(highlight.row, highlight.column, highlight.row, Infinity); range.id = session.addMarker(range, "ace_active-line", "screenLine"); session.$highlightLineMarker = range; } else if (highlight) { session.$highlightLineMarker.start.row = highlight.row; session.$highlightLineMarker.end.row = highlight.row; session.$highlightLineMarker.start.column = highlight.column; session._signal("changeBackMarker"); } }; this.onSelectionChange = function(e) { var session = this.session; if (session.$selectionMarker) { session.removeMarker(session.$selectionMarker); } session.$selectionMarker = null; if (!this.selection.isEmpty()) { var range = this.selection.getRange(); var style = this.getSelectionStyle(); session.$selectionMarker = session.addMarker(range, "ace_selection", style); } else { this.$updateHighlightActiveLine(); } var re = this.$highlightSelectedWord && this.$getSelectionHighLightRegexp(); this.session.highlight(re); this._signal("changeSelection"); }; this.$getSelectionHighLightRegexp = function() { var session = this.session; var selection = this.getSelectionRange(); if (selection.isEmpty() || selection.isMultiLine()) return; var startOuter = selection.start.column - 1; var endOuter = selection.end.column + 1; var line = session.getLine(selection.start.row); var lineCols = line.length; var needle = line.substring(Math.max(startOuter, 0), Math.min(endOuter, lineCols)); if ((startOuter >= 0 && /^[\w\d]/.test(needle)) || (endOuter <= lineCols && /[\w\d]$/.test(needle))) return; needle = line.substring(selection.start.column, selection.end.column); if (!/^[\w\d]+$/.test(needle)) return; var re = this.$search.$assembleRegExp({ wholeWord: true, caseSensitive: true, needle: needle }); return re; }; this.onChangeFrontMarker = function() { this.renderer.updateFrontMarkers(); }; this.onChangeBackMarker = function() { this.renderer.updateBackMarkers(); }; this.onChangeBreakpoint = function() { this.renderer.updateBreakpoints(); }; this.onChangeAnnotation = function() { this.renderer.setAnnotations(this.session.getAnnotations()); }; this.onChangeMode = function(e) { this.renderer.updateText(); this._emit("changeMode", e); }; this.onChangeWrapLimit = function() { this.renderer.updateFull(); }; this.onChangeWrapMode = function() { this.renderer.onResize(true); }; this.onChangeFold = function() { this.$updateHighlightActiveLine(); this.renderer.updateFull(); }; this.getSelectedText = function() { return this.session.getTextRange(this.getSelectionRange()); }; this.getCopyText = function() { var text = this.getSelectedText(); this._signal("copy", text); return text; }; this.onCopy = function() { this.commands.exec("copy", this); }; this.onCut = function() { this.commands.exec("cut", this); }; this.onPaste = function(text, event) { var e = {text: text, event: event}; this.commands.exec("paste", this, e); }; this.$handlePaste = function(e) { if (typeof e == "string") e = {text: e}; this._signal("paste", e); var text = e.text; if (!this.inMultiSelectMode || this.inVirtualSelectionMode) { this.insert(text); } else { var lines = text.split(/\r\n|\r|\n/); var ranges = this.selection.rangeList.ranges; if (lines.length > ranges.length || lines.length < 2 || !lines[1]) return this.commands.exec("insertstring", this, text); for (var i = ranges.length; i--;) { var range = ranges[i]; if (!range.isEmpty()) this.session.remove(range); this.session.insert(range.start, lines[i]); } } }; this.execCommand = function(command, args) { return this.commands.exec(command, this, args); }; this.insert = function(text, pasted) { var session = this.session; var mode = session.getMode(); var cursor = this.getCursorPosition(); if (this.getBehavioursEnabled() && !pasted) { var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text); if (transform) { if (text !== transform.text) { this.session.mergeUndoDeltas = false; this.$mergeNextCommand = false; } text = transform.text; } } if (text == "\t") text = this.session.getTabString(); if (!this.selection.isEmpty()) { var range = this.getSelectionRange(); cursor = this.session.remove(range); this.clearSelection(); } else if (this.session.getOverwrite()) { var range = new Range.fromPoints(cursor, cursor); range.end.column += text.length; this.session.remove(range); } if (text == "\n" || text == "\r\n") { var line = session.getLine(cursor.row); if (cursor.column > line.search(/\S|$/)) { var d = line.substr(cursor.column).search(/\S|$/); session.doc.removeInLine(cursor.row, cursor.column, cursor.column + d); } } this.clearSelection(); var start = cursor.column; var lineState = session.getState(cursor.row); var line = session.getLine(cursor.row); var shouldOutdent = mode.checkOutdent(lineState, line, text); var end = session.insert(cursor, text); if (transform && transform.selection) { if (transform.selection.length == 2) { // Transform relative to the current column this.selection.setSelectionRange( new Range(cursor.row, start + transform.selection[0], cursor.row, start + transform.selection[1])); } else { // Transform relative to the current row. this.selection.setSelectionRange( new Range(cursor.row + transform.selection[0], transform.selection[1], cursor.row + transform.selection[2], transform.selection[3])); } } if (session.getDocument().isNewLine(text)) { var lineIndent = mode.getNextLineIndent(lineState, line.slice(0, cursor.column), session.getTabString()); session.insert({row: cursor.row+1, column: 0}, lineIndent); } if (shouldOutdent) mode.autoOutdent(lineState, session, cursor.row); }; this.onTextInput = function(text) { this.keyBinding.onTextInput(text); }; this.onCommandKey = function(e, hashId, keyCode) { this.keyBinding.onCommandKey(e, hashId, keyCode); }; this.setOverwrite = function(overwrite) { this.session.setOverwrite(overwrite); }; this.getOverwrite = function() { return this.session.getOverwrite(); }; this.toggleOverwrite = function() { this.session.toggleOverwrite(); }; this.setScrollSpeed = function(speed) { this.setOption("scrollSpeed", speed); }; this.getScrollSpeed = function() { return this.getOption("scrollSpeed"); }; this.setDragDelay = function(dragDelay) { this.setOption("dragDelay", dragDelay); }; this.getDragDelay = function() { return this.getOption("dragDelay"); }; this.setSelectionStyle = function(val) { this.setOption("selectionStyle", val); }; this.getSelectionStyle = function() { return this.getOption("selectionStyle"); }; this.setHighlightActiveLine = function(shouldHighlight) { this.setOption("highlightActiveLine", shouldHighlight); }; this.getHighlightActiveLine = function() { return this.getOption("highlightActiveLine"); }; this.setHighlightGutterLine = function(shouldHighlight) { this.setOption("highlightGutterLine", shouldHighlight); }; this.getHighlightGutterLine = function() { return this.getOption("highlightGutterLine"); }; this.setHighlightSelectedWord = function(shouldHighlight) { this.setOption("highlightSelectedWord", shouldHighlight); }; this.getHighlightSelectedWord = function() { return this.$highlightSelectedWord; }; this.setAnimatedScroll = function(shouldAnimate){ this.renderer.setAnimatedScroll(shouldAnimate); }; this.getAnimatedScroll = function(){ return this.renderer.getAnimatedScroll(); }; this.setShowInvisibles = function(showInvisibles) { this.renderer.setShowInvisibles(showInvisibles); }; this.getShowInvisibles = function() { return this.renderer.getShowInvisibles(); }; this.setDisplayIndentGuides = function(display) { this.renderer.setDisplayIndentGuides(display); }; this.getDisplayIndentGuides = function() { return this.renderer.getDisplayIndentGuides(); }; this.setShowPrintMargin = function(showPrintMargin) { this.renderer.setShowPrintMargin(showPrintMargin); }; this.getShowPrintMargin = function() { return this.renderer.getShowPrintMargin(); }; this.setPrintMarginColumn = function(showPrintMargin) { this.renderer.setPrintMarginColumn(showPrintMargin); }; this.getPrintMarginColumn = function() { return this.renderer.getPrintMarginColumn(); }; this.setReadOnly = function(readOnly) { this.setOption("readOnly", readOnly); }; this.getReadOnly = function() { return this.getOption("readOnly"); }; this.setBehavioursEnabled = function (enabled) { this.setOption("behavioursEnabled", enabled); }; this.getBehavioursEnabled = function () { return this.getOption("behavioursEnabled"); }; this.setWrapBehavioursEnabled = function (enabled) { this.setOption("wrapBehavioursEnabled", enabled); }; this.getWrapBehavioursEnabled = function () { return this.getOption("wrapBehavioursEnabled"); }; this.setShowFoldWidgets = function(show) { this.setOption("showFoldWidgets", show); }; this.getShowFoldWidgets = function() { return this.getOption("showFoldWidgets"); }; this.setFadeFoldWidgets = function(fade) { this.setOption("fadeFoldWidgets", fade); }; this.getFadeFoldWidgets = function() { return this.getOption("fadeFoldWidgets"); }; this.remove = function(dir) { if (this.selection.isEmpty()){ if (dir == "left") this.selection.selectLeft(); else this.selection.selectRight(); } var range = this.getSelectionRange(); if (this.getBehavioursEnabled()) { var session = this.session; var state = session.getState(range.start.row); var new_range = session.getMode().transformAction(state, 'deletion', this, session, range); if (range.end.column === 0) { var text = session.getTextRange(range); if (text[text.length - 1] == "\n") { var line = session.getLine(range.end.row); if (/^\s+$/.test(line)) { range.end.column = line.length; } } } if (new_range) range = new_range; } this.session.remove(range); this.clearSelection(); }; this.removeWordRight = function() { if (this.selection.isEmpty()) this.selection.selectWordRight(); this.session.remove(this.getSelectionRange()); this.clearSelection(); }; this.removeWordLeft = function() { if (this.selection.isEmpty()) this.selection.selectWordLeft(); this.session.remove(this.getSelectionRange()); this.clearSelection(); }; this.removeToLineStart = function() { if (this.selection.isEmpty()) this.selection.selectLineStart(); this.session.remove(this.getSelectionRange()); this.clearSelection(); }; this.removeToLineEnd = function() { if (this.selection.isEmpty()) this.selection.selectLineEnd(); var range = this.getSelectionRange(); if (range.start.column == range.end.column && range.start.row == range.end.row) { range.end.column = 0; range.end.row++; } this.session.remove(range); this.clearSelection(); }; this.splitLine = function() { if (!this.selection.isEmpty()) { this.session.remove(this.getSelectionRange()); this.clearSelection(); } var cursor = this.getCursorPosition(); this.insert("\n"); this.moveCursorToPosition(cursor); }; this.transposeLetters = function() { if (!this.selection.isEmpty()) { return; } var cursor = this.getCursorPosition(); var column = cursor.column; if (column === 0) return; var line = this.session.getLine(cursor.row); var swap, range; if (column < line.length) { swap = line.charAt(column) + line.charAt(column-1); range = new Range(cursor.row, column-1, cursor.row, column+1); } else { swap = line.charAt(column-1) + line.charAt(column-2); range = new Range(cursor.row, column-2, cursor.row, column); } this.session.replace(range, swap); }; this.toLowerCase = function() { var originalRange = this.getSelectionRange(); if (this.selection.isEmpty()) { this.selection.selectWord(); } var range = this.getSelectionRange(); var text = this.session.getTextRange(range); this.session.replace(range, text.toLowerCase()); this.selection.setSelectionRange(originalRange); }; this.toUpperCase = function() { var originalRange = this.getSelectionRange(); if (this.selection.isEmpty()) { this.selection.selectWord(); } var range = this.getSelectionRange(); var text = this.session.getTextRange(range); this.session.replace(range, text.toUpperCase()); this.selection.setSelectionRange(originalRange); }; this.indent = function() { var session = this.session; var range = this.getSelectionRange(); if (range.start.row < range.end.row) { var rows = this.$getSelectedRows(); session.indentRows(rows.first, rows.last, "\t"); return; } else if (range.start.column < range.end.column) { var text = session.getTextRange(range); if (!/^\s+$/.test(text)) { var rows = this.$getSelectedRows(); session.indentRows(rows.first, rows.last, "\t"); return; } } var line = session.getLine(range.start.row); var position = range.start; var size = session.getTabSize(); var column = session.documentToScreenColumn(position.row, position.column); if (this.session.getUseSoftTabs()) { var count = (size - column % size); var indentString = lang.stringRepeat(" ", count); } else { var count = column % size; while (line[range.start.column - 1] == " " && count) { range.start.column--; count--; } this.selection.setSelectionRange(range); indentString = "\t"; } return this.insert(indentString); }; this.blockIndent = function() { var rows = this.$getSelectedRows(); this.session.indentRows(rows.first, rows.last, "\t"); }; this.blockOutdent = function() { var selection = this.session.getSelection(); this.session.outdentRows(selection.getRange()); }; this.sortLines = function() { var rows = this.$getSelectedRows(); var session = this.session; var lines = []; for (i = rows.first; i <= rows.last; i++) lines.push(session.getLine(i)); lines.sort(function(a, b) { if (a.toLowerCase() < b.toLowerCase()) return -1; if (a.toLowerCase() > b.toLowerCase()) return 1; return 0; }); var deleteRange = new Range(0, 0, 0, 0); for (var i = rows.first; i <= rows.last; i++) { var line = session.getLine(i); deleteRange.start.row = i; deleteRange.end.row = i; deleteRange.end.column = line.length; session.replace(deleteRange, lines[i-rows.first]); } }; this.toggleCommentLines = function() { var state = this.session.getState(this.getCursorPosition().row); var rows = this.$getSelectedRows(); this.session.getMode().toggleCommentLines(state, this.session, rows.first, rows.last); }; this.toggleBlockComment = function() { var cursor = this.getCursorPosition(); var state = this.session.getState(cursor.row); var range = this.getSelectionRange(); this.session.getMode().toggleBlockComment(state, this.session, range, cursor); }; this.getNumberAt = function(row, column) { var _numberRx = /[\-]?[0-9]+(?:\.[0-9]+)?/g; _numberRx.lastIndex = 0; var s = this.session.getLine(row); while (_numberRx.lastIndex < column) { var m = _numberRx.exec(s); if(m.index <= column && m.index+m[0].length >= column){ var number = { value: m[0], start: m.index, end: m.index+m[0].length }; return number; } } return null; }; this.modifyNumber = function(amount) { var row = this.selection.getCursor().row; var column = this.selection.getCursor().column; var charRange = new Range(row, column-1, row, column); var c = this.session.getTextRange(charRange); if (!isNaN(parseFloat(c)) && isFinite(c)) { var nr = this.getNumberAt(row, column); if (nr) { var fp = nr.value.indexOf(".") >= 0 ? nr.start + nr.value.indexOf(".") + 1 : nr.end; var decimals = nr.start + nr.value.length - fp; var t = parseFloat(nr.value); t *= Math.pow(10, decimals); if(fp !== nr.end && column < fp){ amount *= Math.pow(10, nr.end - column - 1); } else { amount *= Math.pow(10, nr.end - column); } t += amount; t /= Math.pow(10, decimals); var nnr = t.toFixed(decimals); var replaceRange = new Range(row, nr.start, row, nr.end); this.session.replace(replaceRange, nnr); this.moveCursorTo(row, Math.max(nr.start +1, column + nnr.length - nr.value.length)); } } }; this.removeLines = function() { var rows = this.$getSelectedRows(); this.session.removeFullLines(rows.first, rows.last); this.clearSelection(); }; this.duplicateSelection = function() { var sel = this.selection; var doc = this.session; var range = sel.getRange(); var reverse = sel.isBackwards(); if (range.isEmpty()) { var row = range.start.row; doc.duplicateLines(row, row); } else { var point = reverse ? range.start : range.end; var endPoint = doc.insert(point, doc.getTextRange(range), false); range.start = point; range.end = endPoint; sel.setSelectionRange(range, reverse); } }; this.moveLinesDown = function() { this.$moveLines(1, false); }; this.moveLinesUp = function() { this.$moveLines(-1, false); }; this.moveText = function(range, toPosition, copy) { return this.session.moveText(range, toPosition, copy); }; this.copyLinesUp = function() { this.$moveLines(-1, true); }; this.copyLinesDown = function() { this.$moveLines(1, true); }; this.$moveLines = function(dir, copy) { var rows, moved; var selection = this.selection; if (!selection.inMultiSelectMode || this.inVirtualSelectionMode) { var range = selection.toOrientedRange(); rows = this.$getSelectedRows(range); moved = this.session.$moveLines(rows.first, rows.last, copy ? 0 : dir); if (copy && dir == -1) moved = 0; range.moveBy(moved, 0); selection.fromOrientedRange(range); } else { var ranges = selection.rangeList.ranges; selection.rangeList.detach(this.session); this.inVirtualSelectionMode = true; var diff = 0; var totalDiff = 0; var l = ranges.length; for (var i = 0; i < l; i++) { var rangeIndex = i; ranges[i].moveBy(diff, 0); rows = this.$getSelectedRows(ranges[i]); var first = rows.first; var last = rows.last; while (++i < l) { if (totalDiff) ranges[i].moveBy(totalDiff, 0); var subRows = this.$getSelectedRows(ranges[i]); if (copy && subRows.first != last) break; else if (!copy && subRows.first > last + 1) break; last = subRows.last; } i--; diff = this.session.$moveLines(first, last, copy ? 0 : dir); if (copy && dir == -1) rangeIndex = i + 1; while (rangeIndex <= i) { ranges[rangeIndex].moveBy(diff, 0); rangeIndex++; } if (!copy) diff = 0; totalDiff += diff; } selection.fromOrientedRange(selection.ranges[0]); selection.rangeList.attach(this.session); this.inVirtualSelectionMode = false; } }; this.$getSelectedRows = function(range) { range = (range || this.getSelectionRange()).collapseRows(); return { first: this.session.getRowFoldStart(range.start.row), last: this.session.getRowFoldEnd(range.end.row) }; }; this.onCompositionStart = function(text) { this.renderer.showComposition(this.getCursorPosition()); }; this.onCompositionUpdate = function(text) { this.renderer.setCompositionText(text); }; this.onCompositionEnd = function() { this.renderer.hideComposition(); }; this.getFirstVisibleRow = function() { return this.renderer.getFirstVisibleRow(); }; this.getLastVisibleRow = function() { return this.renderer.getLastVisibleRow(); }; this.isRowVisible = function(row) { return (row >= this.getFirstVisibleRow() && row <= this.getLastVisibleRow()); }; this.isRowFullyVisible = function(row) { return (row >= this.renderer.getFirstFullyVisibleRow() && row <= this.renderer.getLastFullyVisibleRow()); }; this.$getVisibleRowCount = function() { return this.renderer.getScrollBottomRow() - this.renderer.getScrollTopRow() + 1; }; this.$moveByPage = function(dir, select) { var renderer = this.renderer; var config = this.renderer.layerConfig; var rows = dir * Math.floor(config.height / config.lineHeight); this.$blockScrolling++; if (select === true) { this.selection.$moveSelection(function(){ this.moveCursorBy(rows, 0); }); } else if (select === false) { this.selection.moveCursorBy(rows, 0); this.selection.clearSelection(); } this.$blockScrolling--; var scrollTop = renderer.scrollTop; renderer.scrollBy(0, rows * config.lineHeight); if (select != null) renderer.scrollCursorIntoView(null, 0.5); renderer.animateScrolling(scrollTop); }; this.selectPageDown = function() { this.$moveByPage(1, true); }; this.selectPageUp = function() { this.$moveByPage(-1, true); }; this.gotoPageDown = function() { this.$moveByPage(1, false); }; this.gotoPageUp = function() { this.$moveByPage(-1, false); }; this.scrollPageDown = function() { this.$moveByPage(1); }; this.scrollPageUp = function() { this.$moveByPage(-1); }; this.scrollToRow = function(row) { this.renderer.scrollToRow(row); }; this.scrollToLine = function(line, center, animate, callback) { this.renderer.scrollToLine(line, center, animate, callback); }; this.centerSelection = function() { var range = this.getSelectionRange(); var pos = { row: Math.floor(range.start.row + (range.end.row - range.start.row) / 2), column: Math.floor(range.start.column + (range.end.column - range.start.column) / 2) }; this.renderer.alignCursor(pos, 0.5); }; this.getCursorPosition = function() { return this.selection.getCursor(); }; this.getCursorPositionScreen = function() { return this.session.documentToScreenPosition(this.getCursorPosition()); }; this.getSelectionRange = function() { return this.selection.getRange(); }; this.selectAll = function() { this.$blockScrolling += 1; this.selection.selectAll(); this.$blockScrolling -= 1; }; this.clearSelection = function() { this.selection.clearSelection(); }; this.moveCursorTo = function(row, column) { this.selection.moveCursorTo(row, column); }; this.moveCursorToPosition = function(pos) { this.selection.moveCursorToPosition(pos); }; this.jumpToMatching = function(select, expand) { var cursor = this.getCursorPosition(); var iterator = new TokenIterator(this.session, cursor.row, cursor.column); var prevToken = iterator.getCurrentToken(); var token = prevToken || iterator.stepForward(); if (!token) return; var matchType; var found = false; var depth = {}; var i = cursor.column - token.start; var bracketType; var brackets = { ")": "(", "(": "(", "]": "[", "[": "[", "{": "{", "}": "{" }; do { if (token.value.match(/[{}()\[\]]/g)) { for (; i < token.value.length && !found; i++) { if (!brackets[token.value[i]]) { continue; } bracketType = brackets[token.value[i]] + '.' + token.type.replace("rparen", "lparen"); if (isNaN(depth[bracketType])) { depth[bracketType] = 0; } switch (token.value[i]) { case '(': case '[': case '{': depth[bracketType]++; break; case ')': case ']': case '}': depth[bracketType]--; if (depth[bracketType] === -1) { matchType = 'bracket'; found = true; } break; } } } else if (token && token.type.indexOf('tag-name') !== -1) { if (isNaN(depth[token.value])) { depth[token.value] = 0; } if (prevToken.value === '<') { depth[token.value]++; } else if (prevToken.value === '= 0; --i) { if(this.$tryReplace(ranges[i], replacement)) { replaced++; } } this.selection.setSelectionRange(selection); this.$blockScrolling -= 1; return replaced; }; this.$tryReplace = function(range, replacement) { var input = this.session.getTextRange(range); replacement = this.$search.replace(input, replacement); if (replacement !== null) { range.end = this.session.replace(range, replacement); return range; } else { return null; } }; this.getLastSearchOptions = function() { return this.$search.getOptions(); }; this.find = function(needle, options, animate) { if (!options) options = {}; if (typeof needle == "string" || needle instanceof RegExp) options.needle = needle; else if (typeof needle == "object") oop.mixin(options, needle); var range = this.selection.getRange(); if (options.needle == null) { needle = this.session.getTextRange(range) || this.$search.$options.needle; if (!needle) { range = this.session.getWordRange(range.start.row, range.start.column); needle = this.session.getTextRange(range); } this.$search.set({needle: needle}); } this.$search.set(options); if (!options.start) this.$search.set({start: range}); var newRange = this.$search.find(this.session); if (options.preventScroll) return newRange; if (newRange) { this.revealRange(newRange, animate); return newRange; } if (options.backwards) range.start = range.end; else range.end = range.start; this.selection.setRange(range); }; this.findNext = function(options, animate) { this.find({skipCurrent: true, backwards: false}, options, animate); }; this.findPrevious = function(options, animate) { this.find(options, {skipCurrent: true, backwards: true}, animate); }; this.revealRange = function(range, animate) { this.$blockScrolling += 1; this.session.unfold(range); this.selection.setSelectionRange(range); this.$blockScrolling -= 1; var scrollTop = this.renderer.scrollTop; this.renderer.scrollSelectionIntoView(range.start, range.end, 0.5); if (animate !== false) this.renderer.animateScrolling(scrollTop); }; this.undo = function() { this.$blockScrolling++; this.session.getUndoManager().undo(); this.$blockScrolling--; this.renderer.scrollCursorIntoView(null, 0.5); }; this.redo = function() { this.$blockScrolling++; this.session.getUndoManager().redo(); this.$blockScrolling--; this.renderer.scrollCursorIntoView(null, 0.5); }; this.destroy = function() { this.renderer.destroy(); this._signal("destroy", this); if (this.session) { this.session.destroy(); } }; this.setAutoScrollEditorIntoView = function(enable) { if (!enable) return; var rect; var self = this; var shouldScroll = false; if (!this.$scrollAnchor) this.$scrollAnchor = document.createElement("div"); var scrollAnchor = this.$scrollAnchor; scrollAnchor.style.cssText = "position:absolute"; this.container.insertBefore(scrollAnchor, this.container.firstChild); var onChangeSelection = this.on("changeSelection", function() { shouldScroll = true; }); var onBeforeRender = this.renderer.on("beforeRender", function() { if (shouldScroll) rect = self.renderer.container.getBoundingClientRect(); }); var onAfterRender = this.renderer.on("afterRender", function() { if (shouldScroll && rect && (self.isFocused() || self.searchBox && self.searchBox.isFocused()) ) { var renderer = self.renderer; var pos = renderer.$cursorLayer.$pixelPos; var config = renderer.layerConfig; var top = pos.top - config.offset; if (pos.top >= 0 && top + rect.top < 0) { shouldScroll = true; } else if (pos.top < config.height && pos.top + rect.top + config.lineHeight > window.innerHeight) { shouldScroll = false; } else { shouldScroll = null; } if (shouldScroll != null) { scrollAnchor.style.top = top + "px"; scrollAnchor.style.left = pos.left + "px"; scrollAnchor.style.height = config.lineHeight + "px"; scrollAnchor.scrollIntoView(shouldScroll); } shouldScroll = rect = null; } }); this.setAutoScrollEditorIntoView = function(enable) { if (enable) return; delete this.setAutoScrollEditorIntoView; this.off("changeSelection", onChangeSelection); this.renderer.off("afterRender", onAfterRender); this.renderer.off("beforeRender", onBeforeRender); }; }; this.$resetCursorStyle = function() { var style = this.$cursorStyle || "ace"; var cursorLayer = this.renderer.$cursorLayer; if (!cursorLayer) return; cursorLayer.setSmoothBlinking(/smooth/.test(style)); cursorLayer.isBlinking = !this.$readOnly && style != "wide"; dom.setCssClass(cursorLayer.element, "ace_slim-cursors", /slim/.test(style)); }; }).call(Editor.prototype); config.defineOptions(Editor.prototype, "editor", { selectionStyle: { set: function(style) { this.onSelectionChange(); this._signal("changeSelectionStyle", {data: style}); }, initialValue: "line" }, highlightActiveLine: { set: function() {this.$updateHighlightActiveLine();}, initialValue: true }, highlightSelectedWord: { set: function(shouldHighlight) {this.$onSelectionChange();}, initialValue: true }, readOnly: { set: function(readOnly) { this.$resetCursorStyle(); }, initialValue: false }, cursorStyle: { set: function(val) { this.$resetCursorStyle(); }, values: ["ace", "slim", "smooth", "wide"], initialValue: "ace" }, mergeUndoDeltas: { values: [false, true, "always"], initialValue: true }, behavioursEnabled: {initialValue: true}, wrapBehavioursEnabled: {initialValue: true}, autoScrollEditorIntoView: { set: function(val) {this.setAutoScrollEditorIntoView(val)} }, keyboardHandler: { set: function(val) { this.setKeyboardHandler(val); }, get: function() { return this.keybindingId; }, handlesSet: true }, hScrollBarAlwaysVisible: "renderer", vScrollBarAlwaysVisible: "renderer", highlightGutterLine: "renderer", animatedScroll: "renderer", showInvisibles: "renderer", showPrintMargin: "renderer", printMarginColumn: "renderer", printMargin: "renderer", fadeFoldWidgets: "renderer", showFoldWidgets: "renderer", showLineNumbers: "renderer", showGutter: "renderer", displayIndentGuides: "renderer", fontSize: "renderer", fontFamily: "renderer", maxLines: "renderer", minLines: "renderer", scrollPastEnd: "renderer", fixedWidthGutter: "renderer", theme: "renderer", scrollSpeed: "$mouseHandler", dragDelay: "$mouseHandler", dragEnabled: "$mouseHandler", focusTimout: "$mouseHandler", tooltipFollowsMouse: "$mouseHandler", firstLineNumber: "session", overwrite: "session", newLineMode: "session", useWorker: "session", useSoftTabs: "session", tabSize: "session", wrap: "session", indentedSoftWrap: "session", foldStyle: "session", mode: "session" }); exports.Editor = Editor; }); define("ace/undomanager",["require","exports","module"], function(require, exports, module) { "use strict"; var UndoManager = function() { this.reset(); }; (function() { this.execute = function(options) { var deltaSets = options.args[0]; this.$doc = options.args[1]; if (options.merge && this.hasUndo()){ this.dirtyCounter--; deltaSets = this.$undoStack.pop().concat(deltaSets); } this.$undoStack.push(deltaSets); this.$redoStack = []; if (this.dirtyCounter < 0) { this.dirtyCounter = NaN; } this.dirtyCounter++; }; this.undo = function(dontSelect) { var deltaSets = this.$undoStack.pop(); var undoSelectionRange = null; if (deltaSets) { undoSelectionRange = this.$doc.undoChanges(deltaSets, dontSelect); this.$redoStack.push(deltaSets); this.dirtyCounter--; } return undoSelectionRange; }; this.redo = function(dontSelect) { var deltaSets = this.$redoStack.pop(); var redoSelectionRange = null; if (deltaSets) { redoSelectionRange = this.$doc.redoChanges(this.$deserializeDeltas(deltaSets), dontSelect); this.$undoStack.push(deltaSets); this.dirtyCounter++; } return redoSelectionRange; }; this.reset = function() { this.$undoStack = []; this.$redoStack = []; this.dirtyCounter = 0; }; this.hasUndo = function() { return this.$undoStack.length > 0; }; this.hasRedo = function() { return this.$redoStack.length > 0; }; this.markClean = function() { this.dirtyCounter = 0; }; this.isClean = function() { return this.dirtyCounter === 0; }; this.$serializeDeltas = function(deltaSets) { return cloneDeltaSetsObj(deltaSets, $serializeDelta); }; this.$deserializeDeltas = function(deltaSets) { return cloneDeltaSetsObj(deltaSets, $deserializeDelta); }; function $serializeDelta(delta){ return { action: delta.action, start: delta.start, end: delta.end, lines: delta.lines.length == 1 ? null : delta.lines, text: delta.lines.length == 1 ? delta.lines[0] : null }; } function $deserializeDelta(delta) { return { action: delta.action, start: delta.start, end: delta.end, lines: delta.lines || [delta.text] }; } function cloneDeltaSetsObj(deltaSets_old, fnGetModifiedDelta) { var deltaSets_new = new Array(deltaSets_old.length); for (var i = 0; i < deltaSets_old.length; i++) { var deltaSet_old = deltaSets_old[i]; var deltaSet_new = { group: deltaSet_old.group, deltas: new Array(deltaSet_old.length)}; for (var j = 0; j < deltaSet_old.deltas.length; j++) { var delta_old = deltaSet_old.deltas[j]; deltaSet_new.deltas[j] = fnGetModifiedDelta(delta_old); } deltaSets_new[i] = deltaSet_new; } return deltaSets_new; } }).call(UndoManager.prototype); exports.UndoManager = UndoManager; }); define("ace/layer/gutter",["require","exports","module","ace/lib/dom","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter"], function(require, exports, module) { "use strict"; var dom = require("../lib/dom"); var oop = require("../lib/oop"); var lang = require("../lib/lang"); var EventEmitter = require("../lib/event_emitter").EventEmitter; var Gutter = function(parentEl) { this.element = dom.createElement("div"); this.element.className = "ace_layer ace_gutter-layer"; parentEl.appendChild(this.element); this.setShowFoldWidgets(this.$showFoldWidgets); this.gutterWidth = 0; this.$annotations = []; this.$updateAnnotations = this.$updateAnnotations.bind(this); this.$cells = []; }; (function() { oop.implement(this, EventEmitter); this.setSession = function(session) { if (this.session) this.session.removeEventListener("change", this.$updateAnnotations); this.session = session; if (session) session.on("change", this.$updateAnnotations); }; this.addGutterDecoration = function(row, className){ if (window.console) console.warn && console.warn("deprecated use session.addGutterDecoration"); this.session.addGutterDecoration(row, className); }; this.removeGutterDecoration = function(row, className){ if (window.console) console.warn && console.warn("deprecated use session.removeGutterDecoration"); this.session.removeGutterDecoration(row, className); }; this.setAnnotations = function(annotations) { this.$annotations = []; for (var i = 0; i < annotations.length; i++) { var annotation = annotations[i]; var row = annotation.row; var rowInfo = this.$annotations[row]; if (!rowInfo) rowInfo = this.$annotations[row] = {text: []}; var annoText = annotation.text; annoText = annoText ? lang.escapeHTML(annoText) : annotation.html || ""; if (rowInfo.text.indexOf(annoText) === -1) rowInfo.text.push(annoText); var type = annotation.type; if (type == "error") rowInfo.className = " ace_error"; else if (type == "warning" && rowInfo.className != " ace_error") rowInfo.className = " ace_warning"; else if (type == "info" && (!rowInfo.className)) rowInfo.className = " ace_info"; } }; this.$updateAnnotations = function (delta) { if (!this.$annotations.length) return; var firstRow = delta.start.row; var len = delta.end.row - firstRow; if (len === 0) { } else if (delta.action == 'remove') { this.$annotations.splice(firstRow, len + 1, null); } else { var args = new Array(len + 1); args.unshift(firstRow, 1); this.$annotations.splice.apply(this.$annotations, args); } }; this.update = function(config) { var session = this.session; var firstRow = config.firstRow; var lastRow = Math.min(config.lastRow + config.gutterOffset, // needed to compensate for hor scollbar session.getLength() - 1); var fold = session.getNextFoldLine(firstRow); var foldStart = fold ? fold.start.row : Infinity; var foldWidgets = this.$showFoldWidgets && session.foldWidgets; var breakpoints = session.$breakpoints; var decorations = session.$decorations; var firstLineNumber = session.$firstLineNumber; var lastLineNumber = 0; var gutterRenderer = session.gutterRenderer || this.$renderer; var cell = null; var index = -1; var row = firstRow; while (true) { if (row > foldStart) { row = fold.end.row + 1; fold = session.getNextFoldLine(row, fold); foldStart = fold ? fold.start.row : Infinity; } if (row > lastRow) { while (this.$cells.length > index + 1) { cell = this.$cells.pop(); this.element.removeChild(cell.element); } break; } cell = this.$cells[++index]; if (!cell) { cell = {element: null, textNode: null, foldWidget: null}; cell.element = dom.createElement("div"); cell.textNode = document.createTextNode(''); cell.element.appendChild(cell.textNode); this.element.appendChild(cell.element); this.$cells[index] = cell; } var className = "ace_gutter-cell "; if (breakpoints[row]) className += breakpoints[row]; if (decorations[row]) className += decorations[row]; if (this.$annotations[row]) className += this.$annotations[row].className; if (cell.element.className != className) cell.element.className = className; var height = session.getRowLength(row) * config.lineHeight + "px"; if (height != cell.element.style.height) cell.element.style.height = height; if (foldWidgets) { var c = foldWidgets[row]; if (c == null) c = foldWidgets[row] = session.getFoldWidget(row); } if (c) { if (!cell.foldWidget) { cell.foldWidget = dom.createElement("span"); cell.element.appendChild(cell.foldWidget); } var className = "ace_fold-widget ace_" + c; if (c == "start" && row == foldStart && row < fold.end.row) className += " ace_closed"; else className += " ace_open"; if (cell.foldWidget.className != className) cell.foldWidget.className = className; var height = config.lineHeight + "px"; if (cell.foldWidget.style.height != height) cell.foldWidget.style.height = height; } else { if (cell.foldWidget) { cell.element.removeChild(cell.foldWidget); cell.foldWidget = null; } } var text = lastLineNumber = gutterRenderer ? gutterRenderer.getText(session, row) : row + firstLineNumber; if (text != cell.textNode.data) cell.textNode.data = text; row++; } this.element.style.height = config.minHeight + "px"; if (this.$fixedWidth || session.$useWrapMode) lastLineNumber = session.getLength() + firstLineNumber; var gutterWidth = gutterRenderer ? gutterRenderer.getWidth(session, lastLineNumber, config) : lastLineNumber.toString().length * config.characterWidth; var padding = this.$padding || this.$computePadding(); gutterWidth += padding.left + padding.right; if (gutterWidth !== this.gutterWidth && !isNaN(gutterWidth)) { this.gutterWidth = gutterWidth; this.element.style.width = Math.ceil(this.gutterWidth) + "px"; this._emit("changeGutterWidth", gutterWidth); } }; this.$fixedWidth = false; this.$showLineNumbers = true; this.$renderer = ""; this.setShowLineNumbers = function(show) { this.$renderer = !show && { getWidth: function() {return ""}, getText: function() {return ""} }; }; this.getShowLineNumbers = function() { return this.$showLineNumbers; }; this.$showFoldWidgets = true; this.setShowFoldWidgets = function(show) { if (show) dom.addCssClass(this.element, "ace_folding-enabled"); else dom.removeCssClass(this.element, "ace_folding-enabled"); this.$showFoldWidgets = show; this.$padding = null; }; this.getShowFoldWidgets = function() { return this.$showFoldWidgets; }; this.$computePadding = function() { if (!this.element.firstChild) return {left: 0, right: 0}; var style = dom.computedStyle(this.element.firstChild); this.$padding = {}; this.$padding.left = parseInt(style.paddingLeft) + 1 || 0; this.$padding.right = parseInt(style.paddingRight) || 0; return this.$padding; }; this.getRegion = function(point) { var padding = this.$padding || this.$computePadding(); var rect = this.element.getBoundingClientRect(); if (point.x < padding.left + rect.left) return "markers"; if (this.$showFoldWidgets && point.x > rect.right - padding.right) return "foldWidgets"; }; }).call(Gutter.prototype); exports.Gutter = Gutter; }); define("ace/layer/marker",["require","exports","module","ace/range","ace/lib/dom"], function(require, exports, module) { "use strict"; var Range = require("../range").Range; var dom = require("../lib/dom"); var Marker = function(parentEl) { this.element = dom.createElement("div"); this.element.className = "ace_layer ace_marker-layer"; parentEl.appendChild(this.element); }; (function() { this.$padding = 0; this.setPadding = function(padding) { this.$padding = padding; }; this.setSession = function(session) { this.session = session; }; this.setMarkers = function(markers) { this.markers = markers; }; this.update = function(config) { var config = config || this.config; if (!config) return; this.config = config; var html = []; for (var key in this.markers) { var marker = this.markers[key]; if (!marker.range) { marker.update(html, this, this.session, config); continue; } var range = marker.range.clipRows(config.firstRow, config.lastRow); if (range.isEmpty()) continue; range = range.toScreenRange(this.session); if (marker.renderer) { var top = this.$getTop(range.start.row, config); var left = this.$padding + range.start.column * config.characterWidth; marker.renderer(html, range, left, top, config); } else if (marker.type == "fullLine") { this.drawFullLineMarker(html, range, marker.clazz, config); } else if (marker.type == "screenLine") { this.drawScreenLineMarker(html, range, marker.clazz, config); } else if (range.isMultiLine()) { if (marker.type == "text") this.drawTextMarker(html, range, marker.clazz, config); else this.drawMultiLineMarker(html, range, marker.clazz, config); } else { this.drawSingleLineMarker(html, range, marker.clazz + " ace_start" + " ace_br15", config); } } this.element.innerHTML = html.join(""); }; this.$getTop = function(row, layerConfig) { return (row - layerConfig.firstRowScreen) * layerConfig.lineHeight; }; function getBorderClass(tl, tr, br, bl) { return (tl ? 1 : 0) | (tr ? 2 : 0) | (br ? 4 : 0) | (bl ? 8 : 0); } this.drawTextMarker = function(stringBuilder, range, clazz, layerConfig, extraStyle) { var session = this.session; var start = range.start.row; var end = range.end.row; var row = start; var prev = 0; var curr = 0; var next = session.getScreenLastRowColumn(row); var lineRange = new Range(row, range.start.column, row, curr); for (; row <= end; row++) { lineRange.start.row = lineRange.end.row = row; lineRange.start.column = row == start ? range.start.column : session.getRowWrapIndent(row); lineRange.end.column = next; prev = curr; curr = next; next = row + 1 < end ? session.getScreenLastRowColumn(row + 1) : row == end ? 0 : range.end.column; this.drawSingleLineMarker(stringBuilder, lineRange, clazz + (row == start ? " ace_start" : "") + " ace_br" + getBorderClass(row == start || row == start + 1 && range.start.column, prev < curr, curr > next, row == end), layerConfig, row == end ? 0 : 1, extraStyle); } }; this.drawMultiLineMarker = function(stringBuilder, range, clazz, config, extraStyle) { var padding = this.$padding; var height = config.lineHeight; var top = this.$getTop(range.start.row, config); var left = padding + range.start.column * config.characterWidth; extraStyle = extraStyle || ""; stringBuilder.push( "
" ); top = this.$getTop(range.end.row, config); var width = range.end.column * config.characterWidth; stringBuilder.push( "
" ); height = (range.end.row - range.start.row - 1) * config.lineHeight; if (height <= 0) return; top = this.$getTop(range.start.row + 1, config); var radiusClass = (range.start.column ? 1 : 0) | (range.end.column ? 0 : 8); stringBuilder.push( "
" ); }; this.drawSingleLineMarker = function(stringBuilder, range, clazz, config, extraLength, extraStyle) { var height = config.lineHeight; var width = (range.end.column + (extraLength || 0) - range.start.column) * config.characterWidth; var top = this.$getTop(range.start.row, config); var left = this.$padding + range.start.column * config.characterWidth; stringBuilder.push( "
" ); }; this.drawFullLineMarker = function(stringBuilder, range, clazz, config, extraStyle) { var top = this.$getTop(range.start.row, config); var height = config.lineHeight; if (range.start.row != range.end.row) height += this.$getTop(range.end.row, config) - top; stringBuilder.push( "
" ); }; this.drawScreenLineMarker = function(stringBuilder, range, clazz, config, extraStyle) { var top = this.$getTop(range.start.row, config); var height = config.lineHeight; stringBuilder.push( "
" ); }; }).call(Marker.prototype); exports.Marker = Marker; }); define("ace/layer/text",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/lib/event_emitter"], function(require, exports, module) { "use strict"; var oop = require("../lib/oop"); var dom = require("../lib/dom"); var lang = require("../lib/lang"); var useragent = require("../lib/useragent"); var EventEmitter = require("../lib/event_emitter").EventEmitter; var Text = function(parentEl) { this.element = dom.createElement("div"); this.element.className = "ace_layer ace_text-layer"; parentEl.appendChild(this.element); this.$updateEolChar = this.$updateEolChar.bind(this); }; (function() { oop.implement(this, EventEmitter); this.EOF_CHAR = "\xB6"; this.EOL_CHAR_LF = "\xAC"; this.EOL_CHAR_CRLF = "\xa4"; this.EOL_CHAR = this.EOL_CHAR_LF; this.TAB_CHAR = "\u2014"; //"\u21E5"; this.SPACE_CHAR = "\xB7"; this.$padding = 0; this.$updateEolChar = function() { var EOL_CHAR = this.session.doc.getNewLineCharacter() == "\n" ? this.EOL_CHAR_LF : this.EOL_CHAR_CRLF; if (this.EOL_CHAR != EOL_CHAR) { this.EOL_CHAR = EOL_CHAR; return true; } } this.setPadding = function(padding) { this.$padding = padding; this.element.style.padding = "0 " + padding + "px"; }; this.getLineHeight = function() { return this.$fontMetrics.$characterSize.height || 0; }; this.getCharacterWidth = function() { return this.$fontMetrics.$characterSize.width || 0; }; this.$setFontMetrics = function(measure) { this.$fontMetrics = measure; this.$fontMetrics.on("changeCharacterSize", function(e) { this._signal("changeCharacterSize", e); }.bind(this)); this.$pollSizeChanges(); } this.checkForSizeChanges = function() { this.$fontMetrics.checkForSizeChanges(); }; this.$pollSizeChanges = function() { return this.$pollSizeChangesTimer = this.$fontMetrics.$pollSizeChanges(); }; this.setSession = function(session) { this.session = session; if (session) this.$computeTabString(); }; this.showInvisibles = false; this.setShowInvisibles = function(showInvisibles) { if (this.showInvisibles == showInvisibles) return false; this.showInvisibles = showInvisibles; this.$computeTabString(); return true; }; this.displayIndentGuides = true; this.setDisplayIndentGuides = function(display) { if (this.displayIndentGuides == display) return false; this.displayIndentGuides = display; this.$computeTabString(); return true; }; this.$tabStrings = []; this.onChangeTabSize = this.$computeTabString = function() { var tabSize = this.session.getTabSize(); this.tabSize = tabSize; var tabStr = this.$tabStrings = [0]; for (var i = 1; i < tabSize + 1; i++) { if (this.showInvisibles) { tabStr.push("" + lang.stringRepeat(this.TAB_CHAR, i) + ""); } else { tabStr.push(lang.stringRepeat(" ", i)); } } if (this.displayIndentGuides) { this.$indentGuideRe = /\s\S| \t|\t |\s$/; var className = "ace_indent-guide"; var spaceClass = ""; var tabClass = ""; if (this.showInvisibles) { className += " ace_invisible"; spaceClass = " ace_invisible_space"; tabClass = " ace_invisible_tab"; var spaceContent = lang.stringRepeat(this.SPACE_CHAR, this.tabSize); var tabContent = lang.stringRepeat(this.TAB_CHAR, this.tabSize); } else{ var spaceContent = lang.stringRepeat(" ", this.tabSize); var tabContent = spaceContent; } this.$tabStrings[" "] = "" + spaceContent + ""; this.$tabStrings["\t"] = "" + tabContent + ""; } }; this.updateLines = function(config, firstRow, lastRow) { if (this.config.lastRow != config.lastRow || this.config.firstRow != config.firstRow) { this.scrollLines(config); } this.config = config; var first = Math.max(firstRow, config.firstRow); var last = Math.min(lastRow, config.lastRow); var lineElements = this.element.childNodes; var lineElementsIdx = 0; for (var row = config.firstRow; row < first; row++) { var foldLine = this.session.getFoldLine(row); if (foldLine) { if (foldLine.containsRow(first)) { first = foldLine.start.row; break; } else { row = foldLine.end.row; } } lineElementsIdx ++; } var row = first; var foldLine = this.session.getNextFoldLine(row); var foldStart = foldLine ? foldLine.start.row : Infinity; while (true) { if (row > foldStart) { row = foldLine.end.row+1; foldLine = this.session.getNextFoldLine(row, foldLine); foldStart = foldLine ? foldLine.start.row :Infinity; } if (row > last) break; var lineElement = lineElements[lineElementsIdx++]; if (lineElement) { var html = []; this.$renderLine( html, row, !this.$useLineGroups(), row == foldStart ? foldLine : false ); lineElement.style.height = config.lineHeight * this.session.getRowLength(row) + "px"; lineElement.innerHTML = html.join(""); } row++; } }; this.scrollLines = function(config) { var oldConfig = this.config; this.config = config; if (!oldConfig || oldConfig.lastRow < config.firstRow) return this.update(config); if (config.lastRow < oldConfig.firstRow) return this.update(config); var el = this.element; if (oldConfig.firstRow < config.firstRow) for (var row=this.session.getFoldedRowCount(oldConfig.firstRow, config.firstRow - 1); row>0; row--) el.removeChild(el.firstChild); if (oldConfig.lastRow > config.lastRow) for (var row=this.session.getFoldedRowCount(config.lastRow + 1, oldConfig.lastRow); row>0; row--) el.removeChild(el.lastChild); if (config.firstRow < oldConfig.firstRow) { var fragment = this.$renderLinesFragment(config, config.firstRow, oldConfig.firstRow - 1); if (el.firstChild) el.insertBefore(fragment, el.firstChild); else el.appendChild(fragment); } if (config.lastRow > oldConfig.lastRow) { var fragment = this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow); el.appendChild(fragment); } }; this.$renderLinesFragment = function(config, firstRow, lastRow) { var fragment = this.element.ownerDocument.createDocumentFragment(); var row = firstRow; var foldLine = this.session.getNextFoldLine(row); var foldStart = foldLine ? foldLine.start.row : Infinity; while (true) { if (row > foldStart) { row = foldLine.end.row+1; foldLine = this.session.getNextFoldLine(row, foldLine); foldStart = foldLine ? foldLine.start.row : Infinity; } if (row > lastRow) break; var container = dom.createElement("div"); var html = []; this.$renderLine(html, row, false, row == foldStart ? foldLine : false); container.innerHTML = html.join(""); if (this.$useLineGroups()) { container.className = 'ace_line_group'; fragment.appendChild(container); container.style.height = config.lineHeight * this.session.getRowLength(row) + "px"; } else { while(container.firstChild) fragment.appendChild(container.firstChild); } row++; } return fragment; }; this.update = function(config) { this.config = config; var html = []; var firstRow = config.firstRow, lastRow = config.lastRow; var row = firstRow; var foldLine = this.session.getNextFoldLine(row); var foldStart = foldLine ? foldLine.start.row : Infinity; while (true) { if (row > foldStart) { row = foldLine.end.row+1; foldLine = this.session.getNextFoldLine(row, foldLine); foldStart = foldLine ? foldLine.start.row :Infinity; } if (row > lastRow) break; if (this.$useLineGroups()) html.push("
") this.$renderLine(html, row, false, row == foldStart ? foldLine : false); if (this.$useLineGroups()) html.push("
"); // end the line group row++; } this.element.innerHTML = html.join(""); }; this.$textToken = { "text": true, "rparen": true, "lparen": true }; this.$renderToken = function(stringBuilder, screenColumn, token, value) { var self = this; var replaceReg = /\t|&|<|>|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\u3000\uFEFF\uFFF9-\uFFFC])|[\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]/g; var replaceFunc = function(c, a, b, tabIdx, idx4) { if (a) { return self.showInvisibles ? "" + lang.stringRepeat(self.SPACE_CHAR, c.length) + "" : c; } else if (c == "&") { return "&"; } else if (c == "<") { return "<"; } else if (c == ">") { return ">"; } else if (c == "\t") { var tabSize = self.session.getScreenTabSize(screenColumn + tabIdx); screenColumn += tabSize - 1; return self.$tabStrings[tabSize]; } else if (c == "\u3000") { var classToUse = self.showInvisibles ? "ace_cjk ace_invisible ace_invisible_space" : "ace_cjk"; var space = self.showInvisibles ? self.SPACE_CHAR : ""; screenColumn += 1; return "" + space + ""; } else if (b) { return "" + self.SPACE_CHAR + ""; } else { screenColumn += 1; return "" + c + ""; } }; var output = value.replace(replaceReg, replaceFunc); if (!this.$textToken[token.type]) { var classes = "ace_" + token.type.replace(/\./g, " ace_"); var style = ""; if (token.type == "fold") style = " style='width:" + (token.value.length * this.config.characterWidth) + "px;' "; stringBuilder.push("", output, ""); } else { stringBuilder.push(output); } return screenColumn + value.length; }; this.renderIndentGuide = function(stringBuilder, value, max) { var cols = value.search(this.$indentGuideRe); if (cols <= 0 || cols >= max) return value; if (value[0] == " ") { cols -= cols % this.tabSize; stringBuilder.push(lang.stringRepeat(this.$tabStrings[" "], cols/this.tabSize)); return value.substr(cols); } else if (value[0] == "\t") { stringBuilder.push(lang.stringRepeat(this.$tabStrings["\t"], cols)); return value.substr(cols); } return value; }; this.$renderWrappedLine = function(stringBuilder, tokens, splits, onlyContents) { var chars = 0; var split = 0; var splitChars = splits[0]; var screenColumn = 0; for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; var value = token.value; if (i == 0 && this.displayIndentGuides) { chars = value.length; value = this.renderIndentGuide(stringBuilder, value, splitChars); if (!value) continue; chars -= value.length; } if (chars + value.length < splitChars) { screenColumn = this.$renderToken(stringBuilder, screenColumn, token, value); chars += value.length; } else { while (chars + value.length >= splitChars) { screenColumn = this.$renderToken( stringBuilder, screenColumn, token, value.substring(0, splitChars - chars) ); value = value.substring(splitChars - chars); chars = splitChars; if (!onlyContents) { stringBuilder.push("", "
" ); } stringBuilder.push(lang.stringRepeat("\xa0", splits.indent)); split ++; screenColumn = 0; splitChars = splits[split] || Number.MAX_VALUE; } if (value.length != 0) { chars += value.length; screenColumn = this.$renderToken( stringBuilder, screenColumn, token, value ); } } } }; this.$renderSimpleLine = function(stringBuilder, tokens) { var screenColumn = 0; var token = tokens[0]; var value = token.value; if (this.displayIndentGuides) value = this.renderIndentGuide(stringBuilder, value); if (value) screenColumn = this.$renderToken(stringBuilder, screenColumn, token, value); for (var i = 1; i < tokens.length; i++) { token = tokens[i]; value = token.value; screenColumn = this.$renderToken(stringBuilder, screenColumn, token, value); } }; this.$renderLine = function(stringBuilder, row, onlyContents, foldLine) { if (!foldLine && foldLine != false) foldLine = this.session.getFoldLine(row); if (foldLine) var tokens = this.$getFoldLineTokens(row, foldLine); else var tokens = this.session.getTokens(row); if (!onlyContents) { stringBuilder.push( "
" ); } if (tokens.length) { var splits = this.session.getRowSplitData(row); if (splits && splits.length) this.$renderWrappedLine(stringBuilder, tokens, splits, onlyContents); else this.$renderSimpleLine(stringBuilder, tokens); } if (this.showInvisibles) { if (foldLine) row = foldLine.end.row stringBuilder.push( "", row == this.session.getLength() - 1 ? this.EOF_CHAR : this.EOL_CHAR, "" ); } if (!onlyContents) stringBuilder.push("
"); }; this.$getFoldLineTokens = function(row, foldLine) { var session = this.session; var renderTokens = []; function addTokens(tokens, from, to) { var idx = 0, col = 0; while ((col + tokens[idx].value.length) < from) { col += tokens[idx].value.length; idx++; if (idx == tokens.length) return; } if (col != from) { var value = tokens[idx].value.substring(from - col); if (value.length > (to - from)) value = value.substring(0, to - from); renderTokens.push({ type: tokens[idx].type, value: value }); col = from + value.length; idx += 1; } while (col < to && idx < tokens.length) { var value = tokens[idx].value; if (value.length + col > to) { renderTokens.push({ type: tokens[idx].type, value: value.substring(0, to - col) }); } else renderTokens.push(tokens[idx]); col += value.length; idx += 1; } } var tokens = session.getTokens(row); foldLine.walk(function(placeholder, row, column, lastColumn, isNewRow) { if (placeholder != null) { renderTokens.push({ type: "fold", value: placeholder }); } else { if (isNewRow) tokens = session.getTokens(row); if (tokens.length) addTokens(tokens, lastColumn, column); } }, foldLine.end.row, this.session.getLine(foldLine.end.row).length); return renderTokens; }; this.$useLineGroups = function() { return this.session.getUseWrapMode(); }; this.destroy = function() { clearInterval(this.$pollSizeChangesTimer); if (this.$measureNode) this.$measureNode.parentNode.removeChild(this.$measureNode); delete this.$measureNode; }; }).call(Text.prototype); exports.Text = Text; }); define("ace/layer/cursor",["require","exports","module","ace/lib/dom"], function(require, exports, module) { "use strict"; var dom = require("../lib/dom"); var isIE8; var Cursor = function(parentEl) { this.element = dom.createElement("div"); this.element.className = "ace_layer ace_cursor-layer"; parentEl.appendChild(this.element); if (isIE8 === undefined) isIE8 = !("opacity" in this.element.style); this.isVisible = false; this.isBlinking = true; this.blinkInterval = 1000; this.smoothBlinking = false; this.cursors = []; this.cursor = this.addCursor(); dom.addCssClass(this.element, "ace_hidden-cursors"); this.$updateCursors = (isIE8 ? this.$updateVisibility : this.$updateOpacity).bind(this); }; (function() { this.$updateVisibility = function(val) { var cursors = this.cursors; for (var i = cursors.length; i--; ) cursors[i].style.visibility = val ? "" : "hidden"; }; this.$updateOpacity = function(val) { var cursors = this.cursors; for (var i = cursors.length; i--; ) cursors[i].style.opacity = val ? "" : "0"; }; this.$padding = 0; this.setPadding = function(padding) { this.$padding = padding; }; this.setSession = function(session) { this.session = session; }; this.setBlinking = function(blinking) { if (blinking != this.isBlinking){ this.isBlinking = blinking; this.restartTimer(); } }; this.setBlinkInterval = function(blinkInterval) { if (blinkInterval != this.blinkInterval){ this.blinkInterval = blinkInterval; this.restartTimer(); } }; this.setSmoothBlinking = function(smoothBlinking) { if (smoothBlinking != this.smoothBlinking && !isIE8) { this.smoothBlinking = smoothBlinking; dom.setCssClass(this.element, "ace_smooth-blinking", smoothBlinking); this.$updateCursors(true); this.$updateCursors = (this.$updateOpacity).bind(this); this.restartTimer(); } }; this.addCursor = function() { var el = dom.createElement("div"); el.className = "ace_cursor"; this.element.appendChild(el); this.cursors.push(el); return el; }; this.removeCursor = function() { if (this.cursors.length > 1) { var el = this.cursors.pop(); el.parentNode.removeChild(el); return el; } }; this.hideCursor = function() { this.isVisible = false; dom.addCssClass(this.element, "ace_hidden-cursors"); this.restartTimer(); }; this.showCursor = function() { this.isVisible = true; dom.removeCssClass(this.element, "ace_hidden-cursors"); this.restartTimer(); }; this.restartTimer = function() { var update = this.$updateCursors; clearInterval(this.intervalId); clearTimeout(this.timeoutId); if (this.smoothBlinking) { dom.removeCssClass(this.element, "ace_smooth-blinking"); } update(true); if (!this.isBlinking || !this.blinkInterval || !this.isVisible) return; if (this.smoothBlinking) { setTimeout(function(){ dom.addCssClass(this.element, "ace_smooth-blinking"); }.bind(this)); } var blink = function(){ this.timeoutId = setTimeout(function() { update(false); }, 0.6 * this.blinkInterval); }.bind(this); this.intervalId = setInterval(function() { update(true); blink(); }, this.blinkInterval); blink(); }; this.getPixelPosition = function(position, onScreen) { if (!this.config || !this.session) return {left : 0, top : 0}; if (!position) position = this.session.selection.getCursor(); var pos = this.session.documentToScreenPosition(position); var cursorLeft = this.$padding + pos.column * this.config.characterWidth; var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) * this.config.lineHeight; return {left : cursorLeft, top : cursorTop}; }; this.update = function(config) { this.config = config; var selections = this.session.$selectionMarkers; var i = 0, cursorIndex = 0; if (selections === undefined || selections.length === 0){ selections = [{cursor: null}]; } for (var i = 0, n = selections.length; i < n; i++) { var pixelPos = this.getPixelPosition(selections[i].cursor, true); if ((pixelPos.top > config.height + config.offset || pixelPos.top < 0) && i > 1) { continue; } var style = (this.cursors[cursorIndex++] || this.addCursor()).style; if (!this.drawCursor) { style.left = pixelPos.left + "px"; style.top = pixelPos.top + "px"; style.width = config.characterWidth + "px"; style.height = config.lineHeight + "px"; } else { this.drawCursor(style, pixelPos, config, selections[i], this.session); } } while (this.cursors.length > cursorIndex) this.removeCursor(); var overwrite = this.session.getOverwrite(); this.$setOverwrite(overwrite); this.$pixelPos = pixelPos; this.restartTimer(); }; this.drawCursor = null; this.$setOverwrite = function(overwrite) { if (overwrite != this.overwrite) { this.overwrite = overwrite; if (overwrite) dom.addCssClass(this.element, "ace_overwrite-cursors"); else dom.removeCssClass(this.element, "ace_overwrite-cursors"); } }; this.destroy = function() { clearInterval(this.intervalId); clearTimeout(this.timeoutId); }; }).call(Cursor.prototype); exports.Cursor = Cursor; }); define("ace/scrollbar",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/event","ace/lib/event_emitter"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var dom = require("./lib/dom"); var event = require("./lib/event"); var EventEmitter = require("./lib/event_emitter").EventEmitter; var MAX_SCROLL_H = 0x8000; var ScrollBar = function(parent) { this.element = dom.createElement("div"); this.element.className = "ace_scrollbar ace_scrollbar" + this.classSuffix; this.inner = dom.createElement("div"); this.inner.className = "ace_scrollbar-inner"; this.element.appendChild(this.inner); parent.appendChild(this.element); this.setVisible(false); this.skipEvent = false; event.addListener(this.element, "scroll", this.onScroll.bind(this)); event.addListener(this.element, "mousedown", event.preventDefault); }; (function() { oop.implement(this, EventEmitter); this.setVisible = function(isVisible) { this.element.style.display = isVisible ? "" : "none"; this.isVisible = isVisible; this.coeff = 1; }; }).call(ScrollBar.prototype); var VScrollBar = function(parent, renderer) { ScrollBar.call(this, parent); this.scrollTop = 0; this.scrollHeight = 0; renderer.$scrollbarWidth = this.width = dom.scrollbarWidth(parent.ownerDocument); this.inner.style.width = this.element.style.width = (this.width || 15) + 5 + "px"; }; oop.inherits(VScrollBar, ScrollBar); (function() { this.classSuffix = '-v'; this.onScroll = function() { if (!this.skipEvent) { this.scrollTop = this.element.scrollTop; if (this.coeff != 1) { var h = this.element.clientHeight / this.scrollHeight; this.scrollTop = this.scrollTop * (1 - h) / (this.coeff - h); } this._emit("scroll", {data: this.scrollTop}); } this.skipEvent = false; }; this.getWidth = function() { return this.isVisible ? this.width : 0; }; this.setHeight = function(height) { this.element.style.height = height + "px"; }; this.setInnerHeight = this.setScrollHeight = function(height) { this.scrollHeight = height; if (height > MAX_SCROLL_H) { this.coeff = MAX_SCROLL_H / height; height = MAX_SCROLL_H; } else if (this.coeff != 1) { this.coeff = 1 } this.inner.style.height = height + "px"; }; this.setScrollTop = function(scrollTop) { if (this.scrollTop != scrollTop) { this.skipEvent = true; this.scrollTop = scrollTop; this.element.scrollTop = scrollTop * this.coeff; } }; }).call(VScrollBar.prototype); var HScrollBar = function(parent, renderer) { ScrollBar.call(this, parent); this.scrollLeft = 0; this.height = renderer.$scrollbarWidth; this.inner.style.height = this.element.style.height = (this.height || 15) + 5 + "px"; }; oop.inherits(HScrollBar, ScrollBar); (function() { this.classSuffix = '-h'; this.onScroll = function() { if (!this.skipEvent) { this.scrollLeft = this.element.scrollLeft; this._emit("scroll", {data: this.scrollLeft}); } this.skipEvent = false; }; this.getHeight = function() { return this.isVisible ? this.height : 0; }; this.setWidth = function(width) { this.element.style.width = width + "px"; }; this.setInnerWidth = function(width) { this.inner.style.width = width + "px"; }; this.setScrollWidth = function(width) { this.inner.style.width = width + "px"; }; this.setScrollLeft = function(scrollLeft) { if (this.scrollLeft != scrollLeft) { this.skipEvent = true; this.scrollLeft = this.element.scrollLeft = scrollLeft; } }; }).call(HScrollBar.prototype); exports.ScrollBar = VScrollBar; // backward compatibility exports.ScrollBarV = VScrollBar; // backward compatibility exports.ScrollBarH = HScrollBar; // backward compatibility exports.VScrollBar = VScrollBar; exports.HScrollBar = HScrollBar; }); define("ace/renderloop",["require","exports","module","ace/lib/event"], function(require, exports, module) { "use strict"; var event = require("./lib/event"); var RenderLoop = function(onRender, win) { this.onRender = onRender; this.pending = false; this.changes = 0; this.window = win || window; }; (function() { this.schedule = function(change) { this.changes = this.changes | change; if (!this.pending && this.changes) { this.pending = true; var _self = this; event.nextFrame(function() { _self.pending = false; var changes; while (changes = _self.changes) { _self.changes = 0; _self.onRender(changes); } }, this.window); } }; }).call(RenderLoop.prototype); exports.RenderLoop = RenderLoop; }); define("ace/layer/font_metrics",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/lib/event_emitter"], function(require, exports, module) { var oop = require("../lib/oop"); var dom = require("../lib/dom"); var lang = require("../lib/lang"); var useragent = require("../lib/useragent"); var EventEmitter = require("../lib/event_emitter").EventEmitter; var CHAR_COUNT = 0; var FontMetrics = exports.FontMetrics = function(parentEl) { this.el = dom.createElement("div"); this.$setMeasureNodeStyles(this.el.style, true); this.$main = dom.createElement("div"); this.$setMeasureNodeStyles(this.$main.style); this.$measureNode = dom.createElement("div"); this.$setMeasureNodeStyles(this.$measureNode.style); this.el.appendChild(this.$main); this.el.appendChild(this.$measureNode); parentEl.appendChild(this.el); if (!CHAR_COUNT) this.$testFractionalRect(); this.$measureNode.innerHTML = lang.stringRepeat("X", CHAR_COUNT); this.$characterSize = {width: 0, height: 0}; this.checkForSizeChanges(); }; (function() { oop.implement(this, EventEmitter); this.$characterSize = {width: 0, height: 0}; this.$testFractionalRect = function() { var el = dom.createElement("div"); this.$setMeasureNodeStyles(el.style); el.style.width = "0.2px"; document.documentElement.appendChild(el); var w = el.getBoundingClientRect().width; if (w > 0 && w < 1) CHAR_COUNT = 50; else CHAR_COUNT = 100; el.parentNode.removeChild(el); }; this.$setMeasureNodeStyles = function(style, isRoot) { style.width = style.height = "auto"; style.left = style.top = "0px"; style.visibility = "hidden"; style.position = "absolute"; style.whiteSpace = "pre"; if (useragent.isIE < 8) { style["font-family"] = "inherit"; } else { style.font = "inherit"; } style.overflow = isRoot ? "hidden" : "visible"; }; this.checkForSizeChanges = function() { var size = this.$measureSizes(); if (size && (this.$characterSize.width !== size.width || this.$characterSize.height !== size.height)) { this.$measureNode.style.fontWeight = "bold"; var boldSize = this.$measureSizes(); this.$measureNode.style.fontWeight = ""; this.$characterSize = size; this.charSizes = Object.create(null); this.allowBoldFonts = boldSize && boldSize.width === size.width && boldSize.height === size.height; this._emit("changeCharacterSize", {data: size}); } }; this.$pollSizeChanges = function() { if (this.$pollSizeChangesTimer) return this.$pollSizeChangesTimer; var self = this; return this.$pollSizeChangesTimer = setInterval(function() { self.checkForSizeChanges(); }, 500); }; this.setPolling = function(val) { if (val) { this.$pollSizeChanges(); } else if (this.$pollSizeChangesTimer) { clearInterval(this.$pollSizeChangesTimer); this.$pollSizeChangesTimer = 0; } }; this.$measureSizes = function() { if (CHAR_COUNT === 50) { var rect = null; try { rect = this.$measureNode.getBoundingClientRect(); } catch(e) { rect = {width: 0, height:0 }; } var size = { height: rect.height, width: rect.width / CHAR_COUNT }; } else { var size = { height: this.$measureNode.clientHeight, width: this.$measureNode.clientWidth / CHAR_COUNT }; } if (size.width === 0 || size.height === 0) return null; return size; }; this.$measureCharWidth = function(ch) { this.$main.innerHTML = lang.stringRepeat(ch, CHAR_COUNT); var rect = this.$main.getBoundingClientRect(); return rect.width / CHAR_COUNT; }; this.getCharacterWidth = function(ch) { var w = this.charSizes[ch]; if (w === undefined) { w = this.charSizes[ch] = this.$measureCharWidth(ch) / this.$characterSize.width; } return w; }; this.destroy = function() { clearInterval(this.$pollSizeChangesTimer); if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el); }; }).call(FontMetrics.prototype); }); define("ace/virtual_renderer",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/config","ace/lib/useragent","ace/layer/gutter","ace/layer/marker","ace/layer/text","ace/layer/cursor","ace/scrollbar","ace/scrollbar","ace/renderloop","ace/layer/font_metrics","ace/lib/event_emitter"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var dom = require("./lib/dom"); var config = require("./config"); var useragent = require("./lib/useragent"); var GutterLayer = require("./layer/gutter").Gutter; var MarkerLayer = require("./layer/marker").Marker; var TextLayer = require("./layer/text").Text; var CursorLayer = require("./layer/cursor").Cursor; var HScrollBar = require("./scrollbar").HScrollBar; var VScrollBar = require("./scrollbar").VScrollBar; var RenderLoop = require("./renderloop").RenderLoop; var FontMetrics = require("./layer/font_metrics").FontMetrics; var EventEmitter = require("./lib/event_emitter").EventEmitter; var editorCss = ".ace_editor {\ position: relative;\ overflow: hidden;\ font: 12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;\ direction: ltr;\ text-align: left;\ }\ .ace_scroller {\ position: absolute;\ overflow: hidden;\ top: 0;\ bottom: 0;\ background-color: inherit;\ -ms-user-select: none;\ -moz-user-select: none;\ -webkit-user-select: none;\ user-select: none;\ cursor: text;\ }\ .ace_content {\ position: absolute;\ -moz-box-sizing: border-box;\ -webkit-box-sizing: border-box;\ box-sizing: border-box;\ min-width: 100%;\ }\ .ace_dragging .ace_scroller:before{\ position: absolute;\ top: 0;\ left: 0;\ right: 0;\ bottom: 0;\ content: '';\ background: rgba(250, 250, 250, 0.01);\ z-index: 1000;\ }\ .ace_dragging.ace_dark .ace_scroller:before{\ background: rgba(0, 0, 0, 0.01);\ }\ .ace_selecting, .ace_selecting * {\ cursor: text !important;\ }\ .ace_gutter {\ position: absolute;\ overflow : hidden;\ width: auto;\ top: 0;\ bottom: 0;\ left: 0;\ cursor: default;\ z-index: 4;\ -ms-user-select: none;\ -moz-user-select: none;\ -webkit-user-select: none;\ user-select: none;\ }\ .ace_gutter-active-line {\ position: absolute;\ left: 0;\ right: 0;\ }\ .ace_scroller.ace_scroll-left {\ box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset;\ }\ .ace_gutter-cell {\ padding-left: 19px;\ padding-right: 6px;\ background-repeat: no-repeat;\ }\ .ace_gutter-cell.ace_error {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABOFBMVEX/////////QRswFAb/Ui4wFAYwFAYwFAaWGAfDRymzOSH/PxswFAb/SiUwFAYwFAbUPRvjQiDllog5HhHdRybsTi3/Tyv9Tir+Syj/UC3////XurebMBIwFAb/RSHbPx/gUzfdwL3kzMivKBAwFAbbvbnhPx66NhowFAYwFAaZJg8wFAaxKBDZurf/RB6mMxb/SCMwFAYwFAbxQB3+RB4wFAb/Qhy4Oh+4QifbNRcwFAYwFAYwFAb/QRzdNhgwFAYwFAbav7v/Uy7oaE68MBK5LxLewr/r2NXewLswFAaxJw4wFAbkPRy2PyYwFAaxKhLm1tMwFAazPiQwFAaUGAb/QBrfOx3bvrv/VC/maE4wFAbRPBq6MRO8Qynew8Dp2tjfwb0wFAbx6eju5+by6uns4uH9/f36+vr/GkHjAAAAYnRSTlMAGt+64rnWu/bo8eAA4InH3+DwoN7j4eLi4xP99Nfg4+b+/u9B/eDs1MD1mO7+4PHg2MXa347g7vDizMLN4eG+Pv7i5evs/v79yu7S3/DV7/498Yv24eH+4ufQ3Ozu/v7+y13sRqwAAADLSURBVHjaZc/XDsFgGIBhtDrshlitmk2IrbHFqL2pvXf/+78DPokj7+Fz9qpU/9UXJIlhmPaTaQ6QPaz0mm+5gwkgovcV6GZzd5JtCQwgsxoHOvJO15kleRLAnMgHFIESUEPmawB9ngmelTtipwwfASilxOLyiV5UVUyVAfbG0cCPHig+GBkzAENHS0AstVF6bacZIOzgLmxsHbt2OecNgJC83JERmePUYq8ARGkJx6XtFsdddBQgZE2nPR6CICZhawjA4Fb/chv+399kfR+MMMDGOQAAAABJRU5ErkJggg==\");\ background-repeat: no-repeat;\ background-position: 2px center;\ }\ .ace_gutter-cell.ace_warning {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAmVBMVEX///8AAAD///8AAAAAAABPSzb/5sAAAAB/blH/73z/ulkAAAAAAAD85pkAAAAAAAACAgP/vGz/rkDerGbGrV7/pkQICAf////e0IsAAAD/oED/qTvhrnUAAAD/yHD/njcAAADuv2r/nz//oTj/p064oGf/zHAAAAA9Nir/tFIAAAD/tlTiuWf/tkIAAACynXEAAAAAAAAtIRW7zBpBAAAAM3RSTlMAABR1m7RXO8Ln31Z36zT+neXe5OzooRDfn+TZ4p3h2hTf4t3k3ucyrN1K5+Xaks52Sfs9CXgrAAAAjklEQVR42o3PbQ+CIBQFYEwboPhSYgoYunIqqLn6/z8uYdH8Vmdnu9vz4WwXgN/xTPRD2+sgOcZjsge/whXZgUaYYvT8QnuJaUrjrHUQreGczuEafQCO/SJTufTbroWsPgsllVhq3wJEk2jUSzX3CUEDJC84707djRc5MTAQxoLgupWRwW6UB5fS++NV8AbOZgnsC7BpEAAAAABJRU5ErkJggg==\");\ background-position: 2px center;\ }\ .ace_gutter-cell.ace_info {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAJ0Uk5TAAB2k804AAAAPklEQVQY02NgIB68QuO3tiLznjAwpKTgNyDbMegwisCHZUETUZV0ZqOquBpXj2rtnpSJT1AEnnRmL2OgGgAAIKkRQap2htgAAAAASUVORK5CYII=\");\ background-position: 2px center;\ }\ .ace_dark .ace_gutter-cell.ace_info {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAChoaGAgIAqKiq+vr6tra1ZWVmUlJSbm5s8PDxubm56enrdgzg3AAAAAXRSTlMAQObYZgAAAClJREFUeNpjYMAPdsMYHegyJZFQBlsUlMFVCWUYKkAZMxZAGdxlDMQBAG+TBP4B6RyJAAAAAElFTkSuQmCC\");\ }\ .ace_scrollbar {\ position: absolute;\ right: 0;\ bottom: 0;\ z-index: 6;\ }\ .ace_scrollbar-inner {\ position: absolute;\ cursor: text;\ left: 0;\ top: 0;\ }\ .ace_scrollbar-v{\ overflow-x: hidden;\ overflow-y: scroll;\ top: 0;\ }\ .ace_scrollbar-h {\ overflow-x: scroll;\ overflow-y: hidden;\ left: 0;\ }\ .ace_print-margin {\ position: absolute;\ height: 100%;\ }\ .ace_text-input {\ position: absolute;\ z-index: 0;\ width: 0.5em;\ height: 1em;\ opacity: 0;\ background: transparent;\ -moz-appearance: none;\ appearance: none;\ border: none;\ resize: none;\ outline: none;\ overflow: hidden;\ font: inherit;\ padding: 0 1px;\ margin: 0 -1px;\ text-indent: -1em;\ -ms-user-select: text;\ -moz-user-select: text;\ -webkit-user-select: text;\ user-select: text;\ white-space: pre!important;\ }\ .ace_text-input.ace_composition {\ background: inherit;\ color: inherit;\ z-index: 1000;\ opacity: 1;\ text-indent: 0;\ }\ .ace_layer {\ z-index: 1;\ position: absolute;\ overflow: hidden;\ word-wrap: normal;\ white-space: pre;\ height: 100%;\ width: 100%;\ -moz-box-sizing: border-box;\ -webkit-box-sizing: border-box;\ box-sizing: border-box;\ pointer-events: none;\ }\ .ace_gutter-layer {\ position: relative;\ width: auto;\ text-align: right;\ pointer-events: auto;\ }\ .ace_text-layer {\ font: inherit !important;\ }\ .ace_cjk {\ display: inline-block;\ text-align: center;\ }\ .ace_cursor-layer {\ z-index: 4;\ }\ .ace_cursor {\ z-index: 4;\ position: absolute;\ -moz-box-sizing: border-box;\ -webkit-box-sizing: border-box;\ box-sizing: border-box;\ border-left: 2px solid;\ transform: translatez(0);\ }\ .ace_slim-cursors .ace_cursor {\ border-left-width: 1px;\ }\ .ace_overwrite-cursors .ace_cursor {\ border-left-width: 0;\ border-bottom: 1px solid;\ }\ .ace_hidden-cursors .ace_cursor {\ opacity: 0.2;\ }\ .ace_smooth-blinking .ace_cursor {\ -webkit-transition: opacity 0.18s;\ transition: opacity 0.18s;\ }\ .ace_editor.ace_multiselect .ace_cursor {\ border-left-width: 1px;\ }\ .ace_marker-layer .ace_step, .ace_marker-layer .ace_stack {\ position: absolute;\ z-index: 3;\ }\ .ace_marker-layer .ace_selection {\ position: absolute;\ z-index: 5;\ }\ .ace_marker-layer .ace_bracket {\ position: absolute;\ z-index: 6;\ }\ .ace_marker-layer .ace_active-line {\ position: absolute;\ z-index: 2;\ }\ .ace_marker-layer .ace_selected-word {\ position: absolute;\ z-index: 4;\ -moz-box-sizing: border-box;\ -webkit-box-sizing: border-box;\ box-sizing: border-box;\ }\ .ace_line .ace_fold {\ -moz-box-sizing: border-box;\ -webkit-box-sizing: border-box;\ box-sizing: border-box;\ display: inline-block;\ height: 11px;\ margin-top: -2px;\ vertical-align: middle;\ background-image:\ url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII=\"),\ url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACJJREFUeNpi+P//fxgTAwPDBxDxD078RSX+YeEyDFMCIMAAI3INmXiwf2YAAAAASUVORK5CYII=\");\ background-repeat: no-repeat, repeat-x;\ background-position: center center, top left;\ color: transparent;\ border: 1px solid black;\ border-radius: 2px;\ cursor: pointer;\ pointer-events: auto;\ }\ .ace_dark .ace_fold {\ }\ .ace_fold:hover{\ background-image:\ url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII=\"),\ url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpi+P//fz4TAwPDZxDxD5X4i5fLMEwJgAADAEPVDbjNw87ZAAAAAElFTkSuQmCC\");\ }\ .ace_tooltip {\ background-color: #FFF;\ background-image: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, 0.1));\ background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));\ border: 1px solid gray;\ border-radius: 1px;\ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\ color: black;\ max-width: 100%;\ padding: 3px 4px;\ position: fixed;\ z-index: 999999;\ -moz-box-sizing: border-box;\ -webkit-box-sizing: border-box;\ box-sizing: border-box;\ cursor: default;\ white-space: pre;\ word-wrap: break-word;\ line-height: normal;\ font-style: normal;\ font-weight: normal;\ letter-spacing: normal;\ pointer-events: none;\ }\ .ace_folding-enabled > .ace_gutter-cell {\ padding-right: 13px;\ }\ .ace_fold-widget {\ -moz-box-sizing: border-box;\ -webkit-box-sizing: border-box;\ box-sizing: border-box;\ margin: 0 -12px 0 1px;\ display: none;\ width: 11px;\ vertical-align: top;\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==\");\ background-repeat: no-repeat;\ background-position: center;\ border-radius: 3px;\ border: 1px solid transparent;\ cursor: pointer;\ }\ .ace_folding-enabled .ace_fold-widget {\ display: inline-block; \ }\ .ace_fold-widget.ace_end {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42m3HwQkAMAhD0YzsRchFKI7sAikeWkrxwScEB0nh5e7KTPWimZki4tYfVbX+MNl4pyZXejUO1QAAAABJRU5ErkJggg==\");\ }\ .ace_fold-widget.ace_closed {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA==\");\ }\ .ace_fold-widget:hover {\ border: 1px solid rgba(0, 0, 0, 0.3);\ background-color: rgba(255, 255, 255, 0.2);\ box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);\ }\ .ace_fold-widget:active {\ border: 1px solid rgba(0, 0, 0, 0.4);\ background-color: rgba(0, 0, 0, 0.05);\ box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);\ }\ .ace_dark .ace_fold-widget {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHklEQVQIW2P4//8/AzoGEQ7oGCaLLAhWiSwB146BAQCSTPYocqT0AAAAAElFTkSuQmCC\");\ }\ .ace_dark .ace_fold-widget.ace_end {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAH0lEQVQIW2P4//8/AxQ7wNjIAjDMgC4AxjCVKBirIAAF0kz2rlhxpAAAAABJRU5ErkJggg==\");\ }\ .ace_dark .ace_fold-widget.ace_closed {\ background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHElEQVQIW2P4//+/AxAzgDADlOOAznHAKgPWAwARji8UIDTfQQAAAABJRU5ErkJggg==\");\ }\ .ace_dark .ace_fold-widget:hover {\ box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);\ background-color: rgba(255, 255, 255, 0.1);\ }\ .ace_dark .ace_fold-widget:active {\ box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);\ }\ .ace_fold-widget.ace_invalid {\ background-color: #FFB4B4;\ border-color: #DE5555;\ }\ .ace_fade-fold-widgets .ace_fold-widget {\ -webkit-transition: opacity 0.4s ease 0.05s;\ transition: opacity 0.4s ease 0.05s;\ opacity: 0;\ }\ .ace_fade-fold-widgets:hover .ace_fold-widget {\ -webkit-transition: opacity 0.05s ease 0.05s;\ transition: opacity 0.05s ease 0.05s;\ opacity:1;\ }\ .ace_underline {\ text-decoration: underline;\ }\ .ace_bold {\ font-weight: bold;\ }\ .ace_nobold .ace_bold {\ font-weight: normal;\ }\ .ace_italic {\ font-style: italic;\ }\ .ace_error-marker {\ background-color: rgba(255, 0, 0,0.2);\ position: absolute;\ z-index: 9;\ }\ .ace_highlight-marker {\ background-color: rgba(255, 255, 0,0.2);\ position: absolute;\ z-index: 8;\ }\ .ace_br1 {border-top-left-radius : 3px;}\ .ace_br2 {border-top-right-radius : 3px;}\ .ace_br3 {border-top-left-radius : 3px; border-top-right-radius: 3px;}\ .ace_br4 {border-bottom-right-radius: 3px;}\ .ace_br5 {border-top-left-radius : 3px; border-bottom-right-radius: 3px;}\ .ace_br6 {border-top-right-radius : 3px; border-bottom-right-radius: 3px;}\ .ace_br7 {border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px;}\ .ace_br8 {border-bottom-left-radius : 3px;}\ .ace_br9 {border-top-left-radius : 3px; border-bottom-left-radius: 3px;}\ .ace_br10{border-top-right-radius : 3px; border-bottom-left-radius: 3px;}\ .ace_br11{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-left-radius: 3px;}\ .ace_br12{border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}\ .ace_br13{border-top-left-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}\ .ace_br14{border-top-right-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}\ .ace_br15{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}\ "; dom.importCssString(editorCss, "ace_editor.css"); var VirtualRenderer = function(container, theme) { var _self = this; this.container = container || dom.createElement("div"); this.$keepTextAreaAtCursor = !useragent.isOldIE; dom.addCssClass(this.container, "ace_editor"); this.setTheme(theme); this.$gutter = dom.createElement("div"); this.$gutter.className = "ace_gutter"; this.container.appendChild(this.$gutter); this.scroller = dom.createElement("div"); this.scroller.className = "ace_scroller"; this.container.appendChild(this.scroller); this.content = dom.createElement("div"); this.content.className = "ace_content"; this.scroller.appendChild(this.content); this.$gutterLayer = new GutterLayer(this.$gutter); this.$gutterLayer.on("changeGutterWidth", this.onGutterResize.bind(this)); this.$markerBack = new MarkerLayer(this.content); var textLayer = this.$textLayer = new TextLayer(this.content); this.canvas = textLayer.element; this.$markerFront = new MarkerLayer(this.content); this.$cursorLayer = new CursorLayer(this.content); this.$horizScroll = false; this.$vScroll = false; this.scrollBar = this.scrollBarV = new VScrollBar(this.container, this); this.scrollBarH = new HScrollBar(this.container, this); this.scrollBarV.addEventListener("scroll", function(e) { if (!_self.$scrollAnimation) _self.session.setScrollTop(e.data - _self.scrollMargin.top); }); this.scrollBarH.addEventListener("scroll", function(e) { if (!_self.$scrollAnimation) _self.session.setScrollLeft(e.data - _self.scrollMargin.left); }); this.scrollTop = 0; this.scrollLeft = 0; this.cursorPos = { row : 0, column : 0 }; this.$fontMetrics = new FontMetrics(this.container); this.$textLayer.$setFontMetrics(this.$fontMetrics); this.$textLayer.addEventListener("changeCharacterSize", function(e) { _self.updateCharacterSize(); _self.onResize(true, _self.gutterWidth, _self.$size.width, _self.$size.height); _self._signal("changeCharacterSize", e); }); this.$size = { width: 0, height: 0, scrollerHeight: 0, scrollerWidth: 0, $dirty: true }; this.layerConfig = { width : 1, padding : 0, firstRow : 0, firstRowScreen: 0, lastRow : 0, lineHeight : 0, characterWidth : 0, minHeight : 1, maxHeight : 1, offset : 0, height : 1, gutterOffset: 1 }; this.scrollMargin = { left: 0, right: 0, top: 0, bottom: 0, v: 0, h: 0 }; this.$loop = new RenderLoop( this.$renderChanges.bind(this), this.container.ownerDocument.defaultView ); this.$loop.schedule(this.CHANGE_FULL); this.updateCharacterSize(); this.setPadding(4); config.resetOptions(this); config._emit("renderer", this); }; (function() { this.CHANGE_CURSOR = 1; this.CHANGE_MARKER = 2; this.CHANGE_GUTTER = 4; this.CHANGE_SCROLL = 8; this.CHANGE_LINES = 16; this.CHANGE_TEXT = 32; this.CHANGE_SIZE = 64; this.CHANGE_MARKER_BACK = 128; this.CHANGE_MARKER_FRONT = 256; this.CHANGE_FULL = 512; this.CHANGE_H_SCROLL = 1024; oop.implement(this, EventEmitter); this.updateCharacterSize = function() { if (this.$textLayer.allowBoldFonts != this.$allowBoldFonts) { this.$allowBoldFonts = this.$textLayer.allowBoldFonts; this.setStyle("ace_nobold", !this.$allowBoldFonts); } this.layerConfig.characterWidth = this.characterWidth = this.$textLayer.getCharacterWidth(); this.layerConfig.lineHeight = this.lineHeight = this.$textLayer.getLineHeight(); this.$updatePrintMargin(); }; this.setSession = function(session) { if (this.session) this.session.doc.off("changeNewLineMode", this.onChangeNewLineMode); this.session = session; if (session && this.scrollMargin.top && session.getScrollTop() <= 0) session.setScrollTop(-this.scrollMargin.top); this.$cursorLayer.setSession(session); this.$markerBack.setSession(session); this.$markerFront.setSession(session); this.$gutterLayer.setSession(session); this.$textLayer.setSession(session); if (!session) return; this.$loop.schedule(this.CHANGE_FULL); this.session.$setFontMetrics(this.$fontMetrics); this.scrollBarV.scrollLeft = this.scrollBarV.scrollTop = null; this.onChangeNewLineMode = this.onChangeNewLineMode.bind(this); this.onChangeNewLineMode() this.session.doc.on("changeNewLineMode", this.onChangeNewLineMode); }; this.updateLines = function(firstRow, lastRow, force) { if (lastRow === undefined) lastRow = Infinity; if (!this.$changedLines) { this.$changedLines = { firstRow: firstRow, lastRow: lastRow }; } else { if (this.$changedLines.firstRow > firstRow) this.$changedLines.firstRow = firstRow; if (this.$changedLines.lastRow < lastRow) this.$changedLines.lastRow = lastRow; } if (this.$changedLines.lastRow < this.layerConfig.firstRow) { if (force) this.$changedLines.lastRow = this.layerConfig.lastRow; else return; } if (this.$changedLines.firstRow > this.layerConfig.lastRow) return; this.$loop.schedule(this.CHANGE_LINES); }; this.onChangeNewLineMode = function() { this.$loop.schedule(this.CHANGE_TEXT); this.$textLayer.$updateEolChar(); }; this.onChangeTabSize = function() { this.$loop.schedule(this.CHANGE_TEXT | this.CHANGE_MARKER); this.$textLayer.onChangeTabSize(); }; this.updateText = function() { this.$loop.schedule(this.CHANGE_TEXT); }; this.updateFull = function(force) { if (force) this.$renderChanges(this.CHANGE_FULL, true); else this.$loop.schedule(this.CHANGE_FULL); }; this.updateFontSize = function() { this.$textLayer.checkForSizeChanges(); }; this.$changes = 0; this.$updateSizeAsync = function() { if (this.$loop.pending) this.$size.$dirty = true; else this.onResize(); }; this.onResize = function(force, gutterWidth, width, height) { if (this.resizing > 2) return; else if (this.resizing > 0) this.resizing++; else this.resizing = force ? 1 : 0; var el = this.container; if (!height) height = el.clientHeight || el.scrollHeight; if (!width) width = el.clientWidth || el.scrollWidth; var changes = this.$updateCachedSize(force, gutterWidth, width, height); if (!this.$size.scrollerHeight || (!width && !height)) return this.resizing = 0; if (force) this.$gutterLayer.$padding = null; if (force) this.$renderChanges(changes | this.$changes, true); else this.$loop.schedule(changes | this.$changes); if (this.resizing) this.resizing = 0; this.scrollBarV.scrollLeft = this.scrollBarV.scrollTop = null; }; this.$updateCachedSize = function(force, gutterWidth, width, height) { height -= (this.$extraHeight || 0); var changes = 0; var size = this.$size; var oldSize = { width: size.width, height: size.height, scrollerHeight: size.scrollerHeight, scrollerWidth: size.scrollerWidth }; if (height && (force || size.height != height)) { size.height = height; changes |= this.CHANGE_SIZE; size.scrollerHeight = size.height; if (this.$horizScroll) size.scrollerHeight -= this.scrollBarH.getHeight(); this.scrollBarV.element.style.bottom = this.scrollBarH.getHeight() + "px"; changes = changes | this.CHANGE_SCROLL; } if (width && (force || size.width != width)) { changes |= this.CHANGE_SIZE; size.width = width; if (gutterWidth == null) gutterWidth = this.$showGutter ? this.$gutter.offsetWidth : 0; this.gutterWidth = gutterWidth; this.scrollBarH.element.style.left = this.scroller.style.left = gutterWidth + "px"; size.scrollerWidth = Math.max(0, width - gutterWidth - this.scrollBarV.getWidth()); this.scrollBarH.element.style.right = this.scroller.style.right = this.scrollBarV.getWidth() + "px"; this.scroller.style.bottom = this.scrollBarH.getHeight() + "px"; if (this.session && this.session.getUseWrapMode() && this.adjustWrapLimit() || force) changes |= this.CHANGE_FULL; } size.$dirty = !width || !height; if (changes) this._signal("resize", oldSize); return changes; }; this.onGutterResize = function() { var gutterWidth = this.$showGutter ? this.$gutter.offsetWidth : 0; if (gutterWidth != this.gutterWidth) this.$changes |= this.$updateCachedSize(true, gutterWidth, this.$size.width, this.$size.height); if (this.session.getUseWrapMode() && this.adjustWrapLimit()) { this.$loop.schedule(this.CHANGE_FULL); } else if (this.$size.$dirty) { this.$loop.schedule(this.CHANGE_FULL); } else { this.$computeLayerConfig(); this.$loop.schedule(this.CHANGE_MARKER); } }; this.adjustWrapLimit = function() { var availableWidth = this.$size.scrollerWidth - this.$padding * 2; var limit = Math.floor(availableWidth / this.characterWidth); return this.session.adjustWrapLimit(limit, this.$showPrintMargin && this.$printMarginColumn); }; this.setAnimatedScroll = function(shouldAnimate){ this.setOption("animatedScroll", shouldAnimate); }; this.getAnimatedScroll = function() { return this.$animatedScroll; }; this.setShowInvisibles = function(showInvisibles) { this.setOption("showInvisibles", showInvisibles); }; this.getShowInvisibles = function() { return this.getOption("showInvisibles"); }; this.getDisplayIndentGuides = function() { return this.getOption("displayIndentGuides"); }; this.setDisplayIndentGuides = function(display) { this.setOption("displayIndentGuides", display); }; this.setShowPrintMargin = function(showPrintMargin) { this.setOption("showPrintMargin", showPrintMargin); }; this.getShowPrintMargin = function() { return this.getOption("showPrintMargin"); }; this.setPrintMarginColumn = function(showPrintMargin) { this.setOption("printMarginColumn", showPrintMargin); }; this.getPrintMarginColumn = function() { return this.getOption("printMarginColumn"); }; this.getShowGutter = function(){ return this.getOption("showGutter"); }; this.setShowGutter = function(show){ return this.setOption("showGutter", show); }; this.getFadeFoldWidgets = function(){ return this.getOption("fadeFoldWidgets") }; this.setFadeFoldWidgets = function(show) { this.setOption("fadeFoldWidgets", show); }; this.setHighlightGutterLine = function(shouldHighlight) { this.setOption("highlightGutterLine", shouldHighlight); }; this.getHighlightGutterLine = function() { return this.getOption("highlightGutterLine"); }; this.$updateGutterLineHighlight = function() { var pos = this.$cursorLayer.$pixelPos; var height = this.layerConfig.lineHeight; if (this.session.getUseWrapMode()) { var cursor = this.session.selection.getCursor(); cursor.column = 0; pos = this.$cursorLayer.getPixelPosition(cursor, true); height *= this.session.getRowLength(cursor.row); } this.$gutterLineHighlight.style.top = pos.top - this.layerConfig.offset + "px"; this.$gutterLineHighlight.style.height = height + "px"; }; this.$updatePrintMargin = function() { if (!this.$showPrintMargin && !this.$printMarginEl) return; if (!this.$printMarginEl) { var containerEl = dom.createElement("div"); containerEl.className = "ace_layer ace_print-margin-layer"; this.$printMarginEl = dom.createElement("div"); this.$printMarginEl.className = "ace_print-margin"; containerEl.appendChild(this.$printMarginEl); this.content.insertBefore(containerEl, this.content.firstChild); } var style = this.$printMarginEl.style; style.left = ((this.characterWidth * this.$printMarginColumn) + this.$padding) + "px"; style.visibility = this.$showPrintMargin ? "visible" : "hidden"; if (this.session && this.session.$wrap == -1) this.adjustWrapLimit(); }; this.getContainerElement = function() { return this.container; }; this.getMouseEventTarget = function() { return this.scroller; }; this.getTextAreaContainer = function() { return this.container; }; this.$moveTextAreaToCursor = function() { if (!this.$keepTextAreaAtCursor) return; var config = this.layerConfig; var posTop = this.$cursorLayer.$pixelPos.top; var posLeft = this.$cursorLayer.$pixelPos.left; posTop -= config.offset; var style = this.textarea.style; var h = this.lineHeight; if (posTop < 0 || posTop > config.height - h) { style.top = style.left = "0"; return; } var w = this.characterWidth; if (this.$composition) { var val = this.textarea.value.replace(/^\x01+/, ""); w *= (this.session.$getStringScreenWidth(val)[0]+2); h += 2; } posLeft -= this.scrollLeft; if (posLeft > this.$size.scrollerWidth - w) posLeft = this.$size.scrollerWidth - w; posLeft += this.gutterWidth; style.height = h + "px"; style.width = w + "px"; style.left = Math.min(posLeft, this.$size.scrollerWidth - w) + "px"; style.top = Math.min(posTop, this.$size.height - h) + "px"; }; this.getFirstVisibleRow = function() { return this.layerConfig.firstRow; }; this.getFirstFullyVisibleRow = function() { return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1); }; this.getLastFullyVisibleRow = function() { var config = this.layerConfig; var lastRow = config.lastRow var top = this.session.documentToScreenRow(lastRow, 0) * config.lineHeight; if (top - this.session.getScrollTop() > config.height - config.lineHeight) return lastRow - 1; return lastRow; }; this.getLastVisibleRow = function() { return this.layerConfig.lastRow; }; this.$padding = null; this.setPadding = function(padding) { this.$padding = padding; this.$textLayer.setPadding(padding); this.$cursorLayer.setPadding(padding); this.$markerFront.setPadding(padding); this.$markerBack.setPadding(padding); this.$loop.schedule(this.CHANGE_FULL); this.$updatePrintMargin(); }; this.setScrollMargin = function(top, bottom, left, right) { var sm = this.scrollMargin; sm.top = top|0; sm.bottom = bottom|0; sm.right = right|0; sm.left = left|0; sm.v = sm.top + sm.bottom; sm.h = sm.left + sm.right; if (sm.top && this.scrollTop <= 0 && this.session) this.session.setScrollTop(-sm.top); this.updateFull(); }; this.getHScrollBarAlwaysVisible = function() { return this.$hScrollBarAlwaysVisible; }; this.setHScrollBarAlwaysVisible = function(alwaysVisible) { this.setOption("hScrollBarAlwaysVisible", alwaysVisible); }; this.getVScrollBarAlwaysVisible = function() { return this.$vScrollBarAlwaysVisible; }; this.setVScrollBarAlwaysVisible = function(alwaysVisible) { this.setOption("vScrollBarAlwaysVisible", alwaysVisible); }; this.$updateScrollBarV = function() { var scrollHeight = this.layerConfig.maxHeight; var scrollerHeight = this.$size.scrollerHeight; if (!this.$maxLines && this.$scrollPastEnd) { scrollHeight -= (scrollerHeight - this.lineHeight) * this.$scrollPastEnd; if (this.scrollTop > scrollHeight - scrollerHeight) { scrollHeight = this.scrollTop + scrollerHeight; this.scrollBarV.scrollTop = null; } } this.scrollBarV.setScrollHeight(scrollHeight + this.scrollMargin.v); this.scrollBarV.setScrollTop(this.scrollTop + this.scrollMargin.top); }; this.$updateScrollBarH = function() { this.scrollBarH.setScrollWidth(this.layerConfig.width + 2 * this.$padding + this.scrollMargin.h); this.scrollBarH.setScrollLeft(this.scrollLeft + this.scrollMargin.left); }; this.$frozen = false; this.freeze = function() { this.$frozen = true; }; this.unfreeze = function() { this.$frozen = false; }; this.$renderChanges = function(changes, force) { if (this.$changes) { changes |= this.$changes; this.$changes = 0; } if ((!this.session || !this.container.offsetWidth || this.$frozen) || (!changes && !force)) { this.$changes |= changes; return; } if (this.$size.$dirty) { this.$changes |= changes; return this.onResize(true); } if (!this.lineHeight) { this.$textLayer.checkForSizeChanges(); } this._signal("beforeRender"); var config = this.layerConfig; if (changes & this.CHANGE_FULL || changes & this.CHANGE_SIZE || changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES || changes & this.CHANGE_SCROLL || changes & this.CHANGE_H_SCROLL ) { changes |= this.$computeLayerConfig(); if (config.firstRow != this.layerConfig.firstRow && config.firstRowScreen == this.layerConfig.firstRowScreen) { var st = this.scrollTop + (config.firstRow - this.layerConfig.firstRow) * this.lineHeight; if (st > 0) { this.scrollTop = st; changes = changes | this.CHANGE_SCROLL; changes |= this.$computeLayerConfig(); } } config = this.layerConfig; this.$updateScrollBarV(); if (changes & this.CHANGE_H_SCROLL) this.$updateScrollBarH(); this.$gutterLayer.element.style.marginTop = (-config.offset) + "px"; this.content.style.marginTop = (-config.offset) + "px"; this.content.style.width = config.width + 2 * this.$padding + "px"; this.content.style.height = config.minHeight + "px"; } if (changes & this.CHANGE_H_SCROLL) { this.content.style.marginLeft = -this.scrollLeft + "px"; this.scroller.className = this.scrollLeft <= 0 ? "ace_scroller" : "ace_scroller ace_scroll-left"; } if (changes & this.CHANGE_FULL) { this.$textLayer.update(config); if (this.$showGutter) this.$gutterLayer.update(config); this.$markerBack.update(config); this.$markerFront.update(config); this.$cursorLayer.update(config); this.$moveTextAreaToCursor(); this.$highlightGutterLine && this.$updateGutterLineHighlight(); this._signal("afterRender"); return; } if (changes & this.CHANGE_SCROLL) { if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES) this.$textLayer.update(config); else this.$textLayer.scrollLines(config); if (this.$showGutter) this.$gutterLayer.update(config); this.$markerBack.update(config); this.$markerFront.update(config); this.$cursorLayer.update(config); this.$highlightGutterLine && this.$updateGutterLineHighlight(); this.$moveTextAreaToCursor(); this._signal("afterRender"); return; } if (changes & this.CHANGE_TEXT) { this.$textLayer.update(config); if (this.$showGutter) this.$gutterLayer.update(config); } else if (changes & this.CHANGE_LINES) { if (this.$updateLines() || (changes & this.CHANGE_GUTTER) && this.$showGutter) this.$gutterLayer.update(config); } else if (changes & this.CHANGE_TEXT || changes & this.CHANGE_GUTTER) { if (this.$showGutter) this.$gutterLayer.update(config); } if (changes & this.CHANGE_CURSOR) { this.$cursorLayer.update(config); this.$moveTextAreaToCursor(); this.$highlightGutterLine && this.$updateGutterLineHighlight(); } if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) { this.$markerFront.update(config); } if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) { this.$markerBack.update(config); } this._signal("afterRender"); }; this.$autosize = function() { var height = this.session.getScreenLength() * this.lineHeight; var maxHeight = this.$maxLines * this.lineHeight; var desiredHeight = Math.min(maxHeight, Math.max((this.$minLines || 1) * this.lineHeight, height) ) + this.scrollMargin.v + (this.$extraHeight || 0); if (this.$horizScroll) desiredHeight += this.scrollBarH.getHeight(); if (this.$maxPixelHeight && desiredHeight > this.$maxPixelHeight) desiredHeight = this.$maxPixelHeight; var vScroll = height > maxHeight; if (desiredHeight != this.desiredHeight || this.$size.height != this.desiredHeight || vScroll != this.$vScroll) { if (vScroll != this.$vScroll) { this.$vScroll = vScroll; this.scrollBarV.setVisible(vScroll); } var w = this.container.clientWidth; this.container.style.height = desiredHeight + "px"; this.$updateCachedSize(true, this.$gutterWidth, w, desiredHeight); this.desiredHeight = desiredHeight; this._signal("autosize"); } }; this.$computeLayerConfig = function() { var session = this.session; var size = this.$size; var hideScrollbars = size.height <= 2 * this.lineHeight; var screenLines = this.session.getScreenLength(); var maxHeight = screenLines * this.lineHeight; var longestLine = this.$getLongestLine(); var horizScroll = !hideScrollbars && (this.$hScrollBarAlwaysVisible || size.scrollerWidth - longestLine - 2 * this.$padding < 0); var hScrollChanged = this.$horizScroll !== horizScroll; if (hScrollChanged) { this.$horizScroll = horizScroll; this.scrollBarH.setVisible(horizScroll); } var vScrollBefore = this.$vScroll; // autosize can change vscroll value in which case we need to update longestLine if (this.$maxLines && this.lineHeight > 1) this.$autosize(); var offset = this.scrollTop % this.lineHeight; var minHeight = size.scrollerHeight + this.lineHeight; var scrollPastEnd = !this.$maxLines && this.$scrollPastEnd ? (size.scrollerHeight - this.lineHeight) * this.$scrollPastEnd : 0; maxHeight += scrollPastEnd; var sm = this.scrollMargin; this.session.setScrollTop(Math.max(-sm.top, Math.min(this.scrollTop, maxHeight - size.scrollerHeight + sm.bottom))); this.session.setScrollLeft(Math.max(-sm.left, Math.min(this.scrollLeft, longestLine + 2 * this.$padding - size.scrollerWidth + sm.right))); var vScroll = !hideScrollbars && (this.$vScrollBarAlwaysVisible || size.scrollerHeight - maxHeight + scrollPastEnd < 0 || this.scrollTop > sm.top); var vScrollChanged = vScrollBefore !== vScroll; if (vScrollChanged) { this.$vScroll = vScroll; this.scrollBarV.setVisible(vScroll); } var lineCount = Math.ceil(minHeight / this.lineHeight) - 1; var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight)); var lastRow = firstRow + lineCount; var firstRowScreen, firstRowHeight; var lineHeight = this.lineHeight; firstRow = session.screenToDocumentRow(firstRow, 0); var foldLine = session.getFoldLine(firstRow); if (foldLine) { firstRow = foldLine.start.row; } firstRowScreen = session.documentToScreenRow(firstRow, 0); firstRowHeight = session.getRowLength(firstRow) * lineHeight; lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1); minHeight = size.scrollerHeight + session.getRowLength(lastRow) * lineHeight + firstRowHeight; offset = this.scrollTop - firstRowScreen * lineHeight; var changes = 0; if (this.layerConfig.width != longestLine) changes = this.CHANGE_H_SCROLL; if (hScrollChanged || vScrollChanged) { changes = this.$updateCachedSize(true, this.gutterWidth, size.width, size.height); this._signal("scrollbarVisibilityChanged"); if (vScrollChanged) longestLine = this.$getLongestLine(); } this.layerConfig = { width : longestLine, padding : this.$padding, firstRow : firstRow, firstRowScreen: firstRowScreen, lastRow : lastRow, lineHeight : lineHeight, characterWidth : this.characterWidth, minHeight : minHeight, maxHeight : maxHeight, offset : offset, gutterOffset : lineHeight ? Math.max(0, Math.ceil((offset + size.height - size.scrollerHeight) / lineHeight)) : 0, height : this.$size.scrollerHeight }; return changes; }; this.$updateLines = function() { var firstRow = this.$changedLines.firstRow; var lastRow = this.$changedLines.lastRow; this.$changedLines = null; var layerConfig = this.layerConfig; if (firstRow > layerConfig.lastRow + 1) { return; } if (lastRow < layerConfig.firstRow) { return; } if (lastRow === Infinity) { if (this.$showGutter) this.$gutterLayer.update(layerConfig); this.$textLayer.update(layerConfig); return; } this.$textLayer.updateLines(layerConfig, firstRow, lastRow); return true; }; this.$getLongestLine = function() { var charCount = this.session.getScreenWidth(); if (this.showInvisibles && !this.session.$useWrapMode) charCount += 1; return Math.max(this.$size.scrollerWidth - 2 * this.$padding, Math.round(charCount * this.characterWidth)); }; this.updateFrontMarkers = function() { this.$markerFront.setMarkers(this.session.getMarkers(true)); this.$loop.schedule(this.CHANGE_MARKER_FRONT); }; this.updateBackMarkers = function() { this.$markerBack.setMarkers(this.session.getMarkers()); this.$loop.schedule(this.CHANGE_MARKER_BACK); }; this.addGutterDecoration = function(row, className){ this.$gutterLayer.addGutterDecoration(row, className); }; this.removeGutterDecoration = function(row, className){ this.$gutterLayer.removeGutterDecoration(row, className); }; this.updateBreakpoints = function(rows) { this.$loop.schedule(this.CHANGE_GUTTER); }; this.setAnnotations = function(annotations) { this.$gutterLayer.setAnnotations(annotations); this.$loop.schedule(this.CHANGE_GUTTER); }; this.updateCursor = function() { this.$loop.schedule(this.CHANGE_CURSOR); }; this.hideCursor = function() { this.$cursorLayer.hideCursor(); }; this.showCursor = function() { this.$cursorLayer.showCursor(); }; this.scrollSelectionIntoView = function(anchor, lead, offset) { this.scrollCursorIntoView(anchor, offset); this.scrollCursorIntoView(lead, offset); }; this.scrollCursorIntoView = function(cursor, offset, $viewMargin) { if (this.$size.scrollerHeight === 0) return; var pos = this.$cursorLayer.getPixelPosition(cursor); var left = pos.left; var top = pos.top; var topMargin = $viewMargin && $viewMargin.top || 0; var bottomMargin = $viewMargin && $viewMargin.bottom || 0; var scrollTop = this.$scrollAnimation ? this.session.getScrollTop() : this.scrollTop; if (scrollTop + topMargin > top) { if (offset && scrollTop + topMargin > top + this.lineHeight) top -= offset * this.$size.scrollerHeight; if (top === 0) top = -this.scrollMargin.top; this.session.setScrollTop(top); } else if (scrollTop + this.$size.scrollerHeight - bottomMargin < top + this.lineHeight) { if (offset && scrollTop + this.$size.scrollerHeight - bottomMargin < top - this.lineHeight) top += offset * this.$size.scrollerHeight; this.session.setScrollTop(top + this.lineHeight - this.$size.scrollerHeight); } var scrollLeft = this.scrollLeft; if (scrollLeft > left) { if (left < this.$padding + 2 * this.layerConfig.characterWidth) left = -this.scrollMargin.left; this.session.setScrollLeft(left); } else if (scrollLeft + this.$size.scrollerWidth < left + this.characterWidth) { this.session.setScrollLeft(Math.round(left + this.characterWidth - this.$size.scrollerWidth)); } else if (scrollLeft <= this.$padding && left - scrollLeft < this.characterWidth) { this.session.setScrollLeft(0); } }; this.getScrollTop = function() { return this.session.getScrollTop(); }; this.getScrollLeft = function() { return this.session.getScrollLeft(); }; this.getScrollTopRow = function() { return this.scrollTop / this.lineHeight; }; this.getScrollBottomRow = function() { return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1); }; this.scrollToRow = function(row) { this.session.setScrollTop(row * this.lineHeight); }; this.alignCursor = function(cursor, alignment) { if (typeof cursor == "number") cursor = {row: cursor, column: 0}; var pos = this.$cursorLayer.getPixelPosition(cursor); var h = this.$size.scrollerHeight - this.lineHeight; var offset = pos.top - h * (alignment || 0); this.session.setScrollTop(offset); return offset; }; this.STEPS = 8; this.$calcSteps = function(fromValue, toValue){ var i = 0; var l = this.STEPS; var steps = []; var func = function(t, x_min, dx) { return dx * (Math.pow(t - 1, 3) + 1) + x_min; }; for (i = 0; i < l; ++i) steps.push(func(i / this.STEPS, fromValue, toValue - fromValue)); return steps; }; this.scrollToLine = function(line, center, animate, callback) { var pos = this.$cursorLayer.getPixelPosition({row: line, column: 0}); var offset = pos.top; if (center) offset -= this.$size.scrollerHeight / 2; var initialScroll = this.scrollTop; this.session.setScrollTop(offset); if (animate !== false) this.animateScrolling(initialScroll, callback); }; this.animateScrolling = function(fromValue, callback) { var toValue = this.scrollTop; if (!this.$animatedScroll) return; var _self = this; if (fromValue == toValue) return; if (this.$scrollAnimation) { var oldSteps = this.$scrollAnimation.steps; if (oldSteps.length) { fromValue = oldSteps[0]; if (fromValue == toValue) return; } } var steps = _self.$calcSteps(fromValue, toValue); this.$scrollAnimation = {from: fromValue, to: toValue, steps: steps}; clearInterval(this.$timer); _self.session.setScrollTop(steps.shift()); _self.session.$scrollTop = toValue; this.$timer = setInterval(function() { if (steps.length) { _self.session.setScrollTop(steps.shift()); _self.session.$scrollTop = toValue; } else if (toValue != null) { _self.session.$scrollTop = -1; _self.session.setScrollTop(toValue); toValue = null; } else { _self.$timer = clearInterval(_self.$timer); _self.$scrollAnimation = null; callback && callback(); } }, 10); }; this.scrollToY = function(scrollTop) { if (this.scrollTop !== scrollTop) { this.$loop.schedule(this.CHANGE_SCROLL); this.scrollTop = scrollTop; } }; this.scrollToX = function(scrollLeft) { if (this.scrollLeft !== scrollLeft) this.scrollLeft = scrollLeft; this.$loop.schedule(this.CHANGE_H_SCROLL); }; this.scrollTo = function(x, y) { this.session.setScrollTop(y); this.session.setScrollLeft(y); }; this.scrollBy = function(deltaX, deltaY) { deltaY && this.session.setScrollTop(this.session.getScrollTop() + deltaY); deltaX && this.session.setScrollLeft(this.session.getScrollLeft() + deltaX); }; this.isScrollableBy = function(deltaX, deltaY) { if (deltaY < 0 && this.session.getScrollTop() >= 1 - this.scrollMargin.top) return true; if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight - this.layerConfig.maxHeight < -1 + this.scrollMargin.bottom) return true; if (deltaX < 0 && this.session.getScrollLeft() >= 1 - this.scrollMargin.left) return true; if (deltaX > 0 && this.session.getScrollLeft() + this.$size.scrollerWidth - this.layerConfig.width < -1 + this.scrollMargin.right) return true; }; this.pixelToScreenCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); var offset = (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth; var row = Math.floor((y + this.scrollTop - canvasPos.top) / this.lineHeight); var col = Math.round(offset); return {row: row, column: col, side: offset - col > 0 ? 1 : -1}; }; this.screenToTextCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); var col = Math.round( (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth ); var row = (y + this.scrollTop - canvasPos.top) / this.lineHeight; return this.session.screenToDocumentPosition(row, Math.max(col, 0)); }; this.textToScreenCoordinates = function(row, column) { var canvasPos = this.scroller.getBoundingClientRect(); var pos = this.session.documentToScreenPosition(row, column); var x = this.$padding + Math.round(pos.column * this.characterWidth); var y = pos.row * this.lineHeight; return { pageX: canvasPos.left + x - this.scrollLeft, pageY: canvasPos.top + y - this.scrollTop }; }; this.visualizeFocus = function() { dom.addCssClass(this.container, "ace_focus"); }; this.visualizeBlur = function() { dom.removeCssClass(this.container, "ace_focus"); }; this.showComposition = function(position) { if (!this.$composition) this.$composition = { keepTextAreaAtCursor: this.$keepTextAreaAtCursor, cssText: this.textarea.style.cssText }; this.$keepTextAreaAtCursor = true; dom.addCssClass(this.textarea, "ace_composition"); this.textarea.style.cssText = ""; this.$moveTextAreaToCursor(); }; this.setCompositionText = function(text) { this.$moveTextAreaToCursor(); }; this.hideComposition = function() { if (!this.$composition) return; dom.removeCssClass(this.textarea, "ace_composition"); this.$keepTextAreaAtCursor = this.$composition.keepTextAreaAtCursor; this.textarea.style.cssText = this.$composition.cssText; this.$composition = null; }; this.setTheme = function(theme, cb) { var _self = this; this.$themeId = theme; _self._dispatchEvent('themeChange',{theme:theme}); if (!theme || typeof theme == "string") { var moduleName = theme || this.$options.theme.initialValue; config.loadModule(["theme", moduleName], afterLoad); } else { afterLoad(theme); } function afterLoad(module) { if (_self.$themeId != theme) return cb && cb(); if (!module || !module.cssClass) throw new Error("couldn't load module " + theme + " or it didn't call define"); dom.importCssString( module.cssText, module.cssClass, _self.container.ownerDocument ); if (_self.theme) dom.removeCssClass(_self.container, _self.theme.cssClass); var padding = "padding" in module ? module.padding : "padding" in (_self.theme || {}) ? 4 : _self.$padding; if (_self.$padding && padding != _self.$padding) _self.setPadding(padding); _self.$theme = module.cssClass; _self.theme = module; dom.addCssClass(_self.container, module.cssClass); dom.setCssClass(_self.container, "ace_dark", module.isDark); if (_self.$size) { _self.$size.width = 0; _self.$updateSizeAsync(); } _self._dispatchEvent('themeLoaded', {theme:module}); cb && cb(); } }; this.getTheme = function() { return this.$themeId; }; this.setStyle = function(style, include) { dom.setCssClass(this.container, style, include !== false); }; this.unsetStyle = function(style) { dom.removeCssClass(this.container, style); }; this.setCursorStyle = function(style) { if (this.scroller.style.cursor != style) this.scroller.style.cursor = style; }; this.setMouseCursor = function(cursorStyle) { this.scroller.style.cursor = cursorStyle; }; this.destroy = function() { this.$textLayer.destroy(); this.$cursorLayer.destroy(); }; }).call(VirtualRenderer.prototype); config.defineOptions(VirtualRenderer.prototype, "renderer", { animatedScroll: {initialValue: false}, showInvisibles: { set: function(value) { if (this.$textLayer.setShowInvisibles(value)) this.$loop.schedule(this.CHANGE_TEXT); }, initialValue: false }, showPrintMargin: { set: function() { this.$updatePrintMargin(); }, initialValue: true }, printMarginColumn: { set: function() { this.$updatePrintMargin(); }, initialValue: 80 }, printMargin: { set: function(val) { if (typeof val == "number") this.$printMarginColumn = val; this.$showPrintMargin = !!val; this.$updatePrintMargin(); }, get: function() { return this.$showPrintMargin && this.$printMarginColumn; } }, showGutter: { set: function(show){ this.$gutter.style.display = show ? "block" : "none"; this.$loop.schedule(this.CHANGE_FULL); this.onGutterResize(); }, initialValue: true }, fadeFoldWidgets: { set: function(show) { dom.setCssClass(this.$gutter, "ace_fade-fold-widgets", show); }, initialValue: false }, showFoldWidgets: { set: function(show) {this.$gutterLayer.setShowFoldWidgets(show)}, initialValue: true }, showLineNumbers: { set: function(show) { this.$gutterLayer.setShowLineNumbers(show); this.$loop.schedule(this.CHANGE_GUTTER); }, initialValue: true }, displayIndentGuides: { set: function(show) { if (this.$textLayer.setDisplayIndentGuides(show)) this.$loop.schedule(this.CHANGE_TEXT); }, initialValue: true }, highlightGutterLine: { set: function(shouldHighlight) { if (!this.$gutterLineHighlight) { this.$gutterLineHighlight = dom.createElement("div"); this.$gutterLineHighlight.className = "ace_gutter-active-line"; this.$gutter.appendChild(this.$gutterLineHighlight); return; } this.$gutterLineHighlight.style.display = shouldHighlight ? "" : "none"; if (this.$cursorLayer.$pixelPos) this.$updateGutterLineHighlight(); }, initialValue: false, value: true }, hScrollBarAlwaysVisible: { set: function(val) { if (!this.$hScrollBarAlwaysVisible || !this.$horizScroll) this.$loop.schedule(this.CHANGE_SCROLL); }, initialValue: false }, vScrollBarAlwaysVisible: { set: function(val) { if (!this.$vScrollBarAlwaysVisible || !this.$vScroll) this.$loop.schedule(this.CHANGE_SCROLL); }, initialValue: false }, fontSize: { set: function(size) { if (typeof size == "number") size = size + "px"; this.container.style.fontSize = size; this.updateFontSize(); }, initialValue: 12 }, fontFamily: { set: function(name) { this.container.style.fontFamily = name; this.updateFontSize(); } }, maxLines: { set: function(val) { this.updateFull(); } }, minLines: { set: function(val) { this.updateFull(); } }, maxPixelHeight: { set: function(val) { this.updateFull(); }, initialValue: 0 }, scrollPastEnd: { set: function(val) { val = +val || 0; if (this.$scrollPastEnd == val) return; this.$scrollPastEnd = val; this.$loop.schedule(this.CHANGE_SCROLL); }, initialValue: 0, handlesSet: true }, fixedWidthGutter: { set: function(val) { this.$gutterLayer.$fixedWidth = !!val; this.$loop.schedule(this.CHANGE_GUTTER); } }, theme: { set: function(val) { this.setTheme(val) }, get: function() { return this.$themeId || this.theme; }, initialValue: "./theme/textmate", handlesSet: true } }); exports.VirtualRenderer = VirtualRenderer; }); define("ace/worker/worker_client",["require","exports","module","ace/lib/oop","ace/lib/net","ace/lib/event_emitter","ace/config"], function(require, exports, module) { "use strict"; var oop = require("../lib/oop"); var net = require("../lib/net"); var EventEmitter = require("../lib/event_emitter").EventEmitter; var config = require("../config"); var WorkerClient = function(topLevelNamespaces, mod, classname, workerUrl) { this.$sendDeltaQueue = this.$sendDeltaQueue.bind(this); this.changeListener = this.changeListener.bind(this); this.onMessage = this.onMessage.bind(this); if (require.nameToUrl && !require.toUrl) require.toUrl = require.nameToUrl; if (config.get("packaged") || !require.toUrl) { workerUrl = workerUrl || config.moduleUrl(mod, "worker"); } else { var normalizePath = this.$normalizePath; workerUrl = workerUrl || normalizePath(require.toUrl("ace/worker/worker.js", null, "_")); var tlns = {}; topLevelNamespaces.forEach(function(ns) { tlns[ns] = normalizePath(require.toUrl(ns, null, "_").replace(/(\.js)?(\?.*)?$/, "")); }); } try { this.$worker = new Worker(workerUrl); } catch(e) { if (e instanceof window.DOMException) { var blob = this.$workerBlob(workerUrl); var URL = window.URL || window.webkitURL; var blobURL = URL.createObjectURL(blob); this.$worker = new Worker(blobURL); URL.revokeObjectURL(blobURL); } else { throw e; } } this.$worker.postMessage({ init : true, tlns : tlns, module : mod, classname : classname }); this.callbackId = 1; this.callbacks = {}; this.$worker.onmessage = this.onMessage; }; (function(){ oop.implement(this, EventEmitter); this.onMessage = function(e) { var msg = e.data; switch(msg.type) { case "event": this._signal(msg.name, {data: msg.data}); break; case "call": var callback = this.callbacks[msg.id]; if (callback) { callback(msg.data); delete this.callbacks[msg.id]; } break; case "error": this.reportError(msg.data); break; case "log": window.console && console.log && console.log.apply(console, msg.data); break; } }; this.reportError = function(err) { window.console && console.error && console.error(err); }; this.$normalizePath = function(path) { return net.qualifyURL(path); }; this.terminate = function() { this._signal("terminate", {}); this.deltaQueue = null; this.$worker.terminate(); this.$worker = null; if (this.$doc) this.$doc.off("change", this.changeListener); this.$doc = null; }; this.send = function(cmd, args) { this.$worker.postMessage({command: cmd, args: args}); }; this.call = function(cmd, args, callback) { if (callback) { var id = this.callbackId++; this.callbacks[id] = callback; args.push(id); } this.send(cmd, args); }; this.emit = function(event, data) { try { this.$worker.postMessage({event: event, data: {data: data.data}}); } catch(ex) { console.error(ex.stack); } }; this.attachToDocument = function(doc) { if(this.$doc) this.terminate(); this.$doc = doc; this.call("setValue", [doc.getValue()]); doc.on("change", this.changeListener); }; this.changeListener = function(delta) { if (!this.deltaQueue) { this.deltaQueue = []; setTimeout(this.$sendDeltaQueue, 0); } if (delta.action == "insert") this.deltaQueue.push(delta.start, delta.lines); else this.deltaQueue.push(delta.start, delta.end); }; this.$sendDeltaQueue = function() { var q = this.deltaQueue; if (!q) return; this.deltaQueue = null; if (q.length > 50 && q.length > this.$doc.getLength() >> 1) { this.call("setValue", [this.$doc.getValue()]); } else this.emit("change", {data: q}); }; this.$workerBlob = function(workerUrl) { var script = "importScripts('" + net.qualifyURL(workerUrl) + "');"; try { return new Blob([script], {"type": "application/javascript"}); } catch (e) { // Backwards-compatibility var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; var blobBuilder = new BlobBuilder(); blobBuilder.append(script); return blobBuilder.getBlob("application/javascript"); } }; }).call(WorkerClient.prototype); var UIWorkerClient = function(topLevelNamespaces, mod, classname) { this.$sendDeltaQueue = this.$sendDeltaQueue.bind(this); this.changeListener = this.changeListener.bind(this); this.callbackId = 1; this.callbacks = {}; this.messageBuffer = []; var main = null; var emitSync = false; var sender = Object.create(EventEmitter); var _self = this; this.$worker = {}; this.$worker.terminate = function() {}; this.$worker.postMessage = function(e) { _self.messageBuffer.push(e); if (main) { if (emitSync) setTimeout(processNext); else processNext(); } }; this.setEmitSync = function(val) { emitSync = val }; var processNext = function() { var msg = _self.messageBuffer.shift(); if (msg.command) main[msg.command].apply(main, msg.args); else if (msg.event) sender._signal(msg.event, msg.data); }; sender.postMessage = function(msg) { _self.onMessage({data: msg}); }; sender.callback = function(data, callbackId) { this.postMessage({type: "call", id: callbackId, data: data}); }; sender.emit = function(name, data) { this.postMessage({type: "event", name: name, data: data}); }; config.loadModule(["worker", mod], function(Main) { main = new Main[classname](sender); while (_self.messageBuffer.length) processNext(); }); }; UIWorkerClient.prototype = WorkerClient.prototype; exports.UIWorkerClient = UIWorkerClient; exports.WorkerClient = WorkerClient; }); define("ace/placeholder",["require","exports","module","ace/range","ace/lib/event_emitter","ace/lib/oop"], function(require, exports, module) { "use strict"; var Range = require("./range").Range; var EventEmitter = require("./lib/event_emitter").EventEmitter; var oop = require("./lib/oop"); var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) { var _self = this; this.length = length; this.session = session; this.doc = session.getDocument(); this.mainClass = mainClass; this.othersClass = othersClass; this.$onUpdate = this.onUpdate.bind(this); this.doc.on("change", this.$onUpdate); this.$others = others; this.$onCursorChange = function() { setTimeout(function() { _self.onCursorChange(); }); }; this.$pos = pos; var undoStack = session.getUndoManager().$undoStack || session.getUndoManager().$undostack || {length: -1}; this.$undoStackDepth = undoStack.length; this.setup(); session.selection.on("changeCursor", this.$onCursorChange); }; (function() { oop.implement(this, EventEmitter); this.setup = function() { var _self = this; var doc = this.doc; var session = this.session; this.selectionBefore = session.selection.toJSON(); if (session.selection.inMultiSelectMode) session.selection.toSingleRange(); this.pos = doc.createAnchor(this.$pos.row, this.$pos.column); var pos = this.pos; pos.$insertRight = true; pos.detach(); pos.markerId = session.addMarker(new Range(pos.row, pos.column, pos.row, pos.column + this.length), this.mainClass, null, false); this.others = []; this.$others.forEach(function(other) { var anchor = doc.createAnchor(other.row, other.column); anchor.$insertRight = true; anchor.detach(); _self.others.push(anchor); }); session.setUndoSelect(false); }; this.showOtherMarkers = function() { if (this.othersActive) return; var session = this.session; var _self = this; this.othersActive = true; this.others.forEach(function(anchor) { anchor.markerId = session.addMarker(new Range(anchor.row, anchor.column, anchor.row, anchor.column+_self.length), _self.othersClass, null, false); }); }; this.hideOtherMarkers = function() { if (!this.othersActive) return; this.othersActive = false; for (var i = 0; i < this.others.length; i++) { this.session.removeMarker(this.others[i].markerId); } }; this.onUpdate = function(delta) { if (this.$updating) return this.updateAnchors(delta); var range = delta; if (range.start.row !== range.end.row) return; if (range.start.row !== this.pos.row) return; this.$updating = true; var lengthDiff = delta.action === "insert" ? range.end.column - range.start.column : range.start.column - range.end.column; var inMainRange = range.start.column >= this.pos.column && range.start.column <= this.pos.column + this.length + 1; var distanceFromStart = range.start.column - this.pos.column; this.updateAnchors(delta); if (inMainRange) this.length += lengthDiff; if (inMainRange && !this.session.$fromUndo) { if (delta.action === 'insert') { for (var i = this.others.length - 1; i >= 0; i--) { var otherPos = this.others[i]; var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; this.doc.insertMergedLines(newPos, delta.lines); } } else if (delta.action === 'remove') { for (var i = this.others.length - 1; i >= 0; i--) { var otherPos = this.others[i]; var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; this.doc.remove(new Range(newPos.row, newPos.column, newPos.row, newPos.column - lengthDiff)); } } } this.$updating = false; this.updateMarkers(); }; this.updateAnchors = function(delta) { this.pos.onChange(delta); for (var i = this.others.length; i--;) this.others[i].onChange(delta); this.updateMarkers(); }; this.updateMarkers = function() { if (this.$updating) return; var _self = this; var session = this.session; var updateMarker = function(pos, className) { session.removeMarker(pos.markerId); pos.markerId = session.addMarker(new Range(pos.row, pos.column, pos.row, pos.column+_self.length), className, null, false); }; updateMarker(this.pos, this.mainClass); for (var i = this.others.length; i--;) updateMarker(this.others[i], this.othersClass); }; this.onCursorChange = function(event) { if (this.$updating || !this.session) return; var pos = this.session.selection.getCursor(); if (pos.row === this.pos.row && pos.column >= this.pos.column && pos.column <= this.pos.column + this.length) { this.showOtherMarkers(); this._emit("cursorEnter", event); } else { this.hideOtherMarkers(); this._emit("cursorLeave", event); } }; this.detach = function() { this.session.removeMarker(this.pos && this.pos.markerId); this.hideOtherMarkers(); this.doc.removeEventListener("change", this.$onUpdate); this.session.selection.removeEventListener("changeCursor", this.$onCursorChange); this.session.setUndoSelect(true); this.session = null; }; this.cancel = function() { if (this.$undoStackDepth === -1) return; var undoManager = this.session.getUndoManager(); var undosRequired = (undoManager.$undoStack || undoManager.$undostack).length - this.$undoStackDepth; for (var i = 0; i < undosRequired; i++) { undoManager.undo(true); } if (this.selectionBefore) this.session.selection.fromJSON(this.selectionBefore); }; }).call(PlaceHolder.prototype); exports.PlaceHolder = PlaceHolder; }); define("ace/mouse/multi_select_handler",["require","exports","module","ace/lib/event","ace/lib/useragent"], function(require, exports, module) { var event = require("../lib/event"); var useragent = require("../lib/useragent"); function isSamePoint(p1, p2) { return p1.row == p2.row && p1.column == p2.column; } function onMouseDown(e) { var ev = e.domEvent; var alt = ev.altKey; var shift = ev.shiftKey; var ctrl = ev.ctrlKey; var accel = e.getAccelKey(); var button = e.getButton(); if (ctrl && useragent.isMac) button = ev.button; if (e.editor.inMultiSelectMode && button == 2) { e.editor.textInput.onContextMenu(e.domEvent); return; } if (!ctrl && !alt && !accel) { if (button === 0 && e.editor.inMultiSelectMode) e.editor.exitMultiSelectMode(); return; } if (button !== 0) return; var editor = e.editor; var selection = editor.selection; var isMultiSelect = editor.inMultiSelectMode; var pos = e.getDocumentPosition(); var cursor = selection.getCursor(); var inSelection = e.inSelection() || (selection.isEmpty() && isSamePoint(pos, cursor)); var mouseX = e.x, mouseY = e.y; var onMouseSelection = function(e) { mouseX = e.clientX; mouseY = e.clientY; }; var session = editor.session; var screenAnchor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY); var screenCursor = screenAnchor; var selectionMode; if (editor.$mouseHandler.$enableJumpToDef) { if (ctrl && alt || accel && alt) selectionMode = shift ? "block" : "add"; else if (alt && editor.$blockSelectEnabled) selectionMode = "block"; } else { if (accel && !alt) { selectionMode = "add"; if (!isMultiSelect && shift) return; } else if (alt && editor.$blockSelectEnabled) { selectionMode = "block"; } } if (selectionMode && useragent.isMac && ev.ctrlKey) { editor.$mouseHandler.cancelContextMenu(); } if (selectionMode == "add") { if (!isMultiSelect && inSelection) return; // dragging if (!isMultiSelect) { var range = selection.toOrientedRange(); editor.addSelectionMarker(range); } var oldRange = selection.rangeList.rangeAtPoint(pos); editor.$blockScrolling++; editor.inVirtualSelectionMode = true; if (shift) { oldRange = null; range = selection.ranges[0] || range; editor.removeSelectionMarker(range); } editor.once("mouseup", function() { var tmpSel = selection.toOrientedRange(); if (oldRange && tmpSel.isEmpty() && isSamePoint(oldRange.cursor, tmpSel.cursor)) selection.substractPoint(tmpSel.cursor); else { if (shift) { selection.substractPoint(range.cursor); } else if (range) { editor.removeSelectionMarker(range); selection.addRange(range); } selection.addRange(tmpSel); } editor.$blockScrolling--; editor.inVirtualSelectionMode = false; }); } else if (selectionMode == "block") { e.stop(); editor.inVirtualSelectionMode = true; var initialRange; var rectSel = []; var blockSelect = function() { var newCursor = editor.renderer.pixelToScreenCoordinates(mouseX, mouseY); var cursor = session.screenToDocumentPosition(newCursor.row, newCursor.column); if (isSamePoint(screenCursor, newCursor) && isSamePoint(cursor, selection.lead)) return; screenCursor = newCursor; editor.$blockScrolling++; editor.selection.moveToPosition(cursor); editor.renderer.scrollCursorIntoView(); editor.removeSelectionMarkers(rectSel); rectSel = selection.rectangularRangeBlock(screenCursor, screenAnchor); if (editor.$mouseHandler.$clickSelection && rectSel.length == 1 && rectSel[0].isEmpty()) rectSel[0] = editor.$mouseHandler.$clickSelection.clone(); rectSel.forEach(editor.addSelectionMarker, editor); editor.updateSelectionMarkers(); editor.$blockScrolling--; }; editor.$blockScrolling++; if (isMultiSelect && !accel) { selection.toSingleRange(); } else if (!isMultiSelect && accel) { initialRange = selection.toOrientedRange(); editor.addSelectionMarker(initialRange); } if (shift) screenAnchor = session.documentToScreenPosition(selection.lead); else selection.moveToPosition(pos); editor.$blockScrolling--; screenCursor = {row: -1, column: -1}; var onMouseSelectionEnd = function(e) { clearInterval(timerId); editor.removeSelectionMarkers(rectSel); if (!rectSel.length) rectSel = [selection.toOrientedRange()]; editor.$blockScrolling++; if (initialRange) { editor.removeSelectionMarker(initialRange); selection.toSingleRange(initialRange); } for (var i = 0; i < rectSel.length; i++) selection.addRange(rectSel[i]); editor.inVirtualSelectionMode = false; editor.$mouseHandler.$clickSelection = null; editor.$blockScrolling--; }; var onSelectionInterval = blockSelect; event.capture(editor.container, onMouseSelection, onMouseSelectionEnd); var timerId = setInterval(function() {onSelectionInterval();}, 20); return e.preventDefault(); } } exports.onMouseDown = onMouseDown; }); define("ace/commands/multi_select_commands",["require","exports","module","ace/keyboard/hash_handler"], function(require, exports, module) { exports.defaultCommands = [{ name: "addCursorAbove", exec: function(editor) { editor.selectMoreLines(-1); }, bindKey: {win: "Ctrl-Alt-Up", mac: "Ctrl-Alt-Up"}, scrollIntoView: "cursor", readOnly: true }, { name: "addCursorBelow", exec: function(editor) { editor.selectMoreLines(1); }, bindKey: {win: "Ctrl-Alt-Down", mac: "Ctrl-Alt-Down"}, scrollIntoView: "cursor", readOnly: true }, { name: "addCursorAboveSkipCurrent", exec: function(editor) { editor.selectMoreLines(-1, true); }, bindKey: {win: "Ctrl-Alt-Shift-Up", mac: "Ctrl-Alt-Shift-Up"}, scrollIntoView: "cursor", readOnly: true }, { name: "addCursorBelowSkipCurrent", exec: function(editor) { editor.selectMoreLines(1, true); }, bindKey: {win: "Ctrl-Alt-Shift-Down", mac: "Ctrl-Alt-Shift-Down"}, scrollIntoView: "cursor", readOnly: true }, { name: "selectMoreBefore", exec: function(editor) { editor.selectMore(-1); }, bindKey: {win: "Ctrl-Alt-Left", mac: "Ctrl-Alt-Left"}, scrollIntoView: "cursor", readOnly: true }, { name: "selectMoreAfter", exec: function(editor) { editor.selectMore(1); }, bindKey: {win: "Ctrl-Alt-Right", mac: "Ctrl-Alt-Right"}, scrollIntoView: "cursor", readOnly: true }, { name: "selectNextBefore", exec: function(editor) { editor.selectMore(-1, true); }, bindKey: {win: "Ctrl-Alt-Shift-Left", mac: "Ctrl-Alt-Shift-Left"}, scrollIntoView: "cursor", readOnly: true }, { name: "selectNextAfter", exec: function(editor) { editor.selectMore(1, true); }, bindKey: {win: "Ctrl-Alt-Shift-Right", mac: "Ctrl-Alt-Shift-Right"}, scrollIntoView: "cursor", readOnly: true }, { name: "splitIntoLines", exec: function(editor) { editor.multiSelect.splitIntoLines(); }, bindKey: {win: "Ctrl-Alt-L", mac: "Ctrl-Alt-L"}, readOnly: true }, { name: "alignCursors", exec: function(editor) { editor.alignCursors(); }, bindKey: {win: "Ctrl-Alt-A", mac: "Ctrl-Alt-A"}, scrollIntoView: "cursor" }, { name: "findAll", exec: function(editor) { editor.findAll(); }, bindKey: {win: "Ctrl-Alt-K", mac: "Ctrl-Alt-G"}, scrollIntoView: "cursor", readOnly: true }]; exports.multiSelectCommands = [{ name: "singleSelection", bindKey: "esc", exec: function(editor) { editor.exitMultiSelectMode(); }, scrollIntoView: "cursor", readOnly: true, isAvailable: function(editor) {return editor && editor.inMultiSelectMode} }]; var HashHandler = require("../keyboard/hash_handler").HashHandler; exports.keyboardHandler = new HashHandler(exports.multiSelectCommands); }); define("ace/multi_select",["require","exports","module","ace/range_list","ace/range","ace/selection","ace/mouse/multi_select_handler","ace/lib/event","ace/lib/lang","ace/commands/multi_select_commands","ace/search","ace/edit_session","ace/editor","ace/config"], function(require, exports, module) { var RangeList = require("./range_list").RangeList; var Range = require("./range").Range; var Selection = require("./selection").Selection; var onMouseDown = require("./mouse/multi_select_handler").onMouseDown; var event = require("./lib/event"); var lang = require("./lib/lang"); var commands = require("./commands/multi_select_commands"); exports.commands = commands.defaultCommands.concat(commands.multiSelectCommands); var Search = require("./search").Search; var search = new Search(); function find(session, needle, dir) { search.$options.wrap = true; search.$options.needle = needle; search.$options.backwards = dir == -1; return search.find(session); } var EditSession = require("./edit_session").EditSession; (function() { this.getSelectionMarkers = function() { return this.$selectionMarkers; }; }).call(EditSession.prototype); (function() { this.ranges = null; this.rangeList = null; this.addRange = function(range, $blockChangeEvents) { if (!range) return; if (!this.inMultiSelectMode && this.rangeCount === 0) { var oldRange = this.toOrientedRange(); this.rangeList.add(oldRange); this.rangeList.add(range); if (this.rangeList.ranges.length != 2) { this.rangeList.removeAll(); return $blockChangeEvents || this.fromOrientedRange(range); } this.rangeList.removeAll(); this.rangeList.add(oldRange); this.$onAddRange(oldRange); } if (!range.cursor) range.cursor = range.end; var removed = this.rangeList.add(range); this.$onAddRange(range); if (removed.length) this.$onRemoveRange(removed); if (this.rangeCount > 1 && !this.inMultiSelectMode) { this._signal("multiSelect"); this.inMultiSelectMode = true; this.session.$undoSelect = false; this.rangeList.attach(this.session); } return $blockChangeEvents || this.fromOrientedRange(range); }; this.toSingleRange = function(range) { range = range || this.ranges[0]; var removed = this.rangeList.removeAll(); if (removed.length) this.$onRemoveRange(removed); range && this.fromOrientedRange(range); }; this.substractPoint = function(pos) { var removed = this.rangeList.substractPoint(pos); if (removed) { this.$onRemoveRange(removed); return removed[0]; } }; this.mergeOverlappingRanges = function() { var removed = this.rangeList.merge(); if (removed.length) this.$onRemoveRange(removed); else if(this.ranges[0]) this.fromOrientedRange(this.ranges[0]); }; this.$onAddRange = function(range) { this.rangeCount = this.rangeList.ranges.length; this.ranges.unshift(range); this._signal("addRange", {range: range}); }; this.$onRemoveRange = function(removed) { this.rangeCount = this.rangeList.ranges.length; if (this.rangeCount == 1 && this.inMultiSelectMode) { var lastRange = this.rangeList.ranges.pop(); removed.push(lastRange); this.rangeCount = 0; } for (var i = removed.length; i--; ) { var index = this.ranges.indexOf(removed[i]); this.ranges.splice(index, 1); } this._signal("removeRange", {ranges: removed}); if (this.rangeCount === 0 && this.inMultiSelectMode) { this.inMultiSelectMode = false; this._signal("singleSelect"); this.session.$undoSelect = true; this.rangeList.detach(this.session); } lastRange = lastRange || this.ranges[0]; if (lastRange && !lastRange.isEqual(this.getRange())) this.fromOrientedRange(lastRange); }; this.$initRangeList = function() { if (this.rangeList) return; this.rangeList = new RangeList(); this.ranges = []; this.rangeCount = 0; }; this.getAllRanges = function() { return this.rangeCount ? this.rangeList.ranges.concat() : [this.getRange()]; }; this.splitIntoLines = function () { if (this.rangeCount > 1) { var ranges = this.rangeList.ranges; var lastRange = ranges[ranges.length - 1]; var range = Range.fromPoints(ranges[0].start, lastRange.end); this.toSingleRange(); this.setSelectionRange(range, lastRange.cursor == lastRange.start); } else { var range = this.getRange(); var isBackwards = this.isBackwards(); var startRow = range.start.row; var endRow = range.end.row; if (startRow == endRow) { if (isBackwards) var start = range.end, end = range.start; else var start = range.start, end = range.end; this.addRange(Range.fromPoints(end, end)); this.addRange(Range.fromPoints(start, start)); return; } var rectSel = []; var r = this.getLineRange(startRow, true); r.start.column = range.start.column; rectSel.push(r); for (var i = startRow + 1; i < endRow; i++) rectSel.push(this.getLineRange(i, true)); r = this.getLineRange(endRow, true); r.end.column = range.end.column; rectSel.push(r); rectSel.forEach(this.addRange, this); } }; this.toggleBlockSelection = function () { if (this.rangeCount > 1) { var ranges = this.rangeList.ranges; var lastRange = ranges[ranges.length - 1]; var range = Range.fromPoints(ranges[0].start, lastRange.end); this.toSingleRange(); this.setSelectionRange(range, lastRange.cursor == lastRange.start); } else { var cursor = this.session.documentToScreenPosition(this.selectionLead); var anchor = this.session.documentToScreenPosition(this.selectionAnchor); var rectSel = this.rectangularRangeBlock(cursor, anchor); rectSel.forEach(this.addRange, this); } }; this.rectangularRangeBlock = function(screenCursor, screenAnchor, includeEmptyLines) { var rectSel = []; var xBackwards = screenCursor.column < screenAnchor.column; if (xBackwards) { var startColumn = screenCursor.column; var endColumn = screenAnchor.column; } else { var startColumn = screenAnchor.column; var endColumn = screenCursor.column; } var yBackwards = screenCursor.row < screenAnchor.row; if (yBackwards) { var startRow = screenCursor.row; var endRow = screenAnchor.row; } else { var startRow = screenAnchor.row; var endRow = screenCursor.row; } if (startColumn < 0) startColumn = 0; if (startRow < 0) startRow = 0; if (startRow == endRow) includeEmptyLines = true; for (var row = startRow; row <= endRow; row++) { var range = Range.fromPoints( this.session.screenToDocumentPosition(row, startColumn), this.session.screenToDocumentPosition(row, endColumn) ); if (range.isEmpty()) { if (docEnd && isSamePoint(range.end, docEnd)) break; var docEnd = range.end; } range.cursor = xBackwards ? range.start : range.end; rectSel.push(range); } if (yBackwards) rectSel.reverse(); if (!includeEmptyLines) { var end = rectSel.length - 1; while (rectSel[end].isEmpty() && end > 0) end--; if (end > 0) { var start = 0; while (rectSel[start].isEmpty()) start++; } for (var i = end; i >= start; i--) { if (rectSel[i].isEmpty()) rectSel.splice(i, 1); } } return rectSel; }; }).call(Selection.prototype); var Editor = require("./editor").Editor; (function() { this.updateSelectionMarkers = function() { this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.addSelectionMarker = function(orientedRange) { if (!orientedRange.cursor) orientedRange.cursor = orientedRange.end; var style = this.getSelectionStyle(); orientedRange.marker = this.session.addMarker(orientedRange, "ace_selection", style); this.session.$selectionMarkers.push(orientedRange); this.session.selectionMarkerCount = this.session.$selectionMarkers.length; return orientedRange; }; this.removeSelectionMarker = function(range) { if (!range.marker) return; this.session.removeMarker(range.marker); var index = this.session.$selectionMarkers.indexOf(range); if (index != -1) this.session.$selectionMarkers.splice(index, 1); this.session.selectionMarkerCount = this.session.$selectionMarkers.length; }; this.removeSelectionMarkers = function(ranges) { var markerList = this.session.$selectionMarkers; for (var i = ranges.length; i--; ) { var range = ranges[i]; if (!range.marker) continue; this.session.removeMarker(range.marker); var index = markerList.indexOf(range); if (index != -1) markerList.splice(index, 1); } this.session.selectionMarkerCount = markerList.length; }; this.$onAddRange = function(e) { this.addSelectionMarker(e.range); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.$onRemoveRange = function(e) { this.removeSelectionMarkers(e.ranges); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.$onMultiSelect = function(e) { if (this.inMultiSelectMode) return; this.inMultiSelectMode = true; this.setStyle("ace_multiselect"); this.keyBinding.addKeyboardHandler(commands.keyboardHandler); this.commands.setDefaultHandler("exec", this.$onMultiSelectExec); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); }; this.$onSingleSelect = function(e) { if (this.session.multiSelect.inVirtualMode) return; this.inMultiSelectMode = false; this.unsetStyle("ace_multiselect"); this.keyBinding.removeKeyboardHandler(commands.keyboardHandler); this.commands.removeDefaultHandler("exec", this.$onMultiSelectExec); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); this._emit("changeSelection"); }; this.$onMultiSelectExec = function(e) { var command = e.command; var editor = e.editor; if (!editor.multiSelect) return; if (!command.multiSelectAction) { var result = command.exec(editor, e.args || {}); editor.multiSelect.addRange(editor.multiSelect.toOrientedRange()); editor.multiSelect.mergeOverlappingRanges(); } else if (command.multiSelectAction == "forEach") { result = editor.forEachSelection(command, e.args); } else if (command.multiSelectAction == "forEachLine") { result = editor.forEachSelection(command, e.args, true); } else if (command.multiSelectAction == "single") { editor.exitMultiSelectMode(); result = command.exec(editor, e.args || {}); } else { result = command.multiSelectAction(editor, e.args || {}); } return result; }; this.forEachSelection = function(cmd, args, options) { if (this.inVirtualSelectionMode) return; var keepOrder = options && options.keepOrder; var $byLines = options == true || options && options.$byLines var session = this.session; var selection = this.selection; var rangeList = selection.rangeList; var ranges = (keepOrder ? selection : rangeList).ranges; var result; if (!ranges.length) return cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {}); var reg = selection._eventRegistry; selection._eventRegistry = {}; var tmpSel = new Selection(session); this.inVirtualSelectionMode = true; for (var i = ranges.length; i--;) { if ($byLines) { while (i > 0 && ranges[i].start.row == ranges[i - 1].end.row) i--; } tmpSel.fromOrientedRange(ranges[i]); tmpSel.index = i; this.selection = session.selection = tmpSel; var cmdResult = cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {}); if (!result && cmdResult !== undefined) result = cmdResult; tmpSel.toOrientedRange(ranges[i]); } tmpSel.detach(); this.selection = session.selection = selection; this.inVirtualSelectionMode = false; selection._eventRegistry = reg; selection.mergeOverlappingRanges(); var anim = this.renderer.$scrollAnimation; this.onCursorChange(); this.onSelectionChange(); if (anim && anim.from == anim.to) this.renderer.animateScrolling(anim.from); return result; }; this.exitMultiSelectMode = function() { if (!this.inMultiSelectMode || this.inVirtualSelectionMode) return; this.multiSelect.toSingleRange(); }; this.getSelectedText = function() { var text = ""; if (this.inMultiSelectMode && !this.inVirtualSelectionMode) { var ranges = this.multiSelect.rangeList.ranges; var buf = []; for (var i = 0; i < ranges.length; i++) { buf.push(this.session.getTextRange(ranges[i])); } var nl = this.session.getDocument().getNewLineCharacter(); text = buf.join(nl); if (text.length == (buf.length - 1) * nl.length) text = ""; } else if (!this.selection.isEmpty()) { text = this.session.getTextRange(this.getSelectionRange()); } return text; }; this.$checkMultiselectChange = function(e, anchor) { if (this.inMultiSelectMode && !this.inVirtualSelectionMode) { var range = this.multiSelect.ranges[0]; if (this.multiSelect.isEmpty() && anchor == this.multiSelect.anchor) return; var pos = anchor == this.multiSelect.anchor ? range.cursor == range.start ? range.end : range.start : range.cursor; if (pos.row != anchor.row || this.session.$clipPositionToDocument(pos.row, pos.column).column != anchor.column) this.multiSelect.toSingleRange(this.multiSelect.toOrientedRange()); } }; this.findAll = function(needle, options, additive) { options = options || {}; options.needle = needle || options.needle; if (options.needle == undefined) { var range = this.selection.isEmpty() ? this.selection.getWordRange() : this.selection.getRange(); options.needle = this.session.getTextRange(range); } this.$search.set(options); var ranges = this.$search.findAll(this.session); if (!ranges.length) return 0; this.$blockScrolling += 1; var selection = this.multiSelect; if (!additive) selection.toSingleRange(ranges[0]); for (var i = ranges.length; i--; ) selection.addRange(ranges[i], true); if (range && selection.rangeList.rangeAtPoint(range.start)) selection.addRange(range, true); this.$blockScrolling -= 1; return ranges.length; }; this.selectMoreLines = function(dir, skip) { var range = this.selection.toOrientedRange(); var isBackwards = range.cursor == range.end; var screenLead = this.session.documentToScreenPosition(range.cursor); if (this.selection.$desiredColumn) screenLead.column = this.selection.$desiredColumn; var lead = this.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column); if (!range.isEmpty()) { var screenAnchor = this.session.documentToScreenPosition(isBackwards ? range.end : range.start); var anchor = this.session.screenToDocumentPosition(screenAnchor.row + dir, screenAnchor.column); } else { var anchor = lead; } if (isBackwards) { var newRange = Range.fromPoints(lead, anchor); newRange.cursor = newRange.start; } else { var newRange = Range.fromPoints(anchor, lead); newRange.cursor = newRange.end; } newRange.desiredColumn = screenLead.column; if (!this.selection.inMultiSelectMode) { this.selection.addRange(range); } else { if (skip) var toRemove = range.cursor; } this.selection.addRange(newRange); if (toRemove) this.selection.substractPoint(toRemove); }; this.transposeSelections = function(dir) { var session = this.session; var sel = session.multiSelect; var all = sel.ranges; for (var i = all.length; i--; ) { var range = all[i]; if (range.isEmpty()) { var tmp = session.getWordRange(range.start.row, range.start.column); range.start.row = tmp.start.row; range.start.column = tmp.start.column; range.end.row = tmp.end.row; range.end.column = tmp.end.column; } } sel.mergeOverlappingRanges(); var words = []; for (var i = all.length; i--; ) { var range = all[i]; words.unshift(session.getTextRange(range)); } if (dir < 0) words.unshift(words.pop()); else words.push(words.shift()); for (var i = all.length; i--; ) { var range = all[i]; var tmp = range.clone(); session.replace(range, words[i]); range.start.row = tmp.start.row; range.start.column = tmp.start.column; } }; this.selectMore = function(dir, skip, stopAtFirst) { var session = this.session; var sel = session.multiSelect; var range = sel.toOrientedRange(); if (range.isEmpty()) { range = session.getWordRange(range.start.row, range.start.column); range.cursor = dir == -1 ? range.start : range.end; this.multiSelect.addRange(range); if (stopAtFirst) return; } var needle = session.getTextRange(range); var newRange = find(session, needle, dir); if (newRange) { newRange.cursor = dir == -1 ? newRange.start : newRange.end; this.$blockScrolling += 1; this.session.unfold(newRange); this.multiSelect.addRange(newRange); this.$blockScrolling -= 1; this.renderer.scrollCursorIntoView(null, 0.5); } if (skip) this.multiSelect.substractPoint(range.cursor); }; this.alignCursors = function() { var session = this.session; var sel = session.multiSelect; var ranges = sel.ranges; var row = -1; var sameRowRanges = ranges.filter(function(r) { if (r.cursor.row == row) return true; row = r.cursor.row; }); if (!ranges.length || sameRowRanges.length == ranges.length - 1) { var range = this.selection.getRange(); var fr = range.start.row, lr = range.end.row; var guessRange = fr == lr; if (guessRange) { var max = this.session.getLength(); var line; do { line = this.session.getLine(lr); } while (/[=:]/.test(line) && ++lr < max); do { line = this.session.getLine(fr); } while (/[=:]/.test(line) && --fr > 0); if (fr < 0) fr = 0; if (lr >= max) lr = max - 1; } var lines = this.session.removeFullLines(fr, lr); lines = this.$reAlignText(lines, guessRange); this.session.insert({row: fr, column: 0}, lines.join("\n") + "\n"); if (!guessRange) { range.start.column = 0; range.end.column = lines[lines.length - 1].length; } this.selection.setRange(range); } else { sameRowRanges.forEach(function(r) { sel.substractPoint(r.cursor); }); var maxCol = 0; var minSpace = Infinity; var spaceOffsets = ranges.map(function(r) { var p = r.cursor; var line = session.getLine(p.row); var spaceOffset = line.substr(p.column).search(/\S/g); if (spaceOffset == -1) spaceOffset = 0; if (p.column > maxCol) maxCol = p.column; if (spaceOffset < minSpace) minSpace = spaceOffset; return spaceOffset; }); ranges.forEach(function(r, i) { var p = r.cursor; var l = maxCol - p.column; var d = spaceOffsets[i] - minSpace; if (l > d) session.insert(p, lang.stringRepeat(" ", l - d)); else session.remove(new Range(p.row, p.column, p.row, p.column - l + d)); r.start.column = r.end.column = maxCol; r.start.row = r.end.row = p.row; r.cursor = r.end; }); sel.fromOrientedRange(ranges[0]); this.renderer.updateCursor(); this.renderer.updateBackMarkers(); } }; this.$reAlignText = function(lines, forceLeft) { var isLeftAligned = true, isRightAligned = true; var startW, textW, endW; return lines.map(function(line) { var m = line.match(/(\s*)(.*?)(\s*)([=:].*)/); if (!m) return [line]; if (startW == null) { startW = m[1].length; textW = m[2].length; endW = m[3].length; return m; } if (startW + textW + endW != m[1].length + m[2].length + m[3].length) isRightAligned = false; if (startW != m[1].length) isLeftAligned = false; if (startW > m[1].length) startW = m[1].length; if (textW < m[2].length) textW = m[2].length; if (endW > m[3].length) endW = m[3].length; return m; }).map(forceLeft ? alignLeft : isLeftAligned ? isRightAligned ? alignRight : alignLeft : unAlign); function spaces(n) { return lang.stringRepeat(" ", n); } function alignLeft(m) { return !m[2] ? m[0] : spaces(startW) + m[2] + spaces(textW - m[2].length + endW) + m[4].replace(/^([=:])\s+/, "$1 "); } function alignRight(m) { return !m[2] ? m[0] : spaces(startW + textW - m[2].length) + m[2] + spaces(endW, " ") + m[4].replace(/^([=:])\s+/, "$1 "); } function unAlign(m) { return !m[2] ? m[0] : spaces(startW) + m[2] + spaces(endW) + m[4].replace(/^([=:])\s+/, "$1 "); } }; }).call(Editor.prototype); function isSamePoint(p1, p2) { return p1.row == p2.row && p1.column == p2.column; } exports.onSessionChange = function(e) { var session = e.session; if (session && !session.multiSelect) { session.$selectionMarkers = []; session.selection.$initRangeList(); session.multiSelect = session.selection; } this.multiSelect = session && session.multiSelect; var oldSession = e.oldSession; if (oldSession) { oldSession.multiSelect.off("addRange", this.$onAddRange); oldSession.multiSelect.off("removeRange", this.$onRemoveRange); oldSession.multiSelect.off("multiSelect", this.$onMultiSelect); oldSession.multiSelect.off("singleSelect", this.$onSingleSelect); oldSession.multiSelect.lead.off("change", this.$checkMultiselectChange); oldSession.multiSelect.anchor.off("change", this.$checkMultiselectChange); } if (session) { session.multiSelect.on("addRange", this.$onAddRange); session.multiSelect.on("removeRange", this.$onRemoveRange); session.multiSelect.on("multiSelect", this.$onMultiSelect); session.multiSelect.on("singleSelect", this.$onSingleSelect); session.multiSelect.lead.on("change", this.$checkMultiselectChange); session.multiSelect.anchor.on("change", this.$checkMultiselectChange); } if (session && this.inMultiSelectMode != session.selection.inMultiSelectMode) { if (session.selection.inMultiSelectMode) this.$onMultiSelect(); else this.$onSingleSelect(); } }; function MultiSelect(editor) { if (editor.$multiselectOnSessionChange) return; editor.$onAddRange = editor.$onAddRange.bind(editor); editor.$onRemoveRange = editor.$onRemoveRange.bind(editor); editor.$onMultiSelect = editor.$onMultiSelect.bind(editor); editor.$onSingleSelect = editor.$onSingleSelect.bind(editor); editor.$multiselectOnSessionChange = exports.onSessionChange.bind(editor); editor.$checkMultiselectChange = editor.$checkMultiselectChange.bind(editor); editor.$multiselectOnSessionChange(editor); editor.on("changeSession", editor.$multiselectOnSessionChange); editor.on("mousedown", onMouseDown); editor.commands.addCommands(commands.defaultCommands); addAltCursorListeners(editor); } function addAltCursorListeners(editor){ var el = editor.textInput.getElement(); var altCursor = false; event.addListener(el, "keydown", function(e) { var altDown = e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey); if (editor.$blockSelectEnabled && altDown) { if (!altCursor) { editor.renderer.setMouseCursor("crosshair"); altCursor = true; } } else if (altCursor) { reset(); } }); event.addListener(el, "keyup", reset); event.addListener(el, "blur", reset); function reset(e) { if (altCursor) { editor.renderer.setMouseCursor(""); altCursor = false; } } } exports.MultiSelect = MultiSelect; require("./config").defineOptions(Editor.prototype, "editor", { enableMultiselect: { set: function(val) { MultiSelect(this); if (val) { this.on("changeSession", this.$multiselectOnSessionChange); this.on("mousedown", onMouseDown); } else { this.off("changeSession", this.$multiselectOnSessionChange); this.off("mousedown", onMouseDown); } }, value: true }, enableBlockSelect: { set: function(val) { this.$blockSelectEnabled = val; }, value: true } }); }); define("ace/mode/folding/fold_mode",["require","exports","module","ace/range"], function(require, exports, module) { "use strict"; var Range = require("../../range").Range; var FoldMode = exports.FoldMode = function() {}; (function() { this.foldingStartMarker = null; this.foldingStopMarker = null; this.getFoldWidget = function(session, foldStyle, row) { var line = session.getLine(row); if (this.foldingStartMarker.test(line)) return "start"; if (foldStyle == "markbeginend" && this.foldingStopMarker && this.foldingStopMarker.test(line)) return "end"; return ""; }; this.getFoldWidgetRange = function(session, foldStyle, row) { return null; }; this.indentationBlock = function(session, row, column) { var re = /\S/; var line = session.getLine(row); var startLevel = line.search(re); if (startLevel == -1) return; var startColumn = column || line.length; var maxRow = session.getLength(); var startRow = row; var endRow = row; while (++row < maxRow) { var level = session.getLine(row).search(re); if (level == -1) continue; if (level <= startLevel) break; endRow = row; } if (endRow > startRow) { var endColumn = session.getLine(endRow).length; return new Range(startRow, startColumn, endRow, endColumn); } }; this.openingBracketBlock = function(session, bracket, row, column, typeRe) { var start = {row: row, column: column + 1}; var end = session.$findClosingBracket(bracket, start, typeRe); if (!end) return; var fw = session.foldWidgets[end.row]; if (fw == null) fw = session.getFoldWidget(end.row); if (fw == "start" && end.row > start.row) { end.row --; end.column = session.getLine(end.row).length; } return Range.fromPoints(start, end); }; this.closingBracketBlock = function(session, bracket, row, column, typeRe) { var end = {row: row, column: column}; var start = session.$findOpeningBracket(bracket, end); if (!start) return; start.column++; end.column--; return Range.fromPoints(start, end); }; }).call(FoldMode.prototype); }); define("ace/theme/textmate",["require","exports","module","ace/lib/dom"], function(require, exports, module) { "use strict"; exports.isDark = false; exports.cssClass = "ace-tm"; exports.cssText = ".ace-tm .ace_gutter {\ background: #f0f0f0;\ color: #333;\ }\ .ace-tm .ace_print-margin {\ width: 1px;\ background: #e8e8e8;\ }\ .ace-tm .ace_fold {\ background-color: #6B72E6;\ }\ .ace-tm {\ background-color: #FFFFFF;\ color: black;\ }\ .ace-tm .ace_cursor {\ color: black;\ }\ .ace-tm .ace_invisible {\ color: rgb(191, 191, 191);\ }\ .ace-tm .ace_storage,\ .ace-tm .ace_keyword {\ color: blue;\ }\ .ace-tm .ace_constant {\ color: rgb(197, 6, 11);\ }\ .ace-tm .ace_constant.ace_buildin {\ color: rgb(88, 72, 246);\ }\ .ace-tm .ace_constant.ace_language {\ color: rgb(88, 92, 246);\ }\ .ace-tm .ace_constant.ace_library {\ color: rgb(6, 150, 14);\ }\ .ace-tm .ace_invalid {\ background-color: rgba(255, 0, 0, 0.1);\ color: red;\ }\ .ace-tm .ace_support.ace_function {\ color: rgb(60, 76, 114);\ }\ .ace-tm .ace_support.ace_constant {\ color: rgb(6, 150, 14);\ }\ .ace-tm .ace_support.ace_type,\ .ace-tm .ace_support.ace_class {\ color: rgb(109, 121, 222);\ }\ .ace-tm .ace_keyword.ace_operator {\ color: rgb(104, 118, 135);\ }\ .ace-tm .ace_string {\ color: rgb(3, 106, 7);\ }\ .ace-tm .ace_comment {\ color: rgb(76, 136, 107);\ }\ .ace-tm .ace_comment.ace_doc {\ color: rgb(0, 102, 255);\ }\ .ace-tm .ace_comment.ace_doc.ace_tag {\ color: rgb(128, 159, 191);\ }\ .ace-tm .ace_constant.ace_numeric {\ color: rgb(0, 0, 205);\ }\ .ace-tm .ace_variable {\ color: rgb(49, 132, 149);\ }\ .ace-tm .ace_xml-pe {\ color: rgb(104, 104, 91);\ }\ .ace-tm .ace_entity.ace_name.ace_function {\ color: #0000A2;\ }\ .ace-tm .ace_heading {\ color: rgb(12, 7, 255);\ }\ .ace-tm .ace_list {\ color:rgb(185, 6, 144);\ }\ .ace-tm .ace_meta.ace_tag {\ color:rgb(0, 22, 142);\ }\ .ace-tm .ace_string.ace_regex {\ color: rgb(255, 0, 0)\ }\ .ace-tm .ace_marker-layer .ace_selection {\ background: rgb(181, 213, 255);\ }\ .ace-tm.ace_multiselect .ace_selection.ace_start {\ box-shadow: 0 0 3px 0px white;\ }\ .ace-tm .ace_marker-layer .ace_step {\ background: rgb(252, 255, 0);\ }\ .ace-tm .ace_marker-layer .ace_stack {\ background: rgb(164, 229, 101);\ }\ .ace-tm .ace_marker-layer .ace_bracket {\ margin: -1px 0 0 -1px;\ border: 1px solid rgb(192, 192, 192);\ }\ .ace-tm .ace_marker-layer .ace_active-line {\ background: rgba(0, 0, 0, 0.07);\ }\ .ace-tm .ace_gutter-active-line {\ background-color : #dcdcdc;\ }\ .ace-tm .ace_marker-layer .ace_selected-word {\ background: rgb(250, 250, 255);\ border: 1px solid rgb(200, 200, 250);\ }\ .ace-tm .ace_indent-guide {\ background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\ }\ "; var dom = require("../lib/dom"); dom.importCssString(exports.cssText, exports.cssClass); }); define("ace/line_widgets",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/range"], function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var dom = require("./lib/dom"); var Range = require("./range").Range; function LineWidgets(session) { this.session = session; this.session.widgetManager = this; this.session.getRowLength = this.getRowLength; this.session.$getWidgetScreenLength = this.$getWidgetScreenLength; this.updateOnChange = this.updateOnChange.bind(this); this.renderWidgets = this.renderWidgets.bind(this); this.measureWidgets = this.measureWidgets.bind(this); this.session._changedWidgets = []; this.$onChangeEditor = this.$onChangeEditor.bind(this); this.session.on("change", this.updateOnChange); this.session.on("changeFold", this.updateOnFold); this.session.on("changeEditor", this.$onChangeEditor); } (function() { this.getRowLength = function(row) { var h; if (this.lineWidgets) h = this.lineWidgets[row] && this.lineWidgets[row].rowCount || 0; else h = 0; if (!this.$useWrapMode || !this.$wrapData[row]) { return 1 + h; } else { return this.$wrapData[row].length + 1 + h; } }; this.$getWidgetScreenLength = function() { var screenRows = 0; this.lineWidgets.forEach(function(w){ if (w && w.rowCount && !w.hidden) screenRows += w.rowCount; }); return screenRows; }; this.$onChangeEditor = function(e) { this.attach(e.editor); }; this.attach = function(editor) { if (editor && editor.widgetManager && editor.widgetManager != this) editor.widgetManager.detach(); if (this.editor == editor) return; this.detach(); this.editor = editor; if (editor) { editor.widgetManager = this; editor.renderer.on("beforeRender", this.measureWidgets); editor.renderer.on("afterRender", this.renderWidgets); } }; this.detach = function(e) { var editor = this.editor; if (!editor) return; this.editor = null; editor.widgetManager = null; editor.renderer.off("beforeRender", this.measureWidgets); editor.renderer.off("afterRender", this.renderWidgets); var lineWidgets = this.session.lineWidgets; lineWidgets && lineWidgets.forEach(function(w) { if (w && w.el && w.el.parentNode) { w._inDocument = false; w.el.parentNode.removeChild(w.el); } }); }; this.updateOnFold = function(e, session) { var lineWidgets = session.lineWidgets; if (!lineWidgets || !e.action) return; var fold = e.data; var start = fold.start.row; var end = fold.end.row; var hide = e.action == "add"; for (var i = start + 1; i < end; i++) { if (lineWidgets[i]) lineWidgets[i].hidden = hide; } if (lineWidgets[end]) { if (hide) { if (!lineWidgets[start]) lineWidgets[start] = lineWidgets[end]; else lineWidgets[end].hidden = hide; } else { if (lineWidgets[start] == lineWidgets[end]) lineWidgets[start] = undefined; lineWidgets[end].hidden = hide; } } }; this.updateOnChange = function(delta) { var lineWidgets = this.session.lineWidgets; if (!lineWidgets) return; var startRow = delta.start.row; var len = delta.end.row - startRow; if (len === 0) { } else if (delta.action == 'remove') { var removed = lineWidgets.splice(startRow + 1, len); removed.forEach(function(w) { w && this.removeLineWidget(w); }, this); this.$updateRows(); } else { var args = new Array(len); args.unshift(startRow, 0); lineWidgets.splice.apply(lineWidgets, args); this.$updateRows(); } }; this.$updateRows = function() { var lineWidgets = this.session.lineWidgets; if (!lineWidgets) return; var noWidgets = true; lineWidgets.forEach(function(w, i) { if (w) { noWidgets = false; w.row = i; while (w.$oldWidget) { w.$oldWidget.row = i; w = w.$oldWidget; } } }); if (noWidgets) this.session.lineWidgets = null; }; this.addLineWidget = function(w) { if (!this.session.lineWidgets) this.session.lineWidgets = new Array(this.session.getLength()); var old = this.session.lineWidgets[w.row]; if (old) { w.$oldWidget = old; if (old.el && old.el.parentNode) { old.el.parentNode.removeChild(old.el); old._inDocument = false; } } this.session.lineWidgets[w.row] = w; w.session = this.session; var renderer = this.editor.renderer; if (w.html && !w.el) { w.el = dom.createElement("div"); w.el.innerHTML = w.html; } if (w.el) { dom.addCssClass(w.el, "ace_lineWidgetContainer"); w.el.style.position = "absolute"; w.el.style.zIndex = 5; renderer.container.appendChild(w.el); w._inDocument = true; } if (!w.coverGutter) { w.el.style.zIndex = 3; } if (w.pixelHeight == null) { w.pixelHeight = w.el.offsetHeight; } if (w.rowCount == null) { w.rowCount = w.pixelHeight / renderer.layerConfig.lineHeight; } var fold = this.session.getFoldAt(w.row, 0); w.$fold = fold; if (fold) { var lineWidgets = this.session.lineWidgets; if (w.row == fold.end.row && !lineWidgets[fold.start.row]) lineWidgets[fold.start.row] = w; else w.hidden = true; } this.session._emit("changeFold", {data:{start:{row: w.row}}}); this.$updateRows(); this.renderWidgets(null, renderer); this.onWidgetChanged(w); return w; }; this.removeLineWidget = function(w) { w._inDocument = false; w.session = null; if (w.el && w.el.parentNode) w.el.parentNode.removeChild(w.el); if (w.editor && w.editor.destroy) try { w.editor.destroy(); } catch(e){} if (this.session.lineWidgets) { var w1 = this.session.lineWidgets[w.row] if (w1 == w) { this.session.lineWidgets[w.row] = w.$oldWidget; if (w.$oldWidget) this.onWidgetChanged(w.$oldWidget); } else { while (w1) { if (w1.$oldWidget == w) { w1.$oldWidget = w.$oldWidget; break; } w1 = w1.$oldWidget; } } } this.session._emit("changeFold", {data:{start:{row: w.row}}}); this.$updateRows(); }; this.getWidgetsAtRow = function(row) { var lineWidgets = this.session.lineWidgets; var w = lineWidgets && lineWidgets[row]; var list = []; while (w) { list.push(w); w = w.$oldWidget; } return list; }; this.onWidgetChanged = function(w) { this.session._changedWidgets.push(w); this.editor && this.editor.renderer.updateFull(); }; this.measureWidgets = function(e, renderer) { var changedWidgets = this.session._changedWidgets; var config = renderer.layerConfig; if (!changedWidgets || !changedWidgets.length) return; var min = Infinity; for (var i = 0; i < changedWidgets.length; i++) { var w = changedWidgets[i]; if (!w || !w.el) continue; if (w.session != this.session) continue; if (!w._inDocument) { if (this.session.lineWidgets[w.row] != w) continue; w._inDocument = true; renderer.container.appendChild(w.el); } w.h = w.el.offsetHeight; if (!w.fixedWidth) { w.w = w.el.offsetWidth; w.screenWidth = Math.ceil(w.w / config.characterWidth); } var rowCount = w.h / config.lineHeight; if (w.coverLine) { rowCount -= this.session.getRowLineCount(w.row); if (rowCount < 0) rowCount = 0; } if (w.rowCount != rowCount) { w.rowCount = rowCount; if (w.row < min) min = w.row; } } if (min != Infinity) { this.session._emit("changeFold", {data:{start:{row: min}}}); this.session.lineWidgetWidth = null; } this.session._changedWidgets = []; }; this.renderWidgets = function(e, renderer) { var config = renderer.layerConfig; var lineWidgets = this.session.lineWidgets; if (!lineWidgets) return; var first = Math.min(this.firstRow, config.firstRow); var last = Math.max(this.lastRow, config.lastRow, lineWidgets.length); while (first > 0 && !lineWidgets[first]) first--; this.firstRow = config.firstRow; this.lastRow = config.lastRow; renderer.$cursorLayer.config = config; for (var i = first; i <= last; i++) { var w = lineWidgets[i]; if (!w || !w.el) continue; if (w.hidden) { w.el.style.top = -100 - (w.pixelHeight || 0) + "px"; continue; } if (!w._inDocument) { w._inDocument = true; renderer.container.appendChild(w.el); } var top = renderer.$cursorLayer.getPixelPosition({row: i, column:0}, true).top; if (!w.coverLine) top += config.lineHeight * this.session.getRowLineCount(w.row); w.el.style.top = top - config.offset + "px"; var left = w.coverGutter ? 0 : renderer.gutterWidth; if (!w.fixedWidth) left -= renderer.scrollLeft; w.el.style.left = left + "px"; if (w.fullWidth && w.screenWidth) { w.el.style.minWidth = config.width + 2 * config.padding + "px"; } if (w.fixedWidth) { w.el.style.right = renderer.scrollBar.getWidth() + "px"; } else { w.el.style.right = ""; } } }; }).call(LineWidgets.prototype); exports.LineWidgets = LineWidgets; }); define("ace/ext/error_marker",["require","exports","module","ace/line_widgets","ace/lib/dom","ace/range"], function(require, exports, module) { "use strict"; var LineWidgets = require("../line_widgets").LineWidgets; var dom = require("../lib/dom"); var Range = require("../range").Range; function binarySearch(array, needle, comparator) { var first = 0; var last = array.length - 1; while (first <= last) { var mid = (first + last) >> 1; var c = comparator(needle, array[mid]); if (c > 0) first = mid + 1; else if (c < 0) last = mid - 1; else return mid; } return -(first + 1); } function findAnnotations(session, row, dir) { var annotations = session.getAnnotations().sort(Range.comparePoints); if (!annotations.length) return; var i = binarySearch(annotations, {row: row, column: -1}, Range.comparePoints); if (i < 0) i = -i - 1; if (i >= annotations.length) i = dir > 0 ? 0 : annotations.length - 1; else if (i === 0 && dir < 0) i = annotations.length - 1; var annotation = annotations[i]; if (!annotation || !dir) return; if (annotation.row === row) { do { annotation = annotations[i += dir]; } while (annotation && annotation.row === row); if (!annotation) return annotations.slice(); } var matched = []; row = annotation.row; do { matched[dir < 0 ? "unshift" : "push"](annotation); annotation = annotations[i += dir]; } while (annotation && annotation.row == row); return matched.length && matched; } exports.showErrorMarker = function(editor, dir) { var session = editor.session; if (!session.widgetManager) { session.widgetManager = new LineWidgets(session); session.widgetManager.attach(editor); } var pos = editor.getCursorPosition(); var row = pos.row; var oldWidget = session.widgetManager.getWidgetsAtRow(row).filter(function(w) { return w.type == "errorMarker"; })[0]; if (oldWidget) { oldWidget.destroy(); } else { row -= dir; } var annotations = findAnnotations(session, row, dir); var gutterAnno; if (annotations) { var annotation = annotations[0]; pos.column = (annotation.pos && typeof annotation.column != "number" ? annotation.pos.sc : annotation.column) || 0; pos.row = annotation.row; gutterAnno = editor.renderer.$gutterLayer.$annotations[pos.row]; } else if (oldWidget) { return; } else { gutterAnno = { text: ["Looks good!"], className: "ace_ok" }; } editor.session.unfold(pos.row); editor.selection.moveToPosition(pos); var w = { row: pos.row, fixedWidth: true, coverGutter: true, el: dom.createElement("div"), type: "errorMarker" }; var el = w.el.appendChild(dom.createElement("div")); var arrow = w.el.appendChild(dom.createElement("div")); arrow.className = "error_widget_arrow " + gutterAnno.className; var left = editor.renderer.$cursorLayer .getPixelPosition(pos).left; arrow.style.left = left + editor.renderer.gutterWidth - 5 + "px"; w.el.className = "error_widget_wrapper"; el.className = "error_widget " + gutterAnno.className; el.innerHTML = gutterAnno.text.join("
"); el.appendChild(dom.createElement("div")); var kb = function(_, hashId, keyString) { if (hashId === 0 && (keyString === "esc" || keyString === "return")) { w.destroy(); return {command: "null"}; } }; w.destroy = function() { if (editor.$mouseHandler.isMousePressed) return; editor.keyBinding.removeKeyboardHandler(kb); session.widgetManager.removeLineWidget(w); editor.off("changeSelection", w.destroy); editor.off("changeSession", w.destroy); editor.off("mouseup", w.destroy); editor.off("change", w.destroy); }; editor.keyBinding.addKeyboardHandler(kb); editor.on("changeSelection", w.destroy); editor.on("changeSession", w.destroy); editor.on("mouseup", w.destroy); editor.on("change", w.destroy); editor.session.widgetManager.addLineWidget(w); w.el.onmousedown = editor.focus.bind(editor); editor.renderer.scrollCursorIntoView(null, 0.5, {bottom: w.el.offsetHeight}); }; dom.importCssString("\ .error_widget_wrapper {\ background: inherit;\ color: inherit;\ border:none\ }\ .error_widget {\ border-top: solid 2px;\ border-bottom: solid 2px;\ margin: 5px 0;\ padding: 10px 40px;\ white-space: pre-wrap;\ }\ .error_widget.ace_error, .error_widget_arrow.ace_error{\ border-color: #ff5a5a\ }\ .error_widget.ace_warning, .error_widget_arrow.ace_warning{\ border-color: #F1D817\ }\ .error_widget.ace_info, .error_widget_arrow.ace_info{\ border-color: #5a5a5a\ }\ .error_widget.ace_ok, .error_widget_arrow.ace_ok{\ border-color: #5aaa5a\ }\ .error_widget_arrow {\ position: absolute;\ border: solid 5px;\ border-top-color: transparent!important;\ border-right-color: transparent!important;\ border-left-color: transparent!important;\ top: -5px;\ }\ ", ""); }); define("ace/ace",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/dom","ace/lib/event","ace/editor","ace/edit_session","ace/undomanager","ace/virtual_renderer","ace/worker/worker_client","ace/keyboard/hash_handler","ace/placeholder","ace/multi_select","ace/mode/folding/fold_mode","ace/theme/textmate","ace/ext/error_marker","ace/config"], function(require, exports, module) { "use strict"; require("./lib/fixoldbrowsers"); var dom = require("./lib/dom"); var event = require("./lib/event"); var Editor = require("./editor").Editor; var EditSession = require("./edit_session").EditSession; var UndoManager = require("./undomanager").UndoManager; var Renderer = require("./virtual_renderer").VirtualRenderer; require("./worker/worker_client"); require("./keyboard/hash_handler"); require("./placeholder"); require("./multi_select"); require("./mode/folding/fold_mode"); require("./theme/textmate"); require("./ext/error_marker"); exports.config = require("./config"); exports.require = require; if (typeof define === "function") exports.define = define; exports.edit = function(el) { if (typeof el == "string") { var _id = el; el = document.getElementById(_id); if (!el) throw new Error("ace.edit can't find div #" + _id); } if (el && el.env && el.env.editor instanceof Editor) return el.env.editor; var value = ""; if (el && /input|textarea/i.test(el.tagName)) { var oldNode = el; value = oldNode.value; el = dom.createElement("pre"); oldNode.parentNode.replaceChild(el, oldNode); } else if (el) { value = dom.getInnerText(el); el.innerHTML = ""; } var doc = exports.createEditSession(value); var editor = new Editor(new Renderer(el)); editor.setSession(doc); var env = { document: doc, editor: editor, onResize: editor.resize.bind(editor, null) }; if (oldNode) env.textarea = oldNode; event.addListener(window, "resize", env.onResize); editor.on("destroy", function() { event.removeListener(window, "resize", env.onResize); env.editor.container.env = null; // prevent memory leak on old ie }); editor.container.env = editor.env = env; return editor; }; exports.createEditSession = function(text, mode) { var doc = new EditSession(text, mode); doc.setUndoManager(new UndoManager()); return doc; } exports.EditSession = EditSession; exports.UndoManager = UndoManager; exports.version = "1.2.6"; }); (function() { window.require(["ace/ace"], function(a) { if (a) { a.config.init(true); a.define = window.define; } if (!window.ace) window.ace = a; for (var key in a) if (a.hasOwnProperty(key)) window.ace[key] = a[key]; }); })(); ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5766385 qutebrowser-3.6.1/tests/end2end/data/hints/angular1/0000755000175100017510000000000015102145351021777 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/angular1/angular.min.js0000644000175100017510000050633315102145205024560 0ustar00runnerrunner/* AngularJS v1.6.4 (c) 2010-2017 Google, Inc. http://angularjs.org License: MIT */ (function(x){'use strict';function L(a,b){b=b||Error;return function(){var d=arguments[0],c;c="["+(a?a+":":"")+d+"] http://errors.angularjs.org/1.6.4/"+(a?a+"/":"")+d;for(d=1;dc)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f").append(a).html();try{return a[0].nodeType===Ia?Q(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+Q(b)})}catch(c){return Q(d)}}function Qc(a){try{return decodeURIComponent(a)}catch(b){}}function Rc(a){var b={};q((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Qc(e),u(e)&&(f= u(f)?Qc(f):!0,ua.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Zb(a){var b=[];q(a,function(a,c){H(a)?q(a,function(a){b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))}):b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))});return b.length?b.join("&"):""}function db(a){return $(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function $(a,b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g, b?"%20":"+")}function te(a,b){var d,c,e=Ja.length;for(c=0;c protocol indicates an extension, document.location.href does not match."))} function Sc(a,b,d){C(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=B(a);if(a.injector()){var c=a[0]===x.document?"document":xa(a);throw Fa("btstrpd",c.replace(//,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=eb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector", d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;x&&e.test(x.name)&&(d.debugInfoEnabled=!0,x.name=x.name.replace(e,""));if(x&&!f.test(x.name))return c();x.name=x.name.replace(f,"");ea.resumeBootstrap=function(a){q(a,function(a){b.push(a)});return c()};D(ea.resumeDeferredBootstrap)&&ea.resumeDeferredBootstrap()}function we(){x.name="NG_ENABLE_DEBUG_INFO!"+x.name;x.location.reload()}function xe(a){a=ea.element(a).injector();if(!a)throw Fa("test");return a.get("$$testability")} function Tc(a,b){b=b||"_";return a.replace(ye,function(a,c){return(c?b:"")+a.toLowerCase()})}function ze(){var a;if(!Uc){var b=rb();(na=w(b)?x.jQuery:b?x[b]:void 0)&&na.fn.on?(B=na,S(na.fn,{scope:Na.scope,isolateScope:Na.isolateScope,controller:Na.controller,injector:Na.injector,inheritedData:Na.inheritedData}),a=na.cleanData,na.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=na._data(f,"events"))&&c.$destroy&&na(f).triggerHandler("$destroy");a(b)}):B=W;ea.element=B;Uc=!0}}function fb(a, b,d){if(!a)throw Fa("areq",b||"?",d||"required");return a}function sb(a,b,d){d&&H(a)&&(a=a[a.length-1]);fb(D(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ka(a,b){if("hasOwnProperty"===a)throw Fa("badname",b);}function Vc(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g")+c[2];for(c=c[0];c--;)d=d.lastChild;f=ab(f,d.childNodes); d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";q(f,function(a){e.appendChild(a)});return e}function W(a){if(a instanceof W)return a;var b;F(a)&&(a=T(a),b=!0);if(!(this instanceof W)){if(b&&"<"!==a.charAt(0))throw dc("nosel");return new W(a)}if(b){b=x.document;var d;a=(d=dg.exec(a))?[b.createElement(d[1])]:(d=dd(a,b))?d.childNodes:[];ec(this,a)}else D(a)?ed(a):ec(this,a)}function fc(a){return a.cloneNode(!0)}function xb(a,b){!b&&bc(a)&&B.cleanData([a]); a.querySelectorAll&&B.cleanData(a.querySelectorAll("*"))}function fd(a,b,d,c){if(u(c))throw dc("offargs");var e=(c=yb(a))&&c.events,f=c&&c.handle;if(f)if(b){var g=function(b){var c=e[b];u(d)&&$a(c||[],d);u(d)&&c&&0l&&this.remove(p.key);return b}},get:function(a){if(l";b=ta.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function La(a, b){try{a.addClass(b)}catch(c){}}function ca(a,b,c,d,e){a instanceof B||(a=B(a));var f=Ma(a,b,a,c,d,e);ca.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw fa("multilink");fb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==wa(d)&&ma.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?B(ha(g,B("
").append(a).html())): c?Na.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);ca.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Ma(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,n,p,r;if(K)for(r=Array(c.length),m=0;my.priority)break;if(x=y.scope)y.templateUrl||(C(x)?($("new/isolated scope",E||K,y,v),E=y):$("new/isolated scope",E,y,v)),K=K||y;P=y.name;if(!u&&(y.replace&&(y.templateUrl||y.template)||y.transclude&&!y.$$tlb)){for(x=z+1;u=a[x++];)if(u.transclude&&!u.$$tlb||u.replace&&(u.templateUrl||u.template)){La=!0;break}u=!0}!y.templateUrl&& y.controller&&(G=G||V(),$("'"+P+"' controller",G[P],y,v),G[P]=y);if(x=y.transclude)if(J=!0,y.$$tlb||($("transclusion",t,y,v),t=y),"element"===x)X=!0,p=y.priority,N=v,v=d.$$element=B(ca.$$createComment(P,d[P])),b=v[0],ka(f,va.call(N,0),b),N[0].$$parentNode=N[0].parentNode,A=kc(La,N,e,p,g&&g.name,{nonTlbTranscludeDirective:t});else{var ja=V();if(C(x)){N=[];var Q=V(),jb=V();q(x,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Q[a]=b;ja[b]=null;jb[b]=c});q(v.contents(),function(a){var b=Q[Ba(wa(a))]; b?(jb[b]=!0,ja[b]=ja[b]||[],ja[b].push(a)):N.push(a)});q(jb,function(a,b){if(!a)throw fa("reqslot",b);});for(var ic in ja)ja[ic]&&(ja[ic]=kc(La,ja[ic],e))}else N=B(fc(b)).contents();v.empty();A=kc(La,N,e,void 0,void 0,{needsNewScope:y.$$isolateScope||y.$$newScope});A.$$slots=ja}if(y.template)if(O=!0,$("template",I,y,v),I=y,x=D(y.template)?y.template(v,d):y.template,x=Ea(x),y.replace){g=y;N=cc.test(x)?pd(ha(y.templateNamespace,T(x))):[];b=N[0];if(1!==N.length||1!==b.nodeType)throw fa("tplrt",P,""); ka(f,v,b);F={$attr:{}};x=jc(b,[],F);var Y=a.splice(z+1,a.length-(z+1));(E||K)&&aa(x,E,K);a=a.concat(x).concat(Y);da(d,F);F=a.length}else v.html(x);if(y.templateUrl)O=!0,$("template",I,y,v),I=y,y.replace&&(g=y),n=ga(a.splice(z,a.length-z),v,d,f,J&&A,h,k,{controllerDirectives:G,newScopeDirective:K!==y&&K,newIsolateScopeDirective:E,templateDirective:I,nonTlbTranscludeDirective:t}),F=a.length;else if(y.compile)try{R=y.compile(v,d,A);var Z=y.$$originalDirective||y;D(R)?m(null,bb(Z,R),Ma,L):R&&m(bb(Z,R.pre), bb(Z,R.post),Ma,L)}catch(ea){c(ea,xa(v))}y.terminal&&(n.terminal=!0,p=Math.max(p,y.priority))}n.scope=K&&!0===K.scope;n.transcludeOnThisElement=J;n.templateOnThisElement=O;n.transclude=A;l.hasElementTranscludeDirective=X;return n}function U(a,b,c,d){var e;if(F(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e=g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw fa("ctreq",b,a);}else if(H(b))for(e= [],g=0,f=b.length;gc.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=Vb(c,{$$start:k,$$end:l}));if(!c.$$bindings){var K=m=c,r=c.name,t={isolateScope:null,bindToController:null};C(K.scope)&&(!0===K.bindToController?(t.bindToController=d(K.scope,r,!0),t.isolateScope={}):t.isolateScope=d(K.scope,r,!1));C(K.bindToController)&&(t.bindToController=d(K.bindToController,r,!0));if(t.bindToController&&!K.controller)throw fa("noctrl", r);m=m.$$bindings=t;C(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function Z(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d"+b+"";return c.childNodes[0].childNodes;default:return b}}function oa(a,b){if("srcdoc"===b)return y.HTML;var c=wa(a);if("src"===b||"ngSrc"===b){if(-1===["img","video","audio","source","track"].indexOf(c))return y.RESOURCE_URL}else if("xlinkHref"===b||"form"===c&&"action"===b||"link"===c&&"href"===b)return y.RESOURCE_URL}function pa(a, c,d,e,f){var g=oa(a,e),h=k[e]||f,l=b(d,!f,g,h);if(l){if("multiple"===e&&"select"===wa(a))throw fa("selmulti",xa(a));if(m.test(e))throw fa("nodomevents");c.push({priority:100,compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=V());var k=f[e];k!==d&&(l=k&&b(k,!0,g,h),d=k);l&&(f[e]=l(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(l,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function ka(a,b,c){var d=b[0],e= b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Ia&&""===d.nodeValue.trim())&&sg.call(a,b,1)}return a}function qg(a,b){if(b&&F(b))return b;if(F(a)){var d=sd.exec(a);if(d)return d[3]}}function wf(){var a={},b=!1;this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,c){Ka(b,"controller");C(b)? S(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector","$window",function(d,c){function e(a,b,c,d){if(!a||!C(a.$scope))throw L("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,n;h=!0===h;k&&F(k)&&(n=k);if(F(f)){k=f.match(sd);if(!k)throw td("ctrlfmt",f);m=k[1];n=n||k[3];f=a.hasOwnProperty(m)?a[m]:Vc(g.$scope,m,!0)||(b?Vc(c,m,!0):void 0);if(!f)throw td("ctrlreg",m);sb(f,m,!0)}if(h)return h=(H(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),n&&e(g,n, l,m||f.name),S(function(){var a=d.invoke(f,l,g,m);a!==l&&(C(a)||D(a))&&(l=a,n&&e(g,n,l,m||f.name));return l},{instance:l,identifier:n});l=d.instantiate(f,g,m);n&&e(g,n,l,m||f.name);return l}}]}function xf(){this.$get=["$window",function(a){return B(a.document)}]}function yf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden;a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function zf(){this.$get= ["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function mc(a){return C(a)?ga(a)?a.toISOString():cb(a):a}function Ef(){this.$get=function(){return function(a){if(!a)return"";var b=[];Kc(a,function(a,c){null===a||w(a)||(H(a)?q(a,function(a){b.push($(c)+"="+$(mc(a)))}):b.push($(c)+"="+$(mc(a))))});return b.join("&")}}}function Ff(){this.$get=function(){return function(a){function b(a,e,f){null===a||w(a)||(H(a)?q(a,function(a,c){b(a,e+"["+(C(a)?c:"")+"]")}):C(a)&&!ga(a)?Kc(a,function(a, c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):d.push($(e)+"="+$(mc(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function nc(a,b){if(F(a)){var d=a.replace(tg,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf(ud))||(c=(c=d.match(ug))&&vg[c[0]].test(d));if(c)try{a=Oc(d)}catch(e){throw oc("baddata",a,e);}}}return a}function vd(a){var b=V(),d;F(a)?q(a.split("\n"),function(a){d=a.indexOf(":");var e=Q(T(a.substr(0,d)));a=T(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):C(a)&&q(a,function(a, d){var f=Q(d),g=T(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function wd(a){var b;return function(d){b||(b=vd(a));return d?(d=b[Q(d)],void 0===d&&(d=null),d):b}}function xd(a,b,d,c){if(D(c))return c(a,b,d);q(c,function(c){a=c(a,b,d)});return a}function Df(){var a=this.defaults={transformResponse:[nc],transformRequest:[function(a){return C(a)&&"[object File]"!==ma.call(a)&&"[object Blob]"!==ma.call(a)&&"[object FormData]"!==ma.call(a)?cb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"}, post:pa(pc),put:pa(pc),patch:pa(pc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return u(a)?(b=!!a,this):b};var d=this.interceptors=[];this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(c,e,f,g,h,k,l,m){function n(b){function d(a,b){for(var c=0,e=b.length;ca?b:k.reject(b)}if(!C(b))throw L("$http")("badreq",b);if(!F(m.valueOf(b.url)))throw L("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers= function(b){var c=a.headers,d=S({},b.headers),f,g,h,c=S({},c.common,c[Q(b.method)]);a:for(f in c){g=Q(f);for(h in d)if(Q(h)===g)continue a;d[f]=c[f]}return e(d,pa(b))}(b);g.method=ub(g.method);g.paramSerializer=F(g.paramSerializer)?l.get(g.paramSerializer):g.paramSerializer;c.$$incOutstandingRequestCount();var h=[],n=[];b=k.resolve(g);q(t,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&n.push(a.response,a.responseError)});b=d(b,h);b=b.then(function(b){var c= b.headers,d=xd(b.data,wd(c),void 0,b.transformRequest);w(d)&&q(c,function(a,b){"content-type"===Q(b)&&delete c[b]});w(b.withCredentials)&&!w(a.withCredentials)&&(b.withCredentials=a.withCredentials);return p(b,d).then(f,f)});b=d(b,n);return b=b.finally(function(){c.$$completeOutstandingRequest(z)})}function p(c,d){function g(a){if(a){var c={};q(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function l(a,c,d,e){function f(){p(c,a,d,e)}O&& (200<=a&&300>a?O.put(R,[a,c,vd(d),e]):O.remove(R));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function p(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?G.resolve:G.reject)({data:a,status:b,headers:wd(d),config:c,statusText:e})}function K(a){p(a.data,a.status,pa(a.headers()),a.statusText)}function t(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var G=k.defer(),y=G.promise,O,X,P=c.headers,s="jsonp"===Q(c.method),R=c.url;s?R=m.getTrustedResourceUrl(R):F(R)||(R=m.valueOf(R));R=r(R, c.paramSerializer(c.params));s&&(R=J(R,c.jsonpCallbackParam));n.pendingRequests.push(c);y.then(t,t);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(O=C(c.cache)?c.cache:C(a.cache)?a.cache:v);O&&(X=O.get(R),u(X)?X&&D(X.then)?X.then(K,K):H(X)?p(X[1],X[0],pa(X[2]),X[3]):p(X,200,{},"OK"):O.put(R,y));w(X)&&((X=yd(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(P[c.xsrfHeaderName||a.xsrfHeaderName]=X),e(c.method,R,d,l,P,c.timeout,c.withCredentials,c.responseType,g(c.eventHandlers), g(c.uploadEventHandlers)));return y}function r(a,b){0=l&&(q.resolve(t),v(A.$$intervalId),delete g[A.$$intervalId]);M||a.$apply()},k);g[A.$$intervalId]=q;return A}var g={};f.cancel=function(a){return a&&a.$$intervalId in g?(g[a.$$intervalId].promise.catch(z),g[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),delete g[a.$$intervalId],!0):!1};return f}]}function qc(a){a=a.split("/");for(var b=a.length;b--;)a[b]= db(a[b]);return a.join("/")}function zd(a,b){var d=Ca(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=Z(d.port)||xg[d.protocol]||null}function Ad(a,b){if(yg.test(a))throw kb("badpath",a);var d="/"!==a.charAt(0);d&&(a="/"+a);var c=Ca(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=Rc(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function rc(a,b){return a.slice(0,b.length)=== b}function ka(a,b){if(rc(b,a))return b.substr(a.length)}function Aa(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function lb(a){return a.replace(/(#.+)|#$/,"$1")}function sc(a,b,d){this.$$html5=!0;d=d||"";zd(a,this);this.$$parse=function(a){var d=ka(b,a);if(!F(d))throw kb("ipthprfx",a,b);Ad(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Zb(this.$$search),d=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(a?"?"+a:"")+d;this.$$absUrl=b+ this.$$url.substr(1);this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;u(f=ka(a,c))?(g=f,g=d&&u(f=ka(d,f))?b+(ka("/",f)||f):a+g):u(f=ka(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function tc(a,b,d){zd(a,this);this.$$parse=function(c){var e=ka(a,c)||ka(b,c),f;w(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",w(e)&&(a=c,this.replace())):(f=ka(d,e),w(f)&&(f=e));Ad(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;rc(f, e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url?d+this.$$url:"");this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(b,d){return Aa(a)===Aa(b)?(this.$$parse(b),!0):!1}}function Bd(a,b,d){this.$$html5=!0;tc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)), !0;var f,g;a===Aa(c)?f=c:(g=ka(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url;this.$$urlUpdatedByLocation=!0}}function Jb(a){return function(){return this[a]}}function Cd(a,b){return function(d){if(w(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Jf(){var a="!",b={enabled:!1,requireBase:!0,rewriteLinks:!0}; this.hashPrefix=function(b){return u(b)?(a=b,this):a};this.html5Mode=function(a){if(Ha(a))return b.enabled=a,this;if(C(a)){Ha(a.enabled)&&(b.enabled=a.enabled);Ha(a.requireBase)&&(b.requireBase=a.requireBase);if(Ha(a.rewriteLinks)||F(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state= f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,b)}var l,m;m=c.baseHref();var n=c.url(),p;if(b.enabled){if(!m&&b.requireBase)throw kb("nobase");p=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(m||"/");m=e.history?sc:Bd}else p=Aa(n),m=tc;var r=p.substr(0,Aa(p).lastIndexOf("/")+1);l=new m(p,r,"#"+a);l.$$parseLinkUrl(n,n);l.$$state=c.state();var J=/^\s*(javascript|mailto):/i;f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&& 2!==a.which&&2!==a.button){for(var h=B(a.target);"a"!==wa(h[0]);)if(h[0]===f[0]||!(h=h.parent())[0])return;if(!F(e)||!w(h.attr(e))){var e=h.prop("href"),k=h.attr("href")||h.attr("xlink:href");C(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=Ca(e.animVal).href);J.test(e)||!e||h.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(e,k)||(a.preventDefault(),l.absUrl()!==c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]=!0))}}});lb(l.absUrl())!==lb(n)&&c.url(l.absUrl(),!0);var v=!0; c.onUrlChange(function(a,b){rc(a,r)?(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=lb(a);l.$$parse(a);l.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(v=!1,k(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(v||l.$$urlUpdatedByLocation){l.$$urlUpdatedByLocation=!1;var a=lb(c.url()),b=lb(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(v||m)v= !1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:l.$$state),k(a,f)))})}l.$$replace=!1});return l}]}function Kf(){var a=!0,b=this;this.debugEnabled=function(b){return u(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&& (a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||z;a=!1;try{a=!!e.apply}catch(f){}return a?function(){var a=[];q(arguments,function(b){a.push(c(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}var f=za||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function zg(a){return a+""}function Ag(a, b){return"undefined"!==typeof a?a:b}function Dd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function U(a,b){var d,c,e;switch(a.type){case s.Program:d=!0;q(a.body,function(a){U(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:U(a.argument,b);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant; a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:U(a.test,b);U(a.alternate,b);U(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant=!1;a.toWatch=[a];break;case s.MemberExpression:U(a.object,b);a.computed&&U(a.property,b);a.constant=a.object.constant&& (!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=e=a.filter?!b(a.callee.name).$stateful:!1;c=[];q(a.arguments,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=e?c:[a];break;case s.AssignmentExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];q(a.elements,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant= d;a.toWatch=c;break;case s.ObjectExpression:d=!0;c=[];q(a.properties,function(a){U(a.value,b);d=d&&a.value.constant&&!a.computed;a.value.constant||c.push.apply(c,a.value.toWatch);a.computed&&(U(a.key,b),a.key.constant||c.push.apply(c,a.key.toWatch))});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1;a.toWatch=[];break;case s.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Ed(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Fd(a){return a.type=== s.Identifier||a.type===s.MemberExpression}function Gd(a){if(1===a.body.length&&Fd(a.body[0].expression))return{type:s.AssignmentExpression,left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function Hd(a){this.$filter=a}function Id(a){this.$filter=a}function uc(a,b,d){this.ast=new s(a,d);this.astCompiler=d.csp?new Id(b):new Hd(b)}function vc(a){return D(a.valueOf)?a.valueOf():Bg.call(a)}function Lf(){var a=V(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral= function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=vc(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function g(a,b,c,d,e){var g=d.inputs,h;if(1===g.length){var k=f,g=g[0];return a.$watch(function(a){var b=g(a);f(b,k,d.literal)||(h=d(a,void 0,void 0,[b]),k=b&&vc(b));return h},b,c,e)}for(var l=[],m=[],n=0,E=g.length;n=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;fa)for(b in l++,f)ua.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1t&&(w=4-t,u[w]||(u[w]=[]),u[w].push({msg:D(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:k}));else if(a===c){r=!1;break a}}catch(B){f(B)}if(!(p=q.$$watchersCount&&q.$$childHead||q!==this&&q.$$nextSibling))for(;q!==this&&!(p=q.$$nextSibling);)q=q.$parent}while(q=p);if((r||s.length)&&!t--)throw M.$$phase= null,d("infdig",b,u);}while(r||s.length);for(M.$$phase=null;Iza)throw ta("iequirks");var c=pa(oa);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ya);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs, f=c.getTrusted,g=c.trustAs;q(oa,function(a,b){var d=Q(b);c[("parse_as_"+d).replace(xc,gb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(xc,gb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(xc,gb)]=function(b){return g(a,b)}});return c}]}function Rf(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=Z((/android (\d+)/.exec(Q((a.navigator|| {}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},h=g.body&&g.body.style,k=!1,l=!1;h&&(k=!!("transition"in h||"webkitTransition"in h),l=!!("animation"in h||"webkitAnimation"in h));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&za)return!1;if(w(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ga(),transitions:k,animations:l,android:e}}]}function Tf(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler", "$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(h,k){g.totalPendingRequests++;if(!F(h)||w(d.get(h)))h=f.getTrustedResourceUrl(h);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==nc}):l===nc&&(l=null);return c.get(h,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){d.put(h,a.data);return a.data},function(a){k||(a=Dg("tpload",h,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests= 0;return g}]}function Uf(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];q(a,function(a){var c=ea.element(a).data("$binding");c&&q(c,function(c){d?(new RegExp("(^|\\s)"+Kd(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;hc&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===zc;e++); if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===zc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Ud&&(d=d.splice(0,Ud-1),b=c-1,c=1);return{d:d,e:b,i:c}}function Lg(a,b,d,c){var e=a.d,f=e.length-a.i;b=w(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d- 1]++;for(;fh;)k.unshift(0),h++;0=b.lgSize&&h.unshift(k.splice(-b.lgSize,k.length).join(""));k.length> b.gSize;)h.unshift(k.splice(-b.gSize,k.length).join(""));k.length&&h.unshift(k.join(""));k=h.join(d);f.length&&(k+=c+f.join(""));e&&(k+="e+"+e)}return 0>a&&!g?b.negPre+k+b.negSuf:b.posPre+k+b.posSuf}function Kb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length-d)f+=d;0===f&&-12===d&&(f=12);return Kb(f,b,c,e)}}function mb(a,b,d){return function(c,e){var f= c["get"+a](),g=ub((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Vd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Wd(a){return function(b){var d=Vd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Kb(b,a)}}function Ac(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Pd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear, k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Z(b[9]+b[10]),g=Z(b[9]+b[11]));h.call(a,Z(b[1]),Z(b[2])-1,Z(b[3]));f=Z(b[4]||0)-f;g=Z(b[5]||0)-g;h=Z(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",h=[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;F(c)&&(c=Mg.test(c)?Z(c):b(c));ba(c)&&(c=new Date(c));if(!ga(c)||!isFinite(c.getTime()))return c; for(;d;)(l=Ng.exec(d))?(h=ab(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=Pc(f,m),c=Yb(c,f,!0));q(h,function(b){k=Og[b];g+=k?k(c,a.DATETIME_FORMATS,m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Fg(){return function(a,b){w(b)&&(b=2);return cb(a,b)}}function Gg(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):Z(b);if(da(b))return a;ba(a)&&(a=a.toString());if(!qa(a))return a;d=!d||isNaN(d)?0:Z(d);d=0>d?Math.max(0,a.length+ d):d;return 0<=b?Bc(a,d,d+b):0===d?Bc(a,b,a.length):Bc(a,Math.max(0,d+b),d)}}function Bc(a,b,d){return F(a)?a.slice(b,d):va.call(a,b,d)}function Rd(a){function b(b){return b.map(function(b){var c=1,d=Ya;if(D(b))d=b;else if(F(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}} function c(a,b){var c=0,d=a.type,k=b.type;if(d===k){var k=a.value,l=b.value;"string"===d?(k=k.toLowerCase(),l=l.toLowerCase()):"object"===d&&(C(k)&&(k=a.index),C(l)&&(l=b.index));k!==l&&(c=kb||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut", m)}b.on("change",l);if(ae[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!k){var b=this.validity,c=b.badInput,d=b.typeMismatch;k=f.defer(function(){k=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Nb(a,b){return function(d,c){var e,f;if(ga(d))return d;if(F(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(Pg.test(d))return new Date(d); a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},q(e,function(a,c){c=v};g.$observe("min",function(a){v=p(a);h.$validate()})}if(u(g.max)||g.ngMax){var t; h.$validators.max=function(a){return!n(a)||w(t)||d(a)<=t};g.$observe("max",function(a){t=p(a);h.$validate()})}}}function Dc(a,b,d,c){(c.$$hasNativeValidators=C(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{};return c.badInput||c.typeMismatch?void 0:a})}function be(a){a.$$parserName="number";a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(Qg.test(b))return parseFloat(b)});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!ba(b))throw pb("numfmt",b);b=b.toString()}return b})} function Sa(a){u(a)&&!ba(a)&&(a=parseFloat(a));return da(a)?void 0:a}function Ec(a){var b=a.toString(),d=b.indexOf(".");return-1===d?-1a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ce(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Ec(a):0,h=e?Ec(b):0,k=f?Ec(d):0,g=Math.max(g,h,k),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function de(a,b,d,c,e){if(u(c)){a=a(c);if(!a.constant)throw pb("constexpr", d,c);return a(b)}return e}function Fc(a,b){function d(a,b){if(!a||!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d(?:<\/\1>|)$/, cc=/<|&#?\w+;/,bg=/<([\w:-]+)/,cg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ha={option:[1,'"],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ha.optgroup=ha.option;ha.tbody=ha.tfoot=ha.colgroup=ha.caption=ha.thead;ha.th=ha.td;var jg=x.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)& 16)},Na=W.prototype={ready:ed,toString:function(){var a=[];q(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?B(this[a]):B(this[this.length+a])},length:0,push:Tg,sort:[].sort,splice:[].splice},Fb={};q("multiple selected checked disabled readOnly required open".split(" "),function(a){Fb[Q(a)]=a});var jd={};q("input select option textarea button form details".split(" "),function(a){jd[a]=!0});var rd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max", ngPattern:"pattern",ngStep:"step"};q({data:hc,removeData:gc,hasData:function(a){for(var b in hb[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b/,mg=/^[^(]*\(\s*([^)]*)\)/m,Wg=/,/,Xg=/^\s*(_?)(\S+?)\1\s*$/,kg=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,ya=L("$injector");eb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw F(d)&&d||(d=a.name||ng(a)),ya("strictdi",d);b=ld(a);q(b[1].split(Wg),function(a){a.replace(Xg,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,sb(a[b],"fn"),c=a.slice(0,b)):sb(a,"fn", !0);return c};var fe=L("$animate"),qf=function(){this.$get=z},rf=function(){var a=new Gb,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=F(b)?b.split(" "):H(b)?b:[],q(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){q(b,function(b){var c=a.get(b);if(c){var d=og(b.attr("class")),e="",f="";q(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});q(b,function(a){e&&Cb(a,e);f&&Bb(a,f)});a.delete(b)}});b.length=0}return{enabled:z, on:z,off:z,pin:z,push:function(g,h,k,l){l&&l();k=k||{};k.from&&g.css(k.from);k.to&&g.css(k.to);if(k.addClass||k.removeClass)if(h=k.addClass,l=k.removeClass,k=a.get(g)||{},h=e(k,h,!0),l=e(k,l,!1),h||l)a.set(g,k),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},of=["$provide",function(a){var b=this,d=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw fe("notcsel",c);var f=c+"-animation";b.$$registeredAnimations[c.substr(1)]= f;a.factory(f,d)};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,fe("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e <= >= && || ! = |".split(" "),function(a){Qb[a]=!0});var $g={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},wc=function(a){this.options=a};wc.prototype={constructor:wc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue? this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"=== a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=u(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ua("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index","<=",">=");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a}, unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=ra(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:s.Literal,value:this.options.literals[this.consume().text]}: this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE"); return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}}, arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:s.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")? (this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");return{type:s.ObjectExpression,properties:a}},throwError:function(a,b){throw Ua("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ua("ueoe", this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ua("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:s.ThisExpression}, $locals:{type:s.LocalsExpression}}};Hd.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};U(a,b.$filter);var d="",c;this.stage="assign";if(c=Gd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Ed(a.body);b.stage="inputs";q(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var h=b.nextId(); b.recurse(a,h);b.return_(h);b.state.inputs.push(d);a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,zg,Ag,Dd);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;q(b,function(b){a.push("var "+ b+"="+d.generateFunction(b,"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;q(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")}, recurse:function(a,b,d,c,e,f){var g,h,k=this,l,m,n;c=c||z;if(!f&&u(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case s.Program:q(a.body,function(b,c){k.recurse(b.expression,void 0,void 0,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case s.UnaryExpression:this.recurse(a.argument,void 0, void 0,function(a){h=a});m=a.operator+"("+this.ifDefined(h,0)+")";this.assign(b,m);c(m);break;case s.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){h=a});m="+"===a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right, b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test,b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.isNull(k.nonComputedMember("s",a.name)), k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s",a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));c(b);break;case s.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,void 0,function(){k.if_(k.notNull(g),function(){a.computed?(h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,h),"{}")),m=k.computedMember(g,h),k.assign(b, m),d&&(d.computed=!0,d.name=h)):(e&&1!==e&&k.if_(k.isNull(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}")),m=k.nonComputedMember(g,a.property.name),k.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();a.filter?(h=k.filter(a.callee.name),l=[],q(a.arguments,function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+l.join(",")+")",k.assign(b,m),c(b)): (h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){q(a.arguments,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})});m=g.name?k.member(g.context,g.name,g.computed)+"("+l.join(",")+")":h+"("+l.join(",")+")";k.assign(b,m)},function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=this.nextId();g={};this.recurse(a.left,void 0,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);m=k.member(g.context, g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=[];q(a.elements,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case s.ObjectExpression:l=[];n=!1;q(a.properties,function(a){a.computed&&(n=!0)});n?(b=b||this.nextId(),this.assign(b,"{}"),q(a.properties,function(a){a.computed?(g=k.nextId(),k.recurse(a.key,g)):g=a.key.type===s.Identifier?a.key.name:""+a.key.value;h=k.nextId(); k.recurse(a.value,h);k.assign(k.member(b,g,a.computed),h)})):(q(a.properties,function(b){k.recurse(b.value,a.constant?void 0:k.nextId(),void 0,function(a){l.push(k.escape(b.key.type===s.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case s.ThisExpression:this.assign(b,"s");c(b||"s");break;case s.LocalsExpression:this.assign(b,"l");c(b||"l");break;case s.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+ b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a, b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a, b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(F(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(ba(a))return a.toString();if(!0===a)return"true";if(!1=== a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ua("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Id.prototype={compile:function(a){var b=this;U(a,b.$filter);var d,c;if(d=Gd(a))c=this.recurse(d);d=Ed(a.body);var e;d&&(e=[],q(d,function(a,c){var d=b.recurse(a);a.input=d;e.push(d);a.watchId=c}));var f=[];q(a.body,function(a){f.push(b.recurse(a.expression))}); a=0===a.body.length?z:1===a.body.length?f[0]:function(a,b){var c;q(f,function(d){c=d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+ a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return f.identifier(a.name,b,d);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d):this.nonComputedMember(c, e,b,d);case s.CallExpression:return g=[],q(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var n=[],p=0;p":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}: c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c=e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]= {});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,h){var k=a(e,f,g,h),l,m;null!=k&&(l=b(e,f,g,h),l+="",c&&1!==c&&k&&!k[l]&&(k[l]={}),m=k[l]);return d?{context:k,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};uc.prototype= {constructor:uc,parse:function(a){a=this.ast.ast(a);var b=this.astCompiler.compile(a);b.literal=0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression);b.constant=a.constant;return b}};var ta=L("$sce"),oa={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},xc=/_([a-z])/g,Dg=L("$compile"),aa=x.document.createElement("a"),Md=Ca(x.location.href);Nd.$inject=["$document"]; cd.$inject=["$provide"];var Ud=22,Td=".",zc="0";Od.$inject=["$locale"];Qd.$inject=["$locale"];var Og={yyyy:Y("FullYear",4,0,!1,!0),yy:Y("FullYear",2,0,!0,!0),y:Y("FullYear",1,0,!1,!0),MMMM:mb("Month"),MMM:mb("Month",!0),MM:Y("Month",2,1),M:Y("Month",1,1),LLLL:mb("Month",!1,!0),dd:Y("Date",2),d:Y("Date",1),HH:Y("Hours",2),H:Y("Hours",1),hh:Y("Hours",2,-12),h:Y("Hours",1,-12),mm:Y("Minutes",2),m:Y("Minutes",1),ss:Y("Seconds",2),s:Y("Seconds",1),sss:Y("Milliseconds",3),EEEE:mb("Day"),EEE:mb("Day",!0), a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Kb(Math[0=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},Ng=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,Mg=/^-?\d+$/;Pd.$inject=["$locale"];var Hg=la(Q),Ig=la(ub);Rd.$inject=["$parse"];var Fe=la({restrict:"E",compile:function(a, b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===ma.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),vb={};q(Fb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=Ba("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});vb[c]=function(){return{restrict:"A",priority:100,link:e}}}});q(rd,function(a,b){vb[b]= function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(Sg))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});q(["src","srcset","href"],function(a){var b=Ba("ng-"+a);vb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===ma.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b), za&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}});var Mb={$addControl:z,$$renameControl:function(a,b){a.$name=b},$removeControl:z,$setValidity:z,$setDirty:z,$setPristine:z,$setSubmitted:z};Lb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Lb.prototype={$rollbackViewValue:function(){q(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){q(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ka(a.$name,"input");this.$$controls.push(a); a.$name&&(this[a.$name]=a);a.$$parentForm=this},$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];q(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);q(this.$error,function(b,d){this.$setValidity(d,null,a)},this);q(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);$a(this.$$controls,a);a.$$parentForm=Mb},$setDirty:function(){this.$$animate.removeClass(this.$$element, Va);this.$$animate.addClass(this.$$element,Rb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Va,Rb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;q(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){q(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){this.$$animate.addClass(this.$$element,"ng-submitted");this.$submitted=!0;this.$$parentForm.$setSubmitted()}}; Zd({clazz:Lb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&($a(c,d),0===c.length&&delete a[b])}});var ge=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||z}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Lb,compile:function(d,f){d.addClass(Va).addClass(nb);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in e)){var p=function(b){a.$apply(function(){n.$commitViewValue();n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",p);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",p)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n);var r=g?c(n.$name):z;g&&(r(a,n),e.$observe(g,function(b){n.$name!==b&&(r(a,void 0),n.$$parentForm.$$renameControl(n,b),r=c(n.$name),r(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n);r(a,void 0);S(n,Mb)})}}}}}]},Ge=ge(), Se=ge(!0),Pg=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,ah=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,bh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/,Qg=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,he=/^(\d{4,})-(\d{2})-(\d{2})$/,ie=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/, Hc=/^(\d{4,})-W(\d\d)$/,je=/^(\d{4,})-(\d\d)$/,ke=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ae=V();q(["date","datetime-local","month","time","week"],function(a){ae[a]=!0});var le={text:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c)},date:ob("date",he,Nb(he,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ob("datetimelocal",ie,Nb(ie,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:ob("time",ke,Nb(ke,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:ob("week",Hc,function(a,b){if(ga(a))return a; if(F(a)){Hc.lastIndex=0;var d=Hc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Vd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:ob("month",je,Nb(je,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Dc(a,b,d,c);be(c);Ra(a,b,d,c,e,f);var g,h;if(u(d.min)||d.ngMin)c.$validators.min=function(a){return c.$isEmpty(a)||w(g)||a>=g},d.$observe("min",function(a){g=Sa(a);c.$validate()}); if(u(d.max)||d.ngMax)c.$validators.max=function(a){return c.$isEmpty(a)||w(h)||a<=h},d.$observe("max",function(a){h=Sa(a);c.$validate()});if(u(d.step)||d.ngStep){var k;c.$validators.step=function(a,b){return c.$isEmpty(b)||w(k)||ce(b,g||0,k)};d.$observe("step",function(a){k=Sa(a);c.$validate()})}},url:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c);c.$$parserName="url";c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||ah.test(d)}},email:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c);c.$$parserName= "email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||bh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==T(d.ngTrim);w(d.name)&&b.attr("name",++qb);b.on("click",function(a){var g;b[0].checked&&(g=d.value,e&&(g=T(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=T(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);d.$observe(a,c)}function h(a){n=Sa(a);da(c.$modelValue)|| (m?(a=b.val(),n>a&&(a=n,b.val(a)),c.$setViewValue(a)):c.$validate())}function k(a){p=Sa(a);da(c.$modelValue)||(m?(a=b.val(),p=n},g("min",h));e&&(c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||w(p)||b<=p},g("max",k));f&&(c.$validators.step=m?function(){return!q.stepMismatch}:function(a,b){return c.$isEmpty(b)||w(r)||ce(b,n||0,r)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,h){var k=de(h,a,"ngTrueValue",d.ngTrueValue,!0),l=de(h,a,"ngFalseValue", d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return sa(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:z,button:z,submit:z,reset:z,file:z},Xc=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(le[Q(g.type)]||le.text)(e,f,g,h[0],b,a,d,c)}}}}],ch=/^(true|false|\d+)$/, kf=function(){function a(a,d,c){var e=u(c)?c:9===za?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return ch.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue,function(b){a(d,f,b)})}}}},Ke=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=$b(a)})}}}}],Me=["$interpolate", "$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=w(a)?"":a})}}}}],Le=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g, function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],jf=la({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ne=Fc("",!0),Pe=Fc("Odd",0),Oe=Fc("Even",1),Qe=Qa({compile:function(a,b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),Re=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],bd={},dh={blur:!0,focus:!0};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "), function(a){var b=Ba("ng-"+a);bd[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g=d(f[b]);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};dh[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var Ue=["$animate","$compile",function(a,b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var h,k,l;d.$watch(e.ngIf,function(d){d?k||g(function(d,f){k=f;d[d.length++]=b.$$createComment("end ngIf", e.ngIf);h={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),k&&(k.$destroy(),k=null),h&&(l=tb(h.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),h=null))})}}}],Ve=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ea.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,p){var r=0,q,s,t,w=function(){s&&(s.remove(),s=null);q&&(q.$destroy(),q= null);t&&(d.leave(t).done(function(a){!1!==a&&(s=null)}),s=t,t=null)};c.$watch(f,function(f){var m=function(a){!1===a||!u(h)||h&&!c.$eval(h)||b()},s=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&s===r){var b=c.$new();n.template=a;a=p(b,function(a){w();d.enter(a,null,e).done(m)});q=b;t=a;q.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){c.$$destroyed||s!==r||(w(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(w(),n.template=null)})}}}}],mf=["$compile",function(a){return{restrict:"ECA", priority:-400,require:"ngInclude",link:function(b,d,c,e){ma.call(d[0]).match(/SVG/)?(d.empty(),a(dd(e.template,x.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],We=Qa({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),hf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?T(e):e;c.$parsers.push(function(a){if(!w(a)){var b= [];a&&q(a.split(g),function(a){a&&b.push(f?T(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},nb="ng-valid",Yd="ng-invalid",Va="ng-pristine",Rb="ng-dirty",pb=L("ngModel");Ob.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" ");Ob.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+ "($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);D(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){D(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw pb("nonassign",this.$$attr.ngModel,xa(this.$$element));},$render:z,$isEmpty:function(a){return w(a)||""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element, "ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Rb);this.$$animate.addClass(this.$$element,Va)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element,Va);this.$$animate.addClass(this.$$element,Rb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched= !0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!da(this.$modelValue)){var a=this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"), f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;q(k.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(q(k.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;q(k.$asyncValidators,function(e,g){var k=e(a,b);if(!k||!D(k.then))throw pb("nopromise",k);f(g,void 0);c.push(k.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))}); c.length?k.$$q.all(c).then(function(){g(d)},z):g(!0)}function f(a,b){h===k.$$currentValidationRunId&&k.$setValidity(a,b)}function g(a){h===k.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var h=this.$$currentValidationRunId,k=this;(function(){var a=k.$$parserName||"parse";if(w(k.$$parserValid))f(a,null);else return k.$$parserValid||(q(k.$validators,function(a,b){f(b,null)}),q(k.$asyncValidators,function(a,b){f(b,null)})),f(a,k.$$parserValid),k.$$parserValid;return!0})()?c()?e():g(!1): g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;if(this.$$parserValid=w(a)?void 0:!0)for(var d=0;de||c.$isEmpty(b)|| b.length<=e}}}}},$c=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=Z(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};x.angular.bootstrap?x.console&&console.log("WARNING: Tried to load angular more than once."):(ze(),Ce(ea),ea.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM", "PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5, 6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a, c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),B(function(){ue(x.document,Sc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(''); //# sourceMappingURL=angular.min.js.map ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/benchmark.html0000644000175100017510000003153315102145205023110 0ustar00runnerrunner Hint benchmark ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5776384 qutebrowser-3.6.1/tests/end2end/data/hints/bootstrap/0000755000175100017510000000000015102145351022302 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/bootstrap/bootstrap.css0000644000175100017510000060325115102145205025036 0ustar00runnerrunner/*! * Bootstrap v4.5.0 (https://getbootstrap.com/) * Copyright 2011-2020 The Bootstrap Authors * Copyright 2011-2020 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ :root { --blue: #007bff; --indigo: #6610f2; --purple: #6f42c1; --pink: #e83e8c; --red: #dc3545; --orange: #fd7e14; --yellow: #ffc107; --green: #28a745; --teal: #20c997; --cyan: #17a2b8; --white: #fff; --gray: #6c757d; --gray-dark: #343a40; --primary: #007bff; --secondary: #6c757d; --success: #28a745; --info: #17a2b8; --warning: #ffc107; --danger: #dc3545; --light: #f8f9fa; --dark: #343a40; --breakpoint-xs: 0; --breakpoint-sm: 576px; --breakpoint-md: 768px; --breakpoint-lg: 992px; --breakpoint-xl: 1200px; --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } *, *::before, *::after { box-sizing: border-box; } html { font-family: sans-serif; line-height: 1.15; -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { display: block; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; text-align: left; background-color: #fff; } [tabindex="-1"]:focus:not(:focus-visible) { outline: 0 !important; } hr { box-sizing: content-box; height: 0; overflow: visible; } h1, h2, h3, h4, h5, h6 { margin-top: 0; margin-bottom: 0.5rem; } p { margin-top: 0; margin-bottom: 1rem; } abbr[title], abbr[data-original-title] { text-decoration: underline; -webkit-text-decoration: underline dotted; text-decoration: underline dotted; cursor: help; border-bottom: 0; -webkit-text-decoration-skip-ink: none; text-decoration-skip-ink: none; } address { margin-bottom: 1rem; font-style: normal; line-height: inherit; } ol, ul, dl { margin-top: 0; margin-bottom: 1rem; } ol ol, ul ul, ol ul, ul ol { margin-bottom: 0; } dt { font-weight: 700; } dd { margin-bottom: .5rem; margin-left: 0; } blockquote { margin: 0 0 1rem; } b, strong { font-weight: bolder; } small { font-size: 80%; } sub, sup { position: relative; font-size: 75%; line-height: 0; vertical-align: baseline; } sub { bottom: -.25em; } sup { top: -.5em; } a { color: #007bff; text-decoration: none; background-color: transparent; } a:hover { color: #0056b3; text-decoration: underline; } a:not([href]) { color: inherit; text-decoration: none; } a:not([href]):hover { color: inherit; text-decoration: none; } pre, code, kbd, samp { font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 1em; } pre { margin-top: 0; margin-bottom: 1rem; overflow: auto; -ms-overflow-style: scrollbar; } figure { margin: 0 0 1rem; } img { vertical-align: middle; border-style: none; } svg { overflow: hidden; vertical-align: middle; } table { border-collapse: collapse; } caption { padding-top: 0.75rem; padding-bottom: 0.75rem; color: #6c757d; text-align: left; caption-side: bottom; } th { text-align: inherit; } label { display: inline-block; margin-bottom: 0.5rem; } button { border-radius: 0; } button:focus { outline: 1px dotted; outline: 5px auto -webkit-focus-ring-color; } input, button, select, optgroup, textarea { margin: 0; font-family: inherit; font-size: inherit; line-height: inherit; } button, input { overflow: visible; } button, select { text-transform: none; } [role="button"] { cursor: pointer; } select { word-wrap: normal; } button, [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } button:not(:disabled), [type="button"]:not(:disabled), [type="reset"]:not(:disabled), [type="submit"]:not(:disabled) { cursor: pointer; } button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { padding: 0; border-style: none; } input[type="radio"], input[type="checkbox"] { box-sizing: border-box; padding: 0; } textarea { overflow: auto; resize: vertical; } fieldset { min-width: 0; padding: 0; margin: 0; border: 0; } legend { display: block; width: 100%; max-width: 100%; padding: 0; margin-bottom: .5rem; font-size: 1.5rem; line-height: inherit; color: inherit; white-space: normal; } progress { vertical-align: baseline; } [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } [type="search"] { outline-offset: -2px; -webkit-appearance: none; } [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } ::-webkit-file-upload-button { font: inherit; -webkit-appearance: button; } output { display: inline-block; } summary { display: list-item; cursor: pointer; } template { display: none; } [hidden] { display: none !important; } h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { margin-bottom: 0.5rem; font-weight: 500; line-height: 1.2; } h1, .h1 { font-size: 2.5rem; } h2, .h2 { font-size: 2rem; } h3, .h3 { font-size: 1.75rem; } h4, .h4 { font-size: 1.5rem; } h5, .h5 { font-size: 1.25rem; } h6, .h6 { font-size: 1rem; } .lead { font-size: 1.25rem; font-weight: 300; } .display-1 { font-size: 6rem; font-weight: 300; line-height: 1.2; } .display-2 { font-size: 5.5rem; font-weight: 300; line-height: 1.2; } .display-3 { font-size: 4.5rem; font-weight: 300; line-height: 1.2; } .display-4 { font-size: 3.5rem; font-weight: 300; line-height: 1.2; } hr { margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); } small, .small { font-size: 80%; font-weight: 400; } mark, .mark { padding: 0.2em; background-color: #fcf8e3; } .list-unstyled { padding-left: 0; list-style: none; } .list-inline { padding-left: 0; list-style: none; } .list-inline-item { display: inline-block; } .list-inline-item:not(:last-child) { margin-right: 0.5rem; } .initialism { font-size: 90%; text-transform: uppercase; } .blockquote { margin-bottom: 1rem; font-size: 1.25rem; } .blockquote-footer { display: block; font-size: 80%; color: #6c757d; } .blockquote-footer::before { content: "\2014\00A0"; } .img-fluid { max-width: 100%; height: auto; } .img-thumbnail { padding: 0.25rem; background-color: #fff; border: 1px solid #dee2e6; border-radius: 0.25rem; max-width: 100%; height: auto; } .figure { display: inline-block; } .figure-img { margin-bottom: 0.5rem; line-height: 1; } .figure-caption { font-size: 90%; color: #6c757d; } code { font-size: 87.5%; color: #e83e8c; word-wrap: break-word; } a > code { color: inherit; } kbd { padding: 0.2rem 0.4rem; font-size: 87.5%; color: #fff; background-color: #212529; border-radius: 0.2rem; } kbd kbd { padding: 0; font-size: 100%; font-weight: 700; } pre { display: block; font-size: 87.5%; color: #212529; } pre code { font-size: inherit; color: inherit; word-break: normal; } .pre-scrollable { max-height: 340px; overflow-y: scroll; } .container { width: 100%; padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } @media (min-width: 576px) { .container { max-width: 540px; } } @media (min-width: 768px) { .container { max-width: 720px; } } @media (min-width: 992px) { .container { max-width: 960px; } } @media (min-width: 1200px) { .container { max-width: 1140px; } } .container-fluid, .container-sm, .container-md, .container-lg, .container-xl { width: 100%; padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } @media (min-width: 576px) { .container, .container-sm { max-width: 540px; } } @media (min-width: 768px) { .container, .container-sm, .container-md { max-width: 720px; } } @media (min-width: 992px) { .container, .container-sm, .container-md, .container-lg { max-width: 960px; } } @media (min-width: 1200px) { .container, .container-sm, .container-md, .container-lg, .container-xl { max-width: 1140px; } } .row { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; margin-right: -15px; margin-left: -15px; } .no-gutters { margin-right: 0; margin-left: 0; } .no-gutters > .col, .no-gutters > [class*="col-"] { padding-right: 0; padding-left: 0; } .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, .col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, .col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, .col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, .col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, .col-xl-auto { position: relative; width: 100%; padding-right: 15px; padding-left: 15px; } .col { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; min-width: 0; max-width: 100%; } .row-cols-1 > * { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .row-cols-2 > * { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .row-cols-3 > * { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .row-cols-4 > * { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .row-cols-5 > * { -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; } .row-cols-6 > * { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: 100%; } .col-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-first { -ms-flex-order: -1; order: -1; } .order-last { -ms-flex-order: 13; order: 13; } .order-0 { -ms-flex-order: 0; order: 0; } .order-1 { -ms-flex-order: 1; order: 1; } .order-2 { -ms-flex-order: 2; order: 2; } .order-3 { -ms-flex-order: 3; order: 3; } .order-4 { -ms-flex-order: 4; order: 4; } .order-5 { -ms-flex-order: 5; order: 5; } .order-6 { -ms-flex-order: 6; order: 6; } .order-7 { -ms-flex-order: 7; order: 7; } .order-8 { -ms-flex-order: 8; order: 8; } .order-9 { -ms-flex-order: 9; order: 9; } .order-10 { -ms-flex-order: 10; order: 10; } .order-11 { -ms-flex-order: 11; order: 11; } .order-12 { -ms-flex-order: 12; order: 12; } .offset-1 { margin-left: 8.333333%; } .offset-2 { margin-left: 16.666667%; } .offset-3 { margin-left: 25%; } .offset-4 { margin-left: 33.333333%; } .offset-5 { margin-left: 41.666667%; } .offset-6 { margin-left: 50%; } .offset-7 { margin-left: 58.333333%; } .offset-8 { margin-left: 66.666667%; } .offset-9 { margin-left: 75%; } .offset-10 { margin-left: 83.333333%; } .offset-11 { margin-left: 91.666667%; } @media (min-width: 576px) { .col-sm { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; min-width: 0; max-width: 100%; } .row-cols-sm-1 > * { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .row-cols-sm-2 > * { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .row-cols-sm-3 > * { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .row-cols-sm-4 > * { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .row-cols-sm-5 > * { -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; } .row-cols-sm-6 > * { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-sm-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: 100%; } .col-sm-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-sm-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-sm-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-sm-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-sm-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-sm-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-sm-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-sm-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-sm-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-sm-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-sm-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-sm-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-sm-first { -ms-flex-order: -1; order: -1; } .order-sm-last { -ms-flex-order: 13; order: 13; } .order-sm-0 { -ms-flex-order: 0; order: 0; } .order-sm-1 { -ms-flex-order: 1; order: 1; } .order-sm-2 { -ms-flex-order: 2; order: 2; } .order-sm-3 { -ms-flex-order: 3; order: 3; } .order-sm-4 { -ms-flex-order: 4; order: 4; } .order-sm-5 { -ms-flex-order: 5; order: 5; } .order-sm-6 { -ms-flex-order: 6; order: 6; } .order-sm-7 { -ms-flex-order: 7; order: 7; } .order-sm-8 { -ms-flex-order: 8; order: 8; } .order-sm-9 { -ms-flex-order: 9; order: 9; } .order-sm-10 { -ms-flex-order: 10; order: 10; } .order-sm-11 { -ms-flex-order: 11; order: 11; } .order-sm-12 { -ms-flex-order: 12; order: 12; } .offset-sm-0 { margin-left: 0; } .offset-sm-1 { margin-left: 8.333333%; } .offset-sm-2 { margin-left: 16.666667%; } .offset-sm-3 { margin-left: 25%; } .offset-sm-4 { margin-left: 33.333333%; } .offset-sm-5 { margin-left: 41.666667%; } .offset-sm-6 { margin-left: 50%; } .offset-sm-7 { margin-left: 58.333333%; } .offset-sm-8 { margin-left: 66.666667%; } .offset-sm-9 { margin-left: 75%; } .offset-sm-10 { margin-left: 83.333333%; } .offset-sm-11 { margin-left: 91.666667%; } } @media (min-width: 768px) { .col-md { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; min-width: 0; max-width: 100%; } .row-cols-md-1 > * { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .row-cols-md-2 > * { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .row-cols-md-3 > * { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .row-cols-md-4 > * { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .row-cols-md-5 > * { -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; } .row-cols-md-6 > * { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-md-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: 100%; } .col-md-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-md-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-md-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-md-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-md-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-md-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-md-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-md-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-md-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-md-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-md-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-md-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-md-first { -ms-flex-order: -1; order: -1; } .order-md-last { -ms-flex-order: 13; order: 13; } .order-md-0 { -ms-flex-order: 0; order: 0; } .order-md-1 { -ms-flex-order: 1; order: 1; } .order-md-2 { -ms-flex-order: 2; order: 2; } .order-md-3 { -ms-flex-order: 3; order: 3; } .order-md-4 { -ms-flex-order: 4; order: 4; } .order-md-5 { -ms-flex-order: 5; order: 5; } .order-md-6 { -ms-flex-order: 6; order: 6; } .order-md-7 { -ms-flex-order: 7; order: 7; } .order-md-8 { -ms-flex-order: 8; order: 8; } .order-md-9 { -ms-flex-order: 9; order: 9; } .order-md-10 { -ms-flex-order: 10; order: 10; } .order-md-11 { -ms-flex-order: 11; order: 11; } .order-md-12 { -ms-flex-order: 12; order: 12; } .offset-md-0 { margin-left: 0; } .offset-md-1 { margin-left: 8.333333%; } .offset-md-2 { margin-left: 16.666667%; } .offset-md-3 { margin-left: 25%; } .offset-md-4 { margin-left: 33.333333%; } .offset-md-5 { margin-left: 41.666667%; } .offset-md-6 { margin-left: 50%; } .offset-md-7 { margin-left: 58.333333%; } .offset-md-8 { margin-left: 66.666667%; } .offset-md-9 { margin-left: 75%; } .offset-md-10 { margin-left: 83.333333%; } .offset-md-11 { margin-left: 91.666667%; } } @media (min-width: 992px) { .col-lg { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; min-width: 0; max-width: 100%; } .row-cols-lg-1 > * { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .row-cols-lg-2 > * { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .row-cols-lg-3 > * { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .row-cols-lg-4 > * { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .row-cols-lg-5 > * { -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; } .row-cols-lg-6 > * { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-lg-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: 100%; } .col-lg-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-lg-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-lg-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-lg-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-lg-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-lg-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-lg-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-lg-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-lg-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-lg-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-lg-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-lg-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-lg-first { -ms-flex-order: -1; order: -1; } .order-lg-last { -ms-flex-order: 13; order: 13; } .order-lg-0 { -ms-flex-order: 0; order: 0; } .order-lg-1 { -ms-flex-order: 1; order: 1; } .order-lg-2 { -ms-flex-order: 2; order: 2; } .order-lg-3 { -ms-flex-order: 3; order: 3; } .order-lg-4 { -ms-flex-order: 4; order: 4; } .order-lg-5 { -ms-flex-order: 5; order: 5; } .order-lg-6 { -ms-flex-order: 6; order: 6; } .order-lg-7 { -ms-flex-order: 7; order: 7; } .order-lg-8 { -ms-flex-order: 8; order: 8; } .order-lg-9 { -ms-flex-order: 9; order: 9; } .order-lg-10 { -ms-flex-order: 10; order: 10; } .order-lg-11 { -ms-flex-order: 11; order: 11; } .order-lg-12 { -ms-flex-order: 12; order: 12; } .offset-lg-0 { margin-left: 0; } .offset-lg-1 { margin-left: 8.333333%; } .offset-lg-2 { margin-left: 16.666667%; } .offset-lg-3 { margin-left: 25%; } .offset-lg-4 { margin-left: 33.333333%; } .offset-lg-5 { margin-left: 41.666667%; } .offset-lg-6 { margin-left: 50%; } .offset-lg-7 { margin-left: 58.333333%; } .offset-lg-8 { margin-left: 66.666667%; } .offset-lg-9 { margin-left: 75%; } .offset-lg-10 { margin-left: 83.333333%; } .offset-lg-11 { margin-left: 91.666667%; } } @media (min-width: 1200px) { .col-xl { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; min-width: 0; max-width: 100%; } .row-cols-xl-1 > * { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .row-cols-xl-2 > * { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .row-cols-xl-3 > * { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .row-cols-xl-4 > * { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .row-cols-xl-5 > * { -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; } .row-cols-xl-6 > * { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-xl-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: 100%; } .col-xl-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-xl-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-xl-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-xl-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-xl-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-xl-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-xl-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-xl-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-xl-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-xl-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-xl-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-xl-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-xl-first { -ms-flex-order: -1; order: -1; } .order-xl-last { -ms-flex-order: 13; order: 13; } .order-xl-0 { -ms-flex-order: 0; order: 0; } .order-xl-1 { -ms-flex-order: 1; order: 1; } .order-xl-2 { -ms-flex-order: 2; order: 2; } .order-xl-3 { -ms-flex-order: 3; order: 3; } .order-xl-4 { -ms-flex-order: 4; order: 4; } .order-xl-5 { -ms-flex-order: 5; order: 5; } .order-xl-6 { -ms-flex-order: 6; order: 6; } .order-xl-7 { -ms-flex-order: 7; order: 7; } .order-xl-8 { -ms-flex-order: 8; order: 8; } .order-xl-9 { -ms-flex-order: 9; order: 9; } .order-xl-10 { -ms-flex-order: 10; order: 10; } .order-xl-11 { -ms-flex-order: 11; order: 11; } .order-xl-12 { -ms-flex-order: 12; order: 12; } .offset-xl-0 { margin-left: 0; } .offset-xl-1 { margin-left: 8.333333%; } .offset-xl-2 { margin-left: 16.666667%; } .offset-xl-3 { margin-left: 25%; } .offset-xl-4 { margin-left: 33.333333%; } .offset-xl-5 { margin-left: 41.666667%; } .offset-xl-6 { margin-left: 50%; } .offset-xl-7 { margin-left: 58.333333%; } .offset-xl-8 { margin-left: 66.666667%; } .offset-xl-9 { margin-left: 75%; } .offset-xl-10 { margin-left: 83.333333%; } .offset-xl-11 { margin-left: 91.666667%; } } .table { width: 100%; margin-bottom: 1rem; color: #212529; } .table th, .table td { padding: 0.75rem; vertical-align: top; border-top: 1px solid #dee2e6; } .table thead th { vertical-align: bottom; border-bottom: 2px solid #dee2e6; } .table tbody + tbody { border-top: 2px solid #dee2e6; } .table-sm th, .table-sm td { padding: 0.3rem; } .table-bordered { border: 1px solid #dee2e6; } .table-bordered th, .table-bordered td { border: 1px solid #dee2e6; } .table-bordered thead th, .table-bordered thead td { border-bottom-width: 2px; } .table-borderless th, .table-borderless td, .table-borderless thead th, .table-borderless tbody + tbody { border: 0; } .table-striped tbody tr:nth-of-type(odd) { background-color: rgba(0, 0, 0, 0.05); } .table-hover tbody tr:hover { color: #212529; background-color: rgba(0, 0, 0, 0.075); } .table-primary, .table-primary > th, .table-primary > td { background-color: #b8daff; } .table-primary th, .table-primary td, .table-primary thead th, .table-primary tbody + tbody { border-color: #7abaff; } .table-hover .table-primary:hover { background-color: #9fcdff; } .table-hover .table-primary:hover > td, .table-hover .table-primary:hover > th { background-color: #9fcdff; } .table-secondary, .table-secondary > th, .table-secondary > td { background-color: #d6d8db; } .table-secondary th, .table-secondary td, .table-secondary thead th, .table-secondary tbody + tbody { border-color: #b3b7bb; } .table-hover .table-secondary:hover { background-color: #c8cbcf; } .table-hover .table-secondary:hover > td, .table-hover .table-secondary:hover > th { background-color: #c8cbcf; } .table-success, .table-success > th, .table-success > td { background-color: #c3e6cb; } .table-success th, .table-success td, .table-success thead th, .table-success tbody + tbody { border-color: #8fd19e; } .table-hover .table-success:hover { background-color: #b1dfbb; } .table-hover .table-success:hover > td, .table-hover .table-success:hover > th { background-color: #b1dfbb; } .table-info, .table-info > th, .table-info > td { background-color: #bee5eb; } .table-info th, .table-info td, .table-info thead th, .table-info tbody + tbody { border-color: #86cfda; } .table-hover .table-info:hover { background-color: #abdde5; } .table-hover .table-info:hover > td, .table-hover .table-info:hover > th { background-color: #abdde5; } .table-warning, .table-warning > th, .table-warning > td { background-color: #ffeeba; } .table-warning th, .table-warning td, .table-warning thead th, .table-warning tbody + tbody { border-color: #ffdf7e; } .table-hover .table-warning:hover { background-color: #ffe8a1; } .table-hover .table-warning:hover > td, .table-hover .table-warning:hover > th { background-color: #ffe8a1; } .table-danger, .table-danger > th, .table-danger > td { background-color: #f5c6cb; } .table-danger th, .table-danger td, .table-danger thead th, .table-danger tbody + tbody { border-color: #ed969e; } .table-hover .table-danger:hover { background-color: #f1b0b7; } .table-hover .table-danger:hover > td, .table-hover .table-danger:hover > th { background-color: #f1b0b7; } .table-light, .table-light > th, .table-light > td { background-color: #fdfdfe; } .table-light th, .table-light td, .table-light thead th, .table-light tbody + tbody { border-color: #fbfcfc; } .table-hover .table-light:hover { background-color: #ececf6; } .table-hover .table-light:hover > td, .table-hover .table-light:hover > th { background-color: #ececf6; } .table-dark, .table-dark > th, .table-dark > td { background-color: #c6c8ca; } .table-dark th, .table-dark td, .table-dark thead th, .table-dark tbody + tbody { border-color: #95999c; } .table-hover .table-dark:hover { background-color: #b9bbbe; } .table-hover .table-dark:hover > td, .table-hover .table-dark:hover > th { background-color: #b9bbbe; } .table-active, .table-active > th, .table-active > td { background-color: rgba(0, 0, 0, 0.075); } .table-hover .table-active:hover { background-color: rgba(0, 0, 0, 0.075); } .table-hover .table-active:hover > td, .table-hover .table-active:hover > th { background-color: rgba(0, 0, 0, 0.075); } .table .thead-dark th { color: #fff; background-color: #343a40; border-color: #454d55; } .table .thead-light th { color: #495057; background-color: #e9ecef; border-color: #dee2e6; } .table-dark { color: #fff; background-color: #343a40; } .table-dark th, .table-dark td, .table-dark thead th { border-color: #454d55; } .table-dark.table-bordered { border: 0; } .table-dark.table-striped tbody tr:nth-of-type(odd) { background-color: rgba(255, 255, 255, 0.05); } .table-dark.table-hover tbody tr:hover { color: #fff; background-color: rgba(255, 255, 255, 0.075); } @media (max-width: 575.98px) { .table-responsive-sm { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; } .table-responsive-sm > .table-bordered { border: 0; } } @media (max-width: 767.98px) { .table-responsive-md { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; } .table-responsive-md > .table-bordered { border: 0; } } @media (max-width: 991.98px) { .table-responsive-lg { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; } .table-responsive-lg > .table-bordered { border: 0; } } @media (max-width: 1199.98px) { .table-responsive-xl { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; } .table-responsive-xl > .table-bordered { border: 0; } } .table-responsive { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; } .table-responsive > .table-bordered { border: 0; } .form-control { display: block; width: 100%; height: calc(1.5em + 0.75rem + 2px); padding: 0.375rem 0.75rem; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #495057; background-color: #fff; background-clip: padding-box; border: 1px solid #ced4da; border-radius: 0.25rem; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { .form-control { transition: none; } } .form-control::-ms-expand { background-color: transparent; border: 0; } .form-control:-moz-focusring { color: transparent; text-shadow: 0 0 0 #495057; } .form-control:focus { color: #495057; background-color: #fff; border-color: #80bdff; outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .form-control::-webkit-input-placeholder { color: #6c757d; opacity: 1; } .form-control::-moz-placeholder { color: #6c757d; opacity: 1; } .form-control:-ms-input-placeholder { color: #6c757d; opacity: 1; } .form-control::-ms-input-placeholder { color: #6c757d; opacity: 1; } .form-control::placeholder { color: #6c757d; opacity: 1; } .form-control:disabled, .form-control[readonly] { background-color: #e9ecef; opacity: 1; } input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control { -webkit-appearance: none; -moz-appearance: none; appearance: none; } select.form-control:focus::-ms-value { color: #495057; background-color: #fff; } .form-control-file, .form-control-range { display: block; width: 100%; } .col-form-label { padding-top: calc(0.375rem + 1px); padding-bottom: calc(0.375rem + 1px); margin-bottom: 0; font-size: inherit; line-height: 1.5; } .col-form-label-lg { padding-top: calc(0.5rem + 1px); padding-bottom: calc(0.5rem + 1px); font-size: 1.25rem; line-height: 1.5; } .col-form-label-sm { padding-top: calc(0.25rem + 1px); padding-bottom: calc(0.25rem + 1px); font-size: 0.875rem; line-height: 1.5; } .form-control-plaintext { display: block; width: 100%; padding: 0.375rem 0; margin-bottom: 0; font-size: 1rem; line-height: 1.5; color: #212529; background-color: transparent; border: solid transparent; border-width: 1px 0; } .form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { padding-right: 0; padding-left: 0; } .form-control-sm { height: calc(1.5em + 0.5rem + 2px); padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; border-radius: 0.2rem; } .form-control-lg { height: calc(1.5em + 1rem + 2px); padding: 0.5rem 1rem; font-size: 1.25rem; line-height: 1.5; border-radius: 0.3rem; } select.form-control[size], select.form-control[multiple] { height: auto; } textarea.form-control { height: auto; } .form-group { margin-bottom: 1rem; } .form-text { display: block; margin-top: 0.25rem; } .form-row { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; margin-right: -5px; margin-left: -5px; } .form-row > .col, .form-row > [class*="col-"] { padding-right: 5px; padding-left: 5px; } .form-check { position: relative; display: block; padding-left: 1.25rem; } .form-check-input { position: absolute; margin-top: 0.3rem; margin-left: -1.25rem; } .form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { color: #6c757d; } .form-check-label { margin-bottom: 0; } .form-check-inline { display: -ms-inline-flexbox; display: inline-flex; -ms-flex-align: center; align-items: center; padding-left: 0; margin-right: 0.75rem; } .form-check-inline .form-check-input { position: static; margin-top: 0; margin-right: 0.3125rem; margin-left: 0; } .valid-feedback { display: none; width: 100%; margin-top: 0.25rem; font-size: 80%; color: #28a745; } .valid-tooltip { position: absolute; top: 100%; z-index: 5; display: none; max-width: 100%; padding: 0.25rem 0.5rem; margin-top: .1rem; font-size: 0.875rem; line-height: 1.5; color: #fff; background-color: rgba(40, 167, 69, 0.9); border-radius: 0.25rem; } .was-validated :valid ~ .valid-feedback, .was-validated :valid ~ .valid-tooltip, .is-valid ~ .valid-feedback, .is-valid ~ .valid-tooltip { display: block; } .was-validated .form-control:valid, .form-control.is-valid { border-color: #28a745; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .form-control:valid:focus, .form-control.is-valid:focus { border-color: #28a745; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .was-validated textarea.form-control:valid, textarea.form-control.is-valid { padding-right: calc(1.5em + 0.75rem); background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); } .was-validated .custom-select:valid, .custom-select.is-valid { border-color: #28a745; padding-right: calc(0.75em + 2.3125rem); background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { border-color: #28a745; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { color: #28a745; } .was-validated .form-check-input:valid ~ .valid-feedback, .was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, .form-check-input.is-valid ~ .valid-tooltip { display: block; } .was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { color: #28a745; } .was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { border-color: #28a745; } .was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { border-color: #34ce57; background-color: #34ce57; } .was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { border-color: #28a745; } .was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { border-color: #28a745; } .was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { border-color: #28a745; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .invalid-feedback { display: none; width: 100%; margin-top: 0.25rem; font-size: 80%; color: #dc3545; } .invalid-tooltip { position: absolute; top: 100%; z-index: 5; display: none; max-width: 100%; padding: 0.25rem 0.5rem; margin-top: .1rem; font-size: 0.875rem; line-height: 1.5; color: #fff; background-color: rgba(220, 53, 69, 0.9); border-radius: 0.25rem; } .was-validated :invalid ~ .invalid-feedback, .was-validated :invalid ~ .invalid-tooltip, .is-invalid ~ .invalid-feedback, .is-invalid ~ .invalid-tooltip { display: block; } .was-validated .form-control:invalid, .form-control.is-invalid { border-color: #dc3545; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { border-color: #dc3545; box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { padding-right: calc(1.5em + 0.75rem); background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); } .was-validated .custom-select:invalid, .custom-select.is-invalid { border-color: #dc3545; padding-right: calc(0.75em + 2.3125rem); background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { border-color: #dc3545; box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { color: #dc3545; } .was-validated .form-check-input:invalid ~ .invalid-feedback, .was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, .form-check-input.is-invalid ~ .invalid-tooltip { display: block; } .was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { color: #dc3545; } .was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { border-color: #dc3545; } .was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { border-color: #e4606d; background-color: #e4606d; } .was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { border-color: #dc3545; } .was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { border-color: #dc3545; } .was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { border-color: #dc3545; box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .form-inline { display: -ms-flexbox; display: flex; -ms-flex-flow: row wrap; flex-flow: row wrap; -ms-flex-align: center; align-items: center; } .form-inline .form-check { width: 100%; } @media (min-width: 576px) { .form-inline label { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; margin-bottom: 0; } .form-inline .form-group { display: -ms-flexbox; display: flex; -ms-flex: 0 0 auto; flex: 0 0 auto; -ms-flex-flow: row wrap; flex-flow: row wrap; -ms-flex-align: center; align-items: center; margin-bottom: 0; } .form-inline .form-control { display: inline-block; width: auto; vertical-align: middle; } .form-inline .form-control-plaintext { display: inline-block; } .form-inline .input-group, .form-inline .custom-select { width: auto; } .form-inline .form-check { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; width: auto; padding-left: 0; } .form-inline .form-check-input { position: relative; -ms-flex-negative: 0; flex-shrink: 0; margin-top: 0; margin-right: 0.25rem; margin-left: 0; } .form-inline .custom-control { -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; } .form-inline .custom-control-label { margin-bottom: 0; } } .btn { display: inline-block; font-weight: 400; color: #212529; text-align: center; vertical-align: middle; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-color: transparent; border: 1px solid transparent; padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; border-radius: 0.25rem; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { .btn { transition: none; } } .btn:hover { color: #212529; text-decoration: none; } .btn:focus, .btn.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .btn.disabled, .btn:disabled { opacity: 0.65; } .btn:not(:disabled):not(.disabled) { cursor: pointer; } a.btn.disabled, fieldset:disabled a.btn { pointer-events: none; } .btn-primary { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-primary:hover { color: #fff; background-color: #0069d9; border-color: #0062cc; } .btn-primary:focus, .btn-primary.focus { color: #fff; background-color: #0069d9; border-color: #0062cc; box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); } .btn-primary.disabled, .btn-primary:disabled { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, .show > .btn-primary.dropdown-toggle { color: #fff; background-color: #0062cc; border-color: #005cbf; } .btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-primary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); } .btn-secondary { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-secondary:hover { color: #fff; background-color: #5a6268; border-color: #545b62; } .btn-secondary:focus, .btn-secondary.focus { color: #fff; background-color: #5a6268; border-color: #545b62; box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); } .btn-secondary.disabled, .btn-secondary:disabled { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, .show > .btn-secondary.dropdown-toggle { color: #fff; background-color: #545b62; border-color: #4e555b; } .btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-secondary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); } .btn-success { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-success:hover { color: #fff; background-color: #218838; border-color: #1e7e34; } .btn-success:focus, .btn-success.focus { color: #fff; background-color: #218838; border-color: #1e7e34; box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); } .btn-success.disabled, .btn-success:disabled { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, .show > .btn-success.dropdown-toggle { color: #fff; background-color: #1e7e34; border-color: #1c7430; } .btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, .show > .btn-success.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); } .btn-info { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-info:hover { color: #fff; background-color: #138496; border-color: #117a8b; } .btn-info:focus, .btn-info.focus { color: #fff; background-color: #138496; border-color: #117a8b; box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); } .btn-info.disabled, .btn-info:disabled { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, .show > .btn-info.dropdown-toggle { color: #fff; background-color: #117a8b; border-color: #10707f; } .btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, .show > .btn-info.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); } .btn-warning { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-warning:hover { color: #212529; background-color: #e0a800; border-color: #d39e00; } .btn-warning:focus, .btn-warning.focus { color: #212529; background-color: #e0a800; border-color: #d39e00; box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); } .btn-warning.disabled, .btn-warning:disabled { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, .show > .btn-warning.dropdown-toggle { color: #212529; background-color: #d39e00; border-color: #c69500; } .btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, .show > .btn-warning.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); } .btn-danger { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-danger:hover { color: #fff; background-color: #c82333; border-color: #bd2130; } .btn-danger:focus, .btn-danger.focus { color: #fff; background-color: #c82333; border-color: #bd2130; box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); } .btn-danger.disabled, .btn-danger:disabled { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, .show > .btn-danger.dropdown-toggle { color: #fff; background-color: #bd2130; border-color: #b21f2d; } .btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, .show > .btn-danger.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); } .btn-light { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-light:hover { color: #212529; background-color: #e2e6ea; border-color: #dae0e5; } .btn-light:focus, .btn-light.focus { color: #212529; background-color: #e2e6ea; border-color: #dae0e5; box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); } .btn-light.disabled, .btn-light:disabled { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, .show > .btn-light.dropdown-toggle { color: #212529; background-color: #dae0e5; border-color: #d3d9df; } .btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, .show > .btn-light.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); } .btn-dark { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-dark:hover { color: #fff; background-color: #23272b; border-color: #1d2124; } .btn-dark:focus, .btn-dark.focus { color: #fff; background-color: #23272b; border-color: #1d2124; box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); } .btn-dark.disabled, .btn-dark:disabled { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, .show > .btn-dark.dropdown-toggle { color: #fff; background-color: #1d2124; border-color: #171a1d; } .btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, .show > .btn-dark.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); } .btn-outline-primary { color: #007bff; border-color: #007bff; } .btn-outline-primary:hover { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-outline-primary:focus, .btn-outline-primary.focus { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } .btn-outline-primary.disabled, .btn-outline-primary:disabled { color: #007bff; background-color: transparent; } .btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, .show > .btn-outline-primary.dropdown-toggle { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-primary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } .btn-outline-secondary { color: #6c757d; border-color: #6c757d; } .btn-outline-secondary:hover { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-outline-secondary:focus, .btn-outline-secondary.focus { box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } .btn-outline-secondary.disabled, .btn-outline-secondary:disabled { color: #6c757d; background-color: transparent; } .btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, .show > .btn-outline-secondary.dropdown-toggle { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-secondary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } .btn-outline-success { color: #28a745; border-color: #28a745; } .btn-outline-success:hover { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-outline-success:focus, .btn-outline-success.focus { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } .btn-outline-success.disabled, .btn-outline-success:disabled { color: #28a745; background-color: transparent; } .btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, .show > .btn-outline-success.dropdown-toggle { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-success.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } .btn-outline-info { color: #17a2b8; border-color: #17a2b8; } .btn-outline-info:hover { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-outline-info:focus, .btn-outline-info.focus { box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } .btn-outline-info.disabled, .btn-outline-info:disabled { color: #17a2b8; background-color: transparent; } .btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, .show > .btn-outline-info.dropdown-toggle { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-info.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } .btn-outline-warning { color: #ffc107; border-color: #ffc107; } .btn-outline-warning:hover { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-outline-warning:focus, .btn-outline-warning.focus { box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } .btn-outline-warning.disabled, .btn-outline-warning:disabled { color: #ffc107; background-color: transparent; } .btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, .show > .btn-outline-warning.dropdown-toggle { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-warning.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } .btn-outline-danger { color: #dc3545; border-color: #dc3545; } .btn-outline-danger:hover { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-outline-danger:focus, .btn-outline-danger.focus { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } .btn-outline-danger.disabled, .btn-outline-danger:disabled { color: #dc3545; background-color: transparent; } .btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, .show > .btn-outline-danger.dropdown-toggle { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-danger.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } .btn-outline-light { color: #f8f9fa; border-color: #f8f9fa; } .btn-outline-light:hover { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-outline-light:focus, .btn-outline-light.focus { box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } .btn-outline-light.disabled, .btn-outline-light:disabled { color: #f8f9fa; background-color: transparent; } .btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, .show > .btn-outline-light.dropdown-toggle { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-light.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } .btn-outline-dark { color: #343a40; border-color: #343a40; } .btn-outline-dark:hover { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-outline-dark:focus, .btn-outline-dark.focus { box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .btn-outline-dark.disabled, .btn-outline-dark:disabled { color: #343a40; background-color: transparent; } .btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, .show > .btn-outline-dark.dropdown-toggle { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-dark.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .btn-link { font-weight: 400; color: #007bff; text-decoration: none; } .btn-link:hover { color: #0056b3; text-decoration: underline; } .btn-link:focus, .btn-link.focus { text-decoration: underline; } .btn-link:disabled, .btn-link.disabled { color: #6c757d; pointer-events: none; } .btn-lg, .btn-group-lg > .btn { padding: 0.5rem 1rem; font-size: 1.25rem; line-height: 1.5; border-radius: 0.3rem; } .btn-sm, .btn-group-sm > .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; border-radius: 0.2rem; } .btn-block { display: block; width: 100%; } .btn-block + .btn-block { margin-top: 0.5rem; } input[type="submit"].btn-block, input[type="reset"].btn-block, input[type="button"].btn-block { width: 100%; } .fade { transition: opacity 0.15s linear; } @media (prefers-reduced-motion: reduce) { .fade { transition: none; } } .fade:not(.show) { opacity: 0; } .collapse:not(.show) { display: none; } .collapsing { position: relative; height: 0; overflow: hidden; transition: height 0.35s ease; } @media (prefers-reduced-motion: reduce) { .collapsing { transition: none; } } .dropup, .dropright, .dropdown, .dropleft { position: relative; } .dropdown-toggle { white-space: nowrap; } .dropdown-toggle::after { display: inline-block; margin-left: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0.3em solid; border-right: 0.3em solid transparent; border-bottom: 0; border-left: 0.3em solid transparent; } .dropdown-toggle:empty::after { margin-left: 0; } .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 10rem; padding: 0.5rem 0; margin: 0.125rem 0 0; font-size: 1rem; color: #212529; text-align: left; list-style: none; background-color: #fff; background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 0.25rem; } .dropdown-menu-left { right: auto; left: 0; } .dropdown-menu-right { right: 0; left: auto; } @media (min-width: 576px) { .dropdown-menu-sm-left { right: auto; left: 0; } .dropdown-menu-sm-right { right: 0; left: auto; } } @media (min-width: 768px) { .dropdown-menu-md-left { right: auto; left: 0; } .dropdown-menu-md-right { right: 0; left: auto; } } @media (min-width: 992px) { .dropdown-menu-lg-left { right: auto; left: 0; } .dropdown-menu-lg-right { right: 0; left: auto; } } @media (min-width: 1200px) { .dropdown-menu-xl-left { right: auto; left: 0; } .dropdown-menu-xl-right { right: 0; left: auto; } } .dropup .dropdown-menu { top: auto; bottom: 100%; margin-top: 0; margin-bottom: 0.125rem; } .dropup .dropdown-toggle::after { display: inline-block; margin-left: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0; border-right: 0.3em solid transparent; border-bottom: 0.3em solid; border-left: 0.3em solid transparent; } .dropup .dropdown-toggle:empty::after { margin-left: 0; } .dropright .dropdown-menu { top: 0; right: auto; left: 100%; margin-top: 0; margin-left: 0.125rem; } .dropright .dropdown-toggle::after { display: inline-block; margin-left: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0.3em solid transparent; border-right: 0; border-bottom: 0.3em solid transparent; border-left: 0.3em solid; } .dropright .dropdown-toggle:empty::after { margin-left: 0; } .dropright .dropdown-toggle::after { vertical-align: 0; } .dropleft .dropdown-menu { top: 0; right: 100%; left: auto; margin-top: 0; margin-right: 0.125rem; } .dropleft .dropdown-toggle::after { display: inline-block; margin-left: 0.255em; vertical-align: 0.255em; content: ""; } .dropleft .dropdown-toggle::after { display: none; } .dropleft .dropdown-toggle::before { display: inline-block; margin-right: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0.3em solid transparent; border-right: 0.3em solid; border-bottom: 0.3em solid transparent; } .dropleft .dropdown-toggle:empty::after { margin-left: 0; } .dropleft .dropdown-toggle::before { vertical-align: 0; } .dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { right: auto; bottom: auto; } .dropdown-divider { height: 0; margin: 0.5rem 0; overflow: hidden; border-top: 1px solid #e9ecef; } .dropdown-item { display: block; width: 100%; padding: 0.25rem 1.5rem; clear: both; font-weight: 400; color: #212529; text-align: inherit; white-space: nowrap; background-color: transparent; border: 0; } .dropdown-item:hover, .dropdown-item:focus { color: #16181b; text-decoration: none; background-color: #f8f9fa; } .dropdown-item.active, .dropdown-item:active { color: #fff; text-decoration: none; background-color: #007bff; } .dropdown-item.disabled, .dropdown-item:disabled { color: #6c757d; pointer-events: none; background-color: transparent; } .dropdown-menu.show { display: block; } .dropdown-header { display: block; padding: 0.5rem 1.5rem; margin-bottom: 0; font-size: 0.875rem; color: #6c757d; white-space: nowrap; } .dropdown-item-text { display: block; padding: 0.25rem 1.5rem; color: #212529; } .btn-group, .btn-group-vertical { position: relative; display: -ms-inline-flexbox; display: inline-flex; vertical-align: middle; } .btn-group > .btn, .btn-group-vertical > .btn { position: relative; -ms-flex: 1 1 auto; flex: 1 1 auto; } .btn-group > .btn:hover, .btn-group-vertical > .btn:hover { z-index: 1; } .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, .btn-group-vertical > .btn:focus, .btn-group-vertical > .btn:active, .btn-group-vertical > .btn.active { z-index: 1; } .btn-toolbar { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-pack: start; justify-content: flex-start; } .btn-toolbar .input-group { width: auto; } .btn-group > .btn:not(:first-child), .btn-group > .btn-group:not(:first-child) { margin-left: -1px; } .btn-group > .btn:not(:last-child):not(.dropdown-toggle), .btn-group > .btn-group:not(:last-child) > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn:not(:first-child), .btn-group > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } .dropdown-toggle-split { padding-right: 0.5625rem; padding-left: 0.5625rem; } .dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropright .dropdown-toggle-split::after { margin-left: 0; } .dropleft .dropdown-toggle-split::before { margin-right: 0; } .btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { padding-right: 0.375rem; padding-left: 0.375rem; } .btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { padding-right: 0.75rem; padding-left: 0.75rem; } .btn-group-vertical { -ms-flex-direction: column; flex-direction: column; -ms-flex-align: start; align-items: flex-start; -ms-flex-pack: center; justify-content: center; } .btn-group-vertical > .btn, .btn-group-vertical > .btn-group { width: 100%; } .btn-group-vertical > .btn:not(:first-child), .btn-group-vertical > .btn-group:not(:first-child) { margin-top: -1px; } .btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), .btn-group-vertical > .btn-group:not(:last-child) > .btn { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn:not(:first-child), .btn-group-vertical > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-top-right-radius: 0; } .btn-group-toggle > .btn, .btn-group-toggle > .btn-group > .btn { margin-bottom: 0; } .btn-group-toggle > .btn input[type="radio"], .btn-group-toggle > .btn input[type="checkbox"], .btn-group-toggle > .btn-group > .btn input[type="radio"], .btn-group-toggle > .btn-group > .btn input[type="checkbox"] { position: absolute; clip: rect(0, 0, 0, 0); pointer-events: none; } .input-group { position: relative; display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-align: stretch; align-items: stretch; width: 100%; } .input-group > .form-control, .input-group > .form-control-plaintext, .input-group > .custom-select, .input-group > .custom-file { position: relative; -ms-flex: 1 1 auto; flex: 1 1 auto; width: 1%; min-width: 0; margin-bottom: 0; } .input-group > .form-control + .form-control, .input-group > .form-control + .custom-select, .input-group > .form-control + .custom-file, .input-group > .form-control-plaintext + .form-control, .input-group > .form-control-plaintext + .custom-select, .input-group > .form-control-plaintext + .custom-file, .input-group > .custom-select + .form-control, .input-group > .custom-select + .custom-select, .input-group > .custom-select + .custom-file, .input-group > .custom-file + .form-control, .input-group > .custom-file + .custom-select, .input-group > .custom-file + .custom-file { margin-left: -1px; } .input-group > .form-control:focus, .input-group > .custom-select:focus, .input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { z-index: 3; } .input-group > .custom-file .custom-file-input:focus { z-index: 4; } .input-group > .form-control:not(:last-child), .input-group > .custom-select:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group > .form-control:not(:first-child), .input-group > .custom-select:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .input-group > .custom-file { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; } .input-group > .custom-file:not(:last-child) .custom-file-label, .input-group > .custom-file:not(:last-child) .custom-file-label::after { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group > .custom-file:not(:first-child) .custom-file-label { border-top-left-radius: 0; border-bottom-left-radius: 0; } .input-group-prepend, .input-group-append { display: -ms-flexbox; display: flex; } .input-group-prepend .btn, .input-group-append .btn { position: relative; z-index: 2; } .input-group-prepend .btn:focus, .input-group-append .btn:focus { z-index: 3; } .input-group-prepend .btn + .btn, .input-group-prepend .btn + .input-group-text, .input-group-prepend .input-group-text + .input-group-text, .input-group-prepend .input-group-text + .btn, .input-group-append .btn + .btn, .input-group-append .btn + .input-group-text, .input-group-append .input-group-text + .input-group-text, .input-group-append .input-group-text + .btn { margin-left: -1px; } .input-group-prepend { margin-right: -1px; } .input-group-append { margin-left: -1px; } .input-group-text { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; padding: 0.375rem 0.75rem; margin-bottom: 0; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #495057; text-align: center; white-space: nowrap; background-color: #e9ecef; border: 1px solid #ced4da; border-radius: 0.25rem; } .input-group-text input[type="radio"], .input-group-text input[type="checkbox"] { margin-top: 0; } .input-group-lg > .form-control:not(textarea), .input-group-lg > .custom-select { height: calc(1.5em + 1rem + 2px); } .input-group-lg > .form-control, .input-group-lg > .custom-select, .input-group-lg > .input-group-prepend > .input-group-text, .input-group-lg > .input-group-append > .input-group-text, .input-group-lg > .input-group-prepend > .btn, .input-group-lg > .input-group-append > .btn { padding: 0.5rem 1rem; font-size: 1.25rem; line-height: 1.5; border-radius: 0.3rem; } .input-group-sm > .form-control:not(textarea), .input-group-sm > .custom-select { height: calc(1.5em + 0.5rem + 2px); } .input-group-sm > .form-control, .input-group-sm > .custom-select, .input-group-sm > .input-group-prepend > .input-group-text, .input-group-sm > .input-group-append > .input-group-text, .input-group-sm > .input-group-prepend > .btn, .input-group-sm > .input-group-append > .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; border-radius: 0.2rem; } .input-group-lg > .custom-select, .input-group-sm > .custom-select { padding-right: 1.75rem; } .input-group > .input-group-prepend > .btn, .input-group > .input-group-prepend > .input-group-text, .input-group > .input-group-append:not(:last-child) > .btn, .input-group > .input-group-append:not(:last-child) > .input-group-text, .input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), .input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group > .input-group-append > .btn, .input-group > .input-group-append > .input-group-text, .input-group > .input-group-prepend:not(:first-child) > .btn, .input-group > .input-group-prepend:not(:first-child) > .input-group-text, .input-group > .input-group-prepend:first-child > .btn:not(:first-child), .input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .custom-control { position: relative; display: block; min-height: 1.5rem; padding-left: 1.5rem; } .custom-control-inline { display: -ms-inline-flexbox; display: inline-flex; margin-right: 1rem; } .custom-control-input { position: absolute; left: 0; z-index: -1; width: 1rem; height: 1.25rem; opacity: 0; } .custom-control-input:checked ~ .custom-control-label::before { color: #fff; border-color: #007bff; background-color: #007bff; } .custom-control-input:focus ~ .custom-control-label::before { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-control-input:focus:not(:checked) ~ .custom-control-label::before { border-color: #80bdff; } .custom-control-input:not(:disabled):active ~ .custom-control-label::before { color: #fff; background-color: #b3d7ff; border-color: #b3d7ff; } .custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label { color: #6c757d; } .custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before { background-color: #e9ecef; } .custom-control-label { position: relative; margin-bottom: 0; vertical-align: top; } .custom-control-label::before { position: absolute; top: 0.25rem; left: -1.5rem; display: block; width: 1rem; height: 1rem; pointer-events: none; content: ""; background-color: #fff; border: #adb5bd solid 1px; } .custom-control-label::after { position: absolute; top: 0.25rem; left: -1.5rem; display: block; width: 1rem; height: 1rem; content: ""; background: no-repeat 50% / 50% 50%; } .custom-checkbox .custom-control-label::before { border-radius: 0.25rem; } .custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e"); } .custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { border-color: #007bff; background-color: #007bff; } .custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e"); } .custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { background-color: rgba(0, 123, 255, 0.5); } .custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { background-color: rgba(0, 123, 255, 0.5); } .custom-radio .custom-control-label::before { border-radius: 50%; } .custom-radio .custom-control-input:checked ~ .custom-control-label::after { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); } .custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { background-color: rgba(0, 123, 255, 0.5); } .custom-switch { padding-left: 2.25rem; } .custom-switch .custom-control-label::before { left: -2.25rem; width: 1.75rem; pointer-events: all; border-radius: 0.5rem; } .custom-switch .custom-control-label::after { top: calc(0.25rem + 2px); left: calc(-2.25rem + 2px); width: calc(1rem - 4px); height: calc(1rem - 4px); background-color: #adb5bd; border-radius: 0.5rem; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { .custom-switch .custom-control-label::after { transition: none; } } .custom-switch .custom-control-input:checked ~ .custom-control-label::after { background-color: #fff; -webkit-transform: translateX(0.75rem); transform: translateX(0.75rem); } .custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { background-color: rgba(0, 123, 255, 0.5); } .custom-select { display: inline-block; width: 100%; height: calc(1.5em + 0.75rem + 2px); padding: 0.375rem 1.75rem 0.375rem 0.75rem; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #495057; vertical-align: middle; background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px; border: 1px solid #ced4da; border-radius: 0.25rem; -webkit-appearance: none; -moz-appearance: none; appearance: none; } .custom-select:focus { border-color: #80bdff; outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-select:focus::-ms-value { color: #495057; background-color: #fff; } .custom-select[multiple], .custom-select[size]:not([size="1"]) { height: auto; padding-right: 0.75rem; background-image: none; } .custom-select:disabled { color: #6c757d; background-color: #e9ecef; } .custom-select::-ms-expand { display: none; } .custom-select:-moz-focusring { color: transparent; text-shadow: 0 0 0 #495057; } .custom-select-sm { height: calc(1.5em + 0.5rem + 2px); padding-top: 0.25rem; padding-bottom: 0.25rem; padding-left: 0.5rem; font-size: 0.875rem; } .custom-select-lg { height: calc(1.5em + 1rem + 2px); padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; font-size: 1.25rem; } .custom-file { position: relative; display: inline-block; width: 100%; height: calc(1.5em + 0.75rem + 2px); margin-bottom: 0; } .custom-file-input { position: relative; z-index: 2; width: 100%; height: calc(1.5em + 0.75rem + 2px); margin: 0; opacity: 0; } .custom-file-input:focus ~ .custom-file-label { border-color: #80bdff; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-file-input[disabled] ~ .custom-file-label, .custom-file-input:disabled ~ .custom-file-label { background-color: #e9ecef; } .custom-file-input:lang(en) ~ .custom-file-label::after { content: "Browse"; } .custom-file-input ~ .custom-file-label[data-browse]::after { content: attr(data-browse); } .custom-file-label { position: absolute; top: 0; right: 0; left: 0; z-index: 1; height: calc(1.5em + 0.75rem + 2px); padding: 0.375rem 0.75rem; font-weight: 400; line-height: 1.5; color: #495057; background-color: #fff; border: 1px solid #ced4da; border-radius: 0.25rem; } .custom-file-label::after { position: absolute; top: 0; right: 0; bottom: 0; z-index: 3; display: block; height: calc(1.5em + 0.75rem); padding: 0.375rem 0.75rem; line-height: 1.5; color: #495057; content: "Browse"; background-color: #e9ecef; border-left: inherit; border-radius: 0 0.25rem 0.25rem 0; } .custom-range { width: 100%; height: 1.4rem; padding: 0; background-color: transparent; -webkit-appearance: none; -moz-appearance: none; appearance: none; } .custom-range:focus { outline: none; } .custom-range:focus::-webkit-slider-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-range:focus::-moz-range-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-range:focus::-ms-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-range::-moz-focus-outer { border: 0; } .custom-range::-webkit-slider-thumb { width: 1rem; height: 1rem; margin-top: -0.25rem; background-color: #007bff; border: 0; border-radius: 1rem; -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -webkit-appearance: none; appearance: none; } @media (prefers-reduced-motion: reduce) { .custom-range::-webkit-slider-thumb { -webkit-transition: none; transition: none; } } .custom-range::-webkit-slider-thumb:active { background-color: #b3d7ff; } .custom-range::-webkit-slider-runnable-track { width: 100%; height: 0.5rem; color: transparent; cursor: pointer; background-color: #dee2e6; border-color: transparent; border-radius: 1rem; } .custom-range::-moz-range-thumb { width: 1rem; height: 1rem; background-color: #007bff; border: 0; border-radius: 1rem; -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -moz-appearance: none; appearance: none; } @media (prefers-reduced-motion: reduce) { .custom-range::-moz-range-thumb { -moz-transition: none; transition: none; } } .custom-range::-moz-range-thumb:active { background-color: #b3d7ff; } .custom-range::-moz-range-track { width: 100%; height: 0.5rem; color: transparent; cursor: pointer; background-color: #dee2e6; border-color: transparent; border-radius: 1rem; } .custom-range::-ms-thumb { width: 1rem; height: 1rem; margin-top: 0; margin-right: 0.2rem; margin-left: 0.2rem; background-color: #007bff; border: 0; border-radius: 1rem; -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; appearance: none; } @media (prefers-reduced-motion: reduce) { .custom-range::-ms-thumb { -ms-transition: none; transition: none; } } .custom-range::-ms-thumb:active { background-color: #b3d7ff; } .custom-range::-ms-track { width: 100%; height: 0.5rem; color: transparent; cursor: pointer; background-color: transparent; border-color: transparent; border-width: 0.5rem; } .custom-range::-ms-fill-lower { background-color: #dee2e6; border-radius: 1rem; } .custom-range::-ms-fill-upper { margin-right: 15px; background-color: #dee2e6; border-radius: 1rem; } .custom-range:disabled::-webkit-slider-thumb { background-color: #adb5bd; } .custom-range:disabled::-webkit-slider-runnable-track { cursor: default; } .custom-range:disabled::-moz-range-thumb { background-color: #adb5bd; } .custom-range:disabled::-moz-range-track { cursor: default; } .custom-range:disabled::-ms-thumb { background-color: #adb5bd; } .custom-control-label::before, .custom-file-label, .custom-select { transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { .custom-control-label::before, .custom-file-label, .custom-select { transition: none; } } .nav { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; padding-left: 0; margin-bottom: 0; list-style: none; } .nav-link { display: block; padding: 0.5rem 1rem; } .nav-link:hover, .nav-link:focus { text-decoration: none; } .nav-link.disabled { color: #6c757d; pointer-events: none; cursor: default; } .nav-tabs { border-bottom: 1px solid #dee2e6; } .nav-tabs .nav-item { margin-bottom: -1px; } .nav-tabs .nav-link { border: 1px solid transparent; border-top-left-radius: 0.25rem; border-top-right-radius: 0.25rem; } .nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { border-color: #e9ecef #e9ecef #dee2e6; } .nav-tabs .nav-link.disabled { color: #6c757d; background-color: transparent; border-color: transparent; } .nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { color: #495057; background-color: #fff; border-color: #dee2e6 #dee2e6 #fff; } .nav-tabs .dropdown-menu { margin-top: -1px; border-top-left-radius: 0; border-top-right-radius: 0; } .nav-pills .nav-link { border-radius: 0.25rem; } .nav-pills .nav-link.active, .nav-pills .show > .nav-link { color: #fff; background-color: #007bff; } .nav-fill .nav-item { -ms-flex: 1 1 auto; flex: 1 1 auto; text-align: center; } .nav-justified .nav-item { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; text-align: center; } .tab-content > .tab-pane { display: none; } .tab-content > .active { display: block; } .navbar { position: relative; display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-align: center; align-items: center; -ms-flex-pack: justify; justify-content: space-between; padding: 0.5rem 1rem; } .navbar .container, .navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-align: center; align-items: center; -ms-flex-pack: justify; justify-content: space-between; } .navbar-brand { display: inline-block; padding-top: 0.3125rem; padding-bottom: 0.3125rem; margin-right: 1rem; font-size: 1.25rem; line-height: inherit; white-space: nowrap; } .navbar-brand:hover, .navbar-brand:focus { text-decoration: none; } .navbar-nav { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; padding-left: 0; margin-bottom: 0; list-style: none; } .navbar-nav .nav-link { padding-right: 0; padding-left: 0; } .navbar-nav .dropdown-menu { position: static; float: none; } .navbar-text { display: inline-block; padding-top: 0.5rem; padding-bottom: 0.5rem; } .navbar-collapse { -ms-flex-preferred-size: 100%; flex-basis: 100%; -ms-flex-positive: 1; flex-grow: 1; -ms-flex-align: center; align-items: center; } .navbar-toggler { padding: 0.25rem 0.75rem; font-size: 1.25rem; line-height: 1; background-color: transparent; border: 1px solid transparent; border-radius: 0.25rem; } .navbar-toggler:hover, .navbar-toggler:focus { text-decoration: none; } .navbar-toggler-icon { display: inline-block; width: 1.5em; height: 1.5em; vertical-align: middle; content: ""; background: no-repeat center center; background-size: 100% 100%; } @media (max-width: 575.98px) { .navbar-expand-sm > .container, .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { padding-right: 0; padding-left: 0; } } @media (min-width: 576px) { .navbar-expand-sm { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-sm .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-sm .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-sm .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-sm > .container, .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-sm .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-sm .navbar-toggler { display: none; } } @media (max-width: 767.98px) { .navbar-expand-md > .container, .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { padding-right: 0; padding-left: 0; } } @media (min-width: 768px) { .navbar-expand-md { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-md .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-md .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-md .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-md > .container, .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-md .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-md .navbar-toggler { display: none; } } @media (max-width: 991.98px) { .navbar-expand-lg > .container, .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { padding-right: 0; padding-left: 0; } } @media (min-width: 992px) { .navbar-expand-lg { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-lg .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-lg .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-lg .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-lg > .container, .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-lg .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-lg .navbar-toggler { display: none; } } @media (max-width: 1199.98px) { .navbar-expand-xl > .container, .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { padding-right: 0; padding-left: 0; } } @media (min-width: 1200px) { .navbar-expand-xl { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-xl .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-xl .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-xl .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-xl > .container, .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-xl .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-xl .navbar-toggler { display: none; } } .navbar-expand { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand > .container, .navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { padding-right: 0; padding-left: 0; } .navbar-expand .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand > .container, .navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand .navbar-toggler { display: none; } .navbar-light .navbar-brand { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-nav .nav-link { color: rgba(0, 0, 0, 0.5); } .navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { color: rgba(0, 0, 0, 0.7); } .navbar-light .navbar-nav .nav-link.disabled { color: rgba(0, 0, 0, 0.3); } .navbar-light .navbar-nav .show > .nav-link, .navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .nav-link.active { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-toggler { color: rgba(0, 0, 0, 0.5); border-color: rgba(0, 0, 0, 0.1); } .navbar-light .navbar-toggler-icon { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } .navbar-light .navbar-text { color: rgba(0, 0, 0, 0.5); } .navbar-light .navbar-text a { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { color: rgba(0, 0, 0, 0.9); } .navbar-dark .navbar-brand { color: #fff; } .navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { color: #fff; } .navbar-dark .navbar-nav .nav-link { color: rgba(255, 255, 255, 0.5); } .navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { color: rgba(255, 255, 255, 0.75); } .navbar-dark .navbar-nav .nav-link.disabled { color: rgba(255, 255, 255, 0.25); } .navbar-dark .navbar-nav .show > .nav-link, .navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .nav-link.active { color: #fff; } .navbar-dark .navbar-toggler { color: rgba(255, 255, 255, 0.5); border-color: rgba(255, 255, 255, 0.1); } .navbar-dark .navbar-toggler-icon { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } .navbar-dark .navbar-text { color: rgba(255, 255, 255, 0.5); } .navbar-dark .navbar-text a { color: #fff; } .navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { color: #fff; } .card { position: relative; display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; min-width: 0; word-wrap: break-word; background-color: #fff; background-clip: border-box; border: 1px solid rgba(0, 0, 0, 0.125); border-radius: 0.25rem; } .card > hr { margin-right: 0; margin-left: 0; } .card > .list-group { border-top: inherit; border-bottom: inherit; } .card > .list-group:first-child { border-top-width: 0; border-top-left-radius: calc(0.25rem - 1px); border-top-right-radius: calc(0.25rem - 1px); } .card > .list-group:last-child { border-bottom-width: 0; border-bottom-right-radius: calc(0.25rem - 1px); border-bottom-left-radius: calc(0.25rem - 1px); } .card-body { -ms-flex: 1 1 auto; flex: 1 1 auto; min-height: 1px; padding: 1.25rem; } .card-title { margin-bottom: 0.75rem; } .card-subtitle { margin-top: -0.375rem; margin-bottom: 0; } .card-text:last-child { margin-bottom: 0; } .card-link:hover { text-decoration: none; } .card-link + .card-link { margin-left: 1.25rem; } .card-header { padding: 0.75rem 1.25rem; margin-bottom: 0; background-color: rgba(0, 0, 0, 0.03); border-bottom: 1px solid rgba(0, 0, 0, 0.125); } .card-header:first-child { border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; } .card-header + .list-group .list-group-item:first-child { border-top: 0; } .card-footer { padding: 0.75rem 1.25rem; background-color: rgba(0, 0, 0, 0.03); border-top: 1px solid rgba(0, 0, 0, 0.125); } .card-footer:last-child { border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); } .card-header-tabs { margin-right: -0.625rem; margin-bottom: -0.75rem; margin-left: -0.625rem; border-bottom: 0; } .card-header-pills { margin-right: -0.625rem; margin-left: -0.625rem; } .card-img-overlay { position: absolute; top: 0; right: 0; bottom: 0; left: 0; padding: 1.25rem; } .card-img, .card-img-top, .card-img-bottom { -ms-flex-negative: 0; flex-shrink: 0; width: 100%; } .card-img, .card-img-top { border-top-left-radius: calc(0.25rem - 1px); border-top-right-radius: calc(0.25rem - 1px); } .card-img, .card-img-bottom { border-bottom-right-radius: calc(0.25rem - 1px); border-bottom-left-radius: calc(0.25rem - 1px); } .card-deck .card { margin-bottom: 15px; } @media (min-width: 576px) { .card-deck { display: -ms-flexbox; display: flex; -ms-flex-flow: row wrap; flex-flow: row wrap; margin-right: -15px; margin-left: -15px; } .card-deck .card { -ms-flex: 1 0 0%; flex: 1 0 0%; margin-right: 15px; margin-bottom: 0; margin-left: 15px; } } .card-group > .card { margin-bottom: 15px; } @media (min-width: 576px) { .card-group { display: -ms-flexbox; display: flex; -ms-flex-flow: row wrap; flex-flow: row wrap; } .card-group > .card { -ms-flex: 1 0 0%; flex: 1 0 0%; margin-bottom: 0; } .card-group > .card + .card { margin-left: 0; border-left: 0; } .card-group > .card:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .card-group > .card:not(:last-child) .card-img-top, .card-group > .card:not(:last-child) .card-header { border-top-right-radius: 0; } .card-group > .card:not(:last-child) .card-img-bottom, .card-group > .card:not(:last-child) .card-footer { border-bottom-right-radius: 0; } .card-group > .card:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .card-group > .card:not(:first-child) .card-img-top, .card-group > .card:not(:first-child) .card-header { border-top-left-radius: 0; } .card-group > .card:not(:first-child) .card-img-bottom, .card-group > .card:not(:first-child) .card-footer { border-bottom-left-radius: 0; } } .card-columns .card { margin-bottom: 0.75rem; } @media (min-width: 576px) { .card-columns { -webkit-column-count: 3; -moz-column-count: 3; column-count: 3; -webkit-column-gap: 1.25rem; -moz-column-gap: 1.25rem; column-gap: 1.25rem; orphans: 1; widows: 1; } .card-columns .card { display: inline-block; width: 100%; } } .accordion > .card { overflow: hidden; } .accordion > .card:not(:last-of-type) { border-bottom: 0; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .accordion > .card:not(:first-of-type) { border-top-left-radius: 0; border-top-right-radius: 0; } .accordion > .card > .card-header { border-radius: 0; margin-bottom: -1px; } .breadcrumb { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; padding: 0.75rem 1rem; margin-bottom: 1rem; list-style: none; background-color: #e9ecef; border-radius: 0.25rem; } .breadcrumb-item { display: -ms-flexbox; display: flex; } .breadcrumb-item + .breadcrumb-item { padding-left: 0.5rem; } .breadcrumb-item + .breadcrumb-item::before { display: inline-block; padding-right: 0.5rem; color: #6c757d; content: "/"; } .breadcrumb-item + .breadcrumb-item:hover::before { text-decoration: underline; } .breadcrumb-item + .breadcrumb-item:hover::before { text-decoration: none; } .breadcrumb-item.active { color: #6c757d; } .pagination { display: -ms-flexbox; display: flex; padding-left: 0; list-style: none; border-radius: 0.25rem; } .page-link { position: relative; display: block; padding: 0.5rem 0.75rem; margin-left: -1px; line-height: 1.25; color: #007bff; background-color: #fff; border: 1px solid #dee2e6; } .page-link:hover { z-index: 2; color: #0056b3; text-decoration: none; background-color: #e9ecef; border-color: #dee2e6; } .page-link:focus { z-index: 3; outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .page-item:first-child .page-link { margin-left: 0; border-top-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem; } .page-item:last-child .page-link { border-top-right-radius: 0.25rem; border-bottom-right-radius: 0.25rem; } .page-item.active .page-link { z-index: 3; color: #fff; background-color: #007bff; border-color: #007bff; } .page-item.disabled .page-link { color: #6c757d; pointer-events: none; cursor: auto; background-color: #fff; border-color: #dee2e6; } .pagination-lg .page-link { padding: 0.75rem 1.5rem; font-size: 1.25rem; line-height: 1.5; } .pagination-lg .page-item:first-child .page-link { border-top-left-radius: 0.3rem; border-bottom-left-radius: 0.3rem; } .pagination-lg .page-item:last-child .page-link { border-top-right-radius: 0.3rem; border-bottom-right-radius: 0.3rem; } .pagination-sm .page-link { padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; } .pagination-sm .page-item:first-child .page-link { border-top-left-radius: 0.2rem; border-bottom-left-radius: 0.2rem; } .pagination-sm .page-item:last-child .page-link { border-top-right-radius: 0.2rem; border-bottom-right-radius: 0.2rem; } .badge { display: inline-block; padding: 0.25em 0.4em; font-size: 75%; font-weight: 700; line-height: 1; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: 0.25rem; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { .badge { transition: none; } } a.badge:hover, a.badge:focus { text-decoration: none; } .badge:empty { display: none; } .btn .badge { position: relative; top: -1px; } .badge-pill { padding-right: 0.6em; padding-left: 0.6em; border-radius: 10rem; } .badge-primary { color: #fff; background-color: #007bff; } a.badge-primary:hover, a.badge-primary:focus { color: #fff; background-color: #0062cc; } a.badge-primary:focus, a.badge-primary.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } .badge-secondary { color: #fff; background-color: #6c757d; } a.badge-secondary:hover, a.badge-secondary:focus { color: #fff; background-color: #545b62; } a.badge-secondary:focus, a.badge-secondary.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } .badge-success { color: #fff; background-color: #28a745; } a.badge-success:hover, a.badge-success:focus { color: #fff; background-color: #1e7e34; } a.badge-success:focus, a.badge-success.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } .badge-info { color: #fff; background-color: #17a2b8; } a.badge-info:hover, a.badge-info:focus { color: #fff; background-color: #117a8b; } a.badge-info:focus, a.badge-info.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } .badge-warning { color: #212529; background-color: #ffc107; } a.badge-warning:hover, a.badge-warning:focus { color: #212529; background-color: #d39e00; } a.badge-warning:focus, a.badge-warning.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } .badge-danger { color: #fff; background-color: #dc3545; } a.badge-danger:hover, a.badge-danger:focus { color: #fff; background-color: #bd2130; } a.badge-danger:focus, a.badge-danger.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } .badge-light { color: #212529; background-color: #f8f9fa; } a.badge-light:hover, a.badge-light:focus { color: #212529; background-color: #dae0e5; } a.badge-light:focus, a.badge-light.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } .badge-dark { color: #fff; background-color: #343a40; } a.badge-dark:hover, a.badge-dark:focus { color: #fff; background-color: #1d2124; } a.badge-dark:focus, a.badge-dark.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .jumbotron { padding: 2rem 1rem; margin-bottom: 2rem; background-color: #e9ecef; border-radius: 0.3rem; } @media (min-width: 576px) { .jumbotron { padding: 4rem 2rem; } } .jumbotron-fluid { padding-right: 0; padding-left: 0; border-radius: 0; } .alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; } .alert-heading { color: inherit; } .alert-link { font-weight: 700; } .alert-dismissible { padding-right: 4rem; } .alert-dismissible .close { position: absolute; top: 0; right: 0; padding: 0.75rem 1.25rem; color: inherit; } .alert-primary { color: #004085; background-color: #cce5ff; border-color: #b8daff; } .alert-primary hr { border-top-color: #9fcdff; } .alert-primary .alert-link { color: #002752; } .alert-secondary { color: #383d41; background-color: #e2e3e5; border-color: #d6d8db; } .alert-secondary hr { border-top-color: #c8cbcf; } .alert-secondary .alert-link { color: #202326; } .alert-success { color: #155724; background-color: #d4edda; border-color: #c3e6cb; } .alert-success hr { border-top-color: #b1dfbb; } .alert-success .alert-link { color: #0b2e13; } .alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; } .alert-info hr { border-top-color: #abdde5; } .alert-info .alert-link { color: #062c33; } .alert-warning { color: #856404; background-color: #fff3cd; border-color: #ffeeba; } .alert-warning hr { border-top-color: #ffe8a1; } .alert-warning .alert-link { color: #533f03; } .alert-danger { color: #721c24; background-color: #f8d7da; border-color: #f5c6cb; } .alert-danger hr { border-top-color: #f1b0b7; } .alert-danger .alert-link { color: #491217; } .alert-light { color: #818182; background-color: #fefefe; border-color: #fdfdfe; } .alert-light hr { border-top-color: #ececf6; } .alert-light .alert-link { color: #686868; } .alert-dark { color: #1b1e21; background-color: #d6d8d9; border-color: #c6c8ca; } .alert-dark hr { border-top-color: #b9bbbe; } .alert-dark .alert-link { color: #040505; } @-webkit-keyframes progress-bar-stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } } @keyframes progress-bar-stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } } .progress { display: -ms-flexbox; display: flex; height: 1rem; overflow: hidden; line-height: 0; font-size: 0.75rem; background-color: #e9ecef; border-radius: 0.25rem; } .progress-bar { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; -ms-flex-pack: center; justify-content: center; overflow: hidden; color: #fff; text-align: center; white-space: nowrap; background-color: #007bff; transition: width 0.6s ease; } @media (prefers-reduced-motion: reduce) { .progress-bar { transition: none; } } .progress-bar-striped { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-size: 1rem 1rem; } .progress-bar-animated { -webkit-animation: progress-bar-stripes 1s linear infinite; animation: progress-bar-stripes 1s linear infinite; } @media (prefers-reduced-motion: reduce) { .progress-bar-animated { -webkit-animation: none; animation: none; } } .media { display: -ms-flexbox; display: flex; -ms-flex-align: start; align-items: flex-start; } .media-body { -ms-flex: 1; flex: 1; } .list-group { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; padding-left: 0; margin-bottom: 0; border-radius: 0.25rem; } .list-group-item-action { width: 100%; color: #495057; text-align: inherit; } .list-group-item-action:hover, .list-group-item-action:focus { z-index: 1; color: #495057; text-decoration: none; background-color: #f8f9fa; } .list-group-item-action:active { color: #212529; background-color: #e9ecef; } .list-group-item { position: relative; display: block; padding: 0.75rem 1.25rem; background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.125); } .list-group-item:first-child { border-top-left-radius: inherit; border-top-right-radius: inherit; } .list-group-item:last-child { border-bottom-right-radius: inherit; border-bottom-left-radius: inherit; } .list-group-item.disabled, .list-group-item:disabled { color: #6c757d; pointer-events: none; background-color: #fff; } .list-group-item.active { z-index: 2; color: #fff; background-color: #007bff; border-color: #007bff; } .list-group-item + .list-group-item { border-top-width: 0; } .list-group-item + .list-group-item.active { margin-top: -1px; border-top-width: 1px; } .list-group-horizontal { -ms-flex-direction: row; flex-direction: row; } .list-group-horizontal > .list-group-item:first-child { border-bottom-left-radius: 0.25rem; border-top-right-radius: 0; } .list-group-horizontal > .list-group-item:last-child { border-top-right-radius: 0.25rem; border-bottom-left-radius: 0; } .list-group-horizontal > .list-group-item.active { margin-top: 0; } .list-group-horizontal > .list-group-item + .list-group-item { border-top-width: 1px; border-left-width: 0; } .list-group-horizontal > .list-group-item + .list-group-item.active { margin-left: -1px; border-left-width: 1px; } @media (min-width: 576px) { .list-group-horizontal-sm { -ms-flex-direction: row; flex-direction: row; } .list-group-horizontal-sm > .list-group-item:first-child { border-bottom-left-radius: 0.25rem; border-top-right-radius: 0; } .list-group-horizontal-sm > .list-group-item:last-child { border-top-right-radius: 0.25rem; border-bottom-left-radius: 0; } .list-group-horizontal-sm > .list-group-item.active { margin-top: 0; } .list-group-horizontal-sm > .list-group-item + .list-group-item { border-top-width: 1px; border-left-width: 0; } .list-group-horizontal-sm > .list-group-item + .list-group-item.active { margin-left: -1px; border-left-width: 1px; } } @media (min-width: 768px) { .list-group-horizontal-md { -ms-flex-direction: row; flex-direction: row; } .list-group-horizontal-md > .list-group-item:first-child { border-bottom-left-radius: 0.25rem; border-top-right-radius: 0; } .list-group-horizontal-md > .list-group-item:last-child { border-top-right-radius: 0.25rem; border-bottom-left-radius: 0; } .list-group-horizontal-md > .list-group-item.active { margin-top: 0; } .list-group-horizontal-md > .list-group-item + .list-group-item { border-top-width: 1px; border-left-width: 0; } .list-group-horizontal-md > .list-group-item + .list-group-item.active { margin-left: -1px; border-left-width: 1px; } } @media (min-width: 992px) { .list-group-horizontal-lg { -ms-flex-direction: row; flex-direction: row; } .list-group-horizontal-lg > .list-group-item:first-child { border-bottom-left-radius: 0.25rem; border-top-right-radius: 0; } .list-group-horizontal-lg > .list-group-item:last-child { border-top-right-radius: 0.25rem; border-bottom-left-radius: 0; } .list-group-horizontal-lg > .list-group-item.active { margin-top: 0; } .list-group-horizontal-lg > .list-group-item + .list-group-item { border-top-width: 1px; border-left-width: 0; } .list-group-horizontal-lg > .list-group-item + .list-group-item.active { margin-left: -1px; border-left-width: 1px; } } @media (min-width: 1200px) { .list-group-horizontal-xl { -ms-flex-direction: row; flex-direction: row; } .list-group-horizontal-xl > .list-group-item:first-child { border-bottom-left-radius: 0.25rem; border-top-right-radius: 0; } .list-group-horizontal-xl > .list-group-item:last-child { border-top-right-radius: 0.25rem; border-bottom-left-radius: 0; } .list-group-horizontal-xl > .list-group-item.active { margin-top: 0; } .list-group-horizontal-xl > .list-group-item + .list-group-item { border-top-width: 1px; border-left-width: 0; } .list-group-horizontal-xl > .list-group-item + .list-group-item.active { margin-left: -1px; border-left-width: 1px; } } .list-group-flush { border-radius: 0; } .list-group-flush > .list-group-item { border-width: 0 0 1px; } .list-group-flush > .list-group-item:last-child { border-bottom-width: 0; } .list-group-item-primary { color: #004085; background-color: #b8daff; } .list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { color: #004085; background-color: #9fcdff; } .list-group-item-primary.list-group-item-action.active { color: #fff; background-color: #004085; border-color: #004085; } .list-group-item-secondary { color: #383d41; background-color: #d6d8db; } .list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { color: #383d41; background-color: #c8cbcf; } .list-group-item-secondary.list-group-item-action.active { color: #fff; background-color: #383d41; border-color: #383d41; } .list-group-item-success { color: #155724; background-color: #c3e6cb; } .list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { color: #155724; background-color: #b1dfbb; } .list-group-item-success.list-group-item-action.active { color: #fff; background-color: #155724; border-color: #155724; } .list-group-item-info { color: #0c5460; background-color: #bee5eb; } .list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { color: #0c5460; background-color: #abdde5; } .list-group-item-info.list-group-item-action.active { color: #fff; background-color: #0c5460; border-color: #0c5460; } .list-group-item-warning { color: #856404; background-color: #ffeeba; } .list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { color: #856404; background-color: #ffe8a1; } .list-group-item-warning.list-group-item-action.active { color: #fff; background-color: #856404; border-color: #856404; } .list-group-item-danger { color: #721c24; background-color: #f5c6cb; } .list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { color: #721c24; background-color: #f1b0b7; } .list-group-item-danger.list-group-item-action.active { color: #fff; background-color: #721c24; border-color: #721c24; } .list-group-item-light { color: #818182; background-color: #fdfdfe; } .list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { color: #818182; background-color: #ececf6; } .list-group-item-light.list-group-item-action.active { color: #fff; background-color: #818182; border-color: #818182; } .list-group-item-dark { color: #1b1e21; background-color: #c6c8ca; } .list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { color: #1b1e21; background-color: #b9bbbe; } .list-group-item-dark.list-group-item-action.active { color: #fff; background-color: #1b1e21; border-color: #1b1e21; } .close { float: right; font-size: 1.5rem; font-weight: 700; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; opacity: .5; } .close:hover { color: #000; text-decoration: none; } .close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { opacity: .75; } button.close { padding: 0; background-color: transparent; border: 0; } a.close.disabled { pointer-events: none; } .toast { max-width: 350px; overflow: hidden; font-size: 0.875rem; background-color: rgba(255, 255, 255, 0.85); background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.1); box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); opacity: 0; border-radius: 0.25rem; } .toast:not(:last-child) { margin-bottom: 0.75rem; } .toast.showing { opacity: 1; } .toast.show { display: block; opacity: 1; } .toast.hide { display: none; } .toast-header { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; padding: 0.25rem 0.75rem; color: #6c757d; background-color: rgba(255, 255, 255, 0.85); background-clip: padding-box; border-bottom: 1px solid rgba(0, 0, 0, 0.05); } .toast-body { padding: 0.75rem; } .modal-open { overflow: hidden; } .modal-open .modal { overflow-x: hidden; overflow-y: auto; } .modal { position: fixed; top: 0; left: 0; z-index: 1050; display: none; width: 100%; height: 100%; overflow: hidden; outline: 0; } .modal-dialog { position: relative; width: auto; margin: 0.5rem; pointer-events: none; } .modal.fade .modal-dialog { transition: -webkit-transform 0.3s ease-out; transition: transform 0.3s ease-out; transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; -webkit-transform: translate(0, -50px); transform: translate(0, -50px); } @media (prefers-reduced-motion: reduce) { .modal.fade .modal-dialog { transition: none; } } .modal.show .modal-dialog { -webkit-transform: none; transform: none; } .modal.modal-static .modal-dialog { -webkit-transform: scale(1.02); transform: scale(1.02); } .modal-dialog-scrollable { display: -ms-flexbox; display: flex; max-height: calc(100% - 1rem); } .modal-dialog-scrollable .modal-content { max-height: calc(100vh - 1rem); overflow: hidden; } .modal-dialog-scrollable .modal-header, .modal-dialog-scrollable .modal-footer { -ms-flex-negative: 0; flex-shrink: 0; } .modal-dialog-scrollable .modal-body { overflow-y: auto; } .modal-dialog-centered { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; min-height: calc(100% - 1rem); } .modal-dialog-centered::before { display: block; height: calc(100vh - 1rem); height: -webkit-min-content; height: -moz-min-content; height: min-content; content: ""; } .modal-dialog-centered.modal-dialog-scrollable { -ms-flex-direction: column; flex-direction: column; -ms-flex-pack: center; justify-content: center; height: 100%; } .modal-dialog-centered.modal-dialog-scrollable .modal-content { max-height: none; } .modal-dialog-centered.modal-dialog-scrollable::before { content: none; } .modal-content { position: relative; display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; width: 100%; pointer-events: auto; background-color: #fff; background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 0.3rem; outline: 0; } .modal-backdrop { position: fixed; top: 0; left: 0; z-index: 1040; width: 100vw; height: 100vh; background-color: #000; } .modal-backdrop.fade { opacity: 0; } .modal-backdrop.show { opacity: 0.5; } .modal-header { display: -ms-flexbox; display: flex; -ms-flex-align: start; align-items: flex-start; -ms-flex-pack: justify; justify-content: space-between; padding: 1rem 1rem; border-bottom: 1px solid #dee2e6; border-top-left-radius: calc(0.3rem - 1px); border-top-right-radius: calc(0.3rem - 1px); } .modal-header .close { padding: 1rem 1rem; margin: -1rem -1rem -1rem auto; } .modal-title { margin-bottom: 0; line-height: 1.5; } .modal-body { position: relative; -ms-flex: 1 1 auto; flex: 1 1 auto; padding: 1rem; } .modal-footer { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-align: center; align-items: center; -ms-flex-pack: end; justify-content: flex-end; padding: 0.75rem; border-top: 1px solid #dee2e6; border-bottom-right-radius: calc(0.3rem - 1px); border-bottom-left-radius: calc(0.3rem - 1px); } .modal-footer > * { margin: 0.25rem; } .modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; } @media (min-width: 576px) { .modal-dialog { max-width: 500px; margin: 1.75rem auto; } .modal-dialog-scrollable { max-height: calc(100% - 3.5rem); } .modal-dialog-scrollable .modal-content { max-height: calc(100vh - 3.5rem); } .modal-dialog-centered { min-height: calc(100% - 3.5rem); } .modal-dialog-centered::before { height: calc(100vh - 3.5rem); height: -webkit-min-content; height: -moz-min-content; height: min-content; } .modal-sm { max-width: 300px; } } @media (min-width: 992px) { .modal-lg, .modal-xl { max-width: 800px; } } @media (min-width: 1200px) { .modal-xl { max-width: 1140px; } } .tooltip { position: absolute; z-index: 1070; display: block; margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-style: normal; font-weight: 400; line-height: 1.5; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; white-space: normal; line-break: auto; font-size: 0.875rem; word-wrap: break-word; opacity: 0; } .tooltip.show { opacity: 0.9; } .tooltip .arrow { position: absolute; display: block; width: 0.8rem; height: 0.4rem; } .tooltip .arrow::before { position: absolute; content: ""; border-color: transparent; border-style: solid; } .bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { padding: 0.4rem 0; } .bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { bottom: 0; } .bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { top: 0; border-width: 0.4rem 0.4rem 0; border-top-color: #000; } .bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { padding: 0 0.4rem; } .bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { left: 0; width: 0.4rem; height: 0.8rem; } .bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { right: 0; border-width: 0.4rem 0.4rem 0.4rem 0; border-right-color: #000; } .bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { padding: 0.4rem 0; } .bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { top: 0; } .bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { bottom: 0; border-width: 0 0.4rem 0.4rem; border-bottom-color: #000; } .bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { padding: 0 0.4rem; } .bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { right: 0; width: 0.4rem; height: 0.8rem; } .bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { left: 0; border-width: 0.4rem 0 0.4rem 0.4rem; border-left-color: #000; } .tooltip-inner { max-width: 200px; padding: 0.25rem 0.5rem; color: #fff; text-align: center; background-color: #000; border-radius: 0.25rem; } .popover { position: absolute; top: 0; left: 0; z-index: 1060; display: block; max-width: 276px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-style: normal; font-weight: 400; line-height: 1.5; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; white-space: normal; line-break: auto; font-size: 0.875rem; word-wrap: break-word; background-color: #fff; background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 0.3rem; } .popover .arrow { position: absolute; display: block; width: 1rem; height: 0.5rem; margin: 0 0.3rem; } .popover .arrow::before, .popover .arrow::after { position: absolute; display: block; content: ""; border-color: transparent; border-style: solid; } .bs-popover-top, .bs-popover-auto[x-placement^="top"] { margin-bottom: 0.5rem; } .bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow { bottom: calc(-0.5rem - 1px); } .bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before { bottom: 0; border-width: 0.5rem 0.5rem 0; border-top-color: rgba(0, 0, 0, 0.25); } .bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after { bottom: 1px; border-width: 0.5rem 0.5rem 0; border-top-color: #fff; } .bs-popover-right, .bs-popover-auto[x-placement^="right"] { margin-left: 0.5rem; } .bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow { left: calc(-0.5rem - 1px); width: 0.5rem; height: 1rem; margin: 0.3rem 0; } .bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before { left: 0; border-width: 0.5rem 0.5rem 0.5rem 0; border-right-color: rgba(0, 0, 0, 0.25); } .bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after { left: 1px; border-width: 0.5rem 0.5rem 0.5rem 0; border-right-color: #fff; } .bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { margin-top: 0.5rem; } .bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow { top: calc(-0.5rem - 1px); } .bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before { top: 0; border-width: 0 0.5rem 0.5rem 0.5rem; border-bottom-color: rgba(0, 0, 0, 0.25); } .bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after { top: 1px; border-width: 0 0.5rem 0.5rem 0.5rem; border-bottom-color: #fff; } .bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { position: absolute; top: 0; left: 50%; display: block; width: 1rem; margin-left: -0.5rem; content: ""; border-bottom: 1px solid #f7f7f7; } .bs-popover-left, .bs-popover-auto[x-placement^="left"] { margin-right: 0.5rem; } .bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow { right: calc(-0.5rem - 1px); width: 0.5rem; height: 1rem; margin: 0.3rem 0; } .bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before { right: 0; border-width: 0.5rem 0 0.5rem 0.5rem; border-left-color: rgba(0, 0, 0, 0.25); } .bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after { right: 1px; border-width: 0.5rem 0 0.5rem 0.5rem; border-left-color: #fff; } .popover-header { padding: 0.5rem 0.75rem; margin-bottom: 0; font-size: 1rem; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-top-left-radius: calc(0.3rem - 1px); border-top-right-radius: calc(0.3rem - 1px); } .popover-header:empty { display: none; } .popover-body { padding: 0.5rem 0.75rem; color: #212529; } .carousel { position: relative; } .carousel.pointer-event { -ms-touch-action: pan-y; touch-action: pan-y; } .carousel-inner { position: relative; width: 100%; overflow: hidden; } .carousel-inner::after { display: block; clear: both; content: ""; } .carousel-item { position: relative; display: none; float: left; width: 100%; margin-right: -100%; -webkit-backface-visibility: hidden; backface-visibility: hidden; transition: -webkit-transform 0.6s ease-in-out; transition: transform 0.6s ease-in-out; transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; } @media (prefers-reduced-motion: reduce) { .carousel-item { transition: none; } } .carousel-item.active, .carousel-item-next, .carousel-item-prev { display: block; } .carousel-item-next:not(.carousel-item-left), .active.carousel-item-right { -webkit-transform: translateX(100%); transform: translateX(100%); } .carousel-item-prev:not(.carousel-item-right), .active.carousel-item-left { -webkit-transform: translateX(-100%); transform: translateX(-100%); } .carousel-fade .carousel-item { opacity: 0; transition-property: opacity; -webkit-transform: none; transform: none; } .carousel-fade .carousel-item.active, .carousel-fade .carousel-item-next.carousel-item-left, .carousel-fade .carousel-item-prev.carousel-item-right { z-index: 1; opacity: 1; } .carousel-fade .active.carousel-item-left, .carousel-fade .active.carousel-item-right { z-index: 0; opacity: 0; transition: opacity 0s 0.6s; } @media (prefers-reduced-motion: reduce) { .carousel-fade .active.carousel-item-left, .carousel-fade .active.carousel-item-right { transition: none; } } .carousel-control-prev, .carousel-control-next { position: absolute; top: 0; bottom: 0; z-index: 1; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; width: 15%; color: #fff; text-align: center; opacity: 0.5; transition: opacity 0.15s ease; } @media (prefers-reduced-motion: reduce) { .carousel-control-prev, .carousel-control-next { transition: none; } } .carousel-control-prev:hover, .carousel-control-prev:focus, .carousel-control-next:hover, .carousel-control-next:focus { color: #fff; text-decoration: none; outline: 0; opacity: 0.9; } .carousel-control-prev { left: 0; } .carousel-control-next { right: 0; } .carousel-control-prev-icon, .carousel-control-next-icon { display: inline-block; width: 20px; height: 20px; background: no-repeat 50% / 100% 100%; } .carousel-control-prev-icon { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e"); } .carousel-control-next-icon { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e"); } .carousel-indicators { position: absolute; right: 0; bottom: 0; left: 0; z-index: 15; display: -ms-flexbox; display: flex; -ms-flex-pack: center; justify-content: center; padding-left: 0; margin-right: 15%; margin-left: 15%; list-style: none; } .carousel-indicators li { box-sizing: content-box; -ms-flex: 0 1 auto; flex: 0 1 auto; width: 30px; height: 3px; margin-right: 3px; margin-left: 3px; text-indent: -999px; cursor: pointer; background-color: #fff; background-clip: padding-box; border-top: 10px solid transparent; border-bottom: 10px solid transparent; opacity: .5; transition: opacity 0.6s ease; } @media (prefers-reduced-motion: reduce) { .carousel-indicators li { transition: none; } } .carousel-indicators .active { opacity: 1; } .carousel-caption { position: absolute; right: 15%; bottom: 20px; left: 15%; z-index: 10; padding-top: 20px; padding-bottom: 20px; color: #fff; text-align: center; } @-webkit-keyframes spinner-border { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes spinner-border { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } .spinner-border { display: inline-block; width: 2rem; height: 2rem; vertical-align: text-bottom; border: 0.25em solid currentColor; border-right-color: transparent; border-radius: 50%; -webkit-animation: spinner-border .75s linear infinite; animation: spinner-border .75s linear infinite; } .spinner-border-sm { width: 1rem; height: 1rem; border-width: 0.2em; } @-webkit-keyframes spinner-grow { 0% { -webkit-transform: scale(0); transform: scale(0); } 50% { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes spinner-grow { 0% { -webkit-transform: scale(0); transform: scale(0); } 50% { opacity: 1; -webkit-transform: none; transform: none; } } .spinner-grow { display: inline-block; width: 2rem; height: 2rem; vertical-align: text-bottom; background-color: currentColor; border-radius: 50%; opacity: 0; -webkit-animation: spinner-grow .75s linear infinite; animation: spinner-grow .75s linear infinite; } .spinner-grow-sm { width: 1rem; height: 1rem; } .align-baseline { vertical-align: baseline !important; } .align-top { vertical-align: top !important; } .align-middle { vertical-align: middle !important; } .align-bottom { vertical-align: bottom !important; } .align-text-bottom { vertical-align: text-bottom !important; } .align-text-top { vertical-align: text-top !important; } .bg-primary { background-color: #007bff !important; } a.bg-primary:hover, a.bg-primary:focus, button.bg-primary:hover, button.bg-primary:focus { background-color: #0062cc !important; } .bg-secondary { background-color: #6c757d !important; } a.bg-secondary:hover, a.bg-secondary:focus, button.bg-secondary:hover, button.bg-secondary:focus { background-color: #545b62 !important; } .bg-success { background-color: #28a745 !important; } a.bg-success:hover, a.bg-success:focus, button.bg-success:hover, button.bg-success:focus { background-color: #1e7e34 !important; } .bg-info { background-color: #17a2b8 !important; } a.bg-info:hover, a.bg-info:focus, button.bg-info:hover, button.bg-info:focus { background-color: #117a8b !important; } .bg-warning { background-color: #ffc107 !important; } a.bg-warning:hover, a.bg-warning:focus, button.bg-warning:hover, button.bg-warning:focus { background-color: #d39e00 !important; } .bg-danger { background-color: #dc3545 !important; } a.bg-danger:hover, a.bg-danger:focus, button.bg-danger:hover, button.bg-danger:focus { background-color: #bd2130 !important; } .bg-light { background-color: #f8f9fa !important; } a.bg-light:hover, a.bg-light:focus, button.bg-light:hover, button.bg-light:focus { background-color: #dae0e5 !important; } .bg-dark { background-color: #343a40 !important; } a.bg-dark:hover, a.bg-dark:focus, button.bg-dark:hover, button.bg-dark:focus { background-color: #1d2124 !important; } .bg-white { background-color: #fff !important; } .bg-transparent { background-color: transparent !important; } .border { border: 1px solid #dee2e6 !important; } .border-top { border-top: 1px solid #dee2e6 !important; } .border-right { border-right: 1px solid #dee2e6 !important; } .border-bottom { border-bottom: 1px solid #dee2e6 !important; } .border-left { border-left: 1px solid #dee2e6 !important; } .border-0 { border: 0 !important; } .border-top-0 { border-top: 0 !important; } .border-right-0 { border-right: 0 !important; } .border-bottom-0 { border-bottom: 0 !important; } .border-left-0 { border-left: 0 !important; } .border-primary { border-color: #007bff !important; } .border-secondary { border-color: #6c757d !important; } .border-success { border-color: #28a745 !important; } .border-info { border-color: #17a2b8 !important; } .border-warning { border-color: #ffc107 !important; } .border-danger { border-color: #dc3545 !important; } .border-light { border-color: #f8f9fa !important; } .border-dark { border-color: #343a40 !important; } .border-white { border-color: #fff !important; } .rounded-sm { border-radius: 0.2rem !important; } .rounded { border-radius: 0.25rem !important; } .rounded-top { border-top-left-radius: 0.25rem !important; border-top-right-radius: 0.25rem !important; } .rounded-right { border-top-right-radius: 0.25rem !important; border-bottom-right-radius: 0.25rem !important; } .rounded-bottom { border-bottom-right-radius: 0.25rem !important; border-bottom-left-radius: 0.25rem !important; } .rounded-left { border-top-left-radius: 0.25rem !important; border-bottom-left-radius: 0.25rem !important; } .rounded-lg { border-radius: 0.3rem !important; } .rounded-circle { border-radius: 50% !important; } .rounded-pill { border-radius: 50rem !important; } .rounded-0 { border-radius: 0 !important; } .clearfix::after { display: block; clear: both; content: ""; } .d-none { display: none !important; } .d-inline { display: inline !important; } .d-inline-block { display: inline-block !important; } .d-block { display: block !important; } .d-table { display: table !important; } .d-table-row { display: table-row !important; } .d-table-cell { display: table-cell !important; } .d-flex { display: -ms-flexbox !important; display: flex !important; } .d-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } @media (min-width: 576px) { .d-sm-none { display: none !important; } .d-sm-inline { display: inline !important; } .d-sm-inline-block { display: inline-block !important; } .d-sm-block { display: block !important; } .d-sm-table { display: table !important; } .d-sm-table-row { display: table-row !important; } .d-sm-table-cell { display: table-cell !important; } .d-sm-flex { display: -ms-flexbox !important; display: flex !important; } .d-sm-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media (min-width: 768px) { .d-md-none { display: none !important; } .d-md-inline { display: inline !important; } .d-md-inline-block { display: inline-block !important; } .d-md-block { display: block !important; } .d-md-table { display: table !important; } .d-md-table-row { display: table-row !important; } .d-md-table-cell { display: table-cell !important; } .d-md-flex { display: -ms-flexbox !important; display: flex !important; } .d-md-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media (min-width: 992px) { .d-lg-none { display: none !important; } .d-lg-inline { display: inline !important; } .d-lg-inline-block { display: inline-block !important; } .d-lg-block { display: block !important; } .d-lg-table { display: table !important; } .d-lg-table-row { display: table-row !important; } .d-lg-table-cell { display: table-cell !important; } .d-lg-flex { display: -ms-flexbox !important; display: flex !important; } .d-lg-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media (min-width: 1200px) { .d-xl-none { display: none !important; } .d-xl-inline { display: inline !important; } .d-xl-inline-block { display: inline-block !important; } .d-xl-block { display: block !important; } .d-xl-table { display: table !important; } .d-xl-table-row { display: table-row !important; } .d-xl-table-cell { display: table-cell !important; } .d-xl-flex { display: -ms-flexbox !important; display: flex !important; } .d-xl-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media print { .d-print-none { display: none !important; } .d-print-inline { display: inline !important; } .d-print-inline-block { display: inline-block !important; } .d-print-block { display: block !important; } .d-print-table { display: table !important; } .d-print-table-row { display: table-row !important; } .d-print-table-cell { display: table-cell !important; } .d-print-flex { display: -ms-flexbox !important; display: flex !important; } .d-print-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } .embed-responsive { position: relative; display: block; width: 100%; padding: 0; overflow: hidden; } .embed-responsive::before { display: block; content: ""; } .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video { position: absolute; top: 0; bottom: 0; left: 0; width: 100%; height: 100%; border: 0; } .embed-responsive-21by9::before { padding-top: 42.857143%; } .embed-responsive-16by9::before { padding-top: 56.25%; } .embed-responsive-4by3::before { padding-top: 75%; } .embed-responsive-1by1::before { padding-top: 100%; } .flex-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } @media (min-width: 576px) { .flex-sm-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-sm-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-sm-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-sm-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-sm-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-sm-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-sm-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-sm-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-sm-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-sm-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-sm-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-sm-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-sm-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-sm-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-sm-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-sm-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-sm-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-sm-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-sm-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-sm-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-sm-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-sm-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-sm-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-sm-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-sm-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-sm-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-sm-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-sm-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-sm-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-sm-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-sm-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-sm-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-sm-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-sm-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } @media (min-width: 768px) { .flex-md-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-md-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-md-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-md-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-md-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-md-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-md-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-md-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-md-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-md-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-md-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-md-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-md-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-md-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-md-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-md-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-md-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-md-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-md-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-md-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-md-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-md-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-md-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-md-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-md-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-md-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-md-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-md-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-md-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-md-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-md-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-md-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-md-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-md-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } @media (min-width: 992px) { .flex-lg-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-lg-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-lg-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-lg-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-lg-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-lg-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-lg-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-lg-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-lg-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-lg-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-lg-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-lg-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-lg-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-lg-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-lg-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-lg-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-lg-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-lg-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-lg-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-lg-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-lg-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-lg-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-lg-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-lg-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-lg-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-lg-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-lg-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-lg-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-lg-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-lg-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-lg-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-lg-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-lg-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-lg-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } @media (min-width: 1200px) { .flex-xl-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-xl-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-xl-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-xl-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-xl-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-xl-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-xl-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-xl-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-xl-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-xl-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-xl-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-xl-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-xl-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-xl-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-xl-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-xl-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-xl-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-xl-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-xl-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-xl-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-xl-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-xl-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-xl-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-xl-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-xl-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-xl-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-xl-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-xl-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-xl-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-xl-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-xl-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-xl-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-xl-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-xl-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } .float-left { float: left !important; } .float-right { float: right !important; } .float-none { float: none !important; } @media (min-width: 576px) { .float-sm-left { float: left !important; } .float-sm-right { float: right !important; } .float-sm-none { float: none !important; } } @media (min-width: 768px) { .float-md-left { float: left !important; } .float-md-right { float: right !important; } .float-md-none { float: none !important; } } @media (min-width: 992px) { .float-lg-left { float: left !important; } .float-lg-right { float: right !important; } .float-lg-none { float: none !important; } } @media (min-width: 1200px) { .float-xl-left { float: left !important; } .float-xl-right { float: right !important; } .float-xl-none { float: none !important; } } .user-select-all { -webkit-user-select: all !important; -moz-user-select: all !important; -ms-user-select: all !important; user-select: all !important; } .user-select-auto { -webkit-user-select: auto !important; -moz-user-select: auto !important; -ms-user-select: auto !important; user-select: auto !important; } .user-select-none { -webkit-user-select: none !important; -moz-user-select: none !important; -ms-user-select: none !important; user-select: none !important; } .overflow-auto { overflow: auto !important; } .overflow-hidden { overflow: hidden !important; } .position-static { position: static !important; } .position-relative { position: relative !important; } .position-absolute { position: absolute !important; } .position-fixed { position: fixed !important; } .position-sticky { position: -webkit-sticky !important; position: sticky !important; } .fixed-top { position: fixed; top: 0; right: 0; left: 0; z-index: 1030; } .fixed-bottom { position: fixed; right: 0; bottom: 0; left: 0; z-index: 1030; } @supports ((position: -webkit-sticky) or (position: sticky)) { .sticky-top { position: -webkit-sticky; position: sticky; top: 0; z-index: 1020; } } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .sr-only-focusable:active, .sr-only-focusable:focus { position: static; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; } .shadow-sm { box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; } .shadow { box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; } .shadow-lg { box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; } .shadow-none { box-shadow: none !important; } .w-25 { width: 25% !important; } .w-50 { width: 50% !important; } .w-75 { width: 75% !important; } .w-100 { width: 100% !important; } .w-auto { width: auto !important; } .h-25 { height: 25% !important; } .h-50 { height: 50% !important; } .h-75 { height: 75% !important; } .h-100 { height: 100% !important; } .h-auto { height: auto !important; } .mw-100 { max-width: 100% !important; } .mh-100 { max-height: 100% !important; } .min-vw-100 { min-width: 100vw !important; } .min-vh-100 { min-height: 100vh !important; } .vw-100 { width: 100vw !important; } .vh-100 { height: 100vh !important; } .m-0 { margin: 0 !important; } .mt-0, .my-0 { margin-top: 0 !important; } .mr-0, .mx-0 { margin-right: 0 !important; } .mb-0, .my-0 { margin-bottom: 0 !important; } .ml-0, .mx-0 { margin-left: 0 !important; } .m-1 { margin: 0.25rem !important; } .mt-1, .my-1 { margin-top: 0.25rem !important; } .mr-1, .mx-1 { margin-right: 0.25rem !important; } .mb-1, .my-1 { margin-bottom: 0.25rem !important; } .ml-1, .mx-1 { margin-left: 0.25rem !important; } .m-2 { margin: 0.5rem !important; } .mt-2, .my-2 { margin-top: 0.5rem !important; } .mr-2, .mx-2 { margin-right: 0.5rem !important; } .mb-2, .my-2 { margin-bottom: 0.5rem !important; } .ml-2, .mx-2 { margin-left: 0.5rem !important; } .m-3 { margin: 1rem !important; } .mt-3, .my-3 { margin-top: 1rem !important; } .mr-3, .mx-3 { margin-right: 1rem !important; } .mb-3, .my-3 { margin-bottom: 1rem !important; } .ml-3, .mx-3 { margin-left: 1rem !important; } .m-4 { margin: 1.5rem !important; } .mt-4, .my-4 { margin-top: 1.5rem !important; } .mr-4, .mx-4 { margin-right: 1.5rem !important; } .mb-4, .my-4 { margin-bottom: 1.5rem !important; } .ml-4, .mx-4 { margin-left: 1.5rem !important; } .m-5 { margin: 3rem !important; } .mt-5, .my-5 { margin-top: 3rem !important; } .mr-5, .mx-5 { margin-right: 3rem !important; } .mb-5, .my-5 { margin-bottom: 3rem !important; } .ml-5, .mx-5 { margin-left: 3rem !important; } .p-0 { padding: 0 !important; } .pt-0, .py-0 { padding-top: 0 !important; } .pr-0, .px-0 { padding-right: 0 !important; } .pb-0, .py-0 { padding-bottom: 0 !important; } .pl-0, .px-0 { padding-left: 0 !important; } .p-1 { padding: 0.25rem !important; } .pt-1, .py-1 { padding-top: 0.25rem !important; } .pr-1, .px-1 { padding-right: 0.25rem !important; } .pb-1, .py-1 { padding-bottom: 0.25rem !important; } .pl-1, .px-1 { padding-left: 0.25rem !important; } .p-2 { padding: 0.5rem !important; } .pt-2, .py-2 { padding-top: 0.5rem !important; } .pr-2, .px-2 { padding-right: 0.5rem !important; } .pb-2, .py-2 { padding-bottom: 0.5rem !important; } .pl-2, .px-2 { padding-left: 0.5rem !important; } .p-3 { padding: 1rem !important; } .pt-3, .py-3 { padding-top: 1rem !important; } .pr-3, .px-3 { padding-right: 1rem !important; } .pb-3, .py-3 { padding-bottom: 1rem !important; } .pl-3, .px-3 { padding-left: 1rem !important; } .p-4 { padding: 1.5rem !important; } .pt-4, .py-4 { padding-top: 1.5rem !important; } .pr-4, .px-4 { padding-right: 1.5rem !important; } .pb-4, .py-4 { padding-bottom: 1.5rem !important; } .pl-4, .px-4 { padding-left: 1.5rem !important; } .p-5 { padding: 3rem !important; } .pt-5, .py-5 { padding-top: 3rem !important; } .pr-5, .px-5 { padding-right: 3rem !important; } .pb-5, .py-5 { padding-bottom: 3rem !important; } .pl-5, .px-5 { padding-left: 3rem !important; } .m-n1 { margin: -0.25rem !important; } .mt-n1, .my-n1 { margin-top: -0.25rem !important; } .mr-n1, .mx-n1 { margin-right: -0.25rem !important; } .mb-n1, .my-n1 { margin-bottom: -0.25rem !important; } .ml-n1, .mx-n1 { margin-left: -0.25rem !important; } .m-n2 { margin: -0.5rem !important; } .mt-n2, .my-n2 { margin-top: -0.5rem !important; } .mr-n2, .mx-n2 { margin-right: -0.5rem !important; } .mb-n2, .my-n2 { margin-bottom: -0.5rem !important; } .ml-n2, .mx-n2 { margin-left: -0.5rem !important; } .m-n3 { margin: -1rem !important; } .mt-n3, .my-n3 { margin-top: -1rem !important; } .mr-n3, .mx-n3 { margin-right: -1rem !important; } .mb-n3, .my-n3 { margin-bottom: -1rem !important; } .ml-n3, .mx-n3 { margin-left: -1rem !important; } .m-n4 { margin: -1.5rem !important; } .mt-n4, .my-n4 { margin-top: -1.5rem !important; } .mr-n4, .mx-n4 { margin-right: -1.5rem !important; } .mb-n4, .my-n4 { margin-bottom: -1.5rem !important; } .ml-n4, .mx-n4 { margin-left: -1.5rem !important; } .m-n5 { margin: -3rem !important; } .mt-n5, .my-n5 { margin-top: -3rem !important; } .mr-n5, .mx-n5 { margin-right: -3rem !important; } .mb-n5, .my-n5 { margin-bottom: -3rem !important; } .ml-n5, .mx-n5 { margin-left: -3rem !important; } .m-auto { margin: auto !important; } .mt-auto, .my-auto { margin-top: auto !important; } .mr-auto, .mx-auto { margin-right: auto !important; } .mb-auto, .my-auto { margin-bottom: auto !important; } .ml-auto, .mx-auto { margin-left: auto !important; } @media (min-width: 576px) { .m-sm-0 { margin: 0 !important; } .mt-sm-0, .my-sm-0 { margin-top: 0 !important; } .mr-sm-0, .mx-sm-0 { margin-right: 0 !important; } .mb-sm-0, .my-sm-0 { margin-bottom: 0 !important; } .ml-sm-0, .mx-sm-0 { margin-left: 0 !important; } .m-sm-1 { margin: 0.25rem !important; } .mt-sm-1, .my-sm-1 { margin-top: 0.25rem !important; } .mr-sm-1, .mx-sm-1 { margin-right: 0.25rem !important; } .mb-sm-1, .my-sm-1 { margin-bottom: 0.25rem !important; } .ml-sm-1, .mx-sm-1 { margin-left: 0.25rem !important; } .m-sm-2 { margin: 0.5rem !important; } .mt-sm-2, .my-sm-2 { margin-top: 0.5rem !important; } .mr-sm-2, .mx-sm-2 { margin-right: 0.5rem !important; } .mb-sm-2, .my-sm-2 { margin-bottom: 0.5rem !important; } .ml-sm-2, .mx-sm-2 { margin-left: 0.5rem !important; } .m-sm-3 { margin: 1rem !important; } .mt-sm-3, .my-sm-3 { margin-top: 1rem !important; } .mr-sm-3, .mx-sm-3 { margin-right: 1rem !important; } .mb-sm-3, .my-sm-3 { margin-bottom: 1rem !important; } .ml-sm-3, .mx-sm-3 { margin-left: 1rem !important; } .m-sm-4 { margin: 1.5rem !important; } .mt-sm-4, .my-sm-4 { margin-top: 1.5rem !important; } .mr-sm-4, .mx-sm-4 { margin-right: 1.5rem !important; } .mb-sm-4, .my-sm-4 { margin-bottom: 1.5rem !important; } .ml-sm-4, .mx-sm-4 { margin-left: 1.5rem !important; } .m-sm-5 { margin: 3rem !important; } .mt-sm-5, .my-sm-5 { margin-top: 3rem !important; } .mr-sm-5, .mx-sm-5 { margin-right: 3rem !important; } .mb-sm-5, .my-sm-5 { margin-bottom: 3rem !important; } .ml-sm-5, .mx-sm-5 { margin-left: 3rem !important; } .p-sm-0 { padding: 0 !important; } .pt-sm-0, .py-sm-0 { padding-top: 0 !important; } .pr-sm-0, .px-sm-0 { padding-right: 0 !important; } .pb-sm-0, .py-sm-0 { padding-bottom: 0 !important; } .pl-sm-0, .px-sm-0 { padding-left: 0 !important; } .p-sm-1 { padding: 0.25rem !important; } .pt-sm-1, .py-sm-1 { padding-top: 0.25rem !important; } .pr-sm-1, .px-sm-1 { padding-right: 0.25rem !important; } .pb-sm-1, .py-sm-1 { padding-bottom: 0.25rem !important; } .pl-sm-1, .px-sm-1 { padding-left: 0.25rem !important; } .p-sm-2 { padding: 0.5rem !important; } .pt-sm-2, .py-sm-2 { padding-top: 0.5rem !important; } .pr-sm-2, .px-sm-2 { padding-right: 0.5rem !important; } .pb-sm-2, .py-sm-2 { padding-bottom: 0.5rem !important; } .pl-sm-2, .px-sm-2 { padding-left: 0.5rem !important; } .p-sm-3 { padding: 1rem !important; } .pt-sm-3, .py-sm-3 { padding-top: 1rem !important; } .pr-sm-3, .px-sm-3 { padding-right: 1rem !important; } .pb-sm-3, .py-sm-3 { padding-bottom: 1rem !important; } .pl-sm-3, .px-sm-3 { padding-left: 1rem !important; } .p-sm-4 { padding: 1.5rem !important; } .pt-sm-4, .py-sm-4 { padding-top: 1.5rem !important; } .pr-sm-4, .px-sm-4 { padding-right: 1.5rem !important; } .pb-sm-4, .py-sm-4 { padding-bottom: 1.5rem !important; } .pl-sm-4, .px-sm-4 { padding-left: 1.5rem !important; } .p-sm-5 { padding: 3rem !important; } .pt-sm-5, .py-sm-5 { padding-top: 3rem !important; } .pr-sm-5, .px-sm-5 { padding-right: 3rem !important; } .pb-sm-5, .py-sm-5 { padding-bottom: 3rem !important; } .pl-sm-5, .px-sm-5 { padding-left: 3rem !important; } .m-sm-n1 { margin: -0.25rem !important; } .mt-sm-n1, .my-sm-n1 { margin-top: -0.25rem !important; } .mr-sm-n1, .mx-sm-n1 { margin-right: -0.25rem !important; } .mb-sm-n1, .my-sm-n1 { margin-bottom: -0.25rem !important; } .ml-sm-n1, .mx-sm-n1 { margin-left: -0.25rem !important; } .m-sm-n2 { margin: -0.5rem !important; } .mt-sm-n2, .my-sm-n2 { margin-top: -0.5rem !important; } .mr-sm-n2, .mx-sm-n2 { margin-right: -0.5rem !important; } .mb-sm-n2, .my-sm-n2 { margin-bottom: -0.5rem !important; } .ml-sm-n2, .mx-sm-n2 { margin-left: -0.5rem !important; } .m-sm-n3 { margin: -1rem !important; } .mt-sm-n3, .my-sm-n3 { margin-top: -1rem !important; } .mr-sm-n3, .mx-sm-n3 { margin-right: -1rem !important; } .mb-sm-n3, .my-sm-n3 { margin-bottom: -1rem !important; } .ml-sm-n3, .mx-sm-n3 { margin-left: -1rem !important; } .m-sm-n4 { margin: -1.5rem !important; } .mt-sm-n4, .my-sm-n4 { margin-top: -1.5rem !important; } .mr-sm-n4, .mx-sm-n4 { margin-right: -1.5rem !important; } .mb-sm-n4, .my-sm-n4 { margin-bottom: -1.5rem !important; } .ml-sm-n4, .mx-sm-n4 { margin-left: -1.5rem !important; } .m-sm-n5 { margin: -3rem !important; } .mt-sm-n5, .my-sm-n5 { margin-top: -3rem !important; } .mr-sm-n5, .mx-sm-n5 { margin-right: -3rem !important; } .mb-sm-n5, .my-sm-n5 { margin-bottom: -3rem !important; } .ml-sm-n5, .mx-sm-n5 { margin-left: -3rem !important; } .m-sm-auto { margin: auto !important; } .mt-sm-auto, .my-sm-auto { margin-top: auto !important; } .mr-sm-auto, .mx-sm-auto { margin-right: auto !important; } .mb-sm-auto, .my-sm-auto { margin-bottom: auto !important; } .ml-sm-auto, .mx-sm-auto { margin-left: auto !important; } } @media (min-width: 768px) { .m-md-0 { margin: 0 !important; } .mt-md-0, .my-md-0 { margin-top: 0 !important; } .mr-md-0, .mx-md-0 { margin-right: 0 !important; } .mb-md-0, .my-md-0 { margin-bottom: 0 !important; } .ml-md-0, .mx-md-0 { margin-left: 0 !important; } .m-md-1 { margin: 0.25rem !important; } .mt-md-1, .my-md-1 { margin-top: 0.25rem !important; } .mr-md-1, .mx-md-1 { margin-right: 0.25rem !important; } .mb-md-1, .my-md-1 { margin-bottom: 0.25rem !important; } .ml-md-1, .mx-md-1 { margin-left: 0.25rem !important; } .m-md-2 { margin: 0.5rem !important; } .mt-md-2, .my-md-2 { margin-top: 0.5rem !important; } .mr-md-2, .mx-md-2 { margin-right: 0.5rem !important; } .mb-md-2, .my-md-2 { margin-bottom: 0.5rem !important; } .ml-md-2, .mx-md-2 { margin-left: 0.5rem !important; } .m-md-3 { margin: 1rem !important; } .mt-md-3, .my-md-3 { margin-top: 1rem !important; } .mr-md-3, .mx-md-3 { margin-right: 1rem !important; } .mb-md-3, .my-md-3 { margin-bottom: 1rem !important; } .ml-md-3, .mx-md-3 { margin-left: 1rem !important; } .m-md-4 { margin: 1.5rem !important; } .mt-md-4, .my-md-4 { margin-top: 1.5rem !important; } .mr-md-4, .mx-md-4 { margin-right: 1.5rem !important; } .mb-md-4, .my-md-4 { margin-bottom: 1.5rem !important; } .ml-md-4, .mx-md-4 { margin-left: 1.5rem !important; } .m-md-5 { margin: 3rem !important; } .mt-md-5, .my-md-5 { margin-top: 3rem !important; } .mr-md-5, .mx-md-5 { margin-right: 3rem !important; } .mb-md-5, .my-md-5 { margin-bottom: 3rem !important; } .ml-md-5, .mx-md-5 { margin-left: 3rem !important; } .p-md-0 { padding: 0 !important; } .pt-md-0, .py-md-0 { padding-top: 0 !important; } .pr-md-0, .px-md-0 { padding-right: 0 !important; } .pb-md-0, .py-md-0 { padding-bottom: 0 !important; } .pl-md-0, .px-md-0 { padding-left: 0 !important; } .p-md-1 { padding: 0.25rem !important; } .pt-md-1, .py-md-1 { padding-top: 0.25rem !important; } .pr-md-1, .px-md-1 { padding-right: 0.25rem !important; } .pb-md-1, .py-md-1 { padding-bottom: 0.25rem !important; } .pl-md-1, .px-md-1 { padding-left: 0.25rem !important; } .p-md-2 { padding: 0.5rem !important; } .pt-md-2, .py-md-2 { padding-top: 0.5rem !important; } .pr-md-2, .px-md-2 { padding-right: 0.5rem !important; } .pb-md-2, .py-md-2 { padding-bottom: 0.5rem !important; } .pl-md-2, .px-md-2 { padding-left: 0.5rem !important; } .p-md-3 { padding: 1rem !important; } .pt-md-3, .py-md-3 { padding-top: 1rem !important; } .pr-md-3, .px-md-3 { padding-right: 1rem !important; } .pb-md-3, .py-md-3 { padding-bottom: 1rem !important; } .pl-md-3, .px-md-3 { padding-left: 1rem !important; } .p-md-4 { padding: 1.5rem !important; } .pt-md-4, .py-md-4 { padding-top: 1.5rem !important; } .pr-md-4, .px-md-4 { padding-right: 1.5rem !important; } .pb-md-4, .py-md-4 { padding-bottom: 1.5rem !important; } .pl-md-4, .px-md-4 { padding-left: 1.5rem !important; } .p-md-5 { padding: 3rem !important; } .pt-md-5, .py-md-5 { padding-top: 3rem !important; } .pr-md-5, .px-md-5 { padding-right: 3rem !important; } .pb-md-5, .py-md-5 { padding-bottom: 3rem !important; } .pl-md-5, .px-md-5 { padding-left: 3rem !important; } .m-md-n1 { margin: -0.25rem !important; } .mt-md-n1, .my-md-n1 { margin-top: -0.25rem !important; } .mr-md-n1, .mx-md-n1 { margin-right: -0.25rem !important; } .mb-md-n1, .my-md-n1 { margin-bottom: -0.25rem !important; } .ml-md-n1, .mx-md-n1 { margin-left: -0.25rem !important; } .m-md-n2 { margin: -0.5rem !important; } .mt-md-n2, .my-md-n2 { margin-top: -0.5rem !important; } .mr-md-n2, .mx-md-n2 { margin-right: -0.5rem !important; } .mb-md-n2, .my-md-n2 { margin-bottom: -0.5rem !important; } .ml-md-n2, .mx-md-n2 { margin-left: -0.5rem !important; } .m-md-n3 { margin: -1rem !important; } .mt-md-n3, .my-md-n3 { margin-top: -1rem !important; } .mr-md-n3, .mx-md-n3 { margin-right: -1rem !important; } .mb-md-n3, .my-md-n3 { margin-bottom: -1rem !important; } .ml-md-n3, .mx-md-n3 { margin-left: -1rem !important; } .m-md-n4 { margin: -1.5rem !important; } .mt-md-n4, .my-md-n4 { margin-top: -1.5rem !important; } .mr-md-n4, .mx-md-n4 { margin-right: -1.5rem !important; } .mb-md-n4, .my-md-n4 { margin-bottom: -1.5rem !important; } .ml-md-n4, .mx-md-n4 { margin-left: -1.5rem !important; } .m-md-n5 { margin: -3rem !important; } .mt-md-n5, .my-md-n5 { margin-top: -3rem !important; } .mr-md-n5, .mx-md-n5 { margin-right: -3rem !important; } .mb-md-n5, .my-md-n5 { margin-bottom: -3rem !important; } .ml-md-n5, .mx-md-n5 { margin-left: -3rem !important; } .m-md-auto { margin: auto !important; } .mt-md-auto, .my-md-auto { margin-top: auto !important; } .mr-md-auto, .mx-md-auto { margin-right: auto !important; } .mb-md-auto, .my-md-auto { margin-bottom: auto !important; } .ml-md-auto, .mx-md-auto { margin-left: auto !important; } } @media (min-width: 992px) { .m-lg-0 { margin: 0 !important; } .mt-lg-0, .my-lg-0 { margin-top: 0 !important; } .mr-lg-0, .mx-lg-0 { margin-right: 0 !important; } .mb-lg-0, .my-lg-0 { margin-bottom: 0 !important; } .ml-lg-0, .mx-lg-0 { margin-left: 0 !important; } .m-lg-1 { margin: 0.25rem !important; } .mt-lg-1, .my-lg-1 { margin-top: 0.25rem !important; } .mr-lg-1, .mx-lg-1 { margin-right: 0.25rem !important; } .mb-lg-1, .my-lg-1 { margin-bottom: 0.25rem !important; } .ml-lg-1, .mx-lg-1 { margin-left: 0.25rem !important; } .m-lg-2 { margin: 0.5rem !important; } .mt-lg-2, .my-lg-2 { margin-top: 0.5rem !important; } .mr-lg-2, .mx-lg-2 { margin-right: 0.5rem !important; } .mb-lg-2, .my-lg-2 { margin-bottom: 0.5rem !important; } .ml-lg-2, .mx-lg-2 { margin-left: 0.5rem !important; } .m-lg-3 { margin: 1rem !important; } .mt-lg-3, .my-lg-3 { margin-top: 1rem !important; } .mr-lg-3, .mx-lg-3 { margin-right: 1rem !important; } .mb-lg-3, .my-lg-3 { margin-bottom: 1rem !important; } .ml-lg-3, .mx-lg-3 { margin-left: 1rem !important; } .m-lg-4 { margin: 1.5rem !important; } .mt-lg-4, .my-lg-4 { margin-top: 1.5rem !important; } .mr-lg-4, .mx-lg-4 { margin-right: 1.5rem !important; } .mb-lg-4, .my-lg-4 { margin-bottom: 1.5rem !important; } .ml-lg-4, .mx-lg-4 { margin-left: 1.5rem !important; } .m-lg-5 { margin: 3rem !important; } .mt-lg-5, .my-lg-5 { margin-top: 3rem !important; } .mr-lg-5, .mx-lg-5 { margin-right: 3rem !important; } .mb-lg-5, .my-lg-5 { margin-bottom: 3rem !important; } .ml-lg-5, .mx-lg-5 { margin-left: 3rem !important; } .p-lg-0 { padding: 0 !important; } .pt-lg-0, .py-lg-0 { padding-top: 0 !important; } .pr-lg-0, .px-lg-0 { padding-right: 0 !important; } .pb-lg-0, .py-lg-0 { padding-bottom: 0 !important; } .pl-lg-0, .px-lg-0 { padding-left: 0 !important; } .p-lg-1 { padding: 0.25rem !important; } .pt-lg-1, .py-lg-1 { padding-top: 0.25rem !important; } .pr-lg-1, .px-lg-1 { padding-right: 0.25rem !important; } .pb-lg-1, .py-lg-1 { padding-bottom: 0.25rem !important; } .pl-lg-1, .px-lg-1 { padding-left: 0.25rem !important; } .p-lg-2 { padding: 0.5rem !important; } .pt-lg-2, .py-lg-2 { padding-top: 0.5rem !important; } .pr-lg-2, .px-lg-2 { padding-right: 0.5rem !important; } .pb-lg-2, .py-lg-2 { padding-bottom: 0.5rem !important; } .pl-lg-2, .px-lg-2 { padding-left: 0.5rem !important; } .p-lg-3 { padding: 1rem !important; } .pt-lg-3, .py-lg-3 { padding-top: 1rem !important; } .pr-lg-3, .px-lg-3 { padding-right: 1rem !important; } .pb-lg-3, .py-lg-3 { padding-bottom: 1rem !important; } .pl-lg-3, .px-lg-3 { padding-left: 1rem !important; } .p-lg-4 { padding: 1.5rem !important; } .pt-lg-4, .py-lg-4 { padding-top: 1.5rem !important; } .pr-lg-4, .px-lg-4 { padding-right: 1.5rem !important; } .pb-lg-4, .py-lg-4 { padding-bottom: 1.5rem !important; } .pl-lg-4, .px-lg-4 { padding-left: 1.5rem !important; } .p-lg-5 { padding: 3rem !important; } .pt-lg-5, .py-lg-5 { padding-top: 3rem !important; } .pr-lg-5, .px-lg-5 { padding-right: 3rem !important; } .pb-lg-5, .py-lg-5 { padding-bottom: 3rem !important; } .pl-lg-5, .px-lg-5 { padding-left: 3rem !important; } .m-lg-n1 { margin: -0.25rem !important; } .mt-lg-n1, .my-lg-n1 { margin-top: -0.25rem !important; } .mr-lg-n1, .mx-lg-n1 { margin-right: -0.25rem !important; } .mb-lg-n1, .my-lg-n1 { margin-bottom: -0.25rem !important; } .ml-lg-n1, .mx-lg-n1 { margin-left: -0.25rem !important; } .m-lg-n2 { margin: -0.5rem !important; } .mt-lg-n2, .my-lg-n2 { margin-top: -0.5rem !important; } .mr-lg-n2, .mx-lg-n2 { margin-right: -0.5rem !important; } .mb-lg-n2, .my-lg-n2 { margin-bottom: -0.5rem !important; } .ml-lg-n2, .mx-lg-n2 { margin-left: -0.5rem !important; } .m-lg-n3 { margin: -1rem !important; } .mt-lg-n3, .my-lg-n3 { margin-top: -1rem !important; } .mr-lg-n3, .mx-lg-n3 { margin-right: -1rem !important; } .mb-lg-n3, .my-lg-n3 { margin-bottom: -1rem !important; } .ml-lg-n3, .mx-lg-n3 { margin-left: -1rem !important; } .m-lg-n4 { margin: -1.5rem !important; } .mt-lg-n4, .my-lg-n4 { margin-top: -1.5rem !important; } .mr-lg-n4, .mx-lg-n4 { margin-right: -1.5rem !important; } .mb-lg-n4, .my-lg-n4 { margin-bottom: -1.5rem !important; } .ml-lg-n4, .mx-lg-n4 { margin-left: -1.5rem !important; } .m-lg-n5 { margin: -3rem !important; } .mt-lg-n5, .my-lg-n5 { margin-top: -3rem !important; } .mr-lg-n5, .mx-lg-n5 { margin-right: -3rem !important; } .mb-lg-n5, .my-lg-n5 { margin-bottom: -3rem !important; } .ml-lg-n5, .mx-lg-n5 { margin-left: -3rem !important; } .m-lg-auto { margin: auto !important; } .mt-lg-auto, .my-lg-auto { margin-top: auto !important; } .mr-lg-auto, .mx-lg-auto { margin-right: auto !important; } .mb-lg-auto, .my-lg-auto { margin-bottom: auto !important; } .ml-lg-auto, .mx-lg-auto { margin-left: auto !important; } } @media (min-width: 1200px) { .m-xl-0 { margin: 0 !important; } .mt-xl-0, .my-xl-0 { margin-top: 0 !important; } .mr-xl-0, .mx-xl-0 { margin-right: 0 !important; } .mb-xl-0, .my-xl-0 { margin-bottom: 0 !important; } .ml-xl-0, .mx-xl-0 { margin-left: 0 !important; } .m-xl-1 { margin: 0.25rem !important; } .mt-xl-1, .my-xl-1 { margin-top: 0.25rem !important; } .mr-xl-1, .mx-xl-1 { margin-right: 0.25rem !important; } .mb-xl-1, .my-xl-1 { margin-bottom: 0.25rem !important; } .ml-xl-1, .mx-xl-1 { margin-left: 0.25rem !important; } .m-xl-2 { margin: 0.5rem !important; } .mt-xl-2, .my-xl-2 { margin-top: 0.5rem !important; } .mr-xl-2, .mx-xl-2 { margin-right: 0.5rem !important; } .mb-xl-2, .my-xl-2 { margin-bottom: 0.5rem !important; } .ml-xl-2, .mx-xl-2 { margin-left: 0.5rem !important; } .m-xl-3 { margin: 1rem !important; } .mt-xl-3, .my-xl-3 { margin-top: 1rem !important; } .mr-xl-3, .mx-xl-3 { margin-right: 1rem !important; } .mb-xl-3, .my-xl-3 { margin-bottom: 1rem !important; } .ml-xl-3, .mx-xl-3 { margin-left: 1rem !important; } .m-xl-4 { margin: 1.5rem !important; } .mt-xl-4, .my-xl-4 { margin-top: 1.5rem !important; } .mr-xl-4, .mx-xl-4 { margin-right: 1.5rem !important; } .mb-xl-4, .my-xl-4 { margin-bottom: 1.5rem !important; } .ml-xl-4, .mx-xl-4 { margin-left: 1.5rem !important; } .m-xl-5 { margin: 3rem !important; } .mt-xl-5, .my-xl-5 { margin-top: 3rem !important; } .mr-xl-5, .mx-xl-5 { margin-right: 3rem !important; } .mb-xl-5, .my-xl-5 { margin-bottom: 3rem !important; } .ml-xl-5, .mx-xl-5 { margin-left: 3rem !important; } .p-xl-0 { padding: 0 !important; } .pt-xl-0, .py-xl-0 { padding-top: 0 !important; } .pr-xl-0, .px-xl-0 { padding-right: 0 !important; } .pb-xl-0, .py-xl-0 { padding-bottom: 0 !important; } .pl-xl-0, .px-xl-0 { padding-left: 0 !important; } .p-xl-1 { padding: 0.25rem !important; } .pt-xl-1, .py-xl-1 { padding-top: 0.25rem !important; } .pr-xl-1, .px-xl-1 { padding-right: 0.25rem !important; } .pb-xl-1, .py-xl-1 { padding-bottom: 0.25rem !important; } .pl-xl-1, .px-xl-1 { padding-left: 0.25rem !important; } .p-xl-2 { padding: 0.5rem !important; } .pt-xl-2, .py-xl-2 { padding-top: 0.5rem !important; } .pr-xl-2, .px-xl-2 { padding-right: 0.5rem !important; } .pb-xl-2, .py-xl-2 { padding-bottom: 0.5rem !important; } .pl-xl-2, .px-xl-2 { padding-left: 0.5rem !important; } .p-xl-3 { padding: 1rem !important; } .pt-xl-3, .py-xl-3 { padding-top: 1rem !important; } .pr-xl-3, .px-xl-3 { padding-right: 1rem !important; } .pb-xl-3, .py-xl-3 { padding-bottom: 1rem !important; } .pl-xl-3, .px-xl-3 { padding-left: 1rem !important; } .p-xl-4 { padding: 1.5rem !important; } .pt-xl-4, .py-xl-4 { padding-top: 1.5rem !important; } .pr-xl-4, .px-xl-4 { padding-right: 1.5rem !important; } .pb-xl-4, .py-xl-4 { padding-bottom: 1.5rem !important; } .pl-xl-4, .px-xl-4 { padding-left: 1.5rem !important; } .p-xl-5 { padding: 3rem !important; } .pt-xl-5, .py-xl-5 { padding-top: 3rem !important; } .pr-xl-5, .px-xl-5 { padding-right: 3rem !important; } .pb-xl-5, .py-xl-5 { padding-bottom: 3rem !important; } .pl-xl-5, .px-xl-5 { padding-left: 3rem !important; } .m-xl-n1 { margin: -0.25rem !important; } .mt-xl-n1, .my-xl-n1 { margin-top: -0.25rem !important; } .mr-xl-n1, .mx-xl-n1 { margin-right: -0.25rem !important; } .mb-xl-n1, .my-xl-n1 { margin-bottom: -0.25rem !important; } .ml-xl-n1, .mx-xl-n1 { margin-left: -0.25rem !important; } .m-xl-n2 { margin: -0.5rem !important; } .mt-xl-n2, .my-xl-n2 { margin-top: -0.5rem !important; } .mr-xl-n2, .mx-xl-n2 { margin-right: -0.5rem !important; } .mb-xl-n2, .my-xl-n2 { margin-bottom: -0.5rem !important; } .ml-xl-n2, .mx-xl-n2 { margin-left: -0.5rem !important; } .m-xl-n3 { margin: -1rem !important; } .mt-xl-n3, .my-xl-n3 { margin-top: -1rem !important; } .mr-xl-n3, .mx-xl-n3 { margin-right: -1rem !important; } .mb-xl-n3, .my-xl-n3 { margin-bottom: -1rem !important; } .ml-xl-n3, .mx-xl-n3 { margin-left: -1rem !important; } .m-xl-n4 { margin: -1.5rem !important; } .mt-xl-n4, .my-xl-n4 { margin-top: -1.5rem !important; } .mr-xl-n4, .mx-xl-n4 { margin-right: -1.5rem !important; } .mb-xl-n4, .my-xl-n4 { margin-bottom: -1.5rem !important; } .ml-xl-n4, .mx-xl-n4 { margin-left: -1.5rem !important; } .m-xl-n5 { margin: -3rem !important; } .mt-xl-n5, .my-xl-n5 { margin-top: -3rem !important; } .mr-xl-n5, .mx-xl-n5 { margin-right: -3rem !important; } .mb-xl-n5, .my-xl-n5 { margin-bottom: -3rem !important; } .ml-xl-n5, .mx-xl-n5 { margin-left: -3rem !important; } .m-xl-auto { margin: auto !important; } .mt-xl-auto, .my-xl-auto { margin-top: auto !important; } .mr-xl-auto, .mx-xl-auto { margin-right: auto !important; } .mb-xl-auto, .my-xl-auto { margin-bottom: auto !important; } .ml-xl-auto, .mx-xl-auto { margin-left: auto !important; } } .stretched-link::after { position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: 1; pointer-events: auto; content: ""; background-color: rgba(0, 0, 0, 0); } .text-monospace { font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; } .text-justify { text-align: justify !important; } .text-wrap { white-space: normal !important; } .text-nowrap { white-space: nowrap !important; } .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .text-left { text-align: left !important; } .text-right { text-align: right !important; } .text-center { text-align: center !important; } @media (min-width: 576px) { .text-sm-left { text-align: left !important; } .text-sm-right { text-align: right !important; } .text-sm-center { text-align: center !important; } } @media (min-width: 768px) { .text-md-left { text-align: left !important; } .text-md-right { text-align: right !important; } .text-md-center { text-align: center !important; } } @media (min-width: 992px) { .text-lg-left { text-align: left !important; } .text-lg-right { text-align: right !important; } .text-lg-center { text-align: center !important; } } @media (min-width: 1200px) { .text-xl-left { text-align: left !important; } .text-xl-right { text-align: right !important; } .text-xl-center { text-align: center !important; } } .text-lowercase { text-transform: lowercase !important; } .text-uppercase { text-transform: uppercase !important; } .text-capitalize { text-transform: capitalize !important; } .font-weight-light { font-weight: 300 !important; } .font-weight-lighter { font-weight: lighter !important; } .font-weight-normal { font-weight: 400 !important; } .font-weight-bold { font-weight: 700 !important; } .font-weight-bolder { font-weight: bolder !important; } .font-italic { font-style: italic !important; } .text-white { color: #fff !important; } .text-primary { color: #007bff !important; } a.text-primary:hover, a.text-primary:focus { color: #0056b3 !important; } .text-secondary { color: #6c757d !important; } a.text-secondary:hover, a.text-secondary:focus { color: #494f54 !important; } .text-success { color: #28a745 !important; } a.text-success:hover, a.text-success:focus { color: #19692c !important; } .text-info { color: #17a2b8 !important; } a.text-info:hover, a.text-info:focus { color: #0f6674 !important; } .text-warning { color: #ffc107 !important; } a.text-warning:hover, a.text-warning:focus { color: #ba8b00 !important; } .text-danger { color: #dc3545 !important; } a.text-danger:hover, a.text-danger:focus { color: #a71d2a !important; } .text-light { color: #f8f9fa !important; } a.text-light:hover, a.text-light:focus { color: #cbd3da !important; } .text-dark { color: #343a40 !important; } a.text-dark:hover, a.text-dark:focus { color: #121416 !important; } .text-body { color: #212529 !important; } .text-muted { color: #6c757d !important; } .text-black-50 { color: rgba(0, 0, 0, 0.5) !important; } .text-white-50 { color: rgba(255, 255, 255, 0.5) !important; } .text-hide { font: 0/0 a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } .text-decoration-none { text-decoration: none !important; } .text-break { word-wrap: break-word !important; } .text-reset { color: inherit !important; } .visible { visibility: visible !important; } .invisible { visibility: hidden !important; } @media print { *, *::before, *::after { text-shadow: none !important; box-shadow: none !important; } a:not(.btn) { text-decoration: underline; } abbr[title]::after { content: " (" attr(title) ")"; } pre { white-space: pre-wrap !important; } pre, blockquote { border: 1px solid #adb5bd; page-break-inside: avoid; } thead { display: table-header-group; } tr, img { page-break-inside: avoid; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } @page { size: a3; } body { min-width: 992px !important; } .container { min-width: 992px !important; } .navbar { display: none; } .badge { border: 1px solid #000; } .table { border-collapse: collapse !important; } .table td, .table th { background-color: #fff !important; } .table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; } .table-dark { color: inherit; } .table-dark th, .table-dark td, .table-dark thead th, .table-dark tbody + tbody { border-color: #dee2e6; } .table .thead-dark th { color: inherit; border-color: #dee2e6; } } /*# sourceMappingURL=bootstrap.css.map */././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/bootstrap/checkbox.html0000644000175100017510000000103115102145205024747 0ustar00runnerrunner Bootstrap checkbox
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/buttons.html0000644000175100017510000000061215102145205022646 0ustar00runnerrunner Rapid hinting with buttons ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/custom_group.html0000644000175100017510000000034115102145205023675 0ustar00runnerrunner Custom hint groups
beep!
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5796385 qutebrowser-3.6.1/tests/end2end/data/hints/html/0000755000175100017510000000000015102145351021231 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/README.md0000644000175100017510000000051315102145205022505 0ustar00runnerrunnerTests in this directory are automatically picked up by `test_hints` in `tests/end2end/test_hints_html.py`. They need to contain a special `` comment which specifies where the hint in it will point to, and will then test that. With ``, the page is expected to not generate any hints. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/angular1.html0000644000175100017510000000065515102145205023635 0ustar00runnerrunner
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/click_handler.html0000644000175100017510000000067415102145205024706 0ustar00runnerrunner Javascript link ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/invisible.html0000644000175100017510000000071515102145205024104 0ustar00runnerrunner Invisible links

None of those invisible links should get a hint.

visibility: hidden display: none opacity: 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/javascript.html0000644000175100017510000000042415102145205024263 0ustar00runnerrunner Javascript link Follow me via JS! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/nested_block_style.html0000644000175100017510000000053315102145205025772 0ustar00runnerrunner Link containing an element with "display: block" style link ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/nested_formatting_tags.html0000644000175100017510000000062215102145205026647 0ustar00runnerrunner Link containing formatting tags link
link
link
link
link
link
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/nested_table_style.html0000644000175100017510000000053315102145205025767 0ustar00runnerrunner Link containing an element with "display: table" style link ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/shadow_dom.html0000644000175100017510000000065315102145205024245 0ustar00runnerrunner
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/simple.html0000644000175100017510000000034715102145205023412 0ustar00runnerrunner Simple link Follow me! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/tabindex-negative.html0000644000175100017510000000040615102145205025513 0ustar00runnerrunner Span with tabindex -1

This text should not get a hint:

test ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/target_blank_js.html0000644000175100017510000000154115102145205025247 0ustar00runnerrunner Link where we insert target=_blank via JS Follow me! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/with_spaces.html0000644000175100017510000000034315102145205024426 0ustar00runnerrunner Simple link Follow me! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/wrapped.html0000644000175100017510000000140415102145205023556 0ustar00runnerrunner Link wrapped across multiple lines
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/wrapped_button.html0000644000175100017510000000177215102145205025161 0ustar00runnerrunner Button link wrapped across multiple lines
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/html/zoom_precision.html0000644000175100017510000000136215102145205025156 0ustar00runnerrunner Test hinting precision with different zoom levels
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/iframe.html0000644000175100017510000000041315102145205022412 0ustar00runnerrunner Hinting inside an iframe

Some text.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/iframe_button.html0000644000175100017510000000043315102145205024007 0ustar00runnerrunner Hinting a button inside an iframe

Some text.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/iframe_input.html0000644000175100017510000000037115102145205023634 0ustar00runnerrunner Hinting an input field inside an iframe ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/iframe_scroll.html0000644000175100017510000000041015102145205023765 0ustar00runnerrunner Scrolling inside an iframe

Some text.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/iframe_target.html0000644000175100017510000000065315102145205023766 0ustar00runnerrunner Opening links in a specific iframe

A link to be opened in the iframe above. A second link for the iframe.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/input.html0000644000175100017510000000163615102145205022316 0ustar00runnerrunner Simple input
With padding:
With existing text (logs to JS)::
Contenteditable attributes

laythe

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/issue1186.html0000644000175100017510000000230515102145205022621 0ustar00runnerrunner Issue 1186

This page contains 10 hints to test backspace handling, see #1186.

This requires setting hints -> mode to number.

When pressing f, x, 0, Backspace, only hints starting with x should be shown.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/issue1393.html0000644000175100017510000000231615102145205022623 0ustar00runnerrunner Let's Hint some words

Word hints

Smart hints

In qutebrowser, urls can not only be hinted with letters and numbers, but also with words. When there is a sensible url text available, qutebrowser will even use that text to create a smart hint.

Filled hints

When no smart hints are available, because the hint text is too short or l33t to use, words from a dictionary will be used.

Hint conflicts

Of course, hints have to be unique. For instance, all hints below should get a different hint, whether they're smart or not:

  • hinting should be a smart hint
  • word is a prefix of words
  • 3 is too 1337
  • 4 is too 1337
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/issue3711.html0000644000175100017510000000063615102145205022622 0ustar00runnerrunner Issue 3711 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/issue3711_frame.html0000644000175100017510000000031515102145205023766 0ustar00runnerrunner Issue 3771 Parent Frame ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/link_blank.html0000644000175100017510000000034515102145205023257 0ustar00runnerrunner A link to use hints on Follow me! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/link_inject.html0000644000175100017510000000120115102145205023434 0ustar00runnerrunner A link to use hints on Follow me! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/link_input.html0000644000175100017510000000155015102145205023326 0ustar00runnerrunner Simple link and input Follow me!
With padding:
With existing text (logs to JS)::
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/link_span.html0000644000175100017510000000040115102145205023122 0ustar00runnerrunner A link to use hints on Follow me! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/number.html0000644000175100017510000000242515102145205022444 0ustar00runnerrunner Numbered hints

This page contains various links to test numbered hints. This requires setting hints -> mode to number.

Related issues:

  • #308 - Renumber hints when filtering them in number mode.
  • #576 - Keep hint filtering when rapid hinting in number mode
  • #674 (comment) - Multi-word matching for hints

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/rapid.html0000644000175100017510000000037515102145205022255 0ustar00runnerrunner Two links Hello Hello 2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/hints/short_dict.html0000644000175100017510000000140115102145205023307 0ustar00runnerrunner Many links 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/iframe_search.html0000644000175100017510000000034715102145205022620 0ustar00runnerrunner Searching inside an iframe ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.4726396 qutebrowser-3.6.1/tests/end2end/data/insert_mode_settings/0000755000175100017510000000000015102145350023367 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5806384 qutebrowser-3.6.1/tests/end2end/data/insert_mode_settings/html/0000755000175100017510000000000015102145351024334 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/insert_mode_settings/html/autofocus.html0000644000175100017510000000150415102145205027230 0ustar00runnerrunner Inputs Autofocus

Some text.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/insert_mode_settings/html/input.html0000644000175100017510000000112215102145205026353 0ustar00runnerrunner Input

Some text.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/insert_mode_settings/html/textarea.html0000644000175100017510000000106215102145205027034 0ustar00runnerrunner Textarea

Some text.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/invalid_link.html0000644000175100017510000000035315102145205022470 0ustar00runnerrunner Invalid link I'm broken Unknown scheme ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/invalid_resource.html0000644000175100017510000000036115102145205023361 0ustar00runnerrunner Invalid resource I'm broken Me too ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/issue2569.html0000644000175100017510000000214215102145205021501 0ustar00runnerrunner Form with tagName child
  • List item
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/issue4011.html0000644000175100017510000000032715102145205021464 0ustar00runnerrunner <img src="x" onerror="console.log('XSS')">foo foo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5816383 qutebrowser-3.6.1/tests/end2end/data/javascript/0000755000175100017510000000000015102145351021306 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/consolelog.html0000644000175100017510000000021615102145205024335 0ustar00runnerrunner

This page logs a line via console.log

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/enabled.html0000644000175100017510000000070215102145205023563 0ustar00runnerrunner

JavaScript is disabled

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5826385 qutebrowser-3.6.1/tests/end2end/data/javascript/img/0000755000175100017510000000000015102145351022062 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/img/big.png0000644000175100017510000000535415102145205023336 0ustar00runnerrunnerPNG  IHDRiCCPICC profile(}=H@_[KT !Cu *U(BP+`r4iHR\ׂUg]\AIEJ_Rhq?{ܽ*S͞1@,#J zE11L}N_.9@ ǚ>#8PXPJ7J9*G!wE˲灣Hx};Ŝz;8j$ {N!}9:Hނq!A//e]$`dq Ea%\*ĂE3K$I!iRJ9Y>ӚQ9KO(rXkiHج[urz깗A44(6e3NiYnj+wr?FoP0M b Z]@-Hh^|xT2"a뤛ݓw7~'[};1hخB7Nkgg|}}}}}}/ iCCPICC profilex}=H@_[*;HqPQGB*ZUKIC(X:8* nnN.RBoTjvfdBV+cID1u_<ܟ_ɛ ijL7, MKOf%I!>'tAG.q.:a#' ; x8.+8kݓ0VNsI,b "Ȩ ,iH1?Er*cU?,LNIb#@phmض'@j$֢G6pq=rz%CrM7[o C]nC`H3~^riD[iTXtXML:com.adobe.xmp r, pHYs.#.#x?vtIME -tEXtCommentCreated with GIMPW;IDATX1 00؉#L HZ^r5 lIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/img/padded2.png0000644000175100017510000001300115102145205024064 0ustar00runnerrunnerPNG  IHDRpzTXtRaw profile type exifx]* Y,~{ LWuf+UPv`~b9$5%'T|)]g=ar-p6= p<(k!Y1:*.zn+q 8kaUb[y;FaBQY/] $1 eY*NWݣo/P$s0~i':aOA^Ü]M!צ[= <r2Um@FX&%Tim6 Ÿp'BMtql+ u^cN1 Ecx%*XVQBG(* ;̎ xҕ[+d T B L)!>BC*wxI$ZsXV>͸B%89UJؐ?9TU4jVSע5KNYsΖWM,Z63bœg7k"bK)b j=#z?Qҧ-7kZis{n{u@*4taGu"զ4uigF>5zPMj =$h]''q[мE8,tY,S /u鴈`:fq ragn_PM6u WPMч|q ;-z BoRH&~@PuriCCPICC profilex}=H@_?*vqP,8jP! :\MGbYWWAqssRtZxp܏wwT38e ![Bt'1S<=||,s~%o2'2ݰ77->q$x̠ ?r]vsa?ό*uE^xwOgoiLraiTXtXML:com.adobe.xmp C&zbKGDC pHYs  tIME  zgIDATxA 0Js%   0/`^0/`^`^yy  0/0/`^0/`^yyy   0/`^0/`^`^yy  0/0/`^0/`^yyy   0/W0/`^`^yy  0/0/`^0/`^yyy  0/0/`^`^yy  0/0/`^0/`^`^yy  0/0/`^`^yyy%   0/`^0/`^`^yy  0/0/`^0/`^yyy   0/`^0/`^`^yy  0/хCIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/img/rgb.png0000644000175100017510000000646015102145205023346 0ustar00runnerrunnerPNG  IHDR@@iqiCCPICC profile(}=H@_SEC;8NDE EjVL.& IZpc⬫ ~9)HK -b<8ǻ{wP/346Sɮ]ba CeIRx?Ggaӛy8ŠJ|NF]QscigJo+u`ZK}uKSɐMٕ4|x?o[g뭹 M]%oC`@>n3~rEI2bKGD 9n2C1 pHYs.#.#x?vtIME(:e(etEXtCommentCreated with GIMPW IDATxype===3 r$p(FV>JܕBrYtw]`ݭX@eAp&'!3{0Ld*kB*Wp?????????uh2UKa0$GrCANA i@&M}CjVZ+VJbc[ESSk&}|T<1 C}^P=?;p(muՍ] k{a.5z-J-㡥T_Ef#\O*Q]Oq\DG2裚58,IhE:3ne8 ф @*cNoft:d> }/+F>͞ j5э:nE^}A {ۓ0GSQn ]xnx+%1&"CHy!iUTy@OZ=#Slx\&ȸnHJ6XB(lDKTbRд @h2JGxY& '22c;HNU- 4oPkFգ1Ez1$)ApǘSPtjB d폢(|ġrƦ LpbX7zH$(ma*ȿܑzOXyWeb$u]L`֢7|9]&eI7QXo9zYŘʿSo`9zReP ~wlA7d3;K[ՙIal^45VK P/@Aˆd0tvormRownD-{Fi FMF @py*Haxn|ukc0EEPxz+ &R=w)gszW41}Vfp%<>GboVqu`/?w5'&u-?v.4Km&Ԅ(䑙(F+01 B#vp*1:ˬ+x"V0Ebɼ}(لj=.sڐK8/mۋ7 oVOUhVGzn)^멇1"X:tYKn]:bhѷhG4'O"5e/0w]||lbIeⴋoql]^=g^e}J&+;1l#j`Wl2\n.$xv>"'(^1}黀vA}TyjqY>Vfl0܀ZxQ] ,h|j4e >=&h+Q ,;4mW^čJ(rx+`O݀ fbQ 2xԸˮGyy& &Db>ew(3} TzjY 7 ۸kW5F{ q]\:Np\dF@t)`:?u%WE|Ìz{3H6gס|ҿkp4nPƷ[srZ"1UtvgK*Ѯ/<$Hl1 57\\G-]44IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/img/rgba.png0000644000175100017510000000420515102145205023502 0ustar00runnerrunnerPNG  IHDR@@iqiCCPICC profile(}=H@_SEC;8NDE EjVL.& IZpc⬫ ~9)HK -b<8ǻ{wP/346Sɮ]ba CeIRx?Ggaӛy8ŠJ|NF]QscigJo+u`ZK}uKSɐMٕ4|x?o[g뭹 M]%oC`@>n3~rEI2bKGD 9n2C1 pHYs.#.#x?vtIME'#.jtEXtCommentCreated with GIMPW]IDATxWe_{ 10jh))-ՒZi Zf2eQβ 2t+s 0CkVYqq`] pq=pׅywvy>sy?B *TPB o!掚[(5y 3.0ٌy?rjA]*868mTz)*|*OPCf5P~92MZ {l_QKT( lC,?{HHl[![.)P~))9Ӂз7N`z$IjSn<9@\6@ d Fkm:^J(~%F\TF}Ls<m/+f؟ ln?I!?Ѭc!nH Qa t  Se [bN KKAu|@[;w4,e U%~C( S5'=Zqc ,S' N ȮٟK-~4&` s"Pǩügd| .W{i <Qs.peNx玒$A`sX鼰mIY@J6|s|-`twBp7X -g$Io/h5GeC{n,d>&/0 N烏|xjL$zG?QPpQ疨'ori5piT8Eer9ueϷ5؈)~i0yك #ȣ HOy0|PD ߭+TPB 8]!t:{$G4k3If.I(XY:W*۔I@Ofo^:С,< 8[J WNLeWYx7z6"`.rȿخWg| =]/ `I N(,{3eϴP'~=_ R@@Ee߬ );tjt]~!tw\ˋeg+tet$m\iEeMb؝N<(QF@pS(ٸ^[mKbEjf,sxLqHY7" Q6JrV>ة50D M.&,n$aW&]2OBt}`Oౖ'+n75>y H~#tvҶOE3z'YY S%IzJc@&E^Rh)Q68ݠO֛l$_]m[5LW"t z : ]Q h1'5d"3l^-y?-F.P?1|0FOՅUӀuXB *TPB /┆IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/localstorage.html0000644000175100017510000000122315102145205024647 0ustar00runnerrunner

Local storage status: checking...

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/notifications.html0000644000175100017510000001305015102145205025042 0ustar00runnerrunner
More advanced test pages:
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/window_open.html0000644000175100017510000000172415102145205024526 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/javascript/windowsize.html0000644000175100017510000000133015102145205024371 0ustar00runnerrunner window sizes

visible: unknown

hidden: unknown

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5826385 qutebrowser-3.6.1/tests/end2end/data/keyinput/0000755000175100017510000000000015102145351021010 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/keyinput/log.html0000644000175100017510000000132215102145205022453 0ustar00runnerrunner Fake keypresses This page logs keypresses via console.log. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/l33t.txt0000644000175100017510000000000515102145205020457 0ustar00runnerrunnerl33t ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/long_load.html0000644000175100017510000000033315102145205021761 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/marks.html0000644000175100017510000000074715102145205021151 0ustar00runnerrunner Marks I

Top

Top Bottom
Holy Grail
Waldo
Holy Grail

Bottom

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5836384 qutebrowser-3.6.1/tests/end2end/data/misc/0000755000175100017510000000000015102145351020073 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/misc/hello.txt.html0000644000175100017510000001163615102145205022707 0ustar00runnerrunner

1
2
<html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">Hello World!
</pre></body></html>
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/misc/jseval.html0000644000175100017510000000055515102145205022250 0ustar00runnerrunner Testing :jseval Nothing to see here, only some JS functions defined. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/misc/jseval_file.js0000644000175100017510000000010215102145205022703 0ustar00runnerrunnerconsole.log("Hello from JS!") console.log("Hello again from JS!") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/misc/pyeval_file.py0000644000175100017510000000035315102145205022743 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Simple test file for :debug-pyeval.""" from qutebrowser.utils import message message.info("Hello World") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/misc/qutescheme_csrf.html0000644000175100017510000000165715102145205024150 0ustar00runnerrunner CSRF issues with qute://settings
Via link Via redirect ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/misc/test.pdf0000644000175100017510000004043315102145205021547 0ustar00runnerrunner%PDF-1.4 %äüöß 2 0 obj <> stream x}]ۮ,m}_tbv)Ւkf\U\)ϯ?=<__ط~}뗏~\[z}ǯ׿.ˮC.;8>>~Υ~뗃!g{}xxa'E/Cww %?H*Iv홝n2}r?o_nn/7=V T4f98(%8*/?+~oXs-,9>l5Q/6\zw+/jZBd6C|>zC 344Թ̆!ɰ YPDc|<IF es|4*N}HXvkeY,ű-zc ' {[Xx"Ylda[<{9YlnD4%b4΅Ɠ%X<E B®vz $zG4Cc<^Co$s` 8ٞWT~oˌ~v%=^CW-Oչx-^C,1ٽX"0Ѡx"vYld4DCކ0O%=PdkHoh([4Ajcr*E #(^7H2PyU!y4W@5S=ifYtS% `6NJ*yo%}rU^=WF)r;xi*E"Gr2yh2vUZ^Hh ) D A8"~+Pq +zt:Rg7ZFE B2ѓ-\%{>)MDhhVEOg=RU Zn\!ߋ_A6L5r{V0FlYjpG421$fP^Tw&wdxA}E\pUzYANb =x#U =TpZIhI&ob1=]f^YQΠ}sYrUs/mUKDèk+`wR!7pHk d^:;;㼷%==cL!D6z;1o'[qV2v aQ?`cO6 QD rV &GFܧ:)˗\Yށ6:(gL ^RS+|h+maS-{5<+@ 4{rd+CWlj\D֖1au=Djfek 7+0Zjp$ Ղ¯I "2,Zb֚rE;iiBES D.zTWըf|_4&ɠqE'-4&o+XNAFr`зlji3%'Y`DEGHb\Fz1ċwOZ\k`ұ);omFiBL9H몮bu*ik׆ b#h++b"3_ D(dГaODf+N)x%=!q=1Z7M\bd_4r_\jVNQ[ZԥZ;yQKJX^7)`=%>D}=h sY '5ZY I Kl{ .k+ʛ1G/Kg߁O = ~U^r7.48^<3,`yzԯa/J83zʡ`v7$," ya1:0$m;qU5$}C/X Ȓu0h`V!/CΰZ"􆌙XZeHbj=~BרBd 2e}nunƜ3j.ךZ(Z}O?w΢um s΀ʢiIlIG2?ClMYהꛯ{'bu#Irh5]g :f& DgVaːdl0fRԫ=8lG܏6eVԃ<$'"DOBh VlEs$*<%zJ;i·TLDKeFԧorμ36}{qj/V\ݰU!+e|K%G:ē"cZQy>/T?WުSY-RIdф>"f!)C2I WTe%grwKV+qYDRgDլIf5x-" Uu ̤͹ %'-Sd1 f&h kYg~$LSU}:f1ː+f:_z̖<_T;ufG8:kҵ`l2U=sfºZcf5d:;/+o d}172wg2.xpD[R HS.3 ߲g ڥɚ䞹]x% bCyiV'G,[yZ1i߷Xv:+,uiу%;6l؈mR; lVmxsYIJm,?}Ģ-KfSu#Sy3Ujunj= *BK U,Mus \yTDvГ}hGϼ"1 !) lgbȳm\ܓ% @m dۮ%6TE`{r\PK UUӲm~l?iJg;-ێ+ҳݶOlT-˧6Z FTsC-Kb6h(m }˒|v2w}\!d,9_Z>ix83=2bh ]amKsln9߶mEoY7r" #C~l^EݶmsoY2nێ8PfwunX?Y kDmmi[ƶݥe?:'vۓw \y1t۹mxg\ȳluOn\Q#un"s}?%(ZMmn?Y m&;*lel2*Xtu{ONf;K+z7=7 ƳC c=#5b,7D{@kh[ Y'bDme;m.1t $|*݆*Z,sqqZJnr"NK1tت KqZD<$ᬔ軲ё-+e-V1pVJnr"J11X' gy('CeܱpVJrY)X[K笔9Dbb;NX]x49'XlmdupJr"NX1X 'On|d}KXY 3d9EDbhcܱpŠ 6W^,[ʜTÝD?(8ADnyI9WE6;\C_o,xplȧĹ*˜<߱pKN?XlZ{i,rԥm|45)'籔,qUH8EOuXjw8Yr/вNV)T8g:B9ulrVY- ga9-DND9PCEfDv95hqj,xϭy('vJ^M3磔('|Ck̦3QJܓq&KsPJ܎=ZĒΦOJsY}2z8Dbb*9@ q1'$'C~s}YvX7,uM3g9$'CIgg ;{:0ņQ?ӟGz_z(.'Lfo5 ; A=1^GF,y4k#, pBblC%!=K7_Xu)CQqYDM4t+d;VP*8mÄ"Rd(\r.9,A7l'Pāb;9Jfjbt\;Q6n(-A͎ qs|4񵡈-OEP̞ߌh!{p~k0C|b#j"nn1[xN(Us]!-8)@.9\6 vqM=aq&H\H_DVYPb$_FiGL> /Length 124 /Filter/FlateDecode >> stream xM0DwsDLuh vq\.H>4قu؎TN^SF*4:2i|;)ٽdrITF*ˉW?]z f." endstream endobj 5 0 obj <> endobj 6 0 obj <> /Length 25 /Filter/FlateDecode >> stream x+T0P0t..@.) endstream endobj 7 0 obj <> endobj 10 0 obj <> stream xe\Tߺ鮡S$b.IaFD%EZ A% s>}q]]kW*mj0/+ @E@{7=! }0_? @߻h(EPCn@R u\ >A 8PTP(!,4`p?} H/Eο)r u 0 @*Z{(($*{zP%$* Ba QS>!@'?$ $ ? A(dĀ ? $ ?B;%곃%|G(;B8zG(;BxE};*%er8ؾƳ>2Yo6B \u?Lb$_fr[??A&‹oᅽNWi~ ƀ Fd~>p0zT8TXX9}Mq"֞aF_k9 gk7~%˘gB")b"Y;`ܲɿ[rC<TWva'\=n >72ޔ߉\lË"ܭIRűVirۣ n3>+ԯȄh[S.ɸ;5]CI"T7yڹM>1#< q'b(H 6GN& W"NW"kSYPˬvI*q*7ٛ7g:jF7ij )|r,"1Āk(sj!yĮS{~we?ϳ !"זl_@87 (! d,,ϸzlVT^w.̮hy g {jIX_B|?{Z}*⌜]g>ncD6~Q?ٴ Hq( N%x48LM<PLA=t 2`?'#8a/ZH7jZ>1XL&6" WAL 8>6QhQg]w\pR17"KK6 FotOӮfGW?d|f# !k~2~uUMذ>5f`yɌ6ufBJS+dw7҇[ndA~XOm3Д+O20r_u<M4EfAV&kюQV Tiל x*@P5(A:-thDh\Y>m܁aV%Vlc)S)eD'ݥ7 2*J=zj|};W|e^){=aE9譍xb^_a9'fL068O][ר pCaGB2"y% ,2Gsu/ ]3TXKٺ|| y$'0׽q/x"G"zظ9~ҷ!rag;XxfvR*Z"foG5[?;>_%-ZH RdDoǤpR xCO˝nX?О/4 pN/;},^;ɳ}j8um% m`tol,hh;OZ_\zva PH=o.#JP!~Iěs]t1g9v>]$W_O4wuto(!FT.iPLoC+B{'W:Z M#̔WbbZu{ ˤ,uO*Za\Sgk+bD3.-rټMJ#DKiqq+P?a9X2*Z9#+fM^;ƅ={xigM"UȉO09cY_%k gl0"Qc[ڒ+c_MF(ϥ֋gI~[J73ME>ܳH&$Ei 泙jJujًvK(!xLVʗy $Z$!򈝶+ѐDCOӒGoΜj/:Hޫ,nT%QqMgc# 5|1@4 6Kc$Zc._Y V휌+CݏО&:5[m#r+g: T eHQrU:L}lʪ~AaRl>1 k@bxOCU 7DCpeCãKjkM+2y'E\1F~0g3b!ʙD5҅ٷ R'p҈\ IIQ.,ǹ$+l5/CvUGJw޹:x}#6$l{ULgcY"&uORbⅾo)EԚ2m@P-~^U>")Iw@Ct9v%[ CFo[q`ٚ}}1mwoXC*cJctv`WWNIdž9 &N9˻Dg&[wwO&h6.3:$PzOkt(00hV62M^'*ayte{dEcE28+CHl!:cΞ)KTPU-db4C[/ AmJw+;T9?F!AK\]%"iO-,XYIC(kd+&xRrSN?V3 -|W:Ole{- ,)׽ ԤM:t|kQk+~_#4pQ}K?7]_==iBPjO ZR ׏ QH,V'95WϜIs٫ 3%Q(ELqQKd?sy/JKsCǾd⻝5ENva/]Ծ1 d^Yv^Ukbz%9SxUoJ S T$>J #AJpF'DV "9 'ݗʗ g7s-;ouϽyHĖi+ >!˭Gl>@8p^oޗdoQ0;V_-jϒ6na(xkC[hK#Y t\%k. >:l}kqɱLڍ_tY!ÜLBw5S˪vVTE𠫩4qyAMwR rAKi ӰBWEصΒ@Yv+%͵LŰJ`mPAGͧ,s[jqLhŰЏ$̛`tNNv.%QoKm\;2$o$hFn84upi#}?W>ކ#wKu%%=ܵf #a8# ";bǿ"}*dcI&no 1KMeN{'vxq +TŒը1KD Ä2oVE>.&4])2uZ&{'v%z'*v7e3!~dtlXG_xFDNQڽdg$ X~ʪ1Kݢ3)c=4:l@E^#X1w*sh8#pjf?Ͱﭒ=<#VxH{7Y5nۅ *NnQ^>n"R6,kgwRnhEcR- )kLB T55#AҶQR\3/a]kXr!kkgV6R)![a/a lYHg#75]R%1ە&kdS~/>Q?ֽD1;XH(%d~x9'ZJni9@9v7 _*h endstream endobj 11 0 obj 5170 endobj 12 0 obj <> endobj 13 0 obj <> stream x]n0 kRK Xs!B88x|x|b C@:3G>`>9s_dKxG"?:"Led5尿»KWώ9#Iںmùܭu@G3 4NXE71s endstream endobj 14 0 obj <> endobj 15 0 obj <> endobj 16 0 obj <> /ExtGState<> /ProcSet[/PDF/Text/ImageC/ImageI/ImageB] >> endobj 1 0 obj <>/Contents 2 0 R>> endobj 9 0 obj <> endobj 8 0 obj <> >> endobj 17 0 obj <> /Lang(en-US) >> endobj 18 0 obj < /Creator /Producer /CreationDate(D:20151220194522+01'00')>> endobj xref 0 19 0000000000 65535 f 0000015254 00000 n 0000000019 00000 n 0000008257 00000 n 0000008278 00000 n 0000008580 00000 n 0000008622 00000 n 0000008819 00000 n 0000015513 00000 n 0000015414 00000 n 0000008859 00000 n 0000014139 00000 n 0000014161 00000 n 0000014350 00000 n 0000014787 00000 n 0000015075 00000 n 0000015108 00000 n 0000015653 00000 n 0000015795 00000 n trailer < <6BEBB7227A7B394104957F2AA1EF5608> ] /DocChecksum /F5E806908C259238705235C490EF29B0 >> startxref 16083 %%EOF ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/misc/xhr_headers.html0000644000175100017510000000203015102145205023246 0ustar00runnerrunner XHR headers test
unknown
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5846384 qutebrowser-3.6.1/tests/end2end/data/navigate/0000755000175100017510000000000015102145351020736 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/navigate/index.html0000644000175100017510000000042215102145205022727 0ustar00runnerrunner Navigate

Index page

previous next ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/navigate/multilinelinks.html0000644000175100017510000000035015102145205024663 0ustar00runnerrunner Navigate Multipline link next
page
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/navigate/next.html0000644000175100017510000000024015102145205022574 0ustar00runnerrunner Navigate

next

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/navigate/prev.html0000644000175100017510000000024015102145205022572 0ustar00runnerrunner Navigate

prev

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/navigate/rel.html0000644000175100017510000000044315102145205022405 0ustar00runnerrunner Navigate

Index page

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/navigate/rel_nofollow.html0000644000175100017510000000046515102145205024330 0ustar00runnerrunner Navigate

Index page

bla blub ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5846384 qutebrowser-3.6.1/tests/end2end/data/navigate/sub/0000755000175100017510000000000015102145351021527 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/navigate/sub/index.html0000644000175100017510000000024415102145205023522 0ustar00runnerrunner Navigate

Sub page

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5876384 qutebrowser-3.6.1/tests/end2end/data/numbers/0000755000175100017510000000000015102145351020613 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/1.txt0000644000175100017510000000000415102145205021504 0ustar00runnerrunnerone ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/10.txt0000644000175100017510000000000415102145205021564 0ustar00runnerrunnerten ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/11.txt0000644000175100017510000000000715102145205021570 0ustar00runnerrunnereleven ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/12.txt0000644000175100017510000000000715102145205021571 0ustar00runnerrunnertwelve ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/13.txt0000644000175100017510000000001115102145205021565 0ustar00runnerrunnerthirteen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/14.txt0000644000175100017510000000001115102145205021566 0ustar00runnerrunnerfourteen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/15.txt0000644000175100017510000000001015102145205021566 0ustar00runnerrunnerfifteen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/16.txt0000644000175100017510000000001015102145205021567 0ustar00runnerrunnersixteen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/17.txt0000644000175100017510000000001215102145205021572 0ustar00runnerrunnerseventeen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/18.txt0000644000175100017510000000001115102145205021572 0ustar00runnerrunnereighteen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/19.txt0000644000175100017510000000001115102145205021573 0ustar00runnerrunnernineteen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/2.txt0000644000175100017510000000000415102145205021505 0ustar00runnerrunnertwo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/3.txt0000644000175100017510000000000615102145205021510 0ustar00runnerrunnerthree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/4.txt0000644000175100017510000000000515102145205021510 0ustar00runnerrunnerfour ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/5.txt0000644000175100017510000000000515102145205021511 0ustar00runnerrunnerfive ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/6.txt0000644000175100017510000000000415102145205021511 0ustar00runnerrunnersix ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/7.txt0000644000175100017510000000001015102145205021507 0ustar00runnerrunnerseven - ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/8.txt0000644000175100017510000000000615102145205021515 0ustar00runnerrunnereight ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/numbers/9.txt0000644000175100017510000000000515102145205021515 0ustar00runnerrunnernine ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/paste_primary.html0000644000175100017510000000155615102145205022712 0ustar00runnerrunner Paste primary selection

Some text.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prefers_reduced_motion.html0000644000175100017510000000142515102145205024554 0ustar00runnerrunner Prefers reduced motion test

Reduced motion preference detected.

No preference detected.

Preference support missing.

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5886383 qutebrowser-3.6.1/tests/end2end/data/prompt/0000755000175100017510000000000015102145351020461 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prompt/clipboard.html0000644000175100017510000001067015102145205023310 0ustar00runnerrunner

Permissions:

        
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prompt/geolocation.html0000644000175100017510000000242215102145205023650 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prompt/jsalert.html0000644000175100017510000000050015102145205023004 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prompt/jsconfirm.html0000644000175100017510000000054015102145205023336 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prompt/jsprompt.html0000644000175100017510000000104015102145205023216 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prompt/notifications.html0000644000175100017510000000276215102145205024225 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/prompt/script.js0000644000175100017510000000027215102145205022322 0ustar00runnerrunnerdocument.addEventListener('DOMContentLoaded', (event) => { const elem = document.getElementById('text'); elem.textContent = 'Script loaded'; console.log('Script loaded'); }) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/reload.txt0000644000175100017510000000000015102145205021133 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5896382 qutebrowser-3.6.1/tests/end2end/data/scroll/0000755000175100017510000000000015102145351020436 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/scroll/no_doctype.html0000644000175100017510000000302715102145205023467 0ustar00runnerrunner Scrolling without doctype
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet. But depending on your screen size, this is not even enough yet, so I'm typing some more gibberish here. Hopefully that helps. I'm glad if it did, I'm always happy to help. Really, you're welcome. Okay, okay, can I stop now?
        
next link to test the --top-navigate argument for :scroll-page. prev link to test the --bottom-navigate argument for :scroll-page. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/scroll/position_absolute.html0000644000175100017510000000320515102145205025064 0ustar00runnerrunner Scrolling Just a link
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet. But depending on your screen size, this is not even enough yet, so I'm typing some more gibberish here. Hopefully that helps. I'm glad if it did, I'm always happy to help. Really, you're welcome. Okay, okay, can I stop now?
        
next link to test the --top-navigate argument for :scroll-page. prev link to test the --bottom-navigate argument for :scroll-page. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/scroll/simple.html0000644000175100017510000000566415102145205022626 0ustar00runnerrunner Scrolling Just a link

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet. But depending on your screen size, this is not even enough yet, so I'm typing some more gibberish here. Hopefully that helps. I'm glad if it did, I'm always happy to help. Really, you're welcome. Okay, okay, can I stop now?
        
next link to test the --top-navigate argument for :scroll-page. prev link to test the --bottom-navigate argument for :scroll-page. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/search.html0000644000175100017510000000107715102145205021276 0ustar00runnerrunner Searching text on the page

foo
Foo
Bar
bar
blüb
baz
Baz
BAZ
space travel
/slash
-r reversed
;; semicolons
follow me!

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/search_select.js0000644000175100017510000000062715102145205022305 0ustar00runnerrunner/* Select all elements marked with toselect */ var toSelect = document.getElementsByClassName("toselect"); var s = window.getSelection(); if(s.rangeCount > 0) s.removeAllRanges(); for(var i = 0; i < toSelect.length; i++) { var range = document.createRange(); if (toSelect[i].childNodes.length > 0) { range.selectNodeContents(toSelect[i].childNodes[0]); s.addRange(range); } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5896382 qutebrowser-3.6.1/tests/end2end/data/service-worker/0000755000175100017510000000000015102145351022107 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/service-worker/data.json0000644000175100017510000000001715102145205023707 0ustar00runnerrunner{"foo": "bar"} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/service-worker/index.html0000644000175100017510000000043415102145205024103 0ustar00runnerrunner Service worker test This page will register a service worker when loaded. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/service-worker/worker.js0000644000175100017510000000035615102145205023760 0ustar00runnerrunnerself.addEventListener('install', event => { console.log("Installing service worker"); event.waitUntil( caches.open('example-cache') .then(cache => cache.add('data.json')) .then(self.skipWaiting()) ); }); ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5906384 qutebrowser-3.6.1/tests/end2end/data/sessions/0000755000175100017510000000000015102145351021006 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/sessions/history_replace_state.html0000644000175100017510000000067715102145205026300 0ustar00runnerrunner Test title This page calls history.replaceState() via JS. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/sessions/snowman.html0000644000175100017510000000033715102145205023357 0ustar00runnerrunner snow☃man
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/smart.txt0000644000175100017510000000000615102145205021021 0ustar00runnerrunnersmart ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5906384 qutebrowser-3.6.1/tests/end2end/data/ssl/0000755000175100017510000000000015102145351017741 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/ssl/cert.csr0000644000175100017510000000174115102145205021410 0ustar00runnerrunner-----BEGIN CERTIFICATE REQUEST----- MIICpTCCAY0CAQAwYDElMCMGA1UECgwccXV0ZWJyb3dzZXIgdGVzdCBjZXJ0aWZp Y2F0ZTESMBAGA1UEAwwJbG9jYWxob3N0MSMwIQYJKoZIhvcNAQkBFhRtYWlsQHF1 dGVicm93c2VyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO77 e6QqjeGDjq8tDCGSEi+7m/cDL6PbX8zNNKoVplcoJjoPC/6KmdsLin4SO3iAd5ti XOpPQqyCBgBUd7axP5Ya6M6rhWJaYUczUMdx8bRr4mdaTbd/UhVM/dI1vS/LvBKH OY+8k3E6Neb5jeDe2dfXgokURL4c/jIS1MDumvYCAteoHRYvjGcTSDERr0DT0DY4 oPyrImabSHRGXLz0euQsMY4d9ZTakomYH52cRMNEOKArU1ARNZ0UyHzumuSkjIFV G5PFgMra0tgAPdCA1sx51cQUBOYxnqMdgOBThonrbusYYR17D7TqsvC6R9E0HWhF b4JJkPB3EDVEzWqQFgcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBC7JrJuHyF YFiujBlXFZIQrPNW7FF28zqBuXLfviwVBF/sKmNMKwC0nUgmCb/wFPxv3yrj+7az r29FWSGVhs6k15GVsqSwnbSJDznh/W1elWwpTo2GODMmRY3VeYSY9WiQUhe5KA5x 56p5Kgtl53wZzdl+Pi93xVYAZFWl2O3GFs4f+GCrORjHC7ejZoq6xfRzNLZbLF0a QyptcnYaZSppDB/nZx4p75GKcj9qWXaJbT8mjqJdgRCFPyUkQjSY6WEEAP3LXrXx ThZUekv81Jh+kPTZjSd1d24Bd0nFkQdFf8SRn21jnP+PrzipBOdvm+bT8dI/71xg 8ZJ631jogV4L -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/ssl/cert.pem0000644000175100017510000000224015102145205021375 0ustar00runnerrunner-----BEGIN CERTIFICATE----- MIIDPDCCAiQCCQCHskwLQC4vHDANBgkqhkiG9w0BAQsFADBgMSUwIwYDVQQKDBxx dXRlYnJvd3NlciB0ZXN0IGNlcnRpZmljYXRlMRIwEAYDVQQDDAlsb2NhbGhvc3Qx IzAhBgkqhkiG9w0BCQEWFG1haWxAcXV0ZWJyb3dzZXIub3JnMB4XDTE2MDExMjE4 NDYyM1oXDTI2MDEwOTE4NDYyM1owYDElMCMGA1UECgwccXV0ZWJyb3dzZXIgdGVz dCBjZXJ0aWZpY2F0ZTESMBAGA1UEAwwJbG9jYWxob3N0MSMwIQYJKoZIhvcNAQkB FhRtYWlsQHF1dGVicm93c2VyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC AQoCggEBAO77e6QqjeGDjq8tDCGSEi+7m/cDL6PbX8zNNKoVplcoJjoPC/6KmdsL in4SO3iAd5tiXOpPQqyCBgBUd7axP5Ya6M6rhWJaYUczUMdx8bRr4mdaTbd/UhVM /dI1vS/LvBKHOY+8k3E6Neb5jeDe2dfXgokURL4c/jIS1MDumvYCAteoHRYvjGcT SDERr0DT0DY4oPyrImabSHRGXLz0euQsMY4d9ZTakomYH52cRMNEOKArU1ARNZ0U yHzumuSkjIFVG5PFgMra0tgAPdCA1sx51cQUBOYxnqMdgOBThonrbusYYR17D7Tq svC6R9E0HWhFb4JJkPB3EDVEzWqQFgcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA lTuJK8wseifpepUaWIev+59ulxxMzeippi+xqoYnjrNjINNdk5Wh+Dj7Crb5R8dn afkC+XE9PMKEvKBmQZj/KVEL/G7bjZBA73oibKpBMWIdxaIwSFN2Xq4zKWLHESrb 2Wy8MiehZiSdgUtnmTPM0BlDmc6u9/0nLdCjsBoKYVOLw2FDcD1P8NOJT0dUjSUu aYmUakcn+lQEjuBplrsGvL0vCGR/kzG2vwoTuGnx66HURuHU6E7yBTQ2diyhzOQc sMwwDfrsY19K3IH6AuVcCgGit1LE/zCqMFQuFrIhYB5Mt5bLSeWVBDzKClxZB0Di OxK2sWZvLdGLsFltKB+IJA== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/ssl/key.pem0000644000175100017510000000321315102145205021231 0ustar00runnerrunner-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA7vt7pCqN4YOOry0MIZISL7ub9wMvo9tfzM00qhWmVygmOg8L /oqZ2wuKfhI7eIB3m2Jc6k9CrIIGAFR3trE/lhrozquFYlphRzNQx3HxtGviZ1pN t39SFUz90jW9L8u8Eoc5j7yTcTo15vmN4N7Z19eCiRREvhz+MhLUwO6a9gIC16gd Fi+MZxNIMRGvQNPQNjig/KsiZptIdEZcvPR65Cwxjh31lNqSiZgfnZxEw0Q4oCtT UBE1nRTIfO6a5KSMgVUbk8WAytrS2AA90IDWzHnVxBQE5jGeox2A4FOGietu6xhh HXsPtOqy8LpH0TQdaEVvgkmQ8HcQNUTNapAWBwIDAQABAoIBADysrryEbVdHLm+9 USooyuNBj5yMO4kvhkgaBXf1XTEdqW7uKQ5sJBnf+T5+5Ih4nWVe+NYoX3Yq4Nku mOJSaCF1HYxzMb9B0RbhqW2puUMkbOvumnKvKajszjiTmj/LSymtGWkr6IdDzzGg RGxGSCqrtaGV+soF1GfkLg35xnAUnwk3pfVqGyXl66+bCCWcqXZTUlOB55KEa+5F 9rkMlS6/X3DGZLvON7ZtZqZe7E8Foo9qU1VSHHfxIkS5P4UNxjf7woQogmhNTRT6 tX0SmDQdP59sdFJ09Expr2AfSFxfkGuQf+JSG/JMprg0ub0ksw7UZvaW1uJNKL9I XQSVPgECgYEA94DlPsGd8wWllMjOIEDkERUP2s4uJjPb6jodqewf9tuyxuwRnpOs fb5uq7mMJXG3sszqom0q3DBoapNdCX1vTywWHKc1Nik5PT7jbEXFaRLfvA/F8WfF 6Rugm/S+nezTc7XhtDnOpfl+7wFSJy0we0C3RvxJqAaLaQRDobeNiQcCgYEA9y+z wdXaOcJnC5bPO3ollFewX00WJaAAFpDnfqC3ALJx94/xJVJW6A7TZnKKJmWQ/bFz 0iuyhMe3Nd2yzAhl0qs0lmVe2V2tgJO/CVVP8OQmwlHKSZssDCjaBrHIkNwdL00j qtSYg/FafLPL24AFSr25+sBn/FfxHTzlWVlWywECgYAUyjX3dIoQ/NtwyQFPgkPm D2/agFEuElMZtLIDMPtqX///Z5r/SAZINbPUJuzXxFqa4U2gQS1Fe6d5tFEvV+L+ soRU+dKlbwcI1vyBfsbbUaOLh4OoCIB+WTy/fOp6F4eXg6Km4egy1udLqj+9XLVi 1QfQJacGPy58rsgDkIiKBwKBgHtVtd91kNlZAolpyiTnIXEO/9XNZMuJNgIMczVf g3A5mVvo2m3A09Qd8aUgaYYXD21F6YBohT5zWBrsb5YWapffDPItylGyyCtrjNpf Uu/jJuO2Y7SuVCANEhxdALIm4fkECFPol+DdwESQgZsYGYvddrqC3l+ukYQBKn6W cRQBAoGBAMA8tN67zOtZWalkokLHPDDK/TRUI/+Idc7xX7Rx/KZLuhfT26pLbe5Q onbhe+TSq+4aYfUdcWJE2oM8DQn6CrNZFXKhz/0DLE+leASwwJLNCBbDdLjij2sy 7x2VeGKVG7V2KEhqcDUH/TO0e9PeGnz0vnebzN2+EZue6J9OTfLr -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/ssl/privkey.pem0000644000175100017510000000345215102145205022137 0ustar00runnerrunner-----BEGIN ENCRYPTED PRIVATE KEY----- MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQISAypJ52ykvkCAggA MBQGCCqGSIb3DQMHBAgcPyy/O0hXkgSCBMgA4rZIvVKE73SsGCpJou1LgGAuPX1m qyOPwRGC9T1p1HPaMcucIKpZPp5JSx3B9xwN/V+gpi3XXU1oTLaJhXwOpp8v106l lR9Us91o4nUWVmo2C6nG1z/GSP573RBqjxChiHQchjT5UufKOi+/0elg6tgpu2cv k+CLcgKp80dUr+UOPLAqIC2B+ex4BQHPrki+wbsTeMoZaXnPcTbl0OjABXbG6X4l Gf2xftM7I+Wr/E7dnOEHGwUUH4hAzqflgUTHTZZtUDU7v99ggBBRux5vp9Zi2gWp ksuAmxfPDMcE1Mpu+ZTZ3+cp4TWuWwKRpCX9USjmwnkhdEhqc/arHID/Db+SNP6z lrdHY7BeAWcwDTo+4KZAEK7LKpAukRvpLcyvufo/smGaXsYytFz6Un8scSoySuqo TEKyAioxNsOGJ2Xz5Jt+tdNLO/5W4jCuvwPx1GDlumwPcMHjDrXlZUa0qfoJCcun lptbxZfqd7ouXLy1OF5FAsLs/iCmBwsyOS/qysFwq442WEwT/qn3ZoGBNkkJahu4 OQ5sA14+nZHsBp1+iXZZxKmAERvQfFIRY0oe+Hmdwvyzb4mbIgFyPzU0CRFb+L1/ x+eyrJymBhUL6FVQtoARcYD9g0ya1q3taJQ+JhGW1Ib+DtZzrV4CfDU6q5hWrOOX d9/CAPM4NsjxuAfsy8nH+IOmcLyOXgfTgNFYVv5REnLVYOEoE630uBxnrOKchtpk 1iBSSGCPVcNioLQdUS3rPxtgkZkthar22xme7RDuUj1cg9p6Gu+6hyJIB7y41NdM rLdZeHcRlgy56yb6YBXTnilPDCFhtOx6L8cXnL4CVYtg7ityq5khDSMVrtgiF8wQ n6hDJbSLdFMQMdm9gIQ6lobZkHi4R3yk9S/rHtl7Gc3Set/2rqnxpyt5WsNHcBoy uNkvGZuP9Pb6n4k7eR0/qX2cg3xycNI/uuxqDTpieHr+/lvOflqcj6+6Fq3Uvg65 8rl5vzsrWArX/3/5sfGG6pqPaCjEHb0FeP8zzxzUTw6J46mzCuG90ERCJ/75wTmT QD3oCtLtu/nI4MsR8I4VVn26u8FO63xDSk8xPvS6o8wU7EoZXH3+74EFf5beGgt8 cMTS1Zil/MrtFOSC+MypihKCaYYjVr66F3h3I1RBef+bwuwOuQacaQCXkLHOWC3S pH1iuKGt7lbpGPz103pkc4ssMYAc66nEYXf9I8MATP1aYOyP5o78yegWqgiUs+jd frdgEsW3fsmeA655+5XZmXLHlmkpbb31KeVfCQXoTbHvExTqK91k73xn7/YRHLKq vFKsz6cuWFnHmhb9gInH8iNzEM8DEJq+lEEhEi9XjeNmgnzd2vVl+3a2GPoy2h7u VoGAwr7phI1PiD2aRoB7ZWiR4xxbwl8n+hHh63hSGNYHOeQ7JosPnqcwvHUZo4JZ CXAI6T9snlZRg2G/BT627LYRGqu8piWl3FJXVaVd8lo6g4ZUrhyuV+48tJy1OvHT gM1IATYnml6FPLXAqouxDrMKToAw45KOLrevGDDaQ91kxPrgEpK3fcnvH0FgJ16x /N7uqBmo2XYZM6QxTrq1iShpGFoZ+DC3FOtDT3TKnsrlEUBLzgP3yqJje9Dn+BRs td8= -----END ENCRYPTED PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/title with spaces.html0000644000175100017510000000023215102145205023335 0ustar00runnerrunner Test title foo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/title.html0000644000175100017510000000023215102145205021142 0ustar00runnerrunner Test title foo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.5916383 qutebrowser-3.6.1/tests/end2end/data/userscripts/0000755000175100017510000000000015102145351021526 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/userscripts/echo.bat0000644000175100017510000000044115102145205023131 0ustar00runnerrunner@echo off rem This is needed because echo does not exist as an external program on rem Windows, so we can't call echo(.exe) from qutebrowser, but it's useful for rem tests. This little file is callable via :spawn and mimics (in a very naive rem way) the echo command line utility. echo %* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/userscripts/echo_hint_text0000755000175100017510000000010715102145205024454 0ustar00runnerrunner#!/bin/bash echo "message-info '$QUTE_SELECTED_TEXT'" >> "$QUTE_FIFO" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/userscripts/hello_if_count0000755000175100017510000000030215102145205024436 0ustar00runnerrunner#!/bin/bash if [ "$QUTE_COUNT" -eq 5 ]; then echo "message-info 'Count is five!'" >> "$QUTE_FIFO" elif [ -z "$QUTE_COUNT" ]; then echo "message-info 'No count!'" >> "$QUTE_FIFO" fi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/userscripts/open_current_url0000755000175100017510000000006615102145205025041 0ustar00runnerrunner#!/bin/bash echo "open -t $QUTE_URL" >> "$QUTE_FIFO" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/userscripts/open_current_url.bat0000644000175100017510000000005115102145205025575 0ustar00runnerrunnerecho open -t %QUTE_URL% >> "%QUTE_FIFO%" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/userscripts/stdinclose.py0000755000175100017510000000052715102145205024254 0ustar00runnerrunner#!/usr/bin/env python3 # SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """A userscript to check if the stdin gets closed.""" import sys import os sys.stdin.read() with open(os.environ['QUTE_FIFO'], 'wb') as fifo: fifo.write(b':message-info "stdin closed"\n') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/words.txt0000644000175100017510000000000615102145205021031 0ustar00runnerrunnerwords ././@PaxHeader0000000000000000000000000000011700000000000010214 xustar0057 path=qutebrowser-3.6.1/tests/end2end/data/äöü.html 22 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/data/???.html0000644000175100017510000000023615102145205020401 0ustar00runnerrunner Chäschüechli foo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6006382 qutebrowser-3.6.1/tests/end2end/features/0000755000175100017510000000000015102145351020045 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/backforward.feature0000644000175100017510000001450015102145205023705 0ustar00runnerrunnerFeature: Going back and forward. Testing the :back/:forward commands. Scenario: Going back/forward Given I open data/backforward/1.txt When I open data/backforward/2.txt And I run :tab-only And I run :back And I wait until data/backforward/1.txt is loaded And I reload data/backforward/1.txt And I run :forward And I wait until data/backforward/2.txt is loaded And I reload data/backforward/2.txt Then the session should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/backforward/1.txt - active: true url: http://localhost:*/data/backforward/2.txt """ # https://travis-ci.org/qutebrowser/qutebrowser/jobs/157941720 @qtwebengine_flaky Scenario: Going back in a new tab Given I open data/backforward/1.txt When I open data/backforward/2.txt And I run :tab-only And I run :back -t And I wait until data/backforward/1.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/backforward/1.txt - active: true url: http://localhost:*/data/backforward/2.txt - active: true history: - active: true url: http://localhost:*/data/backforward/1.txt - url: http://localhost:*/data/backforward/2.txt """ Scenario: Going back in a new tab without history Given I open data/backforward/1.txt When I run :tab-only And I run :back -t Then the error "At beginning of history." should be shown And the session should look like: """ windows: - tabs: - active: true history: - active: true url: http://localhost:*/data/backforward/1.txt """ Scenario: Going back in a new background tab Given I open data/backforward/1.txt When I open data/backforward/2.txt And I run :tab-only And I run :back -b And I wait until data/backforward/1.txt is loaded Then the session should look like: """ windows: - tabs: - active: true history: - url: http://localhost:*/data/backforward/1.txt - active: true url: http://localhost:*/data/backforward/2.txt - history: - active: true url: http://localhost:*/data/backforward/1.txt - url: http://localhost:*/data/backforward/2.txt """ @flaky Scenario: Going back with count. Given I open data/backforward/1.txt When I open data/backforward/2.txt And I open data/backforward/3.txt And I run :tab-only And I run :back with count 2 And I wait until data/backforward/1.txt is loaded And I reload data/backforward/1.txt Then the session should look like: """ windows: - tabs: - history: - active: true url: http://localhost:*/data/backforward/1.txt - url: http://localhost:*/data/backforward/2.txt - url: http://localhost:*/data/backforward/3.txt """ Scenario: Going back too much with count. Given I open data/backforward/1.txt When I open data/backforward/2.txt And I open data/backforward/3.txt And I run :back with count 3 Then the error "At beginning of history." should be shown Scenario: Going back with very big count. Given I open data/backforward/1.txt When I run :back with count 99999999999 # Make sure it doesn't hang And I run :message-info "Still alive!" Then the error "At beginning of history." should be shown And the message "Still alive!" should be shown @qtwebengine_flaky Scenario: Going back in a new window Given I clean up open tabs When I open data/backforward/1.txt And I open data/backforward/2.txt And I run :back -w And I wait until data/backforward/1.txt is loaded Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/backforward/1.txt - active: true url: http://localhost:*/data/backforward/2.txt - tabs: - active: true history: - url: about:blank - active: true url: http://localhost:*/data/backforward/1.txt - url: http://localhost:*/data/backforward/2.txt """ Scenario: Going back without history Given I open data/backforward/1.txt When I run :back Then the error "At beginning of history." should be shown Scenario: Going back without history and --quiet Given I open data/backforward/1.txt When I run :back --quiet Then "At beginning of history." should be logged Scenario: Going forward without history Given I open data/backforward/1.txt When I run :forward Then the error "At end of history." should be shown Scenario: Going forward without history and --quiet Given I open data/backforward/1.txt When I run :forward --quiet Then "At end of history." should be logged @qtwebengine_skip # Getting 'at beginning of history' when going back Scenario: Going forward too much with count. Given I open data/backforward/1.txt When I open data/backforward/2.txt And I open data/backforward/3.txt And I run :back with count 2 And I run :forward with count 3 Then the error "At end of history." should be shown ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/caret.feature0000644000175100017510000000753615102145205022531 0ustar00runnerrunnerFeature: Caret mode In caret mode, the user can select and yank text using the keyboard. Background: Given I open data/caret.html And I run :tab-only And I also run :mode-enter caret # :yank selection Scenario: :yank selection without selection When I run :yank selection Then the message "Nothing to yank" should be shown Scenario: :yank selection message When I run :selection-toggle And I run :move-to-end-of-word And I run :yank selection Then the message "3 chars yanked to clipboard" should be shown Scenario: :yank selection message with one char When I run :selection-toggle And I run :move-to-next-char And I run :yank selection Then the message "1 char yanked to clipboard" should be shown Scenario: :yank selection with primary selection When selection is supported And I run :selection-toggle And I run :move-to-end-of-word And I run :yank selection --sel Then the message "3 chars yanked to primary selection" should be shown And the primary selection should contain "one" Scenario: :yank selection with --keep When I run :selection-toggle And I run :move-to-next-word And I run :yank selection --keep And I run :move-to-end-of-word And I run :yank selection --keep Then the message "4 chars yanked to clipboard" should be shown And the message "7 chars yanked to clipboard" should be shown And the clipboard should contain "one two" # :selection-follow Scenario: :selection-follow with --tab (with JS) When I set content.javascript.enabled to true And I run :tab-only And I run :mode-enter caret And I run :selection-toggle And I run :move-to-end-of-word And I run :selection-follow --tab Then data/hello.txt should be loaded And the following tabs should be open: """ - data/caret.html - data/hello.txt (active) """ Scenario: :selection-follow with --tab (without JS) When I set content.javascript.enabled to false And I run :tab-only And I run :mode-enter caret And I run :selection-toggle And I run :move-to-end-of-word And I run :selection-follow --tab Then data/hello.txt should be loaded And the following tabs should be open: """ - data/caret.html - data/hello.txt """ @flaky Scenario: :selection-follow with link tabbing (without JS) When I set content.javascript.enabled to false And I run :mode-leave And I run :jseval document.activeElement.blur(); And I run :fake-key And I run :selection-follow Then data/hello.txt should be loaded @flaky Scenario: :selection-follow with link tabbing (with JS) When I set content.javascript.enabled to true And I run :mode-leave And I run :jseval document.activeElement.blur(); And I run :fake-key And I run :selection-follow Then data/hello.txt should be loaded @flaky Scenario: :selection-follow with link tabbing in a tab (without JS) When I set content.javascript.enabled to false And I run :mode-leave And I run :jseval document.activeElement.blur(); And I run :fake-key And I run :selection-follow --tab Then data/hello.txt should be loaded @flaky Scenario: :selection-follow with link tabbing in a tab (with JS) When I set content.javascript.enabled to true And I run :mode-leave And I run :jseval document.activeElement.blur(); And I run :fake-key And I run :selection-follow --tab Then data/hello.txt should be loaded ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/completion.feature0000644000175100017510000001024515102145205023573 0ustar00runnerrunnerFeature: Using completion Scenario: No warnings when completing with one entry (#1600) Given I open about:blank When I run :cmd-set-text -s :open And I run :completion-item-focus next Then no crash should happen Scenario: Hang with many spaces in completion (#1919) # Generate some history data When I open data/numbers/1.txt And I open data/numbers/2.txt And I open data/numbers/3.txt And I open data/numbers/4.txt And I open data/numbers/5.txt And I open data/numbers/6.txt And I open data/numbers/7.txt And I open data/numbers/8.txt And I open data/numbers/9.txt And I open data/numbers/10.txt And I run :cmd-set-text :open a b # Make sure qutebrowser doesn't hang And I run :message-info "Still alive!" Then the message "Still alive!" should be shown Scenario: Crash when pasting emoji into the command line (#2007) Given I open about:blank When I run :cmd-set-text -s :🌀 Then no crash should happen Scenario: Using command completion When I run :cmd-set-text : Then the completion model should be command Scenario: Using help completion When I run :cmd-set-text -s :help Then the completion model should be helptopic Scenario: Using quickmark completion When I run :cmd-set-text -s :quickmark-load Then the completion model should be quickmark Scenario: Using bookmark completion When I run :cmd-set-text -s :bookmark-load Then the completion model should be bookmark Scenario: Using bind completion When I run :cmd-set-text -s :bind X Then the completion model should be bind # See #2956 @flaky Scenario: Using session completion Given I open data/hello.txt And I run :session-save hello When I run :cmd-set-text -s :session-load And I run :completion-item-focus next And I run :completion-item-focus next And I run :session-delete hello And I run :command-accept Then the error "Session hello not found!" should be shown Scenario: Using option completion When I run :cmd-set-text -s :set Then the completion model should be option Scenario: Using value completion When I run :cmd-set-text -s :set aliases Then the completion model should be value Scenario: Deleting an open tab via the completion Given I have a fresh instance When I open data/hello.txt And I open data/hello2.txt in a new tab And I run :cmd-set-text -s :tab-select And I wait for "Setting completion pattern ''" in the log And I run :completion-item-focus next And I wait for "setting text = ':tab-select 0/1', *" in the log And I run :completion-item-focus next And I wait for "setting text = ':tab-select 0/2', *" in the log And I run :completion-item-del Then the following tabs should be open: """ - data/hello.txt (active) """ Scenario: Go to tab after moving a tab Given I have a fresh instance When I open data/hello.txt And I open data/hello2.txt in a new tab # Tricking completer into not updating tabs And I run :cmd-set-text -s :tab-select And I run :tab-move 1 And I run :tab-select hello2.txt Then the following tabs should be open: """ - data/hello2.txt (active) - data/hello.txt """ Scenario: Space updates completion model after selecting full command When I run :cmd-set-text :set And I run :completion-item-focus next And I run :cmd-set-text -s :set Then the completion model should be option Scenario: Page focus after using completion (#8750) When I open data/insert_mode_settings/html/input.html And I run :cmd-set-text : And I run :mode-leave And I run :click-element id qute-input And I run :fake-key -g someinput Then the javascript message "contents: someinput" should be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/conftest.py0000644000175100017510000006246415102145205022256 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Steps for bdd-like tests.""" import os import os.path import re import sys import time import json import logging import collections import textwrap import subprocess import shutil import pytest import pytest_bdd as bdd import qutebrowser from qutebrowser.utils import log, utils, docutils, version from qutebrowser.browser import pdfjs from end2end.fixtures import testprocess from helpers import testutils def _get_echo_exe_path(): """Return the path to an echo-like command, depending on the system. Return: Path to the "echo"-utility. """ if utils.is_windows: return str(testutils.abs_datapath() / 'userscripts' / 'echo.bat') else: return shutil.which("echo") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): """Add a BDD section to the test output.""" outcome = yield if call.when not in ['call', 'teardown']: return report = outcome.get_result() if report.passed: return if (not hasattr(report.longrepr, 'addsection') or not hasattr(report, 'scenario')): # In some conditions (on macOS and Windows it seems), report.longrepr # is actually a tuple. This is handled similarly in pytest-qt too. # # Since this hook is invoked for any test, we also need to skip it for # non-BDD ones. return if ((sys.stdout.isatty() or testutils.ON_CI) and item.config.getoption('--color') != 'no'): colors = { 'failed': log.COLOR_ESCAPES['red'], 'passed': log.COLOR_ESCAPES['green'], 'keyword': log.COLOR_ESCAPES['cyan'], 'reset': log.RESET_ESCAPE, } else: colors = { 'failed': '', 'passed': '', 'keyword': '', 'reset': '', } output = [] if testutils.ON_CI: output.append(testutils.gha_group_begin('Scenario')) output.append("{kw_color}Feature:{reset} {name}".format( kw_color=colors['keyword'], name=report.scenario['feature']['name'], reset=colors['reset'], )) output.append( " {kw_color}Scenario:{reset} {name} ({filename}:{line})".format( kw_color=colors['keyword'], name=report.scenario['name'], filename=report.scenario['feature']['rel_filename'], line=report.scenario['line_number'], reset=colors['reset']) ) for step in report.scenario['steps']: output.append( " {kw_color}{keyword}{reset} {color}{name}{reset} " "({duration:.2f}s)".format( kw_color=colors['keyword'], color=colors['failed'] if step['failed'] else colors['passed'], keyword=step['keyword'], name=step['name'], duration=step['duration'], reset=colors['reset']) ) if testutils.ON_CI: output.append(testutils.gha_group_end()) report.longrepr.addsection("BDD scenario", '\n'.join(output)) ## Given @bdd.given(bdd.parsers.parse("I set {opt} to {value}")) def set_setting_given(quteproc, server, opt, value): """Set a qutebrowser setting. This is available as "Given:" step so it can be used as "Background:". """ if value == '': value = '' value = value.replace('(port)', str(server.port)) quteproc.set_setting(opt, value) @bdd.given(bdd.parsers.parse("I open {path}")) def open_path_given(quteproc, path): """Open a URL. This is available as "Given:" step so it can be used as "Background:". It always opens a new tab, unlike "When I open ..." """ quteproc.open_path(path, new_tab=True) @bdd.given(bdd.parsers.parse("I run {command}")) def run_command_given(quteproc, command): """Run a qutebrowser command. This is available as "Given:" step so it can be used as "Background:". """ quteproc.send_cmd(command) @bdd.given(bdd.parsers.parse("I also run {command}")) def run_command_given_2(quteproc, command): """Run a qutebrowser command. Separate from the above as a hack to run two commands in a Background without having to use ";;". This is needed because pytest-bdd doesn't allow re-using a Given step... """ quteproc.send_cmd(command) @bdd.given("I have a fresh instance") def fresh_instance(quteproc): """Restart qutebrowser instance for tests needing a fresh state.""" quteproc.terminate() quteproc.start() @bdd.given("I clean up open tabs") def clean_open_tabs(quteproc): """Clean up open windows and tabs.""" quteproc.set_setting('tabs.last_close', 'blank') quteproc.send_cmd(':window-only') quteproc.send_cmd(':tab-only --pinned close') quteproc.send_cmd(':tab-close --force') quteproc.wait_for_load_finished_url('about:blank') @bdd.given('pdfjs is available') def pdfjs_available(data_tmpdir): if not pdfjs.is_available(): pytest.skip("No pdfjs installation found.") @bdd.given('I clear the log') @bdd.when('I clear the log') def clear_log_lines(quteproc): quteproc.clear_data() ## When @bdd.when(bdd.parsers.parse("I open {path}")) def open_path(quteproc, server, path): """Open a URL. - If used like "When I open ... in a new tab", the URL is opened in a new tab. - With "... in a new window", it's opened in a new window. - With "... in a private window" it's opened in a new private window. - With "... as a URL", it's opened according to new_instance_open_target. """ path = path.replace('(port)', str(server.port)) path = testutils.substitute_testdata(path) new_tab = False new_bg_tab = False new_window = False private = False as_url = False wait = True new_tab_suffix = ' in a new tab' new_bg_tab_suffix = ' in a new background tab' new_window_suffix = ' in a new window' private_suffix = ' in a private window' do_not_wait_suffix = ' without waiting' as_url_suffix = ' as a URL' while True: if path.endswith(new_tab_suffix): path = path.removesuffix(new_tab_suffix) new_tab = True elif path.endswith(new_bg_tab_suffix): path = path.removesuffix(new_bg_tab_suffix) new_bg_tab = True elif path.endswith(new_window_suffix): path = path.removesuffix(new_window_suffix) new_window = True elif path.endswith(private_suffix): path = path.removesuffix(private_suffix) private = True elif path.endswith(as_url_suffix): path = path.removesuffix(as_url_suffix) as_url = True elif path.endswith(do_not_wait_suffix): path = path.removesuffix(do_not_wait_suffix) wait = False else: break quteproc.open_path(path, new_tab=new_tab, new_bg_tab=new_bg_tab, new_window=new_window, private=private, as_url=as_url, wait=wait) @bdd.when(bdd.parsers.parse("I set {opt} to {value}")) def set_setting(quteproc, server, opt, value): """Set a qutebrowser setting.""" if value == '': value = '' value = value.replace('(port)', str(server.port)) quteproc.set_setting(opt, value) @bdd.when(bdd.parsers.parse("I run {command}")) def run_command(quteproc, server, tmpdir, command): """Run a qutebrowser command. The suffix "with count ..." can be used to pass a count to the command. """ if 'with count' in command: command, count = command.split(' with count ') count = int(count) else: count = None invalid_tag = ' (invalid command)' if command.endswith(invalid_tag): command = command.removesuffix(invalid_tag) invalid = True else: invalid = False command = command.replace('(port)', str(server.port)) command = testutils.substitute_testdata(command) command = command.replace('(tmpdir)', str(tmpdir)) command = command.replace('(dirsep)', os.sep) command = command.replace('(rootpath)', 'C:\\' if utils.is_windows else '/') command = command.replace('(echo-exe)', _get_echo_exe_path()) quteproc.send_cmd(command, count=count, invalid=invalid) @bdd.when(bdd.parsers.parse("I reload {path}")) def reload(qtbot, server, quteproc, path): """Reload and wait until a new request is received.""" with qtbot.wait_signal(server.new_request): quteproc.send_cmd(':reload') quteproc.wait_for_load_finished(path) @bdd.when(bdd.parsers.parse("I wait until {path} is loaded")) def wait_until_loaded(quteproc, path): """Wait until the given path is loaded (as per qutebrowser log).""" quteproc.wait_for_load_finished(path) @bdd.when(bdd.parsers.re(r'I wait for (?Pregex )?"' r'(?P[^"]+)" in the log(?P or skip ' r'the test)?')) def wait_in_log(quteproc, is_regex, pattern, do_skip): """Wait for a given pattern in the qutebrowser log. If used like "When I wait for regex ... in the log" the argument is treated as regex. Otherwise, it's treated as a pattern (* can be used as wildcard). """ if is_regex: pattern = re.compile(pattern) line = quteproc.wait_for(message=pattern, do_skip=bool(do_skip)) line.expected = True @bdd.when(bdd.parsers.re(r'I wait for the (?Perror|message|warning) ' r'"(?P.*)"')) def wait_for_message(quteproc, server, category, message): """Wait for a given statusbar message/error/warning.""" quteproc.log_summary('Waiting for {} "{}"'.format(category, message)) expect_message(quteproc, server, category, message) @bdd.when(bdd.parsers.parse("I wait {delay}s")) def wait_time(quteproc, delay): """Sleep for the given delay.""" time.sleep(float(delay)) @bdd.when(bdd.parsers.re('I press the keys? "(?P[^"]*)"')) def press_keys(quteproc, keys): """Send the given fake keys to qutebrowser.""" quteproc.press_keys(keys) @bdd.when("selection is supported") def selection_supported(qapp): """Skip the test if selection isn't supported.""" if not qapp.clipboard().supportsSelection(): pytest.skip("OS doesn't support primary selection!") @bdd.when("selection is not supported") def selection_not_supported(qapp): """Skip the test if selection is supported.""" if qapp.clipboard().supportsSelection(): pytest.skip("OS supports primary selection!") @bdd.when(bdd.parsers.re(r'I put "(?P.*)" into the ' r'(?Pprimary selection|clipboard)')) def fill_clipboard(quteproc, server, what, content): content = content.replace('(port)', str(server.port)) content = content.replace(r'\n', '\n') quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(content)) @bdd.when(bdd.parsers.re(r'I put the following lines into the ' r'(?Pprimary selection|clipboard):', flags=re.DOTALL)) def fill_clipboard_multiline(quteproc, server, what, docstring): fill_clipboard(quteproc, server, what, textwrap.dedent(docstring)) @bdd.when(bdd.parsers.parse('I hint with args "{args}"')) def hint(quteproc, args): quteproc.send_cmd(':hint {}'.format(args)) quteproc.wait_for(message='hints: *') @bdd.when(bdd.parsers.parse('I hint with args "{args}" and follow {letter}')) def hint_and_follow(quteproc, args, letter): args = testutils.substitute_testdata(args) args = args.replace('(python-executable)', sys.executable) quteproc.send_cmd(':hint {}'.format(args)) quteproc.wait_for(message='hints: *') quteproc.send_cmd(':hint-follow {}'.format(letter)) @bdd.when("I wait until the scroll position changed") def wait_scroll_position(quteproc): quteproc.wait_scroll_pos_changed() @bdd.when(bdd.parsers.parse("I wait until the scroll position changed to " "{x}/{y}")) def wait_scroll_position_arg(quteproc, x, y): quteproc.wait_scroll_pos_changed(x, y) @bdd.when(bdd.parsers.parse('I wait for the javascript message "{message}"')) def javascript_message_when(quteproc, message): """Make sure the given message was logged via javascript.""" quteproc.wait_for_js(message) @bdd.when("I clear SSL errors") def clear_ssl_errors(request, quteproc): if request.config.webengine: quteproc.terminate() quteproc.start() else: quteproc.send_cmd(':debug-clear-ssl-errors') @bdd.when("the documentation is up to date") def update_documentation(): """Update the docs before testing :help.""" base_path = os.path.dirname(os.path.abspath(qutebrowser.__file__)) doc_path = os.path.join(base_path, 'html', 'doc') script_path = os.path.join(base_path, '..', 'scripts') try: os.mkdir(doc_path) except FileExistsError: pass files = os.listdir(doc_path) if files and all(docutils.docs_up_to_date(p) for p in files): return try: subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) except (OSError, subprocess.CalledProcessError): pytest.skip("Docs outdated and asciidoc unavailable!") update_script = os.path.join(script_path, 'asciidoc2html.py') subprocess.run([sys.executable, update_script], check=True) @bdd.when("I wait until PDF.js is ready") def wait_pdfjs(quteproc): quteproc.wait_for(message="load status for : LoadStatus.success") try: quteproc.wait_for(message="JS: [qute://pdfjs/web/viewer.html?*] Uncaught TypeError: Cannot read property 'set' of undefined", timeout=100) except testprocess.WaitForTimeout: pass else: pytest.skip(f"Non-legacy PDF.js installation: {version._pdfjs_version()}") quteproc.wait_for(message="[qute://pdfjs/*] PDF * (PDF.js: *)") ## Then @bdd.then(bdd.parsers.parse("{path} should be loaded")) def path_should_be_loaded(quteproc, path): """Make sure the given path was loaded according to the log. This is usually the better check compared to "should be requested" as the page could be loaded from local cache. """ quteproc.wait_for_load_finished(path) @bdd.then(bdd.parsers.parse("{path} should be requested")) def path_should_be_requested(server, path): """Make sure the given path was loaded from the webserver.""" server.wait_for(verb='GET', path='/' + path) @bdd.then(bdd.parsers.parse("The requests should be:")) def list_of_requests(server, docstring): """Make sure the given requests were done from the webserver.""" expected_requests = [server.ExpectedRequest('GET', '/' + path.strip()) for path in docstring.split('\n')] actual_requests = server.get_requests() assert actual_requests == expected_requests @bdd.then(bdd.parsers.parse("The unordered requests should be:")) def list_of_requests_unordered(server, docstring): """Make sure the given requests were done (in no particular order).""" expected_requests = [server.ExpectedRequest('GET', '/' + path.strip()) for path in docstring.split('\n')] actual_requests = server.get_requests() # Requests are not hashable, we need to convert to ExpectedRequests actual_requests = [server.ExpectedRequest.from_request(req) for req in actual_requests] assert (collections.Counter(actual_requests) == collections.Counter(expected_requests)) @bdd.then(bdd.parsers.re(r'the (?Perror|message|warning) ' r'"(?P.*)" should be shown')) def expect_message(quteproc, server, category, message): """Expect the given message in the qutebrowser log.""" category_to_loglevel = { 'message': logging.INFO, 'error': logging.ERROR, 'warning': logging.WARNING, } message = message.replace('(port)', str(server.port)) quteproc.mark_expected(category='message', loglevel=category_to_loglevel[category], message=message) @bdd.then(bdd.parsers.re(r'(?Pregex )?"(?P[^"]+)" should ' r'be logged( with level (?P.*))?')) def should_be_logged(quteproc, server, is_regex, pattern, loglevel): """Expect the given pattern on regex in the log.""" if is_regex: pattern = re.compile(pattern) else: pattern = pattern.replace('(port)', str(server.port)) args = { 'message': pattern, } if loglevel: args['loglevel'] = getattr(logging, loglevel.upper()) line = quteproc.wait_for(**args) line.expected = True @bdd.then(bdd.parsers.parse('"{pattern}" should not be logged')) def ensure_not_logged(quteproc, pattern): """Make sure the given pattern was *not* logged.""" quteproc.ensure_not_logged(message=pattern) @bdd.then(bdd.parsers.parse('the javascript message "{message}" should be ' 'logged')) def javascript_message_logged(quteproc, message): """Make sure the given message was logged via javascript.""" quteproc.wait_for_js(message) @bdd.then(bdd.parsers.parse('the javascript message "{message}" should not be ' 'logged')) def javascript_message_not_logged(quteproc, message): """Make sure the given message was *not* logged via javascript.""" quteproc.ensure_not_logged(category='js', message='[*] {}'.format(message)) @bdd.then(bdd.parsers.parse("The session should look like:")) def compare_session(quteproc, docstring): """Compare the current sessions against the given template. partial_compare is used, which means only the keys/values listed will be compared. """ quteproc.compare_session(docstring) @bdd.then( bdd.parsers.parse("The session saved with {flags} should look like:")) def compare_session_flags(quteproc, flags, docstring): """Compare the current session saved with custom flags.""" quteproc.compare_session(docstring, flags=flags) @bdd.then("no crash should happen") def no_crash(): """Don't do anything. This is actually a NOP as a crash is already checked in the log. """ time.sleep(0.5) @bdd.then(bdd.parsers.parse("the header {header} should be set to {value}")) def check_header(quteproc, header, value): """Check if a given header is set correctly. This assumes we're on the server header page. """ content = quteproc.get_content() data = json.loads(content) print(data) if value == '': assert header not in data['headers'] elif value.startswith("'") and value.endswith("'"): # literal match actual = data['headers'][header] assert actual == value[1:-1] else: actual = data['headers'][header] assert testutils.pattern_match(pattern=value, value=actual) @bdd.then(bdd.parsers.parse('the page should contain the html "{text}"')) def check_contents_html(quteproc, text): """Check the current page's content based on a substring.""" content = quteproc.get_content(plain=False) assert text in content @bdd.then(bdd.parsers.parse('the page should contain the plaintext "{text}"')) def check_contents_plain(quteproc, text): """Check the current page's content based on a substring.""" content = quteproc.get_content().strip() assert text in content @bdd.then(bdd.parsers.parse('the page should not contain the plaintext ' '"{text}"')) def check_not_contents_plain(quteproc, text): """Check the current page's content based on a substring.""" content = quteproc.get_content().strip() assert text not in content @bdd.then(bdd.parsers.parse('the json on the page should be:')) def check_contents_json(quteproc, docstring): """Check the current page's content as json.""" content = quteproc.get_content().strip() expected = json.loads(docstring) actual = json.loads(content) assert actual == expected @bdd.then(bdd.parsers.parse("the following tabs should be open:")) def check_open_tabs(quteproc, docstring): """Check the list of open tabs in the session. This is a lightweight alternative for "The session should look like: ...". It expects a list of URLs, with an optional "(active)" suffix. """ session = quteproc.get_session() active_suffix = ' (active)' pinned_suffix = ' (pinned)' tabs = docstring.splitlines() assert len(session['windows']) == 1 assert len(session['windows'][0]['tabs']) == len(tabs) # If we don't have (active) anywhere, don't check it has_active = any(active_suffix in line for line in tabs) has_pinned = any(pinned_suffix in line for line in tabs) for i, line in enumerate(tabs): line = line.strip() assert line.startswith('- ') line = line[2:] # remove "- " prefix active = False pinned = False while line.endswith(active_suffix) or line.endswith(pinned_suffix): if line.endswith(active_suffix): # active line = line.removesuffix(active_suffix) active = True else: # pinned line = line.removesuffix(pinned_suffix) pinned = True session_tab = session['windows'][0]['tabs'][i] current_page = session_tab['history'][-1] assert current_page['url'] == quteproc.path_to_url(line) if active: assert session_tab['active'] elif has_active: assert 'active' not in session_tab if pinned: assert current_page['pinned'] elif has_pinned: assert not current_page['pinned'] @bdd.then(bdd.parsers.re(r'the (?Pprimary selection|clipboard) should ' r'contain "(?P.*)"')) def clipboard_contains(quteproc, server, what, content): expected = content.replace('(port)', str(server.port)) expected = expected.replace('\\n', '\n') expected = expected.replace('(linesep)', os.linesep) quteproc.wait_for(message='Setting fake {}: {}'.format( what, json.dumps(expected))) @bdd.then(bdd.parsers.parse('the clipboard should contain:')) def clipboard_contains_multiline(quteproc, server, docstring): expected = textwrap.dedent(docstring).replace('(port)', str(server.port)) quteproc.wait_for(message='Setting fake clipboard: {}'.format( json.dumps(expected))) @bdd.then("qutebrowser should quit") def should_quit(qtbot, quteproc): quteproc.wait_for_quit() def _get_scroll_values(quteproc): data = quteproc.get_session() def get_active(things): return next(thing for thing in things if thing.get("active")) active_window = get_active(data["windows"]) active_tab = get_active(active_window["tabs"]) current_entry = get_active(active_tab["history"]) pos = current_entry["scroll-pos"] return (pos["x"], pos["y"]) @bdd.then(bdd.parsers.re(r"the page should be scrolled " r"(?Phorizontally|vertically)")) def check_scrolled(quteproc, direction): quteproc.wait_scroll_pos_changed() x, y = _get_scroll_values(quteproc) if direction == 'horizontally': assert x > 0 assert y == 0 else: assert x == 0 assert y > 0 @bdd.then("the page should not be scrolled") def check_not_scrolled(request, quteproc): x, y = _get_scroll_values(quteproc) assert x == 0 assert y == 0 @bdd.then(bdd.parsers.parse("the option {option} should be set to {value}")) def check_option(quteproc, option, value): actual_value = quteproc.get_setting(option) assert actual_value == value @bdd.then(bdd.parsers.parse("the per-domain option {option} should be set to " "{value} for {pattern}")) def check_option_per_domain(quteproc, option, value, pattern, server): pattern = pattern.replace('(port)', str(server.port)) actual_value = quteproc.get_setting(option, pattern=pattern) assert actual_value == value @bdd.when(bdd.parsers.parse('I setup a fake {kind} fileselector ' 'selecting "{files}" and writes to {output_type}')) def set_up_fileselector(quteproc, py_proc, tmpdir, kind, files, output_type): """Set up fileselect.xxx.command to select the file(s).""" cmd, args = py_proc(r""" import os import sys tmp_file = None for i, arg in enumerate(sys.argv): if arg.startswith('--file='): tmp_file = arg.removeprefix('--file=') sys.argv.pop(i) break selected_files = sys.argv[1:] if tmp_file is None: for selected_file in selected_files: print(os.path.abspath(selected_file)) else: with open(tmp_file, 'w') as f: for selected_file in selected_files: f.write(os.path.abspath(selected_file) + '\n') """) files = files.replace('(tmpdir)', str(tmpdir)) files = files.replace('(dirsep)', os.sep) args += files.split(' ') if output_type == "a temporary file": args += ['--file={}'] fileselect_cmd = json.dumps([cmd, *args]) quteproc.set_setting('fileselect.handler', 'external') quteproc.set_setting(f'fileselect.{kind}.command', fileselect_cmd) @bdd.then(bdd.parsers.parse("I run {command}")) def run_command_then(quteproc, command): """Run a qutebrowser command.""" quteproc.send_cmd(command) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/downloads.feature0000644000175100017510000010717515102145205023425 0ustar00runnerrunnerFeature: Downloading things from a website. Background: Given I set up a temporary download dir And I clean old downloads And I set downloads.remove_finished to -1 ## starting downloads Scenario: Clicking an unknown link When I set downloads.location.prompt to false And I open data/downloads/downloads.html And I run :click-element id download And I wait until the download is finished Then the downloaded file download.bin should exist Scenario: Using :download When I set downloads.location.prompt to false When I run :download http://localhost:(port)/data/downloads/download.bin And I wait until the download is finished Then the downloaded file download.bin should exist Scenario: Using :download with no URL When I set downloads.location.prompt to false And I open data/downloads/downloads.html And I run :download And I wait until the download is finished Then the downloaded file Simple downloads.html should exist Scenario: Using :download with no URL on an image When I set downloads.location.prompt to false And I open data/downloads/qutebrowser.png And I run :download And I wait until the download is finished Then the downloaded file qutebrowser.png should exist Scenario: Using hints When I set downloads.location.prompt to false And I open data/downloads/downloads.html And I hint with args "links download" and follow a And I wait until the download is finished Then the downloaded file download.bin should exist Scenario: Using rapid hints # We don't expect any prompts with rapid hinting even if this is true When I set downloads.location.prompt to true And I open data/downloads/downloads.html And I hint with args "--rapid links download" and follow a And I run :hint-follow s And I wait until the download download.bin is finished And I wait until the download download2.bin is finished Then the downloaded file download.bin should exist Then the downloaded file download2.bin should exist ## Regression tests Scenario: Downloading which redirects with closed tab (issue 889) When I set tabs.last_close to blank And I open data/downloads/issue889.html And I hint with args "links download" and follow a And I run :tab-close And I wait for "redirected: *" in the log Then no crash should happen Scenario: Downloading with error in closed tab (issue 889) When I set tabs.last_close to blank And I open data/downloads/issue889.html And I hint with args "links download" and follow s And I run :tab-close And I wait for the error "Download error: * - server replied: NOT FOUND" And I run :download-retry And I wait for the error "Download error: * - server replied: NOT FOUND" Then no crash should happen Scenario: Downloading a link without path information (issue 1243) When I set downloads.location.suggestion to filename And I set downloads.location.prompt to true And I open data/downloads/issue1243.html And I hint with args "links download" and follow a And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log Then the error "Download error: Invalid host (from path): ''" should be shown And "UrlInvalidError while handling qute://* URL" should be logged Scenario: Downloading a data: link (issue 1214) When I set downloads.location.suggestion to filename And I set downloads.location.prompt to true And I open data/data_link.html And I hint with args "links download" and follow s And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log And I run :mode-leave Then no crash should happen Scenario: Aborting a download in a different window (issue 3378) When I set downloads.location.suggestion to filename And I set downloads.location.prompt to true And I open data/downloads/download.bin in a new window without waiting And I wait for "Asking question *" in the log And I run :window-only And I run :mode-leave Then no crash should happen Scenario: Closing window with downloads.remove_finished timeout (issue 1242) When I set downloads.remove_finished to 500 And I open data/downloads/download.bin in a new window without waiting And I wait until the download is finished And I run :close And I wait 0.5s Then no crash should happen Scenario: Quitting with finished downloads and confirm_quit=downloads (issue 846) Given I have a fresh instance When I set downloads.location.prompt to false And I set confirm_quit to [downloads] And I open data/downloads/download.bin without waiting And I wait until the download is finished And I run :close Then qutebrowser should quit # https://github.com/qutebrowser/qutebrowser/issues/2134 @qtwebengine_skip Scenario: Downloading, then closing a tab When I set downloads.location.prompt to false And I open about:blank And I open data/downloads/issue2134.html in a new tab # This needs to be a download connected to the tabs QNAM And I hint with args "links normal" and follow a And I wait for "fetch: * -> drip" in the log And I run :tab-close And I wait for "Download drip finished" in the log Then the downloaded file drip should be 128 bytes big Scenario: Shutting down with a download question When I set downloads.location.prompt to true And I open data/downloads/download.bin without waiting And I wait for "Asking question option=None text='Please enter a location for http://localhost:*/data/downloads/download.bin' title='Save file to:'>, *" in the log And I run :close Then qutebrowser should quit # (and no crash should happen) Scenario: Downloading a file with spaces When I open data/downloads/download with spaces.bin without waiting And I wait until the download is finished Then the downloaded file download with spaces.bin should exist @qtwebkit_skip Scenario: Downloading a file with evil content-disposition header # Content-Disposition: download; filename=..%2Ffoo When I open response-headers?Content-Disposition=download;%20filename%3D..%252Ffoo without waiting And I wait until the download is finished Then the downloaded file ../foo should not exist And the downloaded file foo should exist @qtwebkit_skip Scenario: Downloading a file with evil content-disposition header 2 # Content-Disposition: download; filename=..%252Ffoo When I open response-headers?Content-Disposition=download;%20filename%3D..%25252Ffoo without waiting And I wait until the download is finished Then the downloaded file ../foo should not exist And the downloaded file ..%2Ffoo should exist @windows Scenario: Downloading a file to a reserved path When I set downloads.location.prompt to true And I open data/downloads/download.bin without waiting And I wait for "Asking question option=None text='Please enter a location for http://localhost:*/data/downloads/download.bin' title='Save file to:'>, *" in the log And I run :prompt-accept COM1 And I run :mode-leave Then the error "Invalid filename" should be shown @windows Scenario: Downloading a file to a drive-relative working directory When I set downloads.location.prompt to true And I open data/downloads/download.bin without waiting And I wait for "Asking question option=None text='Please enter a location for http://localhost:*/data/downloads/download.bin' title='Save file to:'>, *" in the log And I run :prompt-accept C:foobar And I run :mode-leave Then the error "Invalid filename" should be shown @windows Scenario: Downloading a file to a reserved path with :download When I run :download data/downloads/download.bin --dest=COM1 Then the error "Invalid target filename" should be shown @windows Scenario: Download a file to a drive-relative working directory with :download When I run :download data/downloads/download.bin --dest=C:foobar Then the error "Invalid target filename" should be shown ## :download-retry Scenario: Retrying a failed download When I run :download http://localhost:(port)/does-not-exist And I wait for the error "Download error: * - server replied: NOT FOUND" And I run :download-retry And I wait for the error "Download error: * - server replied: NOT FOUND" Then the requests should be: """ does-not-exist does-not-exist """ @flaky Scenario: Retrying with count When I run :download http://localhost:(port)/data/downloads/download.bin And I run :download http://localhost:(port)/does-not-exist And I wait for the error "Download error: * - server replied: NOT FOUND" And I run :download-retry with count 2 And I wait for the error "Download error: * - server replied: NOT FOUND" Then the requests should be: """ data/downloads/download.bin does-not-exist does-not-exist """ Scenario: Retrying with two failed downloads When I run :download http://localhost:(port)/does-not-exist And I run :download http://localhost:(port)/does-not-exist-2 And I wait for the error "Download error: * - server replied: NOT FOUND" And I wait for the error "Download error: * - server replied: NOT FOUND" And I run :download-retry And I wait for the error "Download error: * - server replied: NOT FOUND" Then the requests should be: """ does-not-exist does-not-exist-2 does-not-exist """ Scenario: Retrying a download which does not exist When I run :download-retry with count 42 Then the error "There's no download 42!" should be shown Scenario: Retrying a download which did not fail When I run :download http://localhost:(port)/data/downloads/download.bin And I wait until the download is finished And I run :download-retry with count 1 Then the error "Download 1 did not fail!" should be shown Scenario: Retrying a download with no failed ones When I run :download http://localhost:(port)/data/downloads/download.bin And I wait until the download is finished And I run :download-retry Then the error "No failed downloads!" should be shown ## Wrong invocations Scenario: :download --mhtml with a URL given When I run :download --mhtml http://foobar/ Then the error "Can only download the current page as mhtml." should be shown Scenario: :download with a filename and directory which doesn't exist When I run :download --dest (tmpdir)(dirsep)downloads(dirsep)somedir(dirsep)file http://localhost:(port)/data/downloads/download.bin And I wait for "Asking question option=None text='* does not exist. Create it?' title='Create directory?'>, *" in the log And I run :prompt-accept yes And I wait until the download is finished Then the downloaded file somedir/file should exist Scenario: :download with a directory which doesn't exist When I run :download --dest (tmpdir)(dirsep)downloads(dirsep)somedir(dirsep) http://localhost:(port)/data/downloads/download.bin And I wait for "Asking question option=None text='* does not exist. Create it?' title='Create directory?'>, *" in the log And I run :prompt-accept yes And I wait until the download is finished Then the downloaded file somedir/download.bin should exist ## mhtml downloads Scenario: Downloading as mhtml is available When I open data/title.html And I run :download --mhtml And I wait for "File successfully written." in the log Then the downloaded file Test title.mhtml should exist @qtwebengine_skip # QtWebEngine refuses to load this Scenario: Downloading as mhtml with non-ASCII headers When I open response-headers?Content-Type=text%2Fpl%C3%A4in And I run :download --mhtml --dest mhtml-response-headers.mhtml And I wait for "File successfully written." in the log Then the downloaded file mhtml-response-headers.mhtml should exist @qtwebengine_skip # https://github.com/qutebrowser/qutebrowser/issues/2288 Scenario: Overwriting existing mhtml file When I set downloads.location.prompt to true And I open data/title.html And I run :download --mhtml And I wait for "Asking question option=None text='Please enter a location for http://localhost:*/data/title.html' title='Save file to:'>, *" in the log And I run :prompt-accept And I wait for "File successfully written." in the log And I run :download --mhtml And I wait for "Asking question option=None text='Please enter a location for http://localhost:*/data/title.html' title='Save file to:'>, *" in the log And I run :prompt-accept And I wait for "Asking question option=None text='* already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log And I run :prompt-accept yes And I wait for "File successfully written." in the log Then the downloaded file Test title.mhtml should exist @not_flatpak Scenario: Opening a mhtml download directly When I set downloads.location.prompt to true And I open / And I run :download --mhtml And I wait for the download prompt for "*" And I directly open the download Then "Opening *.mhtml* with [*python*]" should be logged ## :download-cancel Scenario: Cancelling a download When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-cancel Then "cancelled" should be logged Scenario: Cancelling with no download and no ID When I run :download-cancel Then the error "There's no download!" should be shown Scenario: Cancelling a download which does not exist When I run :download-cancel with count 42 Then the error "There's no download 42!" should be shown Scenario: Cancelling a download which is already done When I open data/downloads/download.bin without waiting And I wait until the download is finished And I run :download-cancel Then the error "Download 1 is already done!" should be shown Scenario: Cancelling a download which is already done (with count) When I open data/downloads/download.bin without waiting And I wait until the download is finished And I run :download-cancel with count 1 Then the error "Download 1 is already done!" should be shown Scenario: Cancelling all downloads When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-cancel --all Then "cancelled" should be logged And "cancelled" should be logged # https://github.com/qutebrowser/qutebrowser/issues/1535 @qtwebengine_todo # :download --mhtml is not implemented yet Scenario: Cancelling an MHTML download (issue 1535) When I open data/downloads/issue1535.html And I run :download --mhtml And I wait for "fetch: Py*.QtCore.QUrl('http://localhost:*/drip?numbytes=128&duration=2') -> drip" in the log And I run :download-cancel Then no crash should happen ## :download-remove / :download-clear Scenario: Removing a download When I open data/downloads/download.bin without waiting And I wait until the download is finished And I run :download-remove Then "Removed download *" should be logged Scenario: Removing a download which does not exist When I run :download-remove with count 42 Then the error "There's no download 42!" should be shown Scenario: Removing a download which is not done yet When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-remove Then the error "Download 1 is not done!" should be shown Scenario: Removing a download which is not done yet (with count) When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-remove with count 1 Then the error "Download 1 is not done!" should be shown Scenario: Removing all downloads via :download-remove When I open data/downloads/download.bin without waiting And I wait until the download is finished And I open data/downloads/download2.bin without waiting And I wait until the download is finished And I run :download-remove --all Then "Removed download *" should be logged Scenario: Removing all downloads via :download-clear When I open data/downloads/download.bin without waiting And I wait until the download is finished And I open data/downloads/download2.bin without waiting And I wait until the download is finished And I run :download-clear Then "Removed download *" should be logged ## :download-delete Scenario: Deleting a download When I open data/downloads/download.bin without waiting And I wait until the download is finished And I run :download-delete And I wait for "deleted download *" in the log Then the downloaded file download.bin should not exist Scenario: Deleting a download which does not exist When I run :download-delete with count 42 Then the error "There's no download 42!" should be shown Scenario: Deleting a download which is not done yet When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-delete Then the error "Download 1 is not done!" should be shown Scenario: Deleting a download which is not done yet (with count) When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-delete with count 1 Then the error "Download 1 is not done!" should be shown ## :download-open @not_flatpak Scenario: Opening a download When I open data/downloads/download.bin without waiting And I wait until the download is finished And I open the download Then "Opening *download.bin* with [*python*]" should be logged @not_flatpak Scenario: Opening a download with a placeholder When I open data/downloads/download.bin without waiting And I wait until the download is finished And I open the download with a placeholder Then "Opening *download.bin* with [*python*]" should be logged @not_flatpak Scenario: Opening a download with open_dispatcher set When I set a test python open_dispatcher And I open data/downloads/download.bin without waiting And I wait until the download is finished And I run :download-open Then "Opening *download.bin* with [*python*]" should be logged @not_flatpak Scenario: Opening a download with open_dispatcher set and override When I set downloads.open_dispatcher to cat And I open data/downloads/download.bin without waiting And I wait until the download is finished And I open the download Then "Opening *download.bin* with [*python*]" should be logged Scenario: Opening a download which does not exist When I run :download-open with count 42 Then the error "There's no download 42!" should be shown Scenario: Opening a download which is not done yet When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-open Then the error "Download 1 is not done!" should be shown Scenario: Opening a download which is not done yet (with count) When I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I run :download-open with count 1 Then the error "Download 1 is not done!" should be shown ## opening a file directly (prompt-open-download) @not_flatpak Scenario: Opening a download directly When I set downloads.location.prompt to true And I open data/downloads/download.bin without waiting And I wait for the download prompt for "*" And I directly open the download And I wait until the download is finished Then "Opening *download.bin* with [*python*]" should be logged # https://github.com/qutebrowser/qutebrowser/issues/1728 Scenario: Cancelling a download that should be opened When I set downloads.location.prompt to true And I run :download http://localhost:(port)/drip?numbytes=128&duration=5 And I wait for the download prompt for "*" And I directly open the download And I run :download-cancel Then "* finished but not successful, not opening!" should be logged # https://github.com/qutebrowser/qutebrowser/issues/1725 @not_flatpak Scenario: Directly open a download with a very long filename When I set downloads.location.prompt to true And I open data/downloads/issue1725.html And I run :click-element id long-link And I wait for the download prompt for "*" And I directly open the download And I wait until the download is finished Then "Opening * with [*python*]" should be logged ## downloads.location.suggestion Scenario: downloads.location.suggestion = path When I set downloads.location.prompt to true And I set downloads.location.suggestion to path And I open data/downloads/download.bin without waiting Then the download prompt should be shown with "(tmpdir)/downloads/" Scenario: downloads.location.suggestion = filename When I set downloads.location.prompt to true And I set downloads.location.suggestion to filename And I open data/downloads/download.bin without waiting Then the download prompt should be shown with "download.bin" Scenario: downloads.location.suggestion = both When I set downloads.location.prompt to true And I set downloads.location.suggestion to both And I open data/downloads/download.bin without waiting Then the download prompt should be shown with "(tmpdir)/downloads/download.bin" ## downloads.location.remember Scenario: Remembering the last download directory When I set downloads.location.prompt to true And I set downloads.location.suggestion to both And I set downloads.location.remember to true And I open data/downloads/download.bin without waiting And I wait for the download prompt for "*/download.bin" And I run :prompt-accept (tmpdir)(dirsep)downloads(dirsep)subdir And I open data/downloads/download2.bin without waiting Then the download prompt should be shown with "(tmpdir)/downloads/subdir/download2.bin" Scenario: Clearing the last download directory when changing download location When I set downloads.location.prompt to true And I set downloads.location.suggestion to both And I set downloads.location.remember to true And I open data/downloads/download.bin without waiting And I wait for the download prompt for "*/download.bin" And I run :prompt-accept (tmpdir)(dirsep)downloads(dirsep)subdir And I run :set downloads.location.directory (tmpdir)(dirsep)downloads And I open data/downloads/download2.bin without waiting Then the download prompt should be shown with "(tmpdir)/downloads/download2.bin" Scenario: Not remembering the last download directory When I set downloads.location.prompt to true And I set downloads.location.suggestion to both And I set downloads.location.remember to false And I open data/downloads/download.bin without waiting And I wait for the download prompt for "(tmpdir)/downloads/download.bin" And I run :prompt-accept (tmpdir)(dirsep)downloads(dirsep)subdir And I open data/downloads/download2.bin without waiting Then the download prompt should be shown with "(tmpdir)/downloads/download2.bin" # https://github.com/qutebrowser/qutebrowser/issues/2173 @not_flatpak Scenario: Remembering the temporary download directory (issue 2173) When I set downloads.location.prompt to true And I set downloads.location.suggestion to both And I set downloads.location.remember to true And I open data/downloads/download.bin without waiting And I wait for the download prompt for "*" And I run :prompt-accept (tmpdir)(dirsep)downloads And I open data/downloads/download2.bin without waiting And I wait for the download prompt for "*" And I directly open the download And I open data/downloads/download.bin without waiting Then the download prompt should be shown with "(tmpdir)/downloads/download.bin" # Overwriting files Scenario: Not overwriting an existing file When I set downloads.location.prompt to false And I run :download http://localhost:(port)/data/downloads/download.bin And I wait until the download is finished And I run :download http://localhost:(port)/data/downloads/download2.bin --dest download.bin And I wait for "Entering mode KeyMode.yesno *" in the log And I run :prompt-accept no Then the downloaded file download.bin should be 1 bytes big Scenario: Overwriting an existing file When I set downloads.location.prompt to false And I run :download http://localhost:(port)/data/downloads/download.bin And I wait until the download is finished And I run :download http://localhost:(port)/data/downloads/download2.bin --dest download.bin And I wait for "Entering mode KeyMode.yesno *" in the log And I run :prompt-accept yes And I wait until the download is finished Then the downloaded file download.bin should be 2 bytes big @linux Scenario: Not overwriting a special file When I set downloads.location.prompt to false And I run :download http://localhost:(port)/data/downloads/download.bin --dest fifo And I wait for "Entering mode KeyMode.yesno *" in the log And I run :prompt-accept no Then the FIFO should still be a FIFO ## Redirects Scenario: Downloading with infinite redirect When I set downloads.location.prompt to false And I run :download http://localhost:(port)/redirect/21 --dest redirection Then the error "Download error: Too many redirects" should be shown And the downloaded file redirection should not exist Scenario: Downloading with redirect to itself When I set downloads.location.prompt to false And I run :download http://localhost:(port)/redirect-self Then the error "Download error: Too many redirects" should be shown And the downloaded file redirect-self should not exist Scenario: Downloading with absolute redirect When I set downloads.location.prompt to false And I run :download http://localhost:(port)/absolute-redirect And I wait until the download is finished Then the downloaded file absolute-redirect should exist Scenario: Downloading with relative redirect When I set downloads.location.prompt to false And I run :download http://localhost:(port)/relative-redirect And I wait until the download is finished Then the downloaded file relative-redirect should exist Scenario: Downloading with insecure redirect When I set downloads.location.prompt to false And I set content.tls.certificate_errors to load-insecurely And I download an SSL redirect page # First error is due to the load-insecurely value above Then the error "Certificate error: *The certificate is self-signed, and untrusted" should be shown And the error "Download error: Insecure redirect" should be shown And the downloaded file download.bin should not exist ## Other Scenario: Download without a content-size When I set downloads.location.prompt to false When I run :download http://localhost:(port)/content-size And I wait until the download is finished Then the downloaded file content-size should exist Scenario: Downloading to unwritable destination When the unwritable dir is unwritable And I set downloads.location.prompt to false And I run :download http://localhost:(port)/data/downloads/download.bin --dest (tmpdir)/downloads/unwritable Then the error "Download error: *" should be shown Scenario: Downloading 20MB file When I set downloads.location.prompt to false And I run :download http://localhost:(port)/twenty-mb And I wait until the download is finished Then the downloaded file twenty-mb should be 20971520 bytes big Scenario: Downloading 20MB file with late prompt confirmation When I set downloads.location.prompt to true And I run :download http://localhost:(port)/twenty-mb And I wait 1s And I run :prompt-accept And I wait until the download is finished Then the downloaded file twenty-mb should be 20971520 bytes big Scenario: Downloading invalid URL When I set downloads.location.prompt to false And I set url.auto_search to never And I run :download foo! Then the error "Invalid URL" should be shown Scenario: Downloading via pdfjs Given pdfjs is available When I set downloads.location.prompt to false And I set content.pdfjs to true And I open data/misc/test.pdf without waiting And I wait until PDF.js is ready And I run :jseval (document.getElementById("downloadButton") || document.getElementById("download")).click() And I clear the log And I wait until the download is finished # We get viewer.html as name on QtWebKit... # Then the downloaded file test.pdf should exist Scenario: Answering a question for a cancelled download (#415) When I set downloads.location.prompt to true And I run :download http://localhost:(port)/data/downloads/download.bin And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log And I run :download http://localhost:(port)/data/downloads/download2.bin And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log And I run :download-cancel with count 2 And I run :prompt-accept And I wait until the download is finished Then the downloaded file download.bin should exist And the downloaded file download2.bin should not exist @qt>=6.9 Scenario: Nested download prompts (#8674) When I set downloads.location.prompt to true And I open data/downloads/download.bin without waiting And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log And I open data/downloads/download.bin without waiting And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log And I open data/downloads/download.bin without waiting And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log And I run :prompt-accept And I run :mode-leave And I run :mode-leave And I wait until the download is finished Then the downloaded file download.bin should exist @qtwebengine_skip # We can't get the UA from the page there Scenario: user-agent when using :download When I open user-agent And I run :download --dest user-agent And I wait until the download is finished Then the downloaded file user-agent should contain Safari/ @qtwebengine_skip # We can't get the UA from the page there Scenario: user-agent when using hints When I open / And I run :hint links download And I run :hint-follow a And I wait until the download is finished Then the downloaded file user-agent should contain Safari/ @qtwebengine_skip # Handled by QtWebEngine, not by us Scenario: Downloading a "Internal server error" with disposition: inline (#2304) When I set downloads.location.prompt to false And I open 500-inline Then the error "Download error: *INTERNAL SERVER ERROR" should be shown ## External download path fileselector Scenario: Select download path When I set downloads.location.prompt to true And I setup a fake folder fileselector selecting "(tmpdir)(dirsep)downloads(dirsep)subdir" and writes to a temporary file And I open data/downloads/downloads.html And I run :click-element id download And I wait for the download prompt for "*" And I run :prompt-fileselect-external And I wait until the download is finished Then the downloaded file subdir/download.bin should exist Scenario: No download folder chosen When I set downloads.location.prompt to true And I set fileselect.folder.command to ['echo', '{}'] And I open data/downloads/downloads.html And I run :click-element id download And I wait for the download prompt for "*" And I run :prompt-fileselect-external Then the message "No folder chosen." should be shown And "No prompts left, hiding prompt container." should not be logged Scenario: Using :prompt-fileselect-external with other prompt When I open data/prompt/jsprompt.html And I run :click-element id button And I wait for "Asking question *" in the log And I run :prompt-fileselect-external Then the error "Can only launch external fileselect for FilenamePrompt, not LineEditPrompt" should be shown And I run :mode-leave ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/editor.feature0000644000175100017510000003024715102145205022714 0ustar00runnerrunnerFeature: Opening external editors ## :edit-url Scenario: Editing a URL When I open data/numbers/1.txt And I setup a fake editor replacing "1.txt" by "2.txt" And I run :edit-url Then data/numbers/2.txt should be loaded Scenario: Editing a URL with -t When I run :tab-only And I open data/numbers/1.txt And I setup a fake editor replacing "1.txt" by "2.txt" And I run :edit-url -t Then data/numbers/2.txt should be loaded And the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) """ Scenario: Editing a URL with -rt When I set tabs.new_position.related to prev And I open data/numbers/1.txt And I run :tab-only And I setup a fake editor replacing "1.txt" by "2.txt" And I run :edit-url -rt Then data/numbers/2.txt should be loaded And the following tabs should be open: """ - data/numbers/2.txt (active) - data/numbers/1.txt """ Scenario: Editing a URL with -b When I run :tab-only And I open data/numbers/1.txt And I setup a fake editor replacing "1.txt" by "2.txt" And I run :edit-url -b Then data/numbers/2.txt should be loaded And the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt """ Scenario: Editing a URL with -w When I run :window-only And I open data/numbers/1.txt in a new tab And I run :tab-only And I setup a fake editor replacing "1.txt" by "2.txt" And I run :edit-url -w Then data/numbers/2.txt should be loaded And the session should look like: """ windows: - tabs: - active: true history: - active: true url: http://localhost:*/data/numbers/1.txt - tabs: - active: true history: - active: true url: http://localhost:*/data/numbers/2.txt """ Scenario: Editing a URL with -p When I open data/numbers/1.txt in a new tab And I run :tab-only And I run :window-only And I setup a fake editor replacing "1.txt" by "2.txt" And I run :edit-url -p Then data/numbers/2.txt should be loaded And the session should look like: """ windows: - tabs: - active: true history: - active: true url: http://localhost:*/data/numbers/1.txt - tabs: - active: true history: - active: true url: http://localhost:*/data/numbers/2.txt private: true """ Scenario: Editing a URL with -t and -b When I run :edit-url -t -b Then the error "Only one of -t/-b/-w can be given!" should be shown @flaky Scenario: Editing a URL with invalid URL When I set url.auto_search to never And I open data/hello.txt And I setup a fake editor replacing "http://localhost:(port)/data/hello.txt" by "foo!" And I run :edit-url Then the error "Invalid URL" should be shown Scenario: Spawning an editor successfully Given I have a fresh instance When I setup a fake editor returning "foobar" And I open data/editor.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :edit-text And I wait for "Read back: foobar" in the log Then the javascript message "text: foobar" should be logged Scenario: Spawning an editor in normal mode When I setup a fake editor returning "foobar" And I open data/editor.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :mode-leave And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log And I run :edit-text And I wait for "Read back: foobar" in the log Then the javascript message "text: foobar" should be logged # Could not get signals working on Windows # There's no guarantee that the tab gets deleted... @posix Scenario: Spawning an editor and closing the tab When I setup a fake editor that writes "foobar" on save And I open data/editor.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :edit-text And I wait until the editor has started And I set tabs.last_close to blank And I run :tab-close And I kill the waiting editor Then the error "Edited element vanished" should be shown And the message "Editor backup at *" should be shown # Could not get signals working on Windows @posix Scenario: Spawning an editor and saving When I setup a fake editor that writes "foobar" on save And I open data/editor.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :edit-text And I wait until the editor has started And I save without exiting the editor And I wait for "Read back: foobar" in the log Then the javascript message "text: foobar" should be logged Scenario: Spawning an editor in caret mode When I setup a fake editor returning "foobar" And I open data/editor.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :mode-leave And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log And I run :mode-enter caret And I wait for "Entering mode KeyMode.caret (reason: command)" in the log And I run :edit-text And I wait for "Read back: foobar" in the log And I run :mode-leave Then the javascript message "text: foobar" should be logged Scenario: Spawning an editor with existing text When I setup a fake editor replacing "foo" by "bar" And I open data/editor.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :insert-text foo And I wait for "Inserting text into element *" in the log And I run :edit-text And I wait for "Read back: bar" in the log Then the javascript message "text: bar" should be logged ## :cmd-edit Scenario: Edit a command and run it When I run :cmd-set-text :message-info foo And I setup a fake editor replacing "foo" by "bar" And I run :cmd-edit --run Then the message "bar" should be shown And "Leaving mode KeyMode.command (reason: cmd accept)" should be logged Scenario: Edit a command and omit the start char When I setup a fake editor returning "message-info foo" And I run :cmd-edit Then the error "command must start with one of :/?" should be shown And "Leaving mode KeyMode.command *" should not be logged Scenario: Edit a command to be empty When I run :cmd-set-text : When I setup a fake editor returning empty text And I run :cmd-edit Then the error "command must start with one of :/?" should be shown And "Leaving mode KeyMode.command *" should not be logged And I run :mode-leave And "Leaving mode KeyMode.command *" should be logged ## select single file Scenario: Select one file with single file command When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to a temporary file And I open data/fileselect.html And I run :click-element id single_file Then the javascript message "Files: 1.txt" should be logged Scenario: Select one file with single file command that writes to stdout When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to stdout And I open data/fileselect.html And I run :click-element id single_file Then the javascript message "Files: 1.txt" should be logged Scenario: Select two files with single file command When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt" and writes to a temporary file And I open data/fileselect.html And I run :click-element id single_file Then the javascript message "Files: 1.txt" should be logged And the warning "More than one file/folder chosen, using only the first" should be shown ## select multiple files Scenario: Select one file with multiple files command When I setup a fake multiple_files fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to a temporary file And I open data/fileselect.html And I run :click-element id multiple_files Then the javascript message "Files: 1.txt" should be logged Scenario: Select two files with multiple files command When I setup a fake multiple_files fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt" and writes to a temporary file And I open data/fileselect.html And I run :click-element id multiple_files Then the javascript message "Files: 1.txt, 2.txt" should be logged ## No temporary file created Scenario: File selector deleting temporary file When I set fileselect.handler to external And I set fileselect.single_file.command to ['rm', '{}'] And I open data/fileselect.html And I run :click-element id single_file Then the javascript message "Files: 1.txt" should not be logged And the error "Failed to open tempfile *" should be shown And "Failed to delete tempfile *" should be logged with level error ## Select non-existent file Scenario: Select non-existent file When I set fileselect.handler to external When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/non-existent.txt" and writes to a temporary file And I open data/fileselect.html And I run :click-element id single_file Then the javascript message "Files: non-existent.txt" should not be logged And the warning "Ignoring non-existent file *non-existent.txt'" should be shown ## Select folder when expecting file Scenario: Select folder for file When I set fileselect.handler to external When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers" and writes to a temporary file And I open data/fileselect.html And I run :click-element id single_file Then the javascript message "Files: *" should not be logged And the warning "Expected file but got folder, ignoring *numbers'" should be shown ## Select file when expecting folder @qtwebkit_skip Scenario: Select file for folder When I set fileselect.handler to external When I setup a fake folder fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to a temporary file And I open data/fileselect.html And I run :click-element id folder Then the javascript message "Files: 1.txt" should not be logged And the warning "Expected folder but got file, ignoring *1.txt'" should be shown ## Select folder @qtwebkit_skip Scenario: Select one folder with folder command When I set fileselect.handler to external And I setup a fake folder fileselector selecting "tests/end2end/data/backforward/" and writes to a temporary file And I open data/fileselect.html And I run :click-element id folder Then the javascript message "Files: 1.txt, 2.txt, 3.txt" should be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/hints.feature0000644000175100017510000006740215102145205022556 0ustar00runnerrunnerFeature: Using hints # https://bugreports.qt.io/browse/QTBUG-58381 Background: Given I clean up open tabs Scenario: Using :hint-follow outside of hint mode (issue 1105) When I run :hint-follow Then the error "hint-follow: This command is only allowed in hint mode, not normal." should be shown Scenario: Using :hint-follow with an invalid index. When I open data/hints/html/simple.html And I hint with args "links normal" and follow xyz Then the error "No hint xyz!" should be shown Scenario: Using :hint with invalid mode. When I run :hint --mode=foobar Then the error "Invalid mode: Invalid value 'foobar' - valid values: number, letter, word" should be shown Scenario: Switching tab between :hint and start_cb (issue 3892) When I open data/hints/html/simple.html And I open data/hints/html/simple.html in a new tab And I run :hint ;; tab-prev And I wait for regex "hints: .*|Current tab changed \(\d* -> \d*\) before _start_cb is run\." in the log # 'hints: .*' is logged when _start_cb is called before tab-prev (on # qtwebkit, _start_cb is called synchronously) And I run :hint-follow a Then the error "hint-follow: This command is only allowed in hint mode, not normal." should be shown ### Opening in current or new tab Scenario: Following a hint and force to open in current tab. When I open data/hints/link_blank.html And I hint with args "links current" and follow a And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt (active) """ Scenario: Following a hint and allow to open in new tab. When I open data/hints/link_blank.html And I hint with args "links normal" and follow a And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hints/link_blank.html - data/hello.txt """ # https://github.com/qutebrowser/qutebrowser/issues/7842 @qtwebkit_skip Scenario: Following a hint from a local file to a remote origin When I open file://(testdata)/hints/link_inject.html?port=(port) And I hint with args "links" and follow a Then data/hello.txt should be loaded Scenario: Following a hint to link with sub-element and force to open in current tab. When I open data/hints/link_span.html And I hint with args "links current" and follow a And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt (active) """ Scenario: Entering and leaving hinting mode (issue 1464) When I open data/hints/html/simple.html And I hint with args "all" And I run :fake-key -g Then no crash should happen Scenario: Using :hint spawn with flags and -- (issue 797) When I open data/hints/html/simple.html And I hint with args "-- all spawn -v (python-executable) -c ''" and follow a Then the message "Command exited successfully. See :process * for details." should be shown Scenario: Using :hint spawn with flags (issue 797) When I open data/hints/html/simple.html And I hint with args "all spawn -v (python-executable) -c ''" and follow a Then the message "Command exited successfully. See :process * for details." should be shown Scenario: Using :hint spawn with flags and --rapid (issue 797) When I open data/hints/html/simple.html And I hint with args "--rapid all spawn -v (python-executable) -c ''" and follow a Then the message "Command exited successfully. See :process * for details." should be shown @posix Scenario: Using :hint spawn with flags passed to the command (issue 797) When I open data/hints/html/simple.html And I hint with args "--rapid all spawn -v echo -e foo" and follow a Then the message "Command exited successfully. See :process * for details." should be shown Scenario: Using :hint run When I open data/hints/html/simple.html And I hint with args "all run message-info {hint-url}" and follow a Then the message "http://localhost:(port)/data/hello.txt" should be shown Scenario: Using :hint fill When I open data/hints/html/simple.html And I hint with args "all fill :message-info {hint-url}" and follow a And I press the key "" Then the message "http://localhost:(port)/data/hello.txt" should be shown @posix Scenario: Using :hint userscript When I open data/hints/html/simple.html And I hint with args "all userscript (testdata)/userscripts/echo_hint_text" and follow a Then the message "Follow me!" should be shown Scenario: Using :hint userscript with a script which doesn't exist When I open data/hints/html/simple.html And I hint with args "all userscript (testdata)/does_not_exist" and follow a Then the error "Userscript '*' not found" should be shown Scenario: Yanking to clipboard When I run :debug-set-fake-clipboard And I open data/hints/html/simple.html And I hint with args "links yank" and follow a Then the clipboard should contain "http://localhost:(port)/data/hello.txt" Scenario: Yanking to primary selection When selection is supported And I run :debug-set-fake-clipboard And I open data/hints/html/simple.html And I hint with args "links yank-primary" and follow a Then the primary selection should contain "http://localhost:(port)/data/hello.txt" Scenario: Yanking to primary selection without it being supported (#1336) When selection is not supported And I run :debug-set-fake-clipboard And I open data/hints/html/simple.html And I hint with args "links yank-primary" and follow a Then the clipboard should contain "http://localhost:(port)/data/hello.txt" Scenario: Yanking email address to clipboard When I run :debug-set-fake-clipboard And I open data/email_address.html And I hint with args "links yank" and follow a Then the clipboard should contain "nobody" Scenario: Yanking javascript link to clipboard When I run :debug-set-fake-clipboard And I open data/hints/html/javascript.html And I hint with args "links yank" and follow a Then the clipboard should contain "javascript:window.location.href='/data/hello.txt'" Scenario: Rapid yanking When I run :debug-set-fake-clipboard And I open data/hints/rapid.html And I hint with args "links yank --rapid" And I run :hint-follow a And I run :hint-follow s And I run :mode-leave Then the clipboard should contain "http://localhost:(port)/data/hello.txt(linesep)http://localhost:(port)/data/hello2.txt" Scenario: Rapid hinting When I open data/hints/rapid.html in a new tab And I run :tab-only And I hint with args "all tab-bg --rapid" And I run :hint-follow a And I run :hint-follow s And I run :mode-leave And I wait until data/hello.txt is loaded And I wait until data/hello2.txt is loaded # We should check what the active tab is, but for some reason that makes # the test flaky Then the session should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/hints/rapid.html - history: - url: http://localhost:*/data/hello.txt - history: - url: http://localhost:*/data/hello2.txt """ Scenario: Using hint --rapid to hit multiple buttons When I open data/hints/buttons.html And I hint with args "--rapid" And I run :hint-follow s And I run :hint-follow d And I run :hint-follow f Then the javascript message "beep!" should be logged And the javascript message "bop!" should be logged And the javascript message "boop!" should be logged Scenario: Using :hint run with a URL containing spaces When I open data/hints/html/with_spaces.html And I hint with args "all run message-info {hint-url}" and follow a Then the message "http://localhost:(port)/data/hello.txt" should be shown Scenario: Hinting inputs without type When I open data/hints/input.html And I hint with args "inputs" and follow a And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :mode-leave # The actual check is already done above Then no crash should happen Scenario: Error with invalid hint group When I open data/hints/buttons.html And I run :hint INVALID_GROUP Then the error "Undefined hinting group 'INVALID_GROUP'" should be shown Scenario: Custom hint group When I open data/hints/custom_group.html And I set hints.selectors to {"custom":[".clickable"]} And I hint with args "custom" and follow a Then the javascript message "beep!" should be logged Scenario: Custom hint group with URL pattern When I open data/hints/custom_group.html And I run :set -tu *://*/data/hints/custom_group.html hints.selectors '{"custom": [".clickable"]}' And I hint with args "custom" and follow a Then the javascript message "beep!" should be logged Scenario: Fallback to global value with URL pattern set When I open data/hints/custom_group.html And I set hints.selectors to {"custom":[".clickable"]} And I run :set -tu *://*/data/hints/custom_group.html hints.selectors '{"other": [".other"]}' And I hint with args "custom" and follow a Then the javascript message "beep!" should be logged @qtwebkit_skip Scenario: Invalid custom selector When I open data/hints/custom_group.html And I set hints.selectors to {"custom":["@"]} And I run :hint custom Then the error "SyntaxError: Failed to execute 'querySelectorAll' on 'Document': '@' is not a valid selector." should be shown # https://github.com/qutebrowser/qutebrowser/issues/1613 Scenario: Hinting inputs with padding When I open data/hints/input.html And I hint with args "inputs" and follow s And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :mode-leave # The actual check is already done above Then no crash should happen Scenario: Hinting with ACE editor When I open data/hints/ace/ace.html And I hint with args "inputs" and follow a And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :mode-leave # The actual check is already done above Then no crash should happen Scenario: Hinting Twitter bootstrap checkbox When I open data/hints/bootstrap/checkbox.html And I hint with args "all" and follow a # The actual check is already done above Then "No elements found." should not be logged Scenario: Clicking input with existing text When I open data/hints/input.html And I run :click-element id qute-input-existing And I wait for "Entering mode KeyMode.insert *" in the log And I run :fake-key new Then the javascript message "contents: existingnew" should be logged ### iframes Scenario: Using :hint-follow inside an iframe When I open data/hints/iframe.html And I wait for "* wrapped loaded" in the log And I hint with args "links normal" and follow a Then "navigation request: url http://localhost:*/data/hello.txt (current http://localhost:*/data/hints/iframe.html), type link_clicked, *" should be logged Scenario: Using :hint-follow inside an iframe button When I open data/hints/iframe_button.html And I wait for "* wrapped_button loaded" in the log And I hint with args "all normal" and follow s Then "navigation request: url http://localhost:*/data/hello.txt (current http://localhost:*/data/hints/iframe_button.html), *" should be logged Scenario: Hinting inputs in an iframe without type When I open data/hints/iframe_input.html And I wait for "* input loaded" in the log And I hint with args "inputs" and follow a And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :mode-leave # The actual check is already done above Then no crash should happen Scenario: Using :hint-follow inside a scrolled iframe When I open data/hints/iframe_scroll.html And I wait for "* simple loaded" in the log And I hint with args "all normal" and follow a And I wait for "Clicked non-editable element!" in the log And I run :scroll bottom And I hint with args "links normal" and follow a Then "navigation request: url http://localhost:*/data/hello2.txt (current http://localhost:*/data/hints/iframe_scroll.html), type link_clicked, *" should be logged Scenario: Opening a link inside a specific iframe When I open data/hints/iframe_target.html And I hint with args "links normal" and follow a Then "navigation request: url http://localhost:*/data/hello.txt (current *), type link_clicked, *" should be logged Scenario: Opening a link with specific target frame in a new tab When I open data/hints/iframe_target.html And I run :tab-only And I hint with args "links tab" and follow s And I wait until data/hello2.txt is loaded Then the following tabs should be open: """ - data/hints/iframe_target.html (active) - data/hello2.txt """ Scenario: Clicking on iframe with :hint all current When I open data/hints/iframe.html And I wait for "* wrapped loaded" in the log And I hint with args "all current" and follow a Then no crash should happen Scenario: No error when hinting ranged input in frames When I open data/hints/issue3711_frame.html And I wait for "* issue3711 loaded" in the log And I hint with args "all current" and follow a Then no crash should happen ### hints.auto_follow.timeout @not_mac @flaky Scenario: Ignoring key presses after auto-following hints When I set hints.auto_follow_timeout to 1000 And I set hints.mode to number And I run :bind , message-error "This error message was triggered via a keybinding which should have been inhibited" And I open data/hints/html/simple.html And I hint with args "all" And I press the key "f" And I wait until data/hello.txt is loaded And I press the key "," # Waiting here so we don't affect the next test And I wait for "NormalKeyParser for mode normal: Releasing inhibition state of normal mode." in the log Then "NormalKeyParser for mode normal: Ignoring key ',', because the normal mode is currently inhibited." should be logged Scenario: Turning off auto_follow_timeout When I set hints.auto_follow_timeout to 0 And I set hints.mode to number And I run :bind , message-info "Keypress worked!" And I open data/hints/html/simple.html And I hint with args "all" And I press the key "f" And I wait until data/hello.txt is loaded And I press the key "," Then the message "Keypress worked!" should be shown ### Word hints Scenario: Hinting with a too short dictionary When I open data/hints/short_dict.html And I set hints.mode to word # Test letter fallback And I hint with args "all" and follow d Then the error "Not enough words in the dictionary." should be shown And data/numbers/5.txt should be loaded Scenario: Dictionary file does not exist When I open data/hints/html/simple.html And I set hints.dictionary to no_words And I set hints.mode to word And I run :hint And I wait for "hints: *" in the log And I press the key "a" Then the error "Word hints requires reading the file at *" should be shown And data/hello.txt should be loaded ### Number hint mode # https://github.com/qutebrowser/qutebrowser/issues/308 Scenario: Renumbering hints when filtering When I open data/hints/number.html And I set hints.mode to number And I hint with args "all" And I press the key "s" And I wait for "Filtering hints on 's'" in the log And I run :hint-follow 1 Then data/numbers/7.txt should be loaded # https://github.com/qutebrowser/qutebrowser/issues/576 @qtwebengine_flaky Scenario: Keeping hint filter in rapid mode When I open data/hints/number.html And I set hints.mode to number And I hint with args "all tab-bg --rapid" And I press the key "t" And I run :hint-follow 0 And I run :hint-follow 1 Then data/numbers/2.txt should be loaded And data/numbers/3.txt should be loaded # https://github.com/qutebrowser/qutebrowser/issues/1186 Scenario: Keeping hints filter when using backspace When I open data/hints/issue1186.html And I set hints.mode to number And I hint with args "all" And I press the key "x" And I press the key "0" And I press the key "" And I run :hint-follow 11 Then the error "No hint 11!" should be shown # https://github.com/qutebrowser/qutebrowser/issues/674#issuecomment-165096744 Scenario: Multi-word matching When I open data/hints/number.html And I set hints.mode to number And I set hints.auto_follow to unique-match And I set hints.auto_follow_timeout to 0 And I hint with args "all" And I press the keys "ten p" Then data/numbers/11.txt should be loaded Scenario: Scattering is ignored with number hints When I open data/hints/number.html And I set hints.mode to number And I set hints.scatter to true And I hint with args "all" and follow 00 Then data/numbers/1.txt should be loaded # https://github.com/qutebrowser/qutebrowser/issues/1559 Scenario: Filtering all hints in number mode When I open data/hints/number.html And I set hints.mode to number And I hint with args "all" And I press the key "2" And I wait for "Leaving mode KeyMode.hint (reason: all filtered)" in the log Then no crash should happen # https://github.com/qutebrowser/qutebrowser/issues/1657 Scenario: Using rapid number hinting twice When I open data/hints/number.html And I set hints.mode to number And I hint with args "--rapid" And I run :mode-leave And I hint with args "--rapid" and follow 00 Then data/numbers/1.txt should be loaded Scenario: Changing rapid hint filter after selecting hint When I open data/hints/number.html And I set hints.mode to number And I hint with args "all tab-bg --rapid " And I press the key "e" And I press the key "2" And I press the key "" And I press the key "o" And I press the key "0" Then data/numbers/1.txt should be loaded Scenario: Using a specific hints mode When I open data/hints/number.html And I set hints.mode to letter And I hint with args "--mode number all" And I press the key "s" And I wait for "Filtering hints on 's'" in the log And I run :hint-follow 1 Then data/numbers/7.txt should be loaded ### hints.leave_on_load Scenario: Leaving hint mode on reload When I set hints.leave_on_load to true And I open data/hints/html/wrapped.html And I hint with args "all" And I run :reload Then "Leaving mode KeyMode.hint (reason: load started)" should be logged Scenario: Leaving hint mode on reload without leave_on_load When I set hints.leave_on_load to false And I open data/hints/html/simple.html And I hint with args "all" And I run :reload Then "Leaving mode KeyMode.hint (reason: load started)" should not be logged ### hints.auto_follow option Scenario: Using hints.auto_follow = 'always' in letter mode When I open data/hints/html/simple.html And I set hints.mode to letter And I set hints.auto_follow to always And I hint with args "all" Then data/hello.txt should be loaded # unique-match is actually the same as full-match in letter mode Scenario: Using hints.auto_follow = 'unique-match' in letter mode When I open data/hints/html/simple.html And I set hints.mode to letter And I set hints.auto_follow to unique-match And I hint with args "all" And I press the key "a" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'full-match' in letter mode When I open data/hints/html/simple.html And I set hints.mode to letter And I set hints.auto_follow to full-match And I hint with args "all" And I press the key "a" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'never' without Enter in letter mode When I open data/hints/html/simple.html And I set hints.mode to letter And I set hints.auto_follow to never And I hint with args "all" And I press the key "a" Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged Scenario: Using hints.auto_follow = 'never' in letter mode When I open data/hints/html/simple.html And I set hints.mode to letter And I set hints.auto_follow to never And I hint with args "all" And I press the key "a" And I press the key "" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'always' in number mode When I open data/hints/html/simple.html And I set hints.mode to number And I set hints.auto_follow to always And I hint with args "all" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'unique-match' in number mode When I open data/hints/html/simple.html And I set hints.mode to number And I set hints.auto_follow to unique-match And I hint with args "all" And I press the key "f" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'full-match' in number mode When I open data/hints/html/simple.html And I set hints.mode to number And I set hints.auto_follow to full-match And I hint with args "all" # this actually presses the keys one by one And I press the key "follow me!" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'never' without Enter in number mode When I open data/hints/html/simple.html And I set hints.mode to number And I set hints.auto_follow to never And I hint with args "all" # this actually presses the keys one by one And I press the key "follow me!" Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged Scenario: Using hints.auto_follow = 'never' in number mode When I open data/hints/html/simple.html And I set hints.mode to number And I set hints.auto_follow to never And I hint with args "all" # this actually presses the keys one by one And I press the key "follow me!" And I press the key "" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'always' in word mode When I open data/hints/html/simple.html And I set hints.mode to word And I set hints.auto_follow to always And I hint with args "all" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'unique-match' in word mode When I open data/hints/html/simple.html And I set hints.mode to word And I set hints.auto_follow to unique-match And I hint with args "all" # the link gets "hello" as the hint And I press the key "h" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'full-match' in word mode When I open data/hints/html/simple.html And I set hints.mode to word And I set hints.auto_follow to full-match And I hint with args "all" # this actually presses the keys one by one And I press the key "hello" Then data/hello.txt should be loaded Scenario: Using hints.auto_follow = 'never' without Enter in word mode When I open data/hints/html/simple.html And I set hints.mode to word And I set hints.auto_follow to never And I hint with args "all" # this actually presses the keys one by one And I press the key "hello" Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged Scenario: Using hints.auto_follow = 'never' in word mode When I open data/hints/html/simple.html And I set hints.mode to word And I set hints.auto_follow to never And I hint with args "all" # this actually presses the keys one by one And I press the key "hello" And I press the key "" Then data/hello.txt should be loaded ## Other Scenario: Using --first with normal links When I open data/hints/html/simple.html And I hint with args "all --first" Then data/hello.txt should be loaded Scenario: Using --first with inputs When I open data/hints/input.html And I hint with args "inputs --first" And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log # ensure we clicked the first element And I run :jseval console.log(document.activeElement.id == "qute-input"); And I run :mode-leave Then the javascript message "true" should be logged Scenario: Hinting contenteditable inputs When I open data/hints/input.html And I hint with args "inputs" and follow f And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :mode-leave # The actual check is already done above Then no crash should happen Scenario: Deleting a simple target When I open data/hints/html/simple.html And I hint with args "all delete" and follow a And I run :hint Then the error "No elements found." should be shown Scenario: Statusbar text when entering hint mode from other mode When I open data/hints/html/simple.html And I run :mode-enter insert And I hint with args "all" And I run :debug-pyeval objreg.get('main-window', window='current', scope='window').status.txt.text() # Changing tabs will leave hint mode And I wait until qute://pyeval/ is loaded Then the page should contain the plaintext "'Follow hint...'" Scenario: Hinting an input after undoing a tab close When I open about:blank And I open data/hints/link_input.html in a new tab And I run :tab-close And I run :undo And I wait until data/hints/link_input.html is loaded And I run :click-element id qute-input-existing And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log And I run :fake-key -g something Then the javascript message "contents: existingsomething" should be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/history.feature0000644000175100017510000001102415102145205023117 0ustar00runnerrunnerFeature: Page history Make sure the global page history is saved correctly. Background: Given I run :history-clear --force Scenario: Simple history saving When I open data/numbers/1.txt And I open data/numbers/2.txt Then the history should contain: """ http://localhost:(port)/data/numbers/1.txt http://localhost:(port)/data/numbers/2.txt """ Scenario: History item with title When I open data/title.html Then the history should contain: """ http://localhost:(port)/data/title.html Test title """ Scenario: History item with redirect When I open redirect-to?url=data/title.html without waiting And I wait until data/title.html is loaded Then the history should contain: """ r http://localhost:(port)/redirect-to?url=data/title.html Test title http://localhost:(port)/data/title.html Test title """ Scenario: History item with spaces in URL When I open data/title with spaces.html Then the history should contain: """ http://localhost:(port)/data/title%20with%20spaces.html Test title """ @unicode_locale Scenario: History item with umlauts When I open data/äöü.html Then the history should contain: """ http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli """ @flaky @qtwebengine_todo # Error page message is not implemented Scenario: History with an error When I run :open file:///does/not/exist And I wait for "Error while loading file:///does/not/exist: Error opening /does/not/exist: *" in the log Then the history should contain: """ file:///does/not/exist Error loading page: file:///does/not/exist """ @qtwebengine_todo # Error page message is not implemented Scenario: History with a 404 When I open 404 without waiting And I wait for "Error while loading http://localhost:*/404: NOT FOUND" in the log Then the history should contain: """ http://localhost:(port)/404 Error loading page: http://localhost:(port)/404 """ Scenario: History with invalid URL When I run :tab-only And I open data/javascript/window_open.html And I run :click-element id open-invalid Then "load status for * LoadStatus.success" should be logged Scenario: Clearing history When I run :tab-only And I open data/title.html And I run :history-clear --force Then the history should be empty Scenario: Clearing history with confirmation When I open data/title.html And I run :history-clear And I wait for "Asking question <* title='Clear all browsing history?'>, *" in the log And I run :prompt-accept yes Then the history should be empty Scenario: History with yanked URL and 'add to history' flag When I open data/hints/html/simple.html And I hint with args "--add-history links yank" and follow a Then the history should contain: """ http://localhost:(port)/data/hints/html/simple.html Simple link http://localhost:(port)/data/hello.txt """ @flaky Scenario: Listing history When I open data/numbers/3.txt And I open data/numbers/4.txt And I open qute://history And I wait 2s Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" @flaky Scenario: Listing history with qute:history redirect When I open data/numbers/3.txt And I open data/numbers/4.txt And I open qute:history without waiting And I wait until qute://history is loaded And I wait 2s Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" @flaky Scenario: XSS in :history When I open data/issue4011.html And I open qute://history Then the javascript message "XSS" should not be logged @skip # Too flaky Scenario: Escaping of URLs in :history When I open query?one=1&two=2 And I open qute://history And I wait 2s # JS loads the history async And I hint with args "links normal" and follow a And I wait until query?one=1&two=2 is loaded Then the query parameter two should be set to 2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/invoke.feature0000644000175100017510000001142115102145205022712 0ustar00runnerrunnerFeature: Invoking a new process Simulate what happens when running qutebrowser with an existing instance Background: Given I clean up open tabs Scenario: Using new_instance_open_target = tab When I set new_instance_open_target to tab And I open data/title.html And I open data/search.html as a URL Then the following tabs should be open: """ - data/title.html - data/search.html (active) """ Scenario: Using new_instance_open_target = tab-bg When I set new_instance_open_target to tab-bg And I open data/title.html And I open data/search.html as a URL Then the following tabs should be open: """ - data/title.html (active) - data/search.html """ Scenario: Using new_instance_open_target = window When I set new_instance_open_target to window And I open data/title.html And I open data/search.html as a URL Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - tabs: - history: - url: http://localhost:*/data/search.html """ Scenario: Using new_instance_open_target = private-window When I set new_instance_open_target to private-window And I open data/title.html And I open data/search.html as a URL Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - private: True tabs: - history: - url: http://localhost:*/data/search.html """ Scenario: Using new_instance_open_target_window = last-opened When I set new_instance_open_target to tab And I set new_instance_open_target_window to last-opened And I open data/title.html And I open data/search.html in a new window And I open data/hello.txt as a URL Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - tabs: - history: - url: http://localhost:*/data/search.html - history: - url: http://localhost:*/data/hello.txt """ Scenario: Using new_instance_open_target_window = first-opened When I set new_instance_open_target to tab And I set new_instance_open_target_window to first-opened And I open data/title.html And I open data/search.html in a new window And I open data/hello.txt as a URL Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - history: - url: http://localhost:*/data/hello.txt - tabs: - history: - url: http://localhost:*/data/search.html """ # issue #1060 Scenario: Using target_window = first-opened after tab-give When I set new_instance_open_target to tab And I set new_instance_open_target_window to first-opened And I open data/title.html And I open data/search.html in a new tab And I run :tab-give And I wait until data/search.html is loaded And I open data/hello.txt as a URL Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - history: - url: http://localhost:*/data/hello.txt - tabs: - history: - url: http://localhost:*/data/search.html """ Scenario: Opening a new qutebrowser instance with no parameters When I set new_instance_open_target to tab And I set url.start_pages to ["http://localhost:(port)/data/hello.txt"] And I open data/title.html And I spawn a new window And I wait until data/hello.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - tabs: - history: - url: http://localhost:*/data/hello.txt """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/javascript.feature0000644000175100017510000002026215102145205023570 0ustar00runnerrunnerFeature: Javascript stuff Integration with javascript. Scenario: Using console.log When I open data/javascript/consolelog.html Then the javascript message "console.log works!" should be logged @skip # Too flaky Scenario: Opening/Closing a window via JS When I open data/javascript/window_open.html And I run :tab-only And I run :click-element id open-normal And I wait for "Changing title for idx 1 to 'about:blank'" in the log And I run :tab-focus 1 And I run :click-element id close-normal And I wait for "[*] window closed" in the log Then "Focus object changed: *" should be logged And the following tabs should be open: """ - data/javascript/window_open.html (active) """ @skip # Too flaky Scenario: Opening/closing a modal window via JS When I open data/javascript/window_open.html And I run :tab-only And I run :click-element id open-modal And I wait for "Changing title for idx 1 to 'about:blank'" in the log And I run :tab-focus 1 And I run :click-element id close-normal And I wait for "[*] window closed" in the log Then "Focus object changed: *" should be logged And "Web*Dialog requested, but we don't support that!" should be logged And the following tabs should be open: """ - data/javascript/window_open.html (active) """ # https://github.com/qutebrowser/qutebrowser/issues/906 @qtwebengine_skip Scenario: Closing a JS window twice (issue 906) - qtwebkit When I open about:blank And I run :tab-only And I open data/javascript/window_open.html in a new tab And I run :click-element id open-normal And I wait for "Changing title for idx 2 to 'about:blank'" in the log And I run :tab-focus 2 And I run :click-element id close-twice And I wait for "[*] window closed" in the log Then "Requested to close * which does not exist!" should be logged @qtwebkit_skip @flaky Scenario: Closing a JS window twice (issue 906) - qtwebengine When I open about:blank And I run :tab-only And I open data/javascript/window_open.html in a new tab And I run :click-element id open-normal And I wait for "Changing title for idx 2 to 'about:blank'" in the log And I run :tab-select window_open.html And I run :click-element id close-twice And I wait for "Focus object changed: *" in the log And I wait for "[*] window closed" in the log Then no crash should happen @flaky Scenario: Opening window without user interaction with content.javascript.can_open_tabs_automatically set to true When I open data/hello.txt And I set content.javascript.can_open_tabs_automatically to true And I run :tab-only And I run :jseval if (window.open('about:blank')) { console.log('window opened'); } else { console.log('error while opening window'); } Then the javascript message "window opened" should be logged @flaky Scenario: Opening window without user interaction with javascript.can_open_tabs_automatically set to false When I open data/hello.txt And I set content.javascript.can_open_tabs_automatically to false And I run :tab-only And I run :jseval if (window.open('about:blank')) { console.log('window opened'); } else { console.log('error while opening window'); } Then the javascript message "error while opening window" should be logged Scenario: Executing jseval when javascript is disabled When I set content.javascript.enabled to false And I run :jseval console.log('jseval executed') And I set content.javascript.enabled to true Then the javascript message "jseval executed" should be logged ## webelement issues (mostly with QtWebEngine) # https://github.com/qutebrowser/qutebrowser/issues/2569 Scenario: Clicking on form element with tagName child When I open data/issue2569.html And I run :click-element id tagnameform And I wait for "Sending fake click to *" in the log Then no crash should happen Scenario: Clicking on form element with text child When I open data/issue2569.html And I run :click-element id textform And I wait for "Sending fake click to *" in the log Then no crash should happen Scenario: Clicking on form element with value child When I open data/issue2569.html And I run :click-element id valueform And I wait for "Sending fake click to *" in the log Then no crash should happen Scenario: Clicking on svg element When I open data/issue2569.html And I run :click-element id icon And I wait for "Sending fake click to *" in the log Then no crash should happen Scenario: Clicking on li element When I open data/issue2569.html And I run :click-element id listitem And I wait for "Sending fake click to *" in the log Then no crash should happen # We load the tab in the background, and the HTML sets the window size for # when it's hidden. # Then, "the window sizes should be the same" uses :jseval to set the size # when it's shown, and compares the two. # https://github.com/qutebrowser/qutebrowser/issues/1190 # https://github.com/qutebrowser/qutebrowser/issues/2495 Scenario: Have a GreaseMonkey script run at page start When I have a GreaseMonkey file saved for document-start with noframes unset And I run :greasemonkey-reload And I open data/hints/iframe.html And I wait for "* wrapped loaded" in the log Then the javascript message "Script is running on /data/hints/iframe.html" should be logged Scenario: Have a GreaseMonkey script running on frames When I have a GreaseMonkey file saved for document-end with noframes unset And I run :greasemonkey-reload And I open data/hints/iframe.html And I wait for "* wrapped loaded" in the log Then the javascript message "Script is running on /data/hints/html/wrapped.html" should be logged Scenario: Have a GreaseMonkey script running on noframes When I have a GreaseMonkey file saved for document-end with noframes set And I run :greasemonkey-reload And I open data/hints/iframe.html And I wait for "* wrapped loaded" in the log Then the javascript message "Script is running on /data/hints/html/wrapped.html" should not be logged Scenario: Per-URL localstorage setting When I set content.local_storage to false And I run :set -tu http://localhost:*/data2/* content.local_storage true And I open data/javascript/localstorage.html And I wait for "[*] local storage is not working" in the log And I open data2/javascript/localstorage.html Then the javascript message "local storage is working" should be logged Scenario: Per-URL JavaScript setting When I set content.javascript.enabled to false And I run :set -tu http://localhost:*/data2/* content.javascript.enabled true And I open data2/javascript/enabled.html And I wait for "[*] JavaScript is enabled" in the log And I open data/javascript/enabled.html Then the page should contain the plaintext "JavaScript is disabled" @qtwebkit_skip Scenario: Error pages without JS enabled When I set content.javascript.enabled to false And I open 500 without waiting Then "Showing error page for* 500" should be logged And "Load error: *500" should be logged @skip # Too flaky Scenario: Using JS after window.open When I open data/hello.txt And I set content.javascript.can_open_tabs_automatically to true And I run :jseval window.open('about:blank') And I open data/hello.txt And I run :tab-only And I open data/hints/html/simple.html And I run :hint all And I wait for "hints: a" in the log And I run :mode-leave Then "There was an error while getting hint elements" should not be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/keyinput.feature0000644000175100017510000001762415102145205023302 0ustar00runnerrunnerFeature: Keyboard input Tests for :clear-keychain and other keyboard input related things. # :clear-keychain Scenario: Clearing the keychain When I run :bind ,foo message-error test12 And I run :bind ,bar message-info test12-2 And I press the keys ",fo" And I run :clear-keychain And I press the keys ",bar" And I run :unbind ,foo And I run :unbind ,bar Then the message "test12-2" should be shown # input.forward_unbound_keys Scenario: Forwarding no keys When I open data/keyinput/log.html And I set input.forward_unbound_keys to none And I press the key "" # Then the javascript message "key press: 112" should not be logged And the javascript message "key release: 112" should not be logged # :fake-key Scenario: :fake-key with an unparsable key When I run :fake-key Then the error "Could not parse '': Got invalid key!" should be shown Scenario: :fake-key sending key to the website When I open data/keyinput/log.html And I wait 0.01s And I run :fake-key x Then the javascript message "key press: 88" should be logged And the javascript message "key release: 88" should be logged @no_xvfb @posix @qtwebengine_skip Scenario: :fake-key sending key to the website with other window focused When I open data/keyinput/log.html And I run :devtools And I wait for "Focus object changed: " in the log And I run :fake-key x And I run :devtools And I wait for "Focus object changed: " in the log Then the error "No focused webview!" should be shown Scenario: :fake-key sending special key to the website When I open data/keyinput/log.html And I wait 0.01s And I run :fake-key Then the javascript message "key press: 27" should be logged And the javascript message "key release: 27" should be logged Scenario: :fake-key sending keychain to the website When I open data/keyinput/log.html And I wait 0.01s And I run :fake-key xy" " Then the javascript message "key press: 88" should be logged And the javascript message "key release: 88" should be logged And the javascript message "key press: 190" should be logged And the javascript message "key release: 190" should be logged And the javascript message "key press: 89" should be logged And the javascript message "key release: 89" should be logged And the javascript message "key press: 188" should be logged And the javascript message "key release: 188" should be logged And the javascript message "key press: 32" should be logged And the javascript message "key release: 32" should be logged Scenario: :fake-key sending keypress to qutebrowser When I run :fake-key -g x And I wait for "got keypress in mode KeyMode.normal - delegating to " in the log Then no crash should happen # Macros Scenario: Recording a simple macro When I run :macro-record And I press the key "a" And I run :message-info "foo 1" And I run :message-info "bar 1" And I run :macro-record And I run :macro-run with count 2 And I press the key "a" Then the message "foo 1" should be shown And the message "bar 1" should be shown And the message "foo 1" should be shown And the message "bar 1" should be shown And the message "foo 1" should be shown And the message "bar 1" should be shown Scenario: Recording a named macro When I run :macro-record foo And I run :message-info "foo 2" And I run :message-info "bar 2" And I run :macro-record foo And I run :macro-run foo Then the message "foo 2" should be shown And the message "bar 2" should be shown And the message "foo 2" should be shown And the message "bar 2" should be shown Scenario: Running an invalid macro Given I open data/scroll/simple.html And I run :tab-only When I run :macro-run And I press the key "b" Then the error "No macro recorded in 'b'!" should be shown And no crash should happen Scenario: Running an invalid named macro Given I open data/scroll/simple.html And I run :tab-only When I run :macro-run bar Then the error "No macro recorded in 'bar'!" should be shown And no crash should happen Scenario: Running a macro with a mode-switching command When I open data/hints/html/simple.html And I run :macro-record a And I run :hint links normal And I wait for "hints: *" in the log And I run :mode-leave And I run :macro-record a And I run :macro-run And I press the key "a" And I wait for "hints: *" in the log Then no crash should happen Scenario: Cancelling key input When I run :macro-record And I press the key "" Then "Leaving mode KeyMode.record_macro (reason: leave current)" should be logged Scenario: Ignoring non-register keys When I run :macro-record And I press the key "" And I press the key "c" And I run :message-info "foo 3" And I run :macro-record And I run :macro-run And I press the key "c" Then the message "foo 3" should be shown And the message "foo 3" should be shown # test all tabs.mode_on_change modes Scenario: mode on change normal Given I set tabs.mode_on_change to normal And I clean up open tabs When I open data/hello.txt And I run :mode-enter insert And I wait for "Entering mode KeyMode.insert (reason: command)" in the log And I open data/hello2.txt in a new background tab And I run :tab-focus 2 Then "Leaving mode KeyMode.insert (reason: tab changed)" should be logged And "Mode before tab change: insert (mode_on_change = normal)" should be logged And "Mode after tab change: normal (mode_on_change = normal)" should be logged Scenario: mode on change persist Given I set tabs.mode_on_change to persist And I clean up open tabs When I open data/hello.txt And I run :mode-enter insert And I wait for "Entering mode KeyMode.insert (reason: command)" in the log And I open data/hello2.txt in a new background tab And I run :tab-focus 2 Then "Leaving mode KeyMode.insert (reason: tab changed)" should not be logged And "Mode before tab change: insert (mode_on_change = persist)" should be logged And "Mode after tab change: insert (mode_on_change = persist)" should be logged Scenario: mode on change restore Given I set tabs.mode_on_change to restore And I clean up open tabs When I open data/hello.txt And I run :mode-enter insert And I wait for "Entering mode KeyMode.insert (reason: command)" in the log And I open data/hello2.txt in a new background tab And I run :tab-focus 2 And I wait for "Mode before tab change: insert (mode_on_change = restore)" in the log And I wait for "Mode after tab change: normal (mode_on_change = restore)" in the log And I run :mode-enter passthrough And I wait for "Entering mode KeyMode.passthrough (reason: command)" in the log And I run :tab-focus 1 Then "Mode before tab change: passthrough (mode_on_change = restore)" should be logged And "Entering mode KeyMode.insert (reason: restore)" should be logged And "Mode after tab change: insert (mode_on_change = restore)" should be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/marks.feature0000644000175100017510000001134015102145205022534 0ustar00runnerrunnerFeature: Setting positional marks Background: Given I open data/marks.html And I run :tab-only ## :set-mark, :jump-mark Scenario: Setting and jumping to a local mark When I run :scroll-px 5 10 And I wait until the scroll position changed to 5/10 And I run :set-mark a And I run :scroll-px 0 20 And I wait until the scroll position changed to 5/30 And I run :jump-mark a And I wait until the scroll position changed to 5/10 Then the page should be scrolled to 5 10 Scenario: Jumping back after jumping to a particular percentage When I run :scroll-px 10 20 And I wait until the scroll position changed to 10/20 And I run :scroll-to-perc 100 And I wait until the scroll position changed And I run :jump-mark "'" And I wait until the scroll position changed to 10/20 Then the page should be scrolled to 10 20 @qtwebengine_flaky Scenario: Setting the same local mark on another page When I run :scroll-px 5 10 And I wait until the scroll position changed to 5/10 And I run :set-mark a And I open data/marks.html And I run :scroll-px 0 20 And I wait until the scroll position changed to 0/20 And I run :set-mark a And I run :jump-mark a Then the page should be scrolled to 0 20 Scenario: Jumping to a local mark after returning to a page When I run :scroll-px 5 10 And I wait until the scroll position changed to 5/10 And I run :set-mark a And I run :scroll-px 0 20 And I wait until the scroll position changed to 5/30 And I open data/numbers/1.txt And I run :set-mark a And I open data/marks.html And I run :jump-mark a And I wait until the scroll position changed to 5/10 Then the page should be scrolled to 5 10 Scenario: Setting and jumping to a global mark When I run :scroll-px 5 20 And I wait until the scroll position changed to 5/20 And I run :set-mark A And I open data/numbers/1.txt And I wait until the scroll position changed to 0/0 And I run :jump-mark A And I wait until the scroll position changed to 5/20 Then data/marks.html should be loaded And the page should be scrolled to 5 20 Scenario: Jumping to an unset mark When I run :jump-mark b Then the error "Mark b is not set" should be shown Scenario: Jumping to a local mark that was set on another page When I run :set-mark b And I open data/numbers/1.txt And I run :jump-mark b Then the error "Mark b is not set" should be shown @qtwebengine_skip # Does not emit loaded signal for fragments? Scenario: Jumping to a local mark after changing fragments When I open data/marks.html#top And I run :scroll 'top' And I wait until the scroll position changed to 0/0 And I run :scroll-px 10 10 And I wait until the scroll position changed to 10/10 And I run :set-mark a When I open data/marks.html#bottom And I run :jump-mark a And I wait until the scroll position changed to 10/10 Then the page should be scrolled to 10 10 @qtwebengine_skip # Does not emit loaded signal for fragments? Scenario: Jumping back after following a link When I hint with args "links normal" and follow s And I wait until data/marks.html#bottom is loaded And I run :jump-mark "'" And I wait until the scroll position changed to 0/0 Then the page should be scrolled to 0 0 Scenario: Jumping back after searching When I run :scroll-px 20 15 And I wait until the scroll position changed to 20/15 And I run :search Waldo And I wait until the scroll position changed And I run :jump-mark "'" And I wait until the scroll position changed to 20/15 Then the page should be scrolled to 20 15 Scenario: Jumping back after search-next When I run :search Grail And I run :search-next And I wait until the scroll position changed And I run :jump-mark "'" And I wait until the scroll position changed to 0/0 Then the page should be scrolled to 0 0 Scenario: Hovering a hint does not set the ' mark When I run :scroll-px 10 20 And I wait until the scroll position changed to 10/20 And I run :scroll-to-perc 0 And I wait until the scroll position changed And I hint with args "links hover" and follow s And I run :jump-mark "'" And I wait until the scroll position changed to 10/20 Then the page should be scrolled to 10 20 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/misc.feature0000644000175100017510000006311615102145205022362 0ustar00runnerrunnerFeature: Various utility commands. ## :cmd-set-text Scenario: :cmd-set-text and :command-accept When I run :cmd-set-text :message-info "Hello World" And I run :command-accept Then the message "Hello World" should be shown Scenario: :cmd-set-text and :command-accept --rapid When I run :cmd-set-text :message-info "Hello World" And I run :command-accept --rapid And I run :command-accept Then the message "Hello World" should be shown And the message "Hello World" should be shown Scenario: :cmd-set-text with two commands When I run :cmd-set-text :message-info test ;; message-error error And I run :command-accept Then the message "test" should be shown And the error "error" should be shown Scenario: :cmd-set-text with URL replacement When I open data/hello.txt And I run :cmd-set-text :message-info {url} And I run :command-accept Then the message "http://localhost:*/hello.txt" should be shown Scenario: :cmd-set-text with URL replacement with encoded spaces When I open data/title with spaces.html And I run :cmd-set-text :message-info {url} And I run :command-accept Then the message "http://localhost:*/title%20with%20spaces.html" should be shown Scenario: :cmd-set-text with URL replacement with decoded spaces When I open data/title with spaces.html And I run :cmd-set-text :message-info "> {url:pretty} <" And I run :command-accept Then the message "> http://localhost:*/title with spaces.html <" should be shown Scenario: :cmd-set-text with -s and -a When I run :cmd-set-text -s :message-info "foo And I run :cmd-set-text -a bar" And I run :command-accept Then the message "foo bar" should be shown Scenario: :cmd-set-text with -a but without text When I run :cmd-set-text -a foo Then the error "No current text!" should be shown Scenario: :cmd-set-text with invalid command When I run :cmd-set-text foo Then the error "Invalid command text 'foo'." should be shown Scenario: :cmd-set-text with run on count flag and no count When I run :cmd-set-text --run-on-count :message-info "Hello World" Then "message:info:86 Hello World" should not be logged Scenario: :cmd-set-text with run on count flag and a count When I run :cmd-set-text --run-on-count :message-info "Hello World" with count 1 Then the message "Hello World" should be shown ## :jseval Scenario: :jseval When I run :jseval console.log("Hello from JS!"); And I wait for the javascript message "Hello from JS!" Then the message "No output or error" should be shown Scenario: :jseval without logging When I set content.javascript.log to {"unknown": "none", "info": "none", "warning": "debug", "error": "debug"} And I run :jseval console.log("Hello from JS!"); And I wait for "No output or error" in the log And I set content.javascript.log to {"unknown": "debug", "info": "debug", "warning": "debug", "error": "debug"} Then "[:*] Hello from JS!" should not be logged Scenario: :jseval with --quiet When I run :jseval --quiet console.log("Hello from JS!"); And I wait for the javascript message "Hello from JS!" Then "No output or error" should not be logged Scenario: :jseval with a value When I run :jseval "foo" Then the message "foo" should be shown Scenario: :jseval with a long, truncated value When I run :jseval Array(5002).join("x") Then the message "x* [...trimmed...]" should be shown Scenario: :jseval --url When I run :jseval --url javascript:console.log("hello world?") Then the javascript message "hello world?" should be logged @qtwebengine_skip Scenario: :jseval with --world on QtWebKit When I run :jseval --world=1 console.log("Hello from JS!"); And I wait for the javascript message "Hello from JS!" Then "Ignoring world ID 1" should be logged And "No output or error" should be logged @qtwebkit_skip Scenario: :jseval uses separate world without --world When I open data/misc/jseval.html And I run :jseval do_log() Then the javascript message "Hello from the page!" should not be logged And the javascript message "Uncaught ReferenceError: do_log is not defined" should be logged And "No output or error" should be logged @qtwebkit_skip Scenario: :jseval using the main world When I open data/misc/jseval.html And I run :jseval --world 0 do_log() Then the javascript message "Hello from the page!" should be logged And "No output or error" should be logged @qtwebkit_skip Scenario: :jseval using the main world as name When I open data/misc/jseval.html And I run :jseval --world main do_log() Then the javascript message "Hello from the page!" should be logged And "No output or error" should be logged @qtwebkit_skip Scenario: :jseval using too high of a world When I run :jseval --world=257 console.log("Hello from JS!"); Then the error "World ID should be between 0 and *" should be shown @qtwebkit_skip Scenario: :jseval using a negative world id When I run :jseval --world=-1 console.log("Hello from JS!"); Then the error "World ID should be between 0 and *" should be shown Scenario: :jseval --file using a file that exists as js-code When I run :jseval --file (testdata)/misc/jseval_file.js Then the javascript message "Hello from JS!" should be logged And the javascript message "Hello again from JS!" should be logged And "No output or error" should be logged Scenario: :jseval --file using a file that doesn't exist as js-code When I run :jseval --file (rootpath)nonexistentfile Then the error "[Errno 2] *: '*nonexistentfile'" should be shown And "No output or error" should not be logged @qtwebkit_skip Scenario: CSP errors in qutebrowser stylesheet script When I open restrictive-csp Then the javascript message "Refused to apply inline style because it violates the following Content Security Policy directive: *" should be logged @qtwebkit_skip Scenario: Third-party iframes in qutebrowser stylesheet script When I load a third-party iframe # rerun set_css in stylesheet.js And I set content.user_stylesheets to [] Then the javascript message "Failed to style frame:* Blocked a frame with origin * from accessing *" should be logged # :debug-webaction Scenario: :debug-webaction with valid value Given I open data/backforward/1.txt When I open data/backforward/2.txt And I run :tab-only And I run :debug-webaction Back And I wait until data/backforward/1.txt is loaded Then the session should look like: """ windows: - tabs: - history: - active: true url: http://localhost:*/data/backforward/1.txt - url: http://localhost:*/data/backforward/2.txt """ Scenario: :debug-webaction with invalid value When I open data/hello.txt And I run :debug-webaction blah Then the error "blah is not a valid web action!" should be shown Scenario: :debug-webaction with non-webaction member When I open data/hello.txt And I run :debug-webaction PermissionUnknown Then the error "PermissionUnknown is not a valid web action!" should be shown # :inspect @no_xvfb @posix @qtwebengine_skip Scenario: Inspector smoke test When I run :devtools And I wait for "Focus object changed: " in the log And I run :devtools And I wait for "Focus object changed: *" in the log Then no crash should happen # Different code path as an inspector got created now @no_xvfb @posix @qtwebengine_skip Scenario: Inspector smoke test 2 When I run :devtools And I wait for "Focus object changed: " in the log And I run :devtools And I wait for "Focus object changed: *" in the log Then no crash should happen # :stop/:reload @flaky Scenario: :stop Given I have a fresh instance # We can't use "When I open" because we don't want to wait for load # finished When I run :open http://localhost:(port)/redirect-later?delay=-1 And I wait for "emitting: cur_load_status_changed() (tab *)" in the log And I wait 1s And I run :stop And I open redirect-later-continue in a new tab And I wait 1s Then the unordered requests should be: """ redirect-later-continue redirect-later?delay=-1 """ # no request on / because we stopped the redirect Scenario: :stop with wrong count When I open data/hello.txt And I run :tab-only And I run :stop with count 2 Then no crash should happen Scenario: :reload When I open data/reload.txt And I run :reload And I wait until data/reload.txt is loaded Then the requests should be: """ data/reload.txt data/reload.txt """ Scenario: :reload with force When I open headers And I run :reload --force And I wait until headers is loaded Then the header Cache-Control should be set to no-cache Scenario: :reload with wrong count When I open data/hello.txt And I run :tab-only And I run :reload with count 2 Then no crash should happen # :view-source # Flaky due to :view-source being async? @qtwebengine_flaky Scenario: :view-source Given I open data/hello.txt When I run :tab-only And I run :view-source Then the session should look like: """ windows: - tabs: - history: - active: true url: http://localhost:*/data/hello.txt - active: true history: [] """ And the page should contain the html "/* Literal.Number.Integer */" # Flaky due to :view-source being async? @qtwebengine_flaky Scenario: :view-source on source page. When I open data/hello.txt And I run :view-source And I run :view-source Then the error "Already viewing source!" should be shown # :home Scenario: :home with single page When I set url.start_pages to ["http://localhost:(port)/data/hello2.txt"] And I run :home Then data/hello2.txt should be loaded Scenario: :home with multiple pages When I set url.start_pages to ["http://localhost:(port)/data/numbers/1.txt", "http://localhost:(port)/data/numbers/2.txt"] And I run :home Then data/numbers/1.txt should be loaded # :print # Disabled because it causes weird segfaults and QPainter warnings in Qt... @xfail_norun Scenario: print preview When I open data/hello.txt And I run :print --preview And I wait for "Focus object changed: *" in the log And I run :debug-pyeval QApplication.instance().activeModalWidget().close() Then no crash should happen # On Windows/macOS, we get a "QPrintDialog: Cannot be used on non-native # printers" qWarning. # # Disabled because it causes weird segfaults and QPainter warnings in Qt... @xfail_norun Scenario: print When I open data/hello.txt And I run :print And I wait for "Focus object changed: *" in the log or skip the test And I run :debug-pyeval QApplication.instance().activeModalWidget().close() Then no crash should happen Scenario: print --pdf When I open data/hello.txt And I run :print --pdf (tmpdir)/hello.pdf And I wait for "Printed to *" in the log Then the PDF hello.pdf should exist in the tmpdir Scenario: print --pdf with subdirectory When I open data/hello.txt And I run :print --pdf (tmpdir)/subdir/subdir2/hello.pdf And I wait for "Print to file: *" in the log or skip the test Then no crash should happen ## https://github.com/qutebrowser/qutebrowser/issues/504 Scenario: Focusing download widget via Tab When I open about:blank And I press the key "" And I press the key "" Then no crash should happen Scenario: Focusing download widget via Tab (original issue) When I open data/prompt/jsprompt.html And I run :click-element id button And I wait for "Entering mode KeyMode.prompt *" in the log And I press the key "" And I press the key "" And I run :mode-leave Then no crash should happen ## Custom headers Scenario: Setting a custom header When I set content.headers.custom to {"X-Qute-Test": "testvalue"} And I open headers Then the header X-Qute-Test should be set to testvalue Scenario: Setting accept header When I set content.headers.custom to {"Accept": "testvalue"} And I open headers Then the header Accept should be set to testvalue Scenario: DNT header When I set content.headers.do_not_track to true And I open headers Then the header Dnt should be set to 1 Scenario: DNT header (off) When I set content.headers.do_not_track to false And I open headers Then the header Dnt should be set to 0 Scenario: DNT header (unset) When I set content.headers.do_not_track to And I open headers Then the header Dnt should be set to Scenario: Accept-Language header When I set content.headers.accept_language to en,de And I open headers Then the header Accept-Language should be set to en,de # This still doesn't set window.navigator.language # See https://bugreports.qt.io/browse/QTBUG-61949 @qtwebkit_skip Scenario: Accept-Language header (JS) When I set content.headers.accept_language to it,fr And I run :jseval console.log(window.navigator.languages) Then the javascript message "it,fr" should be logged Scenario: User-agent header When I set content.headers.user_agent to toaster And I open headers And I run :jseval console.log(window.navigator.userAgent) Then the header User-Agent should be set to toaster Scenario: User-agent header with redirect When I run :set -u localhost content.headers.user_agent toaster And I open redirect-to?url=headers without waiting And I wait until headers is loaded And I run :jseval console.log(window.navigator.userAgent) Then the header User-Agent should be set to toaster Scenario: User-agent header (JS) When I set content.headers.user_agent to toaster And I open about:blank And I run :jseval console.log(window.navigator.userAgent) Then the javascript message "toaster" should be logged @qtwebkit_skip Scenario: Custom headers via XHR When I set content.headers.custom to {"Accept": "config-value", "X-Qute-Test": "config-value"} When I set content.headers.accept_language to "config-value" And I open data/misc/xhr_headers.html And I wait for the javascript message "Got headers via XHR" Then the header Accept should be set to '*/*' And the header Accept-Language should be set to 'from XHR' And the header X-Qute-Test should be set to config-value ## https://github.com/qutebrowser/qutebrowser/issues/1523 Scenario: Completing a single option argument When I run :cmd-set-text -s :-- Then no crash should happen ## https://github.com/qutebrowser/qutebrowser/issues/1386 Scenario: Partial commandline matching with startup command When I run :message-i "Hello World" (invalid command) Then the error "message-i: no such command (did you mean :message-info?)" should be shown Scenario: Multiple leading : in command When I run :::::cmd-set-text ::::message-i "Hello World" And I run :command-accept Then the message "Hello World" should be shown Scenario: Whitespace in command When I run : : cmd-set-text : : message-i "Hello World" And I run :command-accept Then the message "Hello World" should be shown ## https://github.com/qutebrowser/qutebrowser/issues/4720 Scenario: Chaining failing commands When I run :scroll x ;; message-info foo Then the error "Invalid value 'x' for direction - *" should be shown And the message "foo" should be shown # We can't run :message-i as startup command, so we use # :cmd-set-text Scenario: Partial commandline matching When I run :cmd-set-text :message-i "Hello World" And I run :command-accept Then the message "Hello World" should be shown @no_xvfb Scenario: :window-only Given I run :tab-only And I open data/hello.txt When I open data/hello2.txt in a new tab And I open data/hello3.txt in a new window And I run :window-only And I wait for "Closing window *" in the log And I wait for "removed: main-window" in the log Then the session should look like: """ windows: - tabs: - active: true history: - url: http://localhost:*/data/hello3.txt """ ## :click-element Scenario: Clicking an element with unknown ID When I open data/click_element.html And I run :click-element id blah Then the error "No element found with ID "blah"!" should be shown Scenario: Clicking an element by ID When I open data/click_element.html And I run :click-element id qute-input Then "Entering mode KeyMode.insert (reason: clicking input)" should be logged Scenario: Clicking an element by ID with dot When I open data/click_element.html And I run :click-element id foo.bar Then the javascript message "id with dot" should be logged Scenario: Clicking an element with tab target When I open data/click_element.html And I run :tab-only And I run :click-element id link --target=tab Then data/hello.txt should be loaded And the following tabs should be open: """ - data/click_element.html - data/hello.txt (active) """ Scenario: Clicking an element by CSS selector When I open data/click_element.html And I run :click-element css .clickable Then the javascript message "click_element CSS selector" should be logged Scenario: Clicking an element with non-unique filter When I open data/click_element.html And I run :click-element css span Then the error "Multiple elements found matching CSS selector "span"!" should be shown Scenario: Clicking first element matching a selector When I open data/click_element.html And I run :click-element --select-first css span Then the javascript message "click_element clicked" should be logged Scenario: Clicking an element by position When I open data/click_element.html And I run :click-element position 20,42 Then the javascript message "click_element position" should be logged Scenario: Clicking an element with invalid position When I open data/click_element.html And I run :click-element position 20.42 Then the error "String 20.42 does not match X,Y" should be shown Scenario: Clicking an element with non-integer position When I open data/click_element.html And I run :click-element position 20,42.001 Then the error "String 20,42.001 does not match X,Y" should be shown Scenario: Clicking on focused element when there is none When I open data/click_element.html And I run :click-element focused Then the error "No element found with focus!" should be shown Scenario: Clicking on focused element When I open data/click_element.html And I run :jseval document.getElementById("qute-input").focus() And I wait for the javascript message "qute-input focused" And I run :click-element focused Then "Entering mode KeyMode.insert (reason: clicking input)" should be logged ## :command-history-{prev,next} Scenario: Calling previous command When I run :cmd-set-text :message-info blah And I run :command-accept And I wait for "blah" in the log And I run :cmd-set-text : And I run :command-history-prev And I run :command-accept Then the message "blah" should be shown Scenario: Command starting with space and calling previous command When I run :cmd-set-text :message-info first And I run :command-accept And I wait for "first" in the log When I run :cmd-set-text : message-info second And I run :command-accept And I wait for "second" in the log And I run :cmd-set-text : And I run :command-history-prev And I run :command-accept Then the message "first" should be shown Scenario: Calling previous command with :completion-item-focus When I run :cmd-set-text :message-info blah And I wait for "Entering mode KeyMode.command (reason: *)" in the log And I run :command-accept And I wait for "blah" in the log And I run :cmd-set-text : And I wait for "Entering mode KeyMode.command (reason: *)" in the log And I run :completion-item-focus prev --history And I run :command-accept Then the message "blah" should be shown Scenario: Browsing through commands When I run :cmd-set-text :message-info blarg And I run :command-accept And I wait for "blarg" in the log And I run :cmd-set-text : And I run :command-history-prev And I run :command-history-prev And I run :command-history-next And I run :command-history-next And I run :command-accept Then the message "blarg" should be shown Scenario: Calling previous command when history is empty Given I have a fresh instance When I run :cmd-set-text : And I run :command-history-prev And I run :command-accept Then the error "No command given" should be shown Scenario: Calling next command when there's no next command When I run :cmd-set-text : And I run :command-history-next And I run :command-accept Then the error "No command given" should be shown ## Modes blacklisted for :mode-enter Scenario: Trying to enter command mode with :mode-enter When I run :mode-enter command Then the error "Mode command can't be entered manually!" should be shown ## Renderer crashes # Skipped on Windows as "... has stopped working" hangs. @qtwebkit_skip @no_invalid_lines @posix Scenario: Renderer crash When I run :open -t chrome://crash Then "Renderer process crashed (status *)" should be logged And "* 'Error loading chrome://crash/'" should be logged @qtwebkit_skip @no_invalid_lines @flaky Scenario: Renderer kill When I run :open -t chrome://kill Then "Renderer process was killed (status *)" should be logged And "* 'Error loading chrome://kill/'" should be logged # https://github.com/qutebrowser/qutebrowser/issues/2290 @qtwebkit_skip @no_invalid_lines @flaky Scenario: Navigating to URL after renderer process is gone When I run :tab-only And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :open chrome://kill And I wait for "Renderer process was killed (status *)" in the log And I open data/numbers/3.txt Then no crash should happen # https://github.com/qutebrowser/qutebrowser/issues/5721 @qtwebkit_skip @qt!=5.15.1 Scenario: WebRTC renderer process crash When I open data/crashers/webrtc.html in a new tab And I run :reload And I wait until data/crashers/webrtc.html is loaded Then "Renderer process crashed (status *)" should not be logged Scenario: InstalledApps crash When I open data/crashers/installedapp.html in a new tab Then "Renderer process was killed (status *)" should not be logged ## Other Scenario: Resource with invalid URL When I open data/invalid_resource.html in a new tab Then "Ignoring invalid * URL: Invalid hostname (contains invalid characters); *" should be logged And no crash should happen @skip # Too flaky Scenario: Keyboard focus after cross-origin navigation When I turn on scroll logging And I open qute://gpl in a new tab And I run :tab-only And I open data/scroll/simple.html And I run :fake-key "" Then the page should be scrolled vertically @qtwebkit_skip Scenario: Using DocumentPictureInPicture API When I set content.javascript.can_open_tabs_automatically to true And I open data/crashers/document_picture_in_picture.html And I run :click-element id toggle Then the javascript message "documentPictureInPicture support disabled!" should be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/navigate.feature0000644000175100017510000000554115102145205023223 0ustar00runnerrunnerFeature: Using :navigate Scenario: :navigate with invalid argument When I run :navigate foo Then the error "where: Invalid value foo - expected one of: prev, next, up, increment, decrement, strip" should be shown # up Scenario: Navigating up in qute://help/ When the documentation is up to date And I open qute://help/commands.html And I run :navigate up Then qute://help/ should be loaded # prev/next Scenario: Navigating to previous page When I open data/navigate in a new tab And I run :navigate prev Then data/navigate/prev.html should be loaded Scenario: Navigating to next page When I open data/navigate And I run :navigate next Then data/navigate/next.html should be loaded Scenario: Navigating to previous page without links When I open data/numbers/1.txt And I run :navigate prev Then the error "No prev links found!" should be shown Scenario: Navigating to next page without links When I open data/numbers/1.txt And I run :navigate next Then the error "No forward links found!" should be shown Scenario: Navigating to next page to a fragment When I open data/navigate#fragment And I run :navigate next Then data/navigate/next.html should be loaded Scenario: Navigating to previous page with rel When I open data/navigate/rel.html And I run :navigate prev Then data/navigate/prev.html should be loaded Scenario: Navigating to next page with rel When I open data/navigate/rel.html And I run :navigate next Then data/navigate/next.html should be loaded Scenario: Navigating to previous page with rel nofollow When I open data/navigate/rel_nofollow.html And I run :navigate prev Then data/navigate/prev.html should be loaded Scenario: Navigating to next page with rel nofollow When I open data/navigate/rel_nofollow.html And I run :navigate next Then data/navigate/next.html should be loaded @qtwebkit_skip Scenario: Navigating with invalid selector When I open data/navigate And I set hints.selectors to {"links": ["@"]} And I run :navigate next Then the error "SyntaxError: Failed to execute 'querySelectorAll' on 'Document': '@' is not a valid selector." should be shown Scenario: Navigating with no next selector When I set hints.selectors to {'all': ['a']} And I run :navigate next Then the error "Undefined hinting group 'links'" should be shown # increment/decrement @qtwebengine_todo # Doesn't find any elements Scenario: Navigating multiline links When I open data/navigate/multilinelinks.html And I run :navigate next Then data/numbers/5.txt should be loaded ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/notifications.feature0000644000175100017510000001776315102145205024307 0ustar00runnerrunnerFeature: Notifications HTML5 notification API interaction Background: Given I clean up open tabs And I open data/javascript/notifications.html And I set content.notifications.enabled to true And I run :click-element id button And I clean up the notification server Scenario: Notification is shown When I run :click-element id show-button Then the javascript message "notification shown" should be logged And 1 notification should be presented # qutebrowser logo And the notification should have image dimensions 64x64 Scenario: Notification containing escaped characters Given the notification server supports body markup When I run :click-element id show-symbols-button Then the javascript message "notification shown" should be logged And the notification should have body "<< && >>" And the notification should have title "<< && >>" Scenario: Notification containing escaped characters with no body markup Given the notification server doesn't support body markup When I run :click-element id show-symbols-button Then the javascript message "notification shown" should be logged And the notification should have body "<< && >>" And the notification should have title "<< && >>" Scenario: Notification with RGB image When I run :click-element id show-image-button-noalpha Then the javascript message "notification shown" should be logged And the notification should have title "RGB" And the notification should have image dimensions 64x64 Scenario: Notification with RGBA image When I run :click-element id show-image-button Then the javascript message "notification shown" should be logged And the notification should have title "RGBA" And the notification should have image dimensions 64x64 Scenario: Notification with big image When I run :click-element id show-image-button-big Then the javascript message "notification shown" should be logged And the notification should have title "Big" And the notification should have image dimensions 320x160 Scenario: Notification with padded image When I run :click-element id show-image-button-padded Then the javascript message "notification shown" should be logged And the notification should have title "Padded" And the notification should have image dimensions 46x46 Scenario: Notification with padded image 2 When I run :click-element id show-image-button-padded-2 Then the javascript message "notification shown" should be logged And the notification should have title "Padded 2" And the notification should have image dimensions 239x239 Scenario: Closing notification via web When I run :click-element id show-closing-button Then the javascript message "notification shown" should be logged And the javascript message "notification closed" should be logged And the notification should be closed via web # As a WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html # and other issues, those can only run with PyQtWebEngine >= 5.15.0 # # For these tests, we need to wait for the notification to be shown before # we try to close it, otherwise we wind up in race-condition-ish # situations. @pyqtwebengine>=5.15.0 Scenario: Replacing existing notifications When I run :click-element id show-replacing-button Then the javascript message "i=1 notification shown" should be logged And the javascript message "i=2 notification shown" should be logged And the javascript message "i=3 notification shown" should be logged And 1 notification should be presented And the notification should have title "i=3" @pyqtwebengine<5.15.0 Scenario: Replacing existing notifications (old Qt) When I run :click-element id show-replacing-button Then the javascript message "i=1 notification shown" should be logged And "Ignoring notification tag 'counter' due to PyQt bug" should be logged And the javascript message "i=2 notification shown" should be logged And "Ignoring notification tag 'counter' due to PyQt bug" should be logged And the javascript message "i=3 notification shown" should be logged And "Ignoring notification tag 'counter' due to PyQt bug" should be logged And 3 notifications should be presented # last one And the notification should have title "i=3" @pyqtwebengine>=5.15.0 Scenario: User closes presented notification When I run :click-element id show-button And I wait for the javascript message "notification shown" And I close the notification Then the javascript message "notification closed" should be logged @pyqtwebengine<5.15.0 Scenario: User closes presented notification (old Qt) When I run :click-element id show-button And I wait for the javascript message "notification shown" And I close the notification Then "Ignoring close request for notification * due to PyQt bug" should be logged And the javascript message "notification closed" should not be logged And no crash should happen @pyqtwebengine>=5.15.0 Scenario: User closes some other application's notification When I run :click-element id show-button And I wait for the javascript message "notification shown" And I close the notification with id 1234 Then the javascript message "notification closed" should not be logged @pyqtwebengine>=5.15.0 Scenario: User clicks presented notification When I run :click-element id show-button And I wait for the javascript message "notification shown" And I open about:blank in a new tab And I click the notification Then the javascript message "notification clicked" should be logged And the following tabs should be open: """ - about:blank - data/javascript/notifications.html (active) - about:blank """ @pyqtwebengine<5.15.0 Scenario: User clicks presented notification (old Qt) When I run :click-element id show-button And I wait for the javascript message "notification shown" And I click the notification Then "Ignoring click request for notification * due to PyQt bug" should be logged Then the javascript message "notification clicked" should not be logged And no crash should happen @pyqtwebengine>=5.15.0 Scenario: User clicks some other application's notification When I run :click-element id show-button And I wait for the javascript message "notification shown" And I click the notification with id 1234 Then the javascript message "notification clicked" should not be logged @pyqtwebengine>=5.15.0 Scenario: Unknown action with some other application's notification When I run :click-element id show-button And I wait for the javascript message "notification shown" And I trigger a custom action on the notification with id 1234 Then no crash should happen Scenario: Notification via messages When I set content.notifications.presenter to messages And I run :click-element id show-button And I wait for the javascript message "notification shown" Then the message "Notification from http://localhost:*/:

notification title
notification body" should be shown Scenario: Notification via messages with image When I set content.notifications.presenter to messages And I run :click-element id show-image-button And I wait for the javascript message "notification shown" Then the message "Notification from http://localhost:*/: (image not shown)

RGBA
" should be shown ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/open.feature0000644000175100017510000001114515102145205022363 0ustar00runnerrunnerFeature: Opening pages Scenario: :open with URL Given I open about:blank When I run :open http://localhost:(port)/data/numbers/1.txt And I wait until data/numbers/1.txt is loaded And I run :tab-only Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - active: true url: http://localhost:*/data/numbers/1.txt """ Scenario: :open without URL When I set url.default_page to http://localhost:(port)/data/numbers/11.txt And I run :open Then data/numbers/11.txt should be loaded Scenario: :open without URL and -t When I set url.default_page to http://localhost:(port)/data/numbers/2.txt And I run :open -t Then data/numbers/2.txt should be loaded Scenario: :open with invalid URL When I set url.auto_search to never And I run :open foo! Then the error "Invalid URL" should be shown Scenario: :open with -t and -b When I run :open -t -b foo.bar Then the error "Only one of -t/-b/-w/-p can be given!" should be shown Scenario: Searching with :open When I set url.auto_search to naive And I set url.searchengines to {"DEFAULT": "http://localhost:(port)/data/numbers/{}.txt"} And I run :open 3 Then data/numbers/3.txt should be loaded @flaky Scenario: Opening in a new tab Given I open about:blank When I run :tab-only And I run :open -t http://localhost:(port)/data/numbers/4.txt And I wait until data/numbers/4.txt is loaded Then the following tabs should be open: """ - about:blank - data/numbers/4.txt (active) """ Scenario: Opening in a new background tab Given I open about:blank When I run :tab-only And I run :open -b http://localhost:(port)/data/numbers/5.txt And I wait until data/numbers/5.txt is loaded Then the following tabs should be open: """ - about:blank (active) - data/numbers/5.txt """ Scenario: :open with count Given I open about:blank When I run :tab-only And I open about:blank in a new tab And I run :open http://localhost:(port)/data/numbers/6.txt with count 2 And I wait until data/numbers/6.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - active: true history: - url: about:blank - active: true url: http://localhost:*/data/numbers/6.txt """ Scenario: Opening in a new tab (unrelated) Given I open about:blank When I set tabs.new_position.unrelated to next And I set tabs.new_position.related to prev And I run :tab-only And I run :open -t http://localhost:(port)/data/numbers/7.txt And I wait until data/numbers/7.txt is loaded Then the following tabs should be open: """ - about:blank - data/numbers/7.txt (active) """ Scenario: Opening in a new tab (related) Given I open about:blank When I set tabs.new_position.unrelated to next And I set tabs.new_position.related to prev And I run :tab-only And I run :open -t --related http://localhost:(port)/data/numbers/8.txt And I wait until data/numbers/8.txt is loaded Then the following tabs should be open: """ - data/numbers/8.txt (active) - about:blank """ Scenario: Opening in a new window Given I open about:blank When I run :tab-only And I run :open -w http://localhost:(port)/data/numbers/9.txt And I wait until data/numbers/9.txt is loaded Then the session should look like: """ windows: - tabs: - active: true history: - active: true url: about:blank - tabs: - active: true history: - active: true url: http://localhost:*/data/numbers/9.txt """ Scenario: Opening a quickmark When I run :quickmark-add http://localhost:(port)/data/numbers/10.txt quickmarktest And I run :open quickmarktest Then data/numbers/10.txt should be loaded ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/private.feature0000644000175100017510000002510315102145205023073 0ustar00runnerrunnerFeature: Using private browsing Background: Given I open about:blank And I clean up open tabs Scenario: Opening new tab in private window When I open about:blank in a private window And I open cookies/set?qute-private-test=42 without waiting in a new tab And I wait until cookies is loaded And I run :close And I wait for "removed: main-window" in the log And I open cookies Then the cookie qute-private-test should not be set Scenario: Opening new tab in private window with :navigate next When I open data/navigate in a private window And I run :navigate -t next And I wait until data/navigate/next.html is loaded And I open cookies/set?qute-private-test=42 without waiting And I wait until cookies is loaded And I run :close And I wait for "removed: main-window" in the log And I open cookies Then the cookie qute-private-test should not be set Scenario: Using command history in a new private browsing window When I run :cmd-set-text :message-info "Hello World" And I run :command-accept And I open about:blank in a private window And I run :cmd-set-text :message-error "This should only be shown once" And I run :command-accept And I wait for the error "This should only be shown once" And I run :close And I wait for "removed: main-window" in the log And I run :cmd-set-text : And I run :command-history-prev And I run :command-accept # Then the error should not be shown again ## https://github.com/qutebrowser/qutebrowser/issues/1219 Scenario: Make sure private data is cleared when closing last private window When I open about:blank in a private window And I open cookies/set?cookie-to-delete=1 without waiting in a new tab And I wait until cookies is loaded And I run :close And I open about:blank in a private window And I open cookies Then the cookie cookie-to-delete should not be set Scenario: Make sure private data is not cleared when closing a private window but another remains When I open about:blank in a private window And I open about:blank in a private window And I open cookies/set?cookie-to-preserve=1 without waiting in a new tab And I wait until cookies is loaded And I run :close And I open about:blank in a private window And I open cookies Then the cookie cookie-to-preserve should be set to 1 Scenario: Sharing cookies with private browsing When I open cookies/set?qute-test=42 without waiting in a private window And I wait until cookies is loaded And I open cookies in a new tab And I set content.private_browsing to false Then the cookie qute-test should be set to 42 Scenario: Opening private window with :navigate increment # Private window handled in commands.py When I open data/numbers/1.txt in a private window And I run :window-only And I run :navigate -w increment And I wait until data/numbers/2.txt is loaded Then the session should look like: """ windows: - private: True tabs: - history: - url: http://localhost:*/data/numbers/1.txt - private: True tabs: - history: - url: http://localhost:*/data/numbers/2.txt """ Scenario: Opening private window with :navigate next # Private window handled in navigate.py When I open data/navigate in a private window And I run :window-only And I run :navigate -w next And I wait until data/navigate/next.html is loaded Then the session should look like: """ windows: - private: True tabs: - history: - url: http://localhost:*/data/navigate - private: True tabs: - history: - url: http://localhost:*/data/navigate/next.html """ Scenario: Opening private window with :tab-clone When I open data/hello.txt in a private window And I run :window-only And I run :tab-clone -w And I wait until data/hello.txt is loaded Then the session should look like: """ windows: - private: True tabs: - history: - url: http://localhost:*/data/hello.txt - private: True tabs: - history: - url: http://localhost:*/data/hello.txt """ Scenario: Opening private window via :click-element When I open data/click_element.html in a private window And I run :window-only And I run :click-element --target window id link And I wait until data/hello.txt is loaded Then the session should look like: """ windows: - private: True tabs: - history: - url: http://localhost:*/data/click_element.html - private: True tabs: - history: - url: http://localhost:*/data/hello.txt """ Scenario: Skipping private window when saving session When I open data/hello.txt in a private window And I run :session-save (tmpdir)/session.yml And I wait for "Saved session */session.yml." in the log Then the file session.yml should not contain "hello.txt" # https://github.com/qutebrowser/qutebrowser/issues/2638 Scenario: Turning off javascript with private browsing When I set content.javascript.enabled to false And I open data/javascript/consolelog.html in a private window Then the javascript message "console.log works!" should not be logged # Probably needs qutewm to work properly... @qtwebkit_skip @xfail_norun # Only applies to QtWebEngine Scenario: Make sure local storage is isolated with private browsing When I open data/hello.txt in a private window And I run :jseval localStorage.qute_private_test = 42 And I wait for "42" in the log And I run :close And I wait for "removed: main-window" in the log And I open data/hello.txt And I run :jseval localStorage.qute_private_test Then "No output or error" should be logged Scenario: Opening quickmark in private window When I open data/numbers/1.txt in a private window And I run :window-only And I run :quickmark-add http://localhost:(port)/data/numbers/2.txt two And I run :quickmark-load two And I wait until data/numbers/2.txt is loaded Then the session should look like: """ windows: - private: True tabs: - history: - url: http://localhost:*/data/numbers/1.txt - url: http://localhost:*/data/numbers/2.txt """ @skip # Too flaky Scenario: Saving a private session with only-active-window When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a private window And I open data/numbers/4.txt in a new tab And I open data/numbers/5.txt in a new tab And I run :session-save --only-active-window window_session_name And I run :window-only And I wait for "removed: tab" in the log And I wait for "removed: tab" in the log And I run :tab-only And I wait for "removed: tab" in the log And I wait for "removed: tab" in the log And I wait for "removed: tab" in the log And I run :session-load -c window_session_name And I wait until data/numbers/5.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/numbers/3.txt - history: - url: http://localhost:*/data/numbers/4.txt - history: - active: true url: http://localhost:*/data/numbers/5.txt """ # https://github.com/qutebrowser/qutebrowser/issues/5810 Scenario: Using qute:// scheme after reiniting private profile When I open about:blank in a private window And I run :close And I open qute://version in a private window Then the page should contain the plaintext "Version info" Scenario: Downloading after reiniting private profile When I open about:blank in a private window And I run :close And I open data/downloads/downloads.html in a private window And I run :click-element id download And I wait for "*PromptMode.download*" in the log And I run :mode-leave Then "Removed download *: download.bin *" should be logged Scenario: Adblocking after reiniting private profile When I open about:blank in a private window And I run :close And I set content.blocking.hosts.lists to ["http://localhost:(port)/data/blocking/qutebrowser-hosts"] And I set content.blocking.adblock.lists to [] And I set content.blocking.method to hosts And I run :adblock-update And I wait for the message "hostblock: Read 1 hosts from 1 sources." And I open data/blocking/external_logo.html in a private window Then "Request to qutebrowser.org blocked by host blocker." should be logged @pyqt!=5.15.0 # cookie filtering is broken on QtWebEngine 5.15.0 Scenario: Cookie filtering after reiniting private profile When I open about:blank in a private window And I run :close And I set content.cookies.accept to never And I open data/title.html in a private window And I open cookies/set?unsuccessful-cookie=1 without waiting in a new tab And I wait until cookies is loaded And I open cookies Then the cookie unsuccessful-cookie should not be set Scenario: Disabling JS after reiniting private profile When I open about:blank in a new window And I run :window-only And I set content.javascript.enabled to false And I open about:blank in a private window And I run :close And I open data/javascript/enabled.html in a private window Then the page should contain the plaintext "JavaScript is disabled" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/prompts.feature0000644000175100017510000006651715102145205023143 0ustar00runnerrunnerFeature: Prompts Various prompts (javascript, SSL errors, authentication, etc.) # Javascript Scenario: Javascript alert When I open data/prompt/jsalert.html And I run :click-element id button And I wait for a prompt And I run :prompt-accept Then the javascript message "Alert done" should be logged Scenario: Using content.javascript.alert When I set content.javascript.alert to false And I open data/prompt/jsalert.html And I run :click-element id button Then the javascript message "Alert done" should be logged Scenario: Javascript confirm - yes When I open data/prompt/jsconfirm.html And I run :click-element id button And I wait for a prompt And I run :prompt-accept yes Then the javascript message "confirm reply: true" should be logged Scenario: Javascript confirm - no When I open data/prompt/jsconfirm.html And I run :click-element id button And I wait for a prompt And I run :prompt-accept no Then the javascript message "confirm reply: false" should be logged Scenario: Javascript confirm - aborted When I open data/prompt/jsconfirm.html And I run :click-element id button And I wait for a prompt And I run :mode-leave Then the javascript message "confirm reply: false" should be logged Scenario: Javascript prompt When I open data/prompt/jsprompt.html And I run :click-element id button And I wait for a prompt And I press the keys "prompt test" And I run :prompt-accept Then the javascript message "Prompt reply: prompt test" should be logged Scenario: Javascript prompt with default When I open data/prompt/jsprompt.html And I run :click-element id button-default And I wait for a prompt And I run :prompt-accept Then the javascript message "Prompt reply: default" should be logged Scenario: Rejected javascript prompt When I open data/prompt/jsprompt.html And I run :click-element id button And I wait for a prompt And I press the keys "prompt test" And I run :mode-leave Then the javascript message "Prompt reply: null" should be logged # Multiple prompts @qtwebengine_skip # QtWebEngine refuses to load anything with a JS question Scenario: Blocking question interrupted by blocking one When I set content.javascript.alert to true And I open data/prompt/jsalert.html And I run :click-element id button And I wait for a prompt And I open data/prompt/jsconfirm.html in a new tab And I run :click-element id button And I wait for a prompt # JS confirm And I run :prompt-accept yes # JS alert And I run :prompt-accept Then the javascript message "confirm reply: true" should be logged And the javascript message "Alert done" should be logged @qtwebengine_skip # QtWebEngine refuses to load anything with a JS question Scenario: Blocking question interrupted by async one Given I have a fresh instance When I set content.javascript.alert to true And I set content.notifications.enabled to ask And I open data/prompt/jsalert.html And I run :click-element id button And I wait for a prompt And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt # JS alert And I run :prompt-accept # notification permission And I run :prompt-accept yes Then the javascript message "Alert done" should be logged And the javascript message "notification permission granted" should be logged @qtwebkit_skip Scenario: Async question interrupted by async one Given I have a fresh instance When I set content.notifications.enabled to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I run :quickmark-save And I wait for a prompt # notification permission And I run :prompt-accept yes # quickmark And I run :prompt-accept test Then the javascript message "notification permission granted" should be logged And "Added quickmark test for *" should be logged @qtwebkit_skip Scenario: Async question interrupted by blocking one Given I have a fresh instance When I set content.notifications.enabled to ask And I set content.javascript.alert to true And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I open data/prompt/jsalert.html in a new tab And I run :click-element id button And I wait for a prompt # JS alert And I run :prompt-accept # notification permission And I run :prompt-accept yes Then the javascript message "Alert done" should be logged And the javascript message "notification permission granted" should be logged # Shift-Insert with prompt (issue 1299) Scenario: Pasting via shift-insert in prompt mode When selection is supported And I put "insert test" into the primary selection And I open data/prompt/jsprompt.html And I run :click-element id button And I wait for a prompt And I press the keys "" And I run :prompt-accept Then the javascript message "Prompt reply: insert test" should be logged Scenario: Pasting via shift-insert without it being supported When selection is not supported And I put "insert test" into the primary selection And I put "clipboard test" into the clipboard And I open data/prompt/jsprompt.html And I run :click-element id button And I wait for a prompt And I press the keys "" And I run :prompt-accept Then the javascript message "Prompt reply: clipboard test" should be logged Scenario: Using content.javascript.prompt When I set content.javascript.prompt to false And I open data/prompt/jsprompt.html And I run :click-element id button Then the javascript message "Prompt reply: null" should be logged # Clipboard permissions - static @qtwebkit_skip Scenario: Clipboard - no permission - copy When I set content.javascript.clipboard to none And I open data/prompt/clipboard.html And I run :click-element id copy Then the javascript message "Failed to copy text." should be logged @qtwebkit_skip Scenario: Clipboard - no permission - paste When I set content.javascript.clipboard to none And I open data/prompt/clipboard.html And I run :click-element id paste Then the javascript message "Failed to read from clipboard." should be logged # access permission no longer allows copy permission on 6.8 because it # falls back to a permission prompt that we don't support # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-130599 @qt<6.8 @qtwebkit_skip Scenario: Clipboard - access permission - copy When I set content.javascript.clipboard to access And I open data/prompt/clipboard.html And I run :click-element id copy Then the javascript message "Text copied: default text" should be logged @qtwebkit_skip Scenario: Clipboard - access permission - paste When I set content.javascript.clipboard to access And I open data/prompt/clipboard.html And I run :click-element id paste Then the javascript message "Failed to read from clipboard." should be logged @qtwebkit_skip Scenario: Clipboard - full permission - copy When I set content.javascript.clipboard to access-paste And I open data/prompt/clipboard.html And I run :click-element id copy Then the javascript message "Text copied: default text" should be logged @qtwebkit_skip Scenario: Clipboard - full permission - paste When I set content.javascript.clipboard to access-paste And I open data/prompt/clipboard.html And I run :click-element id paste Then the javascript message "Text pasted: *" should be logged # Clipboard permissions - prompt # A fresh instance is only required for these tests on Qt<6.8 @qt>=6.8 Scenario: Clipboard - ask allow - copy Given I may need a fresh instance When I set content.javascript.clipboard to ask And I open data/prompt/clipboard.html And I run :click-element id copy And I wait for a prompt And I run :prompt-accept yes Then the javascript message "Text copied: default text" should be logged @qt>=6.8 Scenario: Clipboard - ask allow - paste Given I may need a fresh instance When I set content.javascript.clipboard to ask And I open data/prompt/clipboard.html And I run :click-element id paste And I wait for a prompt And I run :prompt-accept yes Then the javascript message "Text pasted: *" should be logged @qt>=6.8 Scenario: Clipboard - ask deny - copy Given I may need a fresh instance When I set content.javascript.clipboard to ask And I open data/prompt/clipboard.html And I run :click-element id copy And I wait for a prompt And I run :prompt-accept no Then the javascript message "Failed to copy text." should be logged @qt>=6.8 Scenario: Clipboard - ask deny - paste Given I may need a fresh instance When I set content.javascript.clipboard to ask And I open data/prompt/clipboard.html And I run :click-element id paste And I wait for a prompt And I run :prompt-accept no Then the javascript message "Failed to read from clipboard." should be logged @qt>=6.8 Scenario: Clipboard - ask per url - paste Given I may need a fresh instance When I set content.javascript.clipboard to none And I run :set -u localhost:* content.javascript.clipboard ask And I open data/prompt/clipboard.html And I run :click-element id paste And I wait for a prompt And I run :prompt-accept yes Then the javascript message "Text pasted: *" should be logged And I run :config-unset -u localhost:* content.javascript.clipboard @qt>=6.8 Scenario: Clipboard - deny per url - paste Given I may need a fresh instance When I set content.javascript.clipboard to access-paste And I run :set -u localhost:* content.javascript.clipboard none And I open data/prompt/clipboard.html And I run :click-element id paste Then the javascript message "Failed to read from clipboard." should be logged And I run :config-unset -u localhost:* content.javascript.clipboard @qt>=6.8 Scenario: Clipboard - ask allow persistent - paste Given I may need a fresh instance When I set content.javascript.clipboard to ask And I open data/prompt/clipboard.html And I run :click-element id paste And I wait for a prompt And I run :prompt-accept --save yes And I wait for "*Text pasted: *" in the log And I reload data/prompt/clipboard.html And I run :click-element id paste Then the javascript message "Text pasted: *" should be logged # SSL Scenario: SSL error with content.tls.certificate_errors = load-insecurely When I clear SSL errors And I set content.tls.certificate_errors to load-insecurely And I load an SSL page And I wait until the SSL page finished loading Then the error "Certificate error: *" should be shown And the page should contain the plaintext "Hello World via SSL!" @qtwebkit_openssl3_skip Scenario: SSL error with content.tls.certificate_errors = block When I clear SSL errors And I set content.tls.certificate_errors to block And I load an SSL page Then a SSL error page should be shown Scenario: SSL error with content.tls.certificate_errors = ask -> yes When I clear SSL errors And I set content.tls.certificate_errors to ask And I load an SSL page And I wait for a prompt And I run :prompt-accept yes And I wait until the SSL page finished loading Then the page should contain the plaintext "Hello World via SSL!" @qtwebkit_openssl3_skip Scenario: SSL error with content.tls.certificate_errors = ask -> no When I clear SSL errors And I set content.tls.certificate_errors to ask And I load an SSL page And I wait for a prompt And I run :prompt-accept no Then a SSL error page should be shown @qtwebkit_openssl3_skip Scenario: SSL error with content.tls.certificate_errors = ask -> abort When I clear SSL errors And I set content.tls.certificate_errors to ask And I load an SSL page And I wait for a prompt And I run :mode-leave Then a SSL error page should be shown Scenario: SSL error with content.tls.certificate_errors = ask-block-thirdparty -> yes When I clear SSL errors And I set content.tls.certificate_errors to ask-block-thirdparty And I load an SSL page And I wait for a prompt And I run :prompt-accept yes And I wait until the SSL page finished loading Then the page should contain the plaintext "Hello World via SSL!" Scenario: SSL resource error with content.tls.certificate_errors = ask -> yes When I clear SSL errors And I set content.tls.certificate_errors to ask And I load an SSL resource page And I wait for a prompt And I run :prompt-accept yes And I wait until the SSL resource page finished loading Then the javascript message "Script loaded" should be logged And the page should contain the plaintext "Script loaded" @qtwebkit_openssl3_skip Scenario: SSL resource error with content.tls.certificate_errors = ask -> no When I clear SSL errors And I set content.tls.certificate_errors to ask And I load an SSL resource page And I wait for a prompt And I run :prompt-accept no And I wait until the SSL resource page finished loading Then the javascript message "Script loaded" should not be logged And the page should contain the plaintext "Script not loaded" @qtwebkit_openssl3_skip Scenario: SSL resource error with content.tls.certificate_errors = ask-block-thirdparty When I clear SSL errors And I set content.tls.certificate_errors to ask-block-thirdparty And I load an SSL resource page And I wait until the SSL resource page finished loading Then "Certificate error in resource load: *" should be logged And the javascript message "Script loaded" should not be logged And the page should contain the plaintext "Script not loaded" # Geolocation Scenario: Always rejecting geolocation When I set content.geolocation to false And I open data/prompt/geolocation.html in a new tab And I run :click-element id button Then the javascript message "geolocation permission denied" should be logged Scenario: geolocation with ask -> false Given I may need a fresh instance When I set content.geolocation to ask And I open data/prompt/geolocation.html in a new tab And I run :click-element id button And I wait for a prompt And I run :prompt-accept no Then the javascript message "geolocation permission denied" should be logged Scenario: geolocation with ask -> false and save Given I may need a fresh instance When I set content.geolocation to ask And I open data/prompt/geolocation.html in a new tab And I run :click-element id button And I wait for a prompt And I run :prompt-accept --save no Then the javascript message "geolocation permission denied" should be logged And the per-domain option content.geolocation should be set to false for http://localhost:(port) Scenario: geolocation with ask -> abort Given I may need a fresh instance When I set content.geolocation to ask And I open data/prompt/geolocation.html in a new tab And I run :click-element id button And I wait for a prompt And I run :mode-leave Then the javascript message "geolocation permission denied" should be logged # Notifications @qtwebkit_skip Scenario: Always rejecting notifications Given I have a fresh instance When I set content.notifications.enabled to false And I open data/prompt/notifications.html in a new tab And I run :click-element id button Then the javascript message "notification permission denied" should be logged @qtwebkit_skip Scenario: Always accepting notifications Given I have a fresh instance When I set content.notifications.enabled to true And I open data/prompt/notifications.html in a new tab And I run :click-element id button Then the javascript message "notification permission granted" should be logged @qtwebkit_skip Scenario: notifications with ask -> false Given I have a fresh instance When I set content.notifications.enabled to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I run :prompt-accept no Then the javascript message "notification permission denied" should be logged @qtwebkit_skip Scenario: notifications with ask -> false and save Given I have a fresh instance When I set content.notifications.enabled to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I run :prompt-accept --save no Then the javascript message "notification permission denied" should be logged And the per-domain option content.notifications.enabled should be set to false for http://localhost:(port) @qtwebkit_skip Scenario: notifications with ask -> true Given I have a fresh instance When I set content.notifications.enabled to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I run :prompt-accept yes Then the javascript message "notification permission granted" should be logged @qtwebkit_skip Scenario: notifications with ask -> true and save Given I have a fresh instance When I set content.notifications.enabled to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I run :prompt-accept --save yes Then the javascript message "notification permission granted" should be logged And the per-domain option content.notifications.enabled should be set to true for http://localhost:(port) # This actually gives us a denied rather than an aborted @xfail_norun Scenario: notifications with ask -> abort Given I have a fresh instance When I set content.notifications.enabled to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I run :mode-leave Then the javascript message "notification permission aborted" should be logged @qtwebkit_skip Scenario: answering notification after closing tab Given I have a fresh instance When I set content.notifications.enabled to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I run :tab-close And I wait for "Leaving mode KeyMode.yesno (reason: aborted)" in the log Then no crash should happen # Page authentication Scenario: Successful webpage authentication When I open basic-auth/user1/password1 without waiting And I wait for a prompt And I press the keys "user1" And I run :prompt-accept And I press the keys "password1" And I run :prompt-accept And I wait until basic-auth/user1/password1 is loaded Then the json on the page should be: """ { "authenticated": true, "user": "user1" } """ Scenario: Authentication with :prompt-accept value When I open about:blank in a new tab And I open basic-auth/user2/password2 without waiting And I wait for a prompt And I run :prompt-accept user2:password2 And I wait until basic-auth/user2/password2 is loaded Then the json on the page should be: """ { "authenticated": true, "user": "user2" } """ Scenario: Authentication with invalid :prompt-accept value When I open about:blank in a new tab And I open basic-auth/user3/password3 without waiting And I wait for a prompt And I run :prompt-accept foo And I run :prompt-accept user3:password3 Then the error "Value needs to be in the format username:password, but foo was given" should be shown Scenario: Tabbing between username and password When I open about:blank in a new tab And I open basic-auth/user4/password4 without waiting And I wait for a prompt And I press the keys "us" And I run :prompt-item-focus next And I press the keys "password4" And I run :prompt-item-focus prev And I press the keys "er4" And I run :prompt-accept And I run :prompt-accept And I wait until basic-auth/user4/password4 is loaded Then the json on the page should be: """ { "authenticated": true, "user": "user4" } """ @qtwebengine_skip Scenario: Cancelling webpage authentication with QtWebKit When I open basic-auth/user6/password6 without waiting And I wait for a prompt And I run :mode-leave Then basic-auth/user6/password6 should be loaded # :prompt-accept with value argument Scenario: Javascript alert with value When I set content.javascript.alert to true And I open data/prompt/jsalert.html And I run :click-element id button And I wait for a prompt And I run :prompt-accept foobar And I run :prompt-accept Then the javascript message "Alert done" should be logged And the error "No value is permitted with alert prompts!" should be shown Scenario: Javascript prompt with value When I set content.javascript.prompt to true And I open data/prompt/jsprompt.html And I run :click-element id button And I wait for a prompt And I press the keys "prompt test" And I run :prompt-accept "overridden value" Then the javascript message "Prompt reply: overridden value" should be logged Scenario: Javascript confirm with invalid value When I open data/prompt/jsconfirm.html And I run :click-element id button And I wait for a prompt And I run :prompt-accept nope And I run :prompt-accept yes Then the javascript message "confirm reply: true" should be logged And the error "Invalid value nope - expected yes/no!" should be shown Scenario: Javascript confirm with default value When I open data/prompt/jsconfirm.html And I run :click-element id button And I wait for a prompt And I run :prompt-accept And I run :prompt-accept yes Then the javascript message "confirm reply: true" should be logged And the error "No default value was set for this question!" should be shown # Other @qtwebengine_skip Scenario: Shutting down with a question When I open data/prompt/jsconfirm.html And I run :click-element id button And I wait for a prompt And I run :quit Then the javascript message "confirm reply: false" should be logged And qutebrowser should quit Scenario: Using :prompt-open-download with a prompt which does not support it When I open data/hello.txt And I run :quickmark-save And I wait for a prompt And I run :prompt-open-download And I run :prompt-accept test-prompt-open-download Then "Added quickmark test-prompt-open-download for *" should be logged Scenario: Using :prompt-item-focus with a prompt which does not support it When I open data/hello.txt And I run :quickmark-save And I wait for a prompt And I run :prompt-item-focus next And I run :prompt-accept test-prompt-item-focus Then "Added quickmark test-prompt-item-focus for *" should be logged Scenario: Getting question in command mode When I open data/hello.txt And I run :cmd-later 500 quickmark-save And I run :cmd-set-text : And I wait for a prompt And I run :prompt-accept prompt-in-command-mode Then "Added quickmark prompt-in-command-mode for *" should be logged # https://github.com/qutebrowser/qutebrowser/issues/1093 @qtwebengine_skip # QtWebEngine doesn't open the second page/prompt Scenario: Keyboard focus with multiple auth prompts When I open basic-auth/user5/password5 without waiting And I open basic-auth/user6/password6 in a new tab without waiting And I wait for a prompt And I wait for a prompt # Second prompt (showed first) And I press the keys "user6" And I press the key "" And I press the keys "password6" And I press the key "" And I wait until basic-auth/user6/password6 is loaded # First prompt And I press the keys "user5" And I press the key "" And I press the keys "password5" And I press the key "" And I wait until basic-auth/user5/password5 is loaded # We're on the second page Then the json on the page should be: """ { "authenticated": true, "user": "user6" } """ # https://github.com/qutebrowser/qutebrowser/issues/1249#issuecomment-175205531 # https://github.com/qutebrowser/qutebrowser/pull/2054#issuecomment-258285544 @qtwebkit_skip Scenario: Interrupting SSL prompt during a notification prompt Given I have a fresh instance When I set content.notifications.enabled to ask And I set content.tls.certificate_errors to ask And I open data/prompt/notifications.html in a new tab And I run :click-element id button And I wait for a prompt And I open about:blank in a new tab And I load an SSL page And I wait for a prompt And I run :tab-close And I run :prompt-accept yes Then the javascript message "notification permission granted" should be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/qutescheme.feature0000644000175100017510000003177615102145205023601 0ustar00runnerrunnerFeature: Special qute:// pages Background: Given I open about:blank # :help Scenario: :help without topic When the documentation is up to date And I run :tab-only And I run :help And I wait until qute://help/index.html is loaded Then the following tabs should be open: """ - qute://help/index.html (active) """ Scenario: :help with invalid topic When I run :help foo Then the error "Invalid help topic foo!" should be shown Scenario: :help with command When the documentation is up to date And I run :tab-only And I run :help :back And I wait until qute://help/commands.html#back is loaded Then the following tabs should be open: """ - qute://help/commands.html#back (active) """ Scenario: :help with invalid command When I run :help :foo Then the error "Invalid command foo!" should be shown Scenario: :help with setting When the documentation is up to date And I run :tab-only And I run :help editor.command And I wait until qute://help/settings.html#editor.command is loaded Then the following tabs should be open: """ - qute://help/settings.html#editor.command (active) """ Scenario: :help with -t When the documentation is up to date And I run :tab-only And I run :help -t And I wait until qute://help/index.html is loaded Then the following tabs should be open: """ - about:blank - qute://help/index.html (active) """ # https://github.com/qutebrowser/qutebrowser/issues/2513 Scenario: Opening link with qute:help When the documentation is up to date And I run :tab-only And I open qute:help without waiting And I wait for "Changing title for idx 0 to 'qutebrowser help'" in the log And I hint with args "links normal" and follow ls Then qute://help/quickstart.html should be loaded Scenario: Opening a link with qute://help When the documentation is up to date And I run :tab-only And I open qute://help without waiting And I wait until qute://help/ is loaded And I hint with args "links normal" and follow ls Then qute://help/quickstart.html should be loaded Scenario: Opening a link with qute://help/index.html/.. When the documentation is up to date And I open qute://help/index.html/.. without waiting Then qute://help/ should be loaded Scenario: Opening a link with qute://help/index.html/../ When the documentation is up to date And I open qute://help/index.html/../ without waiting Then qute://help/ should be loaded @qtwebengine_skip Scenario: Opening a link with qute://help/img/ (QtWebKit) When the documentation is up to date And I open qute://help/img/ without waiting Then "*Error while * qute://*" should be logged And "*Is a directory*" should be logged And "* url='qute://help/img'* LoadStatus.error" should be logged @qtwebkit_skip Scenario: Opening a link with qute://help/img/ (QtWebEngine) When the documentation is up to date And I open qute://help/img/ without waiting Then "*Error while * qute://*" should be logged And "* url='qute://help/img'* LoadStatus.error" should be logged And "Load error: ERR_FILE_NOT_FOUND" should be logged # :history Scenario: :history without arguments When I run :tab-only And I run :history And I wait until qute://history/ is loaded Then the following tabs should be open: """ - qute://history/ (active) """ Scenario: :history with -t When I run :tab-only And I run :history -t And I wait until qute://history/ is loaded Then the following tabs should be open: """ - about:blank - qute://history/ (active) """ # qute://settings # Sometimes, an unrelated value gets set, which also breaks other tests @skip Scenario: Focusing input fields in qute://settings and entering valid value When I set search.ignore_case to never And I open qute://settings # scroll to the right - the table does not fit in the default screen And I run :scroll-to-perc -x 100 And I run :jseval document.getElementById('input-search.ignore_case').value = '' And I run :click-element id input-search.ignore_case And I wait for "Entering mode KeyMode.insert *" in the log And I press the keys "always" And I press the key "" # an explicit Tab to unfocus the input field seems to stabilize the tests And I press the key "" And I wait for "Config option changed: search.ignore_case *" in the log Then the option search.ignore_case should be set to always # Sometimes, an unrelated value gets set # Too flaky... @skip Scenario: Focusing input fields in qute://settings and entering invalid value When I open qute://settings # scroll to the right - the table does not fit in the default screen And I run :scroll-to-perc -x 100 And I run :jseval document.getElementById('input-search.ignore_case').value = '' And I run :click-element id input-search.ignore_case And I wait for "Entering mode KeyMode.insert *" in the log And I press the keys "foo" And I press the key "" # an explicit Tab to unfocus the input field seems to stabilize the tests And I press the key "" Then "Invalid value 'foo' *" should be logged Scenario: qute://settings CSRF via img When I open data/misc/qutescheme_csrf.html And I run :click-element id via-img Then the img request should be blocked Scenario: qute://settings CSRF via link When I open data/misc/qutescheme_csrf.html And I run :click-element id via-link Then the link request should be blocked Scenario: qute://settings CSRF via redirect When I open data/misc/qutescheme_csrf.html And I run :click-element id via-redirect Then the redirect request should be blocked Scenario: qute://settings CSRF via form When I open data/misc/qutescheme_csrf.html And I run :click-element id via-form Then the form request should be blocked @qtwebkit_skip Scenario: qute://settings CSRF token (webengine) When I open qute://settings And I run :jseval const xhr = new XMLHttpRequest(); xhr.open("GET", "qute://settings/set"); xhr.send() Then "RequestDeniedError while handling qute://* URL: Invalid CSRF token!" should be logged And the error "Invalid CSRF token for qute://settings!" should be shown # pdfjs support Scenario: pdfjs is used for pdf files Given pdfjs is available When I set content.pdfjs to true And I open data/misc/test.pdf without waiting And I wait until PDF.js is ready # No "Then" @qtwebkit_pdf_imageformat_skip Scenario: pdfjs is not used when disabled When I set content.pdfjs to false And I set downloads.location.prompt to false And I open data/misc/test.pdf without waiting Then "Download test.pdf finished" should be logged Scenario: Downloading a pdf via pdf.js button (issue 1214) Given pdfjs is available When I set content.pdfjs to true And I set downloads.location.prompt to true And I open data/misc/test.pdf without waiting And I wait until PDF.js is ready And I run :jseval (document.getElementById("downloadButton") || document.getElementById("download")).click() And I wait for "Asking question option=None text=* title='Save file to:'>, *" in the log And I run :mode-leave Then no crash should happen # :pyeval Scenario: Running :pyeval When I run :debug-pyeval 1+1 And I wait until qute://pyeval/ is loaded Then the page should contain the plaintext "2" Scenario: Causing exception in :pyeval When I run :debug-pyeval 1/0 And I wait until qute://pyeval/ is loaded Then the page should contain the plaintext "ZeroDivisionError" Scenario: Running :pyveal with --file using a file that exists as python code When I run :debug-pyeval --file (testdata)/misc/pyeval_file.py Then the message "Hello World" should be shown And "pyeval output: No error" should be logged Scenario: Running :pyeval --file using a non existing file When I run :debug-pyeval --file nonexistentfile Then the error "[Errno 2] *: 'nonexistentfile'" should be shown Scenario: Running :pyeval with --quiet When I run :debug-pyeval --quiet 1+1 Then "pyeval output: 2" should be logged ## :messages Scenario: :messages without level When I run :message-error the-error-message And I run :message-warning the-warning-message And I run :message-info the-info-message And I run :messages Then qute://log/?level=info should be loaded And the error "the-error-message" should be shown And the warning "the-warning-message" should be shown And the page should contain the plaintext "the-error-message" And the page should contain the plaintext "the-warning-message" And the page should contain the plaintext "the-info-message" Scenario: Showing messages of type 'warning' or greater When I run :message-error the-error-message And I run :message-warning the-warning-message And I run :message-info the-info-message And I run :messages warning Then qute://log/?level=warning should be loaded And the error "the-error-message" should be shown And the warning "the-warning-message" should be shown And the page should contain the plaintext "the-error-message" And the page should contain the plaintext "the-warning-message" And the page should not contain the plaintext "the-info-message" Scenario: Showing messages of type 'info' or greater When I run :message-error the-error-message And I run :message-warning the-warning-message And I run :message-info the-info-message And I run :messages info Then qute://log/?level=info should be loaded And the error "the-error-message" should be shown And the warning "the-warning-message" should be shown And the page should contain the plaintext "the-error-message" And the page should contain the plaintext "the-warning-message" And the page should contain the plaintext "the-info-message" Scenario: Showing messages of category 'message' When I run :message-info the-info-message And I run :messages -f message Then qute://log/?level=info&logfilter=message should be loaded And the page should contain the plaintext "the-info-message" Scenario: Showing messages of category 'misc' When I run :message-info the-info-message And I run :messages -f misc Then qute://log/?level=info&logfilter=misc should be loaded And the page should not contain the plaintext "the-info-message" @qtwebengine_flaky Scenario: Showing messages of an invalid level When I run :messages cataclysmic Then the error "Invalid log level cataclysmic!" should be shown Scenario: Showing messages with an invalid category When I run :messages -f invalid Then the error "Invalid log category invalid - *" should be shown Scenario: Using qute://log directly When I open qute://log without waiting And I wait for "Changing title for idx * to 'log'" in the log Then no crash should happen # FIXME More possible tests: # :message --plain # Using qute://log directly with invalid category # same with invalid level # :version @qt69_ci_skip Scenario: Open qute://version When I open qute://version Then the page should contain the plaintext "Version info" # qute://gpl @qt69_ci_skip Scenario: Open qute://gpl When I open qute://gpl Then the page should contain the plaintext "GNU GENERAL PUBLIC LICENSE" # qute://start # QtWebKit doesn't support formaction; unknown Qt 6.9 renderer process crashes @qtwebkit_skip @qt69_ci_skip Scenario: Searching on qute://start When I set url.searchengines to {"DEFAULT": "http://localhost:(port)/data/title.html?q={}"} And I open qute://start And I run :click-element id search-field And I wait for "Entering mode KeyMode.insert *" in the log And I press the keys "test" And I press the key "" Then data/title.html?q=test should be loaded ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/scroll.feature0000644000175100017510000003156015102145205022723 0ustar00runnerrunnerFeature: Scrolling Tests the various scroll commands. Background: Given I open data/scroll/simple.html And I run :tab-only ## :scroll-px Scenario: Scrolling pixel-wise vertically When I run :scroll-px 0 10 Then the page should be scrolled vertically Scenario: Scrolling pixel-wise horizontally When I run :scroll-px 10 0 Then the page should be scrolled horizontally Scenario: Scrolling down and up When I run :scroll-px 10 0 And I wait until the scroll position changed to 10/0 And I run :scroll-px -10 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling right and left When I run :scroll-px 0 10 And I wait until the scroll position changed to 0/10 And I run :scroll-px 0 -10 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling down and up with count When I run :scroll-px 0 10 with count 2 And I wait until the scroll position changed to 0/20 When I run :scroll-px 0 -10 When I run :scroll-px 0 -10 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled @qtwebengine_flaky Scenario: Scrolling left and right with count When I run :scroll-px 10 0 with count 2 And I wait until the scroll position changed to 20/0 When I run :scroll-px -10 0 When I run :scroll-px -10 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: :scroll-px with a very big value When I run :scroll-px 99999999999 0 Then the error "Numeric argument is too large for internal int representation." should be shown Scenario: :scroll-px on a page without scrolling When I open data/hello.txt And I run :scroll-px 10 10 Then the page should not be scrolled Scenario: :scroll-px with floats # This used to be allowed, but doesn't make much sense. When I run :scroll-px 2.5 2.5 Then the error "dx: Invalid int value 2.5" should be shown And the page should not be scrolled ## :scroll Scenario: Scrolling down When I run :scroll down Then the page should be scrolled vertically Scenario: Scrolling down and up When I run :scroll down And I wait until the scroll position changed And I run :scroll up And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling right When I run :scroll right Then the page should be scrolled horizontally Scenario: Scrolling right and left When I run :scroll right And I wait until the scroll position changed And I run :scroll left And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling down with count 10 When I run :scroll down with count 10 Then no crash should happen Scenario: Scrolling with page down When I run :scroll page-down Then the page should be scrolled vertically Scenario: Scrolling with page down and page up When I run :scroll page-down And I wait until the scroll position changed And I run :scroll page-up And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling to bottom When I run :scroll bottom Then the page should be scrolled vertically @flaky Scenario: Scrolling to bottom and to top When I run :scroll bottom And I wait until the scroll position changed And I run :scroll top And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: :scroll with invalid argument When I run :scroll foobar Then the error "Invalid value 'foobar' for direction - expected one of: bottom, down, left, page-down, page-up, right, top, up" should be shown And the page should not be scrolled Scenario: Scrolling down and up with count When I run :scroll down with count 2 And I wait until the scroll position changed And I run :scroll up And I run :scroll up And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling right When I run :scroll right Then the page should be scrolled horizontally Scenario: Scrolling right and left When I run :scroll right And I wait until the scroll position changed And I run :scroll left And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling right and left with count When I run :scroll right with count 2 And I wait until the scroll position changed And I run :scroll left And I run :scroll left And I wait until the scroll position changed to 0/0 Then the page should not be scrolled @skip # Too flaky Scenario: Scrolling down with a very big count When I run :scroll down with count 99999999999 # Make sure it doesn't hang And I run :message-info "Still alive!" Then the message "Still alive!" should be shown Scenario: :scroll on a page without scrolling When I open data/hello.txt And I run :scroll down Then the page should not be scrolled ## :scroll-to-perc Scenario: Scrolling to bottom with :scroll-to-perc When I run :scroll-to-perc 100 Then the page should be scrolled vertically @flaky Scenario: Scrolling to bottom and to top with :scroll-to-perc When I run :scroll-to-perc 100 And I wait until the scroll position changed And I run :scroll-to-perc 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling to middle with :scroll-to-perc When I run :scroll-to-perc 50 Then the page should be scrolled vertically @flaky Scenario: Scrolling to middle with :scroll-to-perc (float) When I run :scroll-to-perc 50.5 Then the page should be scrolled vertically @flaky Scenario: Scrolling to middle and to top with :scroll-to-perc When I run :scroll-to-perc 50 And I wait until the scroll position changed And I run :scroll-to-perc 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling to right with :scroll-to-perc When I run :scroll-to-perc --horizontal 100 Then the page should be scrolled horizontally @flaky Scenario: Scrolling to right and to left with :scroll-to-perc When I run :scroll-to-perc --horizontal 100 And I wait until the scroll position changed And I run :scroll-to-perc --horizontal 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling to middle (horizontally) with :scroll-to-perc When I run :scroll-to-perc --horizontal 50 Then the page should be scrolled horizontally Scenario: Scrolling to middle and to left with :scroll-to-perc When I run :scroll-to-perc --horizontal 50 And I wait until the scroll position changed And I run :scroll-to-perc --horizontal 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: :scroll-to-perc without argument When I run :scroll-to-perc Then the page should be scrolled vertically Scenario: :scroll-to-perc without argument and --horizontal When I run :scroll-to-perc --horizontal Then the page should be scrolled horizontally @flaky Scenario: :scroll-to-perc with count When I run :scroll-to-perc with count 50 Then the page should be scrolled vertically @qtwebengine_skip # Causes memory leak... Scenario: :scroll-to-perc with a very big value When I run :scroll-to-perc 99999999999 Then no crash should happen Scenario: :scroll-to-perc on a page without scrolling When I open data/hello.txt And I run :scroll-to-perc 20 Then the page should not be scrolled Scenario: :scroll-to-perc with count and argument When I run :scroll-to-perc 0 with count 50 Then the page should be scrolled vertically # https://github.com/qutebrowser/qutebrowser/issues/1821 @flaky Scenario: :scroll-to-perc without doctype When I open data/scroll/no_doctype.html And I run :scroll-to-perc 100 Then the page should be scrolled vertically ## :scroll-page Scenario: Scrolling down with :scroll-page When I run :scroll-page 0 1 Then the page should be scrolled vertically Scenario: Scrolling down with :scroll-page (float) When I run :scroll-page 0 1.5 Then the page should be scrolled vertically @flaky Scenario: Scrolling down and up with :scroll-page When I run :scroll-page 0 1 And I wait until the scroll position changed And I run :scroll-page 0 -1 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling right with :scroll-page When I run :scroll-page 1 0 Then the page should be scrolled horizontally Scenario: Scrolling right with :scroll-page (float) When I run :scroll-page 1.5 0 Then the page should be scrolled horizontally Scenario: Scrolling right and left with :scroll-page When I run :scroll-page 1 0 And I wait until the scroll position changed And I run :scroll-page -1 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: Scrolling right and left with :scroll-page and count When I run :scroll-page 1 0 with count 2 And I wait until the scroll position changed And I run :scroll-page -1 0 And I wait until the scroll position changed And I run :scroll-page -1 0 And I wait until the scroll position changed to 0/0 Then the page should not be scrolled Scenario: :scroll-page with --bottom-navigate When I run :scroll-to-perc 100 And I wait until the scroll position changed And I run :scroll-page --bottom-navigate next 0 1 Then data/hello2.txt should be loaded Scenario: :scroll-page with --bottom-navigate and zoom When I run :zoom 200 And I wait 0.5s And I run :scroll-to-perc 100 And I wait until the scroll position changed And I run :scroll-page --bottom-navigate next 0 1 Then data/hello2.txt should be loaded Scenario: :scroll-page with --bottom-navigate when not at the bottom When I run :scroll-px 0 10 And I wait until the scroll position changed And I run :scroll-page --bottom-navigate next 0 1 Then the following tabs should be open: """ - data/scroll/simple.html """ Scenario: :scroll-page with --top-navigate When I run :scroll-page --top-navigate prev 0 -1 Then data/hello3.txt should be loaded @qtwebengine_skip # Causes memory leak... Scenario: :scroll-page with a very big value When I run :scroll-page 99999999999 99999999999 Then the error "Numeric argument is too large for internal int representation." should be shown Scenario: :scroll-page on a page without scrolling When I open data/hello.txt And I run :scroll-page 1 1 Then the page should not be scrolled ## issues Scenario: Relative scroll position with a position:absolute page When I open data/scroll/position_absolute.html And I wait for "* position_absolute loaded" in the log And I run :scroll-to-perc 100 And I wait until the scroll position changed And I run :scroll-page --bottom-navigate next 0 1 Then data/hello2.txt should be loaded Scenario: Scrolling to anchor in background tab When I open about:blank And I run :tab-only And I open data/scroll/simple.html#anchor in a new background tab And I run :tab-next And I run :jseval --world main checkAnchor() Then "[*] [PASS] Positions equal: *" should be logged Scenario: Showing/hiding statusbar (#2236, #8223) When I set statusbar.show to never And I run :scroll-to-perc 100 And I wait until the scroll position changed And I run :cmd-set-text / And I run :fake-key -g Then "Scroll position changed to Py*.QtCore.QPoint()" should not be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/search.feature0000644000175100017510000003767315102145205022705 0ustar00runnerrunnerFeature: Searching on a page Searching text on the page (like /foo) with different options. Background: Given I open data/search.html And I run :tab-only ## searching Scenario: Searching text When I run :search foo And I wait for "search found foo" in the log Then "foo" should be found Scenario: Searching twice When I run :search foo And I wait for "search found foo" in the log And I run :search bar And I wait for "search found bar" in the log Then "Bar" should be found Scenario: Searching with --reverse When I set search.ignore_case to always And I run :search -r foo And I wait for "search found foo with flags FindBackward" in the log Then "Foo" should be found Scenario: Searching without matches When I run :search doesnotmatch And I wait for "search didn't find doesnotmatch" in the log Then the warning "Text 'doesnotmatch' not found on page!" should be shown @xfail_norun Scenario: Searching with / and spaces at the end (issue 874) When I run :cmd-set-text -s /space And I run :command-accept And I wait for "search found space " in the log Then "space " should be found Scenario: Searching with / and slash in search term (issue 507) When I run :cmd-set-text //slash And I run :command-accept And I wait for "search found /slash" in the log Then "/slash" should be found Scenario: Searching with arguments at start of search term When I run :cmd-set-text /-r reversed And I run :command-accept And I wait for "search found -r reversed" in the log Then "-r reversed" should be found Scenario: Searching with semicolons in search term When I run :cmd-set-text /; And I run :fake-key -g ; And I run :fake-key -g And I run :fake-key -g semi And I run :command-accept And I wait for "search found ;; semi" in the log Then ";; semi" should be found # This doesn't work because this is QtWebKit behavior. @xfail_norun Scenario: Searching text with umlauts When I run :search blub And I wait for "search didn't find blub" in the log Then the warning "Text 'blub' not found on page!" should be shown Scenario: Searching text duplicates When I run :search foo And I wait for "search found foo" in the log And I run :search foo Then "Ignoring duplicate search request for foo, but resetting flags" should be logged Scenario: Reset search direction on duplicate search, forward-to-back When I run :search baz And I wait for "search found baz" in the log And I run :search -r baz And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log And I run :search-next And I wait for "next_result found baz with flags FindBackward" in the log Then "BAZ" should be found Scenario: Reset search direction on duplicate search, back-to-forward When I run :search -r baz And I wait for "search found baz with flags FindBackward" in the log And I run :search baz And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log And I run :search-next And I wait for "next_result found baz" in the log Then "baz" should be found ## search.ignore_case Scenario: Searching text with search.ignore_case = always When I set search.ignore_case to always And I run :search bar And I wait for "search found bar" in the log Then "Bar" should be found Scenario: Searching text with search.ignore_case = never When I set search.ignore_case to never And I run :search bar And I wait for "search found bar with flags FindCaseSensitively" in the log Then "bar" should be found Scenario: Searching text with search.ignore_case = smart (lower-case) When I set search.ignore_case to smart And I run :search bar And I wait for "search found bar" in the log Then "Bar" should be found Scenario: Searching text with search.ignore_case = smart (upper-case) When I set search.ignore_case to smart And I run :search Foo And I wait for "search found Foo with flags FindCaseSensitively" in the log # even though foo was first Then "Foo" should be found ## :search-next Scenario: Jumping to next match When I set search.ignore_case to always And I run :search foo And I wait for "search found foo" in the log And I run :search-next And I wait for "next_result found foo" in the log Then "Foo" should be found Scenario: Jumping to next match with count When I set search.ignore_case to always And I run :search baz And I wait for "search found baz" in the log And I run :search-next with count 2 And I wait for "next_result found baz" in the log Then "BAZ" should be found Scenario: Jumping to next match with --reverse When I set search.ignore_case to always And I run :search --reverse foo And I wait for "search found foo with flags FindBackward" in the log And I run :search-next And I wait for "next_result found foo with flags FindBackward" in the log Then "foo" should be found Scenario: Jumping to next match without search # Make sure there was no search in the same window before When I open data/search.html in a new window And I run :search-next Then the error "No search done yet." should be shown # https://github.com/qutebrowser/qutebrowser/issues/7275 @qtwebkit_skip Scenario: Jumping to next without matches When I run :search doesnotmatch And I wait for the warning "Text 'doesnotmatch' not found on page!" And I run :search-next Then the warning "Text 'doesnotmatch' not found on page!" should be shown Scenario: Repeating search in a second tab (issue #940) When I open data/search.html in a new tab And I run :search foo And I wait for "search found foo" in the log And I run :tab-prev And I run :search-next And I wait for "search found foo" in the log Then "foo" should be found # https://github.com/qutebrowser/qutebrowser/issues/2438 Scenario: Jumping to next match after clearing When I set search.ignore_case to always And I run :search foo And I wait for "search found foo" in the log And I run :search And I run :search-next And I wait for "next_result found foo" in the log Then "foo" should be found ## :search-prev Scenario: Jumping to previous match When I set search.ignore_case to always And I run :search foo And I wait for "search found foo" in the log And I run :search-next And I wait for "next_result found foo" in the log And I run :search-prev And I wait for "prev_result found foo with flags FindBackward" in the log Then "foo" should be found Scenario: Jumping to previous match with count When I set search.ignore_case to always And I run :search baz And I wait for "search found baz" in the log And I run :search-next And I wait for "next_result found baz" in the log And I run :search-next And I wait for "next_result found baz" in the log And I run :search-prev with count 2 And I wait for "prev_result found baz with flags FindBackward" in the log Then "baz" should be found Scenario: Jumping to previous match with --reverse When I set search.ignore_case to always And I run :search --reverse foo And I wait for "search found foo with flags FindBackward" in the log And I run :search-next And I wait for "next_result found foo with flags FindBackward" in the log And I run :search-prev And I wait for "prev_result found foo" in the log Then "Foo" should be found # This makes sure we don't mutate the original flags # Seems to be broken with QtWebKit, wontfix @qtwebkit_skip Scenario: Jumping to previous match with --reverse twice When I set search.ignore_case to always And I run :search --reverse baz # BAZ And I wait for "search found baz with flags FindBackward" in the log And I run :search-prev # Baz And I wait for "prev_result found baz" in the log And I run :search-prev # baz And I wait for "prev_result found baz" in the log Then "baz" should be found Scenario: Jumping to previous match without search # Make sure there was no search in the same window before When I open data/search.html in a new window And I run :search-prev Then the error "No search done yet." should be shown # https://github.com/qutebrowser/qutebrowser/issues/7275 @qtwebkit_skip Scenario: Jumping to previous without matches When I run :search doesnotmatch And I wait for the warning "Text 'doesnotmatch' not found on page!" And I run :search-prev Then the warning "Text 'doesnotmatch' not found on page!" should be shown ## wrapping Scenario: Wrapping around page When I run :search foo And I wait for "search found foo" in the log And I run :search-next And I wait for "next_result found foo" in the log And I run :search-next And I wait for "next_result found foo" in the log Then "foo" should be found Scenario: Wrapping around page with --reverse When I run :search --reverse foo And I wait for "search found foo with flags FindBackward" in the log And I run :search-next And I wait for "next_result found foo with flags FindBackward" in the log And I run :search-next And I wait for "next_result found foo with flags FindBackward" in the log Then "Foo" should be found # TODO: wrapping message with scrolling # TODO: wrapping message without scrolling ## wrapping prevented Scenario: Preventing wrapping at the top of the page When I set search.ignore_case to always And I set search.wrap to false And I set search.wrap_messages to true And I run :search --reverse foo And I wait for "search found foo with flags FindBackward" in the log And I run :search-next And I wait for "next_result found foo with flags FindBackward" in the log And I run :search-next Then the message "Search hit TOP" should be shown Scenario: Preventing wrapping at the bottom of the page When I set search.ignore_case to always And I set search.wrap to false And I run :search foo And I wait for "search found foo" in the log And I run :search-next And I wait for "next_result found foo" in the log And I run :search-next Then the message "Search hit BOTTOM" should be shown ## search match counter @qtwebkit_skip Scenario: Setting search match counter on search When I set search.ignore_case to always And I set search.wrap to true And I run :search ba And I wait for "search found ba" in the log Then "Setting search match text to 1/5" should be logged @qtwebkit_skip Scenario: Updating search match counter on search-next When I set search.ignore_case to always And I set search.wrap to true And I run :search ba And I wait for "search found ba" in the log And I run :search-next And I wait for "next_result found ba" in the log And I run :search-next And I wait for "next_result found ba" in the log Then "Setting search match text to 3/5" should be logged @qtwebkit_skip Scenario: Updating search match counter on search-prev with wrapping When I set search.ignore_case to always And I set search.wrap to true And I run :search ba And I wait for "search found ba" in the log And I run :search-prev And I wait for the message "Search hit TOP, continuing at BOTTOM" Then "Setting search match text to 5/5" should be logged @qtwebkit_skip Scenario: Updating search match counter on search-prev without wrapping When I set search.ignore_case to always And I set search.wrap to false And I run :search ba And I wait for "search found ba" in the log And I run :search-prev And I wait for the message "Search hit TOP" Then "Setting search match text to 1/5" should be logged ## follow searched links @skip # Too flaky Scenario: Follow a searched link When I run :search follow And I wait for "search found follow" in the log And I wait 0.5s And I run :selection-follow Then data/hello.txt should be loaded @skip # Too flaky Scenario: Follow a searched link in a new tab When I run :window-only And I run :search follow And I wait for "search found follow" in the log And I wait 0.5s And I run :selection-follow -t And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/search.html - data/hello.txt (active) """ Scenario: Don't follow searched text When I run :window-only And I run :search foo And I wait for "search found foo" in the log And I run :selection-follow Then the following tabs should be open: """ - data/search.html (active) """ Scenario: Don't follow searched text in a new tab When I run :window-only And I run :search foo And I wait for "search found foo" in the log And I run :selection-follow -t Then the following tabs should be open: """ - data/search.html (active) """ Scenario: Follow a manually selected link When I run :jseval --file (testdata)/search_select.js And I run :selection-follow Then data/hello.txt should be loaded Scenario: Follow a manually selected link in a new tab When I run :window-only And I run :jseval --file (testdata)/search_select.js And I run :selection-follow -t And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/search.html - data/hello.txt (active) """ @qtwebkit_skip @skip # Not supported in qtwebkit Scenario: Follow a searched link in an iframe When I open data/iframe_search.html And I wait for "* search loaded" in the log And I run :tab-only And I run :search follow And I wait for "search found follow" in the log And I run :selection-follow Then "navigation request: url http://localhost:*/data/hello.txt (current http://localhost:*/data/iframe_search.html), type link_clicked, is_main_frame False" should be logged @qtwebkit_skip @skip # Not supported in qtwebkit Scenario: Follow a tabbed searched link in an iframe When I open data/iframe_search.html And I wait for "* search loaded" in the log And I run :tab-only And I run :search follow And I wait for "search found follow" in the log And I run :selection-follow -t And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/iframe_search.html - data/hello.txt (active) """ Scenario: Closing a tab during a search When I run :open -b about:blank And I run :search a And I run :tab-close Then no crash should happen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/sessions.feature0000644000175100017510000003676315102145205023305 0ustar00runnerrunnerFeature: Saving and loading sessions Background: Given I clean up open tabs Scenario: Saving a simple session When I open data/hello.txt And I open data/title.html in a new tab Then the session should look like: """ windows: - active: true tabs: - history: - url: about:blank - active: true url: http://localhost:*/data/hello.txt - active: true history: - active: true url: http://localhost:*/data/title.html title: Test title """ @qtwebengine_skip Scenario: Zooming (qtwebkit) When I open data/hello.txt And I run :zoom 50 Then the session should look like: """ windows: - tabs: - history: - url: about:blank zoom: 1.0 - url: http://localhost:*/data/hello.txt zoom: 0.5 """ # The zoom level is only stored for the newest element for QtWebEngine. @qtwebkit_skip Scenario: Zooming (qtwebengine) When I open data/hello.txt And I run :zoom 50 Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/hello.txt zoom: 0.5 """ @qtwebengine_skip Scenario: Scrolling (qtwebkit) When I open data/scroll/simple.html And I run :scroll-px 10 20 Then the session should look like: """ windows: - tabs: - history: - url: about:blank scroll-pos: x: 0 y: 0 - url: http://localhost:*/data/scroll/simple.html scroll-pos: x: 10 y: 20 """ # The scroll position is only stored for the newest element for QtWebEngine. @qtwebkit_skip Scenario: Scrolling (qtwebengine) When I open data/scroll/simple.html And I run :scroll-px 10 20 And I wait until the scroll position changed to 10/20 Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/scroll/simple.html scroll-pos: x: 10 y: 20 """ Scenario: Redirect When I open redirect-to?url=data/title.html without waiting And I wait until data/title.html is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - active: true url: http://localhost:*/data/title.html original-url: http://localhost:*/redirect-to?url=data/title.html title: Test title """ Scenario: Valid UTF-8 data When I open data/sessions/snowman.html Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/sessions/snowman.html title: snow☃man """ @qtwebengine_skip Scenario: Long output comparison (qtwebkit) When I open data/numbers/1.txt And I open data/title.html And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new window # Full output apart from "geometry:" and the active window (needs qutewm) Then the session should look like: """ windows: - tabs: - history: - scroll-pos: x: 0 y: 0 title: about:blank url: about:blank zoom: 1.0 - scroll-pos: x: 0 y: 0 title: http://localhost:*/data/numbers/1.txt url: http://localhost:*/data/numbers/1.txt zoom: 1.0 - active: true scroll-pos: x: 0 y: 0 title: Test title url: http://localhost:*/data/title.html zoom: 1.0 - active: true history: - active: true scroll-pos: x: 0 y: 0 title: '' url: http://localhost:*/data/numbers/2.txt zoom: 1.0 - tabs: - active: true history: - active: true scroll-pos: x: 0 y: 0 title: '' url: http://localhost:*/data/numbers/3.txt zoom: 1.0 """ # FIXME:qtwebengine what's up with the titles there? @qtwebkit_skip Scenario: Long output comparison (qtwebengine) When I open data/numbers/1.txt And I open data/title.html And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new window # Full output apart from "geometry:" and the active window (needs qutewm) Then the session should look like: """ windows: - tabs: - history: - title: about:blank url: about:blank - title: http://localhost:*/data/numbers/1.txt url: http://localhost:*/data/numbers/1.txt - active: true scroll-pos: x: 0 y: 0 title: Test title url: http://localhost:*/data/title.html zoom: 1.0 - active: true history: - active: true scroll-pos: x: 0 y: 0 title: localhost:*/data/numbers/2.txt url: http://localhost:*/data/numbers/2.txt zoom: 1.0 - tabs: - active: true history: - active: true scroll-pos: x: 0 y: 0 title: localhost:*/data/numbers/3.txt url: http://localhost:*/data/numbers/3.txt zoom: 1.0 """ Scenario: Saving with --no-history When I open data/numbers/1.txt And I open data/numbers/2.txt And I open data/numbers/3.txt Then the session saved with --no-history should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/numbers/3.txt """ Scenario: Saving with --no-history and --only-active-window When I open data/numbers/1.txt And I open data/numbers/2.txt And I open data/numbers/3.txt Then the session saved with --no-history --only-active-window should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/numbers/3.txt """ # https://github.com/qutebrowser/qutebrowser/issues/879 Scenario: Saving a session with a page using history.replaceState() When I open data/sessions/history_replace_state.html without waiting Then the javascript message "Called history.replaceState" should be logged And the session should look like: """ windows: - tabs: - history: - url: about:blank - active: true url: http://localhost:*/data/sessions/history_replace_state.html?state=2 title: Test title """ @qtwebengine_skip Scenario: Saving a session with a page using history.replaceState() and navigating away (qtwebkit) When I open data/sessions/history_replace_state.html And I open data/hello.txt Then the javascript message "Called history.replaceState" should be logged And the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/sessions/history_replace_state.html?state=2 # What we'd *really* expect here is "Test title", but that # workaround is the best we can do. title: http://localhost:*/data/sessions/history_replace_state.html?state=2 - active: true url: http://localhost:*/data/hello.txt """ # Seems like that bug is fixed upstream in QtWebEngine @skip # Too flaky Scenario: Saving a session with a page using history.replaceState() and navigating away When I open data/sessions/history_replace_state.html without waiting And I wait for "* Called history.replaceState" in the log And I open data/hello.txt Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/sessions/history_replace_state.html?state=2 title: Test title - active: true url: http://localhost:*/data/hello.txt """ # :session-save Scenario: Saving to a directory When I run :session-save (tmpdir) Then the error "Error while saving session: *" should be shown Scenario: Saving internal session without --force When I run :session-save _internal Then the error "_internal is an internal session, use --force to save anyways." should be shown And the session _internal should not exist Scenario: Saving internal session with --force When I run :session-save --force _internal_force Then the message "Saved session _internal_force." should be shown And the session _internal_force should exist Scenario: Saving current session without one loaded Given I have a fresh instance And I run :session-save --current Then the error "No session loaded currently!" should be shown Scenario: Saving current session after one is loaded When I open data/numbers/1.txt When I run :session-save current_session And I run :session-load current_session And I wait until data/numbers/1.txt is loaded And I run :session-save --current Then the message "Saved session current_session." should be shown Scenario: Saving session When I run :session-save session_name Then the message "Saved session session_name." should be shown And the session session_name should exist Scenario: Saving session with --quiet When I run :session-save --quiet quiet_session Then "Saved session quiet_session." should be logged with level debug And the session quiet_session should exist Scenario: Saving session with --only-active-window When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new window And I open data/numbers/4.txt in a new tab And I open data/numbers/5.txt in a new tab And I run :session-save --only-active-window window_session_name And I run :window-only And I run :tab-only And I run :session-load window_session_name And I wait until data/numbers/3.txt is loaded And I wait until data/numbers/4.txt is loaded And I wait until data/numbers/5.txt is loaded Then the session should look like: """ windows: - tabs: - history: - active: true url: http://localhost:*/data/numbers/5.txt - tabs: - history: - url: http://localhost:*/data/numbers/3.txt - history: - url: http://localhost:*/data/numbers/4.txt - history: - active: true url: http://localhost:*/data/numbers/5.txt """ # https://github.com/qutebrowser/qutebrowser/issues/7696 @qtwebkit_skip Scenario: Saving session with an empty download tab When I open data/downloads/downloads.html And I run :click-element --force-event -t tab id download And I wait for "Asking question *" in the log And I run :mode-leave And I run :session-save current And I run :session-load --clear current And I wait until data/downloads/downloads.html is loaded Then the session should look like: """ windows: - tabs: - history: - active: true title: Simple downloads url: http://localhost:*/data/downloads/downloads.html - active: true history: [] """ # :session-delete Scenario: Deleting a directory When I run :session-delete (tmpdir) Then "Error while deleting session!" should be logged And the error "Error while deleting session: *" should be shown Scenario: Deleting internal session without --force When I run :session-save --force _internal And I run :session-delete _internal Then the error "_internal is an internal session, use --force to delete anyways." should be shown And the session _internal should exist Scenario: Deleting internal session with --force When I run :session-save --force _internal And I run :session-delete --force _internal And I wait for "Deleted session _internal." in the log Then the session _internal should not exist Scenario: Normally deleting a session When I run :session-save deleted_session And I run :session-delete deleted_session And I wait for "Deleted session deleted_session." in the log Then the session deleted_session should not exist Scenario: Deleting a session which doesn't exist When I run :session-delete inexistent_session Then the error "Session inexistent_session not found!" should be shown # :session-load Scenario: Loading a directory When I run :session-load (tmpdir) Then the error "Error while loading session: *" should be shown Scenario: Loading internal session without --force When I run :session-save --force _internal And I run :session-load _internal Then the error "_internal is an internal session, use --force to load anyways." should be shown @qtwebengine_flaky Scenario: Loading internal session with --force When I open about:blank And I run :session-save --force _internal And I replace "about:blank" by "http://localhost:(port)/data/numbers/1.txt" in the "_internal" session file And I run :session-load --force _internal Then data/numbers/1.txt should be loaded @qtwebengine_flaky Scenario: Normally loading a session When I open about:blank And I run :session-save loaded_session And I replace "about:blank" by "http://localhost:(port)/data/numbers/2.txt" in the "loaded_session" session file And I run :session-load loaded_session Then data/numbers/2.txt should be loaded @qtwebengine_flaky Scenario: Loading and deleting a session When I open about:blank And I run :session-save loaded_session And I replace "about:blank" by "http://localhost:(port)/data/numbers/2.txt" in the "loaded_session" session file And I run :session-load --delete loaded_session And I wait for "Loaded & deleted session loaded_session." in the log Then data/numbers/2.txt should be loaded And the session loaded_session should not exist Scenario: Loading a session which doesn't exist When I run :session-load inexistent_session Then the error "Session inexistent_session not found!" should be shown # Test load/save of pinned tabs @qtwebengine_flaky Scenario: Saving/Loading a session with pinned tabs When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-pin with count 2 And I run :session-save pin_session And I run :tab-only --pinned close And I run :tab-close --force And I run :session-load -c pin_session And I wait until data/numbers/3.txt is loaded And I run :tab-focus 2 And I open data/numbers/4.txt Then the message "Tab is pinned! Opening in new tab." should be shown And the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) (pinned) - data/numbers/4.txt - data/numbers/3.txt """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/spawn.feature0000644000175100017510000000724715102145205022562 0ustar00runnerrunnerFeature: :spawn Scenario: Running :spawn When I run :spawn -v (echo-exe) "Hello" Then the message "Command exited successfully. See :process * for details." should be shown Scenario: Running :spawn with command that does not exist When I run :spawn command_does_not_exist127623 Then the error "Command 'command_does_not_exist127623' failed to start: *" should be shown Scenario: Starting a userscript which doesn't exist When I run :spawn -u this_does_not_exist Then the error "Userscript 'this_does_not_exist' not found in userscript directories *" should be shown Scenario: Starting a userscript with absolute path which doesn't exist When I run :spawn -u (rootpath)this_does_not_exist Then the error "Userscript '*this_does_not_exist' not found" should be shown Scenario: Running :spawn with invalid quoting When I run :spawn ""'"" Then the error "Error while splitting command: No closing quotation" should be shown Scenario: Running :spawn with url variable When I run :spawn (echo-exe) {url} Then "Executing * with args ['about:blank'], userscript=False" should be logged Scenario: Running :spawn with url variable in fully encoded format When I open data/title with spaces.html And I run :spawn (echo-exe) {url} Then "Executing * with args ['http://localhost:(port)/data/title%20with%20spaces.html'], userscript=False" should be logged Scenario: Running :spawn with url variable in pretty decoded format When I open data/title with spaces.html And I run :spawn (echo-exe) {url:pretty} Then "Executing * with args ['http://localhost:(port)/data/title with spaces.html'], userscript=False" should be logged Scenario: Running :spawn with -m When I run :spawn -m (echo-exe) Message 1 Then the message "Message 1" should be shown Scenario: Running :spawn with -u -m When I run :spawn -u -m (echo-exe) Message 2 Then the message "Message 2" should be shown Scenario: Running :spawn with -u -o When I run :spawn -u -o (echo-exe) Message 3 And I wait for "load status for * url='qute://process/*'>: LoadStatus.success" in the log Then the page should contain the plaintext "Message 3" @posix Scenario: Running :spawn with userscript Given I clean up open tabs When I open data/hello.txt And I run :spawn -u (testdata)/userscripts/open_current_url And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt - data/hello.txt (active) """ @posix Scenario: Running :spawn with userscript and count When I run :spawn -u (testdata)/userscripts/hello_if_count with count 5 Then the message "Count is five!" should be shown @posix Scenario: Running :spawn with userscript and no count When I run :spawn -u (testdata)/userscripts/hello_if_count Then the message "No count!" should be shown @windows Scenario: Running :spawn with userscript on Windows Given I clean up open tabs When I open data/hello.txt And I run :spawn -u (testdata)/userscripts/open_current_url.bat And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt - data/hello.txt (active) """ @posix Scenario: Running :spawn with userscript that expects the stdin getting closed When I run :spawn -u (testdata)/userscripts/stdinclose.py Then the message "stdin closed" should be shown ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/tabs.feature0000644000175100017510000021163015102145205022354 0ustar00runnerrunnerFeature: Tab management Tests for various :tab-* commands. Background: Given I clean up open tabs And I set tabs.tabs_are_windows to false And I clear the log # :tab-close Scenario: :tab-close When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) """ Scenario: :tab-close with count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close with count 1 Then the following tabs should be open: """ - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-close with invalid count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close with count 23 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-close with tabs.select_on_remove = next When I set tabs.select_on_remove to next And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-close Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) """ Scenario: :tab-close with tabs.select_on_remove = prev When I set tabs.select_on_remove to prev And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-close Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/3.txt """ Scenario: :tab-close with tabs.select_on_remove = last-used When I set tabs.select_on_remove to last-used And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I open data/numbers/4.txt in a new tab And I run :tab-focus 2 And I run :tab-close Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt - data/numbers/4.txt (active) """ Scenario: :tab-close with tabs.select_on_remove = prev and --next When I set tabs.select_on_remove to prev And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-close --next Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) """ Scenario: :tab-close with tabs.select_on_remove = next and --prev When I set tabs.select_on_remove to next And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-close --prev Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/3.txt """ Scenario: :tab-close with tabs.select_on_remove = prev and --opposite When I set tabs.select_on_remove to prev And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-close --opposite Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) """ Scenario: :tab-close with tabs.select_on_remove = next and --opposite When I set tabs.select_on_remove to next And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-close --opposite Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/3.txt """ Scenario: :tab-close with tabs.select_on_remove = last-used and --opposite When I set tabs.select_on_remove to last-used And I run :tab-close --opposite Then the error "-o is not supported with 'tabs.select_on_remove' set to 'last-used'!" should be shown Scenario: :tab-close should restore selection behavior When I set tabs.select_on_remove to next And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I open data/numbers/4.txt in a new tab And I run :tab-focus 2 And I run :tab-close --prev And I run :tab-focus 2 And I run :tab-close Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/4.txt (active) """ # :tab-only Scenario: :tab-only When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-only Then the following tabs should be open: """ - data/numbers/3.txt (active) """ Scenario: :tab-only with --prev When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-only --prev Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) """ Scenario: :tab-only with --next When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-only --next Then the following tabs should be open: """ - data/numbers/2.txt (active) - data/numbers/3.txt """ Scenario: :tab-only with --prev and --next When I run :tab-only --prev --next Then the error "Only one of -p/-n can be given!" should be shown # :tab-focus Scenario: :tab-focus with invalid index When I run :tab-focus foo Then the error "Invalid value foo." should be shown Scenario: :tab-focus with index When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) - data/numbers/3.txt """ Scenario: :tab-focus without index/count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-focus Then the warning "Using :tab-focus without count is deprecated, use :tab-next instead." should be shown And the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-focus with invalid index When I run :tab-focus 23 Then the error "There's no tab with index 23!" should be shown Scenario: :tab-focus with very big index When I run :tab-focus 99999999999999 Then the error "There's no tab with index 99999999999999!" should be shown Scenario: :tab-focus with count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) - data/numbers/3.txt """ Scenario: :tab-focus with count and index When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 4 with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) - data/numbers/3.txt """ Scenario: :tab-focus last When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-focus 3 And I run :tab-focus last Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ Scenario: :tab-focus with current tab number When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-focus 3 And I run :tab-focus 3 Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ Scenario: :tab-focus with current tab number and --no-last When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-focus 3 And I run :tab-focus --no-last 3 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-focus with -1 When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-focus -1 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-focus negative index When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus -2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) - data/numbers/3.txt """ Scenario: :tab-focus with invalid negative index When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus -5 Then the error "There's no tab with index -1!" should be shown Scenario: :tab-focus last with no last focused tab When I run :tab-focus last Then the error "Could not find requested tab!" should be shown Scenario: :tab-focus prev stacking When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I open data/numbers/4.txt in a new tab And I open data/numbers/5.txt in a new tab And I run :tab-focus 1 And I run :tab-focus 5 And I run :tab-focus 2 And I run :tab-focus 4 And I run :tab-focus 3 And I run :cmd-repeat 2 tab-focus stack-prev Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) - data/numbers/3.txt - data/numbers/4.txt - data/numbers/5.txt """ Scenario: :tab-focus next stacking When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I open data/numbers/4.txt in a new tab And I open data/numbers/5.txt in a new tab And I run :tab-focus 1 And I run :tab-focus 5 And I run :tab-focus 2 And I run :tab-focus 4 And I run :tab-focus 3 And I run :cmd-repeat 3 tab-focus stack-prev And I run :cmd-repeat 2 tab-focus stack-next Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt - data/numbers/4.txt (active) - data/numbers/5.txt """ Scenario: :tab-focus stacking limit When I set tabs.focus_stack_size to 1 And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I open data/numbers/4.txt in a new tab And I open data/numbers/5.txt in a new tab And I run :cmd-repeat 2 tab-focus stack-prev And I run :tab-focus stack-next And I set tabs.focus_stack_size to 10 And I run :tab-focus 1 And I run :tab-focus 5 And I run :tab-focus 2 And I run :tab-focus 4 And I run :tab-focus 3 And I run :cmd-repeat 4 tab-focus stack-prev Then the error "Could not find requested tab!" should be shown And the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt - data/numbers/4.txt - data/numbers/5.txt """ Scenario: :tab-focus stacking and last When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I open data/numbers/4.txt in a new tab And I open data/numbers/5.txt in a new tab And I run :tab-focus 1 And I run :tab-focus 5 And I run :tab-focus 2 And I run :tab-focus 4 And I run :tab-focus 3 And I run :cmd-repeat 2 tab-focus stack-prev And I run :cmd-repeat 3 tab-focus last Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt - data/numbers/4.txt (active) - data/numbers/5.txt """ Scenario: :tab-focus last after moving current tab When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move 2 And I run :tab-focus last Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt - data/numbers/2.txt (active) """ Scenario: :tab-focus last after closing a lower number tab When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close with count 1 And I run :tab-focus last Then the following tabs should be open: """ - data/numbers/2.txt (active) - data/numbers/3.txt """ # tab-prev/tab-next Scenario: :tab-prev When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-prev Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt """ Scenario: :tab-next When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-focus 1 And I run :tab-next Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) """ Scenario: :tab-prev with count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-prev with count 2 Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ Scenario: :tab-next with count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-next with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-prev on first tab without wrap When I set tabs.wrap to false And I open data/numbers/1.txt And I run :tab-prev Then "First tab" should be logged Scenario: :tab-next with last tab without wrap When I set tabs.wrap to false And I open data/numbers/1.txt And I run :tab-next Then "Last tab" should be logged Scenario: :tab-prev on first tab with wrap When I set tabs.wrap to true And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-prev Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-next with last tab with wrap When I set tabs.wrap to true And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-next Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ Scenario: :tab-next with last tab, wrap and count When I set tabs.wrap to true And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-next with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (active) - data/numbers/3.txt """ # :tab-move Scenario: :tab-move with absolute position. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move Then the following tabs should be open: """ - data/numbers/3.txt (active) - data/numbers/1.txt - data/numbers/2.txt """ Scenario: :tab-move with absolute position and count. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) - data/numbers/2.txt """ Scenario: :tab-move with absolute position and invalid count. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move with count 23 Then the error "Can't move tab to position 23!" should be shown And the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-move with index. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) - data/numbers/2.txt """ Scenario: :tab-move with negative index. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move -3 Then the following tabs should be open: """ - data/numbers/3.txt (active) - data/numbers/1.txt - data/numbers/2.txt """ Scenario: :tab-move with invalid index. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move -5 Then the error "Can't move tab to position -1!" should be shown And the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-move with index and count. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move 1 with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) - data/numbers/2.txt """ Scenario: :tab-move with index and invalid count. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move -2 with count 4 Then the error "Can't move tab to position 4!" should be shown And the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-move with relative position (negative). When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move - Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) - data/numbers/2.txt """ Scenario: :tab-move with relative position (positive). When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-move + Then the following tabs should be open: """ - data/numbers/2.txt - data/numbers/1.txt (active) - data/numbers/3.txt """ Scenario: :tab-move with relative position (negative) and count. When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move - with count 2 Then the following tabs should be open: """ - data/numbers/3.txt (active) - data/numbers/1.txt - data/numbers/2.txt """ Scenario: :tab-move with relative position and too big count. When I set tabs.wrap to false And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-move + with count 3 Then the error "Can't move tab to position 4!" should be shown Scenario: :tab-move with relative position (positive) and wrap When I set tabs.wrap to true And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move + Then the following tabs should be open: """ - data/numbers/3.txt (active) - data/numbers/1.txt - data/numbers/2.txt """ Scenario: :tab-move with relative position (negative), wrap and count When I set tabs.wrap to true And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-move - with count 8 Then the following tabs should be open: """ - data/numbers/2.txt - data/numbers/1.txt (active) - data/numbers/3.txt """ Scenario: :tab-move with absolute position When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 1 And I run :tab-move end Then the following tabs should be open: """ - data/numbers/2.txt - data/numbers/3.txt - data/numbers/1.txt (active) """ Scenario: :tab-move with absolute position When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-move start Then the following tabs should be open: """ - data/numbers/3.txt (active) - data/numbers/1.txt - data/numbers/2.txt """ Scenario: Make sure :tab-move retains metadata When I open data/title.html And I open data/hello.txt in a new tab And I run :tab-focus 1 And I run :tab-move + Then the session should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/hello.txt - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title """ # :tab-clone Scenario: :tab-clone with -b and -w When I run :tab-clone -b -w Then the error "Only one of -b/-w/-p can be given!" should be shown Scenario: Cloning a tab with history and title When I open data/title.html And I run :tab-clone And I wait until data/title.html is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title """ Scenario: Cloning zoom value When I open data/hello.txt And I run :zoom 120 And I run :tab-clone And I wait until data/hello.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/hello.txt zoom: 1.2 - active: true history: - url: about:blank - url: http://localhost:*/data/hello.txt zoom: 1.2 """ Scenario: Cloning to background tab When I open data/hello2.txt And I run :tab-clone -b And I wait until data/hello2.txt is loaded Then the following tabs should be open: """ - data/hello2.txt (active) - data/hello2.txt """ Scenario: Cloning to new window When I open data/title.html And I run :tab-clone -w And I wait until data/title.html is loaded Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title """ Scenario: Cloning with tabs_are_windows = true When I open data/title.html And I set tabs.tabs_are_windows to true And I run :tab-clone And I wait until data/title.html is loaded Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title """ Scenario: Cloning to private window When I open data/title.html And I run :tab-clone -p And I wait until data/title.html is loaded Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title - private: true tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/title.html title: Test title """ # https://github.com/qutebrowser/qutebrowser/issues/2289 @qtwebkit_skip @windows_skip Scenario: Cloning a tab with a special URL When I open chrome://sandbox/ And I run :tab-clone Then no crash should happen # :undo Scenario: Undo without any closed tabs Given I have a fresh instance When I run :undo Then the error "Nothing to undo (use :undo --window to reopen a closed window)" should be shown Scenario: Undo closing a tab When I open data/numbers/1.txt And I run :tab-only And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt And I run :tab-close And I run :undo And I wait until data/numbers/3.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt - active: true history: - url: http://localhost:*/data/numbers/2.txt - url: http://localhost:*/data/numbers/3.txt """ @qtwebengine_flaky Scenario: Undo with auto-created last tab When I open data/hello.txt And I run :tab-only And I set tabs.last_close to blank And I run :tab-close And I wait until about:blank is loaded And I run :undo And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt (active) """ @qtwebengine_flaky Scenario: Undo with auto-created last tab, with history When I open data/hello.txt And I open data/hello2.txt And I run :tab-only And I set tabs.last_close to blank And I run :tab-close And I wait until about:blank is loaded And I run :undo And I wait until data/hello2.txt is loaded Then the following tabs should be open: """ - data/hello2.txt (active) """ Scenario: Undo with auto-created last tab (startpage) When I open data/hello.txt And I run :tab-only And I set tabs.last_close to startpage And I set url.start_pages to ["http://localhost:(port)/data/numbers/4.txt"] And I run :tab-close And I wait until data/numbers/4.txt is loaded And I run :undo And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt (active) """ Scenario: Undo with auto-created last tab (default-page) When I open data/hello.txt And I run :tab-only And I set tabs.last_close to default-page And I set url.default_page to http://localhost:(port)/data/numbers/6.txt And I run :tab-close And I wait until data/numbers/6.txt is loaded And I run :undo And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt (active) """ @skip # Too flaky Scenario: Double-undo with single tab on tabs.last_close default page Given I have a fresh instance When I open about:blank And I set tabs.last_close to default-page And I set url.default_page to about:blank And I run :undo And I run :undo Then the error "Nothing to undo (use :undo --window to reopen a closed window)" should be shown And the error "Nothing to undo (use :undo --window to reopen a closed window)" should be shown Scenario: Undo a tab closed by index When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close with count 1 And I run :undo Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ Scenario: Undo a tab closed after switching tabs When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close with count 1 And I run :tab-focus 2 And I run :undo Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ Scenario: Undo a tab closed after rearranging tabs When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close with count 1 And I run :tab-move with count 1 And I run :undo Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/3.txt - data/numbers/2.txt """ @flaky Scenario: Undo a tab closed after new tab opened When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-close with count 1 And I open data/numbers/3.txt in a new tab And I run :undo And I wait until data/numbers/1.txt is loaded Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ Scenario: Undo the closing of tabs using :tab-only When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-focus 2 And I run :tab-only And I run :undo Then the following tabs should be open: """ - data/numbers/1.txt (active) - data/numbers/2.txt - data/numbers/3.txt """ # :undo --window Scenario: Undo the closing of a window Given I clear the log When I open data/numbers/1.txt And I open data/numbers/2.txt in a new window And I run :close And I wait for "removed: tabbed-browser" in the log And I run :undo -w And I wait for "Focus object changed: *" in the log Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt - active: true tabs: - active: true history: - url: http://localhost:*/data/numbers/2.txt """ Scenario: Undo the closing of a window with multiple tabs Given I clear the log When I open data/numbers/1.txt And I open data/numbers/2.txt in a new window And I open data/numbers/3.txt in a new tab And I run :close And I wait for "removed: tabbed-browser" in the log And I run :undo -w And I wait for "Focus object changed: *" in the log Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt - active: true tabs: - history: - url: http://localhost:*/data/numbers/2.txt - active: true history: - url: http://localhost:*/data/numbers/3.txt """ Scenario: Undo the closing of a window with multiple tabs with undo stack Given I clear the log When I open data/numbers/1.txt And I open data/numbers/2.txt in a new window And I open data/numbers/3.txt in a new tab And I run :tab-close And I run :close And I wait for "removed: tabbed-browser" in the log And I run :undo -w And I run :undo And I wait for "Focus object changed: *" in the log Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt - active: true tabs: - history: - url: http://localhost:*/data/numbers/2.txt - active: true history: - url: http://localhost:*/data/numbers/3.txt """ Scenario: Undo the closing of a window with tabs are windows Given I clear the log When I set tabs.last_close to close And I set tabs.tabs_are_windows to true And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-close And I wait for "removed: tabbed-browser" in the log And I run :undo -w And I wait for "Focus object changed: *" in the log Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt - tabs: - active: true history: - url: http://localhost:*/data/numbers/2.txt """ # :undo with count Scenario: Undo the second to last closed tab When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-close And I run :tab-close And I run :undo with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/3.txt (active) """ Scenario: Undo with a too-high count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-close And I run :undo with count 100 Then the error "Nothing to undo" should be shown Scenario: Undo with --window and count When I run :undo --window with count 2 Then the error ":undo --window does not support a count/depth" should be shown Scenario: Undo with --window and depth When I run :undo --window 1 Then the error ":undo --window does not support a count/depth" should be shown # tabs.last_close # FIXME:qtwebengine @qtwebengine_skip # Waits for an earlier about:blank and fails Scenario: tabs.last_close = blank When I open data/hello.txt And I set tabs.last_close to blank And I run :tab-only And I run :tab-close And I wait until about:blank is loaded Then the following tabs should be open: """ - about:blank (active) """ Scenario: tabs.last_close = startpage When I set url.start_pages to ["http://localhost:(port)/data/numbers/7.txt", "http://localhost:(port)/data/numbers/8.txt"] And I set tabs.last_close to startpage And I open data/hello.txt And I run :tab-only And I run :tab-close And I wait until data/numbers/7.txt is loaded And I wait until data/numbers/8.txt is loaded Then the following tabs should be open: """ - data/numbers/7.txt - data/numbers/8.txt (active) """ Scenario: tabs.last_close = default-page When I set url.default_page to http://localhost:(port)/data/numbers/9.txt And I set tabs.last_close to default-page And I open data/hello.txt And I run :tab-only And I run :tab-close And I wait until data/numbers/9.txt is loaded Then the following tabs should be open: """ - data/numbers/9.txt (active) """ Scenario: tabs.last_close = close When I open data/hello.txt And I set tabs.last_close to close And I run :tab-only And I run :tab-close Then qutebrowser should quit # tab settings Scenario: opening links with tabs.background true When I set tabs.background to true And I open data/hints/html/simple.html And I hint with args "all tab" and follow a And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hints/html/simple.html (active) - data/hello.txt """ Scenario: opening tab with tabs.new_position.related prev When I set tabs.new_position.related to prev And I set tabs.background to false And I open about:blank And I open data/hints/html/simple.html in a new tab And I run :click-element id link --target=tab And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - about:blank - data/hello.txt (active) - data/hints/html/simple.html """ Scenario: opening tab with tabs.new_position.related next When I set tabs.new_position.related to next And I set tabs.background to false And I open about:blank And I open data/hints/html/simple.html in a new tab And I run :click-element id link --target=tab And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - about:blank - data/hints/html/simple.html - data/hello.txt (active) """ Scenario: opening tab with tabs.new_position.related first When I set tabs.new_position.related to first And I set tabs.background to false And I open about:blank And I open data/hints/html/simple.html in a new tab And I run :click-element id link --target=tab And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hello.txt (active) - about:blank - data/hints/html/simple.html """ Scenario: opening tab with tabs.new_position.related last When I set tabs.new_position.related to last And I set tabs.background to false And I open data/hints/html/simple.html And I open about:blank in a new tab And I run :tab-focus last And I run :click-element id link --target=tab And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hints/html/simple.html - about:blank - data/hello.txt (active) """ # stacking tabs Scenario: stacking tabs opening tab with tabs.new_position.related next When I set tabs.new_position.related to next And I set tabs.new_position.stacking to true And I set tabs.background to true And I open about:blank And I open data/navigate/index.html in a new tab And I hint with args "all tab-bg" and follow a And I hint with args "all tab-bg" and follow s And I wait until data/navigate/prev.html is loaded And I wait until data/navigate/next.html is loaded Then the following tabs should be open: """ - about:blank - data/navigate/index.html (active) - data/navigate/prev.html - data/navigate/next.html """ Scenario: stacking tabs opening tab with tabs.new_position.related prev When I set tabs.new_position.related to prev And I set tabs.new_position.stacking to true And I set tabs.background to true And I open about:blank And I open data/navigate/index.html in a new tab And I hint with args "all tab-bg" and follow a And I hint with args "all tab-bg" and follow s And I wait until data/navigate/prev.html is loaded And I wait until data/navigate/next.html is loaded Then the following tabs should be open: """ - about:blank - data/navigate/next.html - data/navigate/prev.html - data/navigate/index.html (active) """ Scenario: no stacking tabs opening tab with tabs.new_position.related next When I set tabs.new_position.related to next And I set tabs.new_position.stacking to false And I set tabs.background to true And I open about:blank And I open data/navigate/index.html in a new tab And I hint with args "all tab-bg" and follow a And I hint with args "all tab-bg" and follow s And I wait until data/navigate/prev.html is loaded And I wait until data/navigate/next.html is loaded Then the following tabs should be open: """ - about:blank - data/navigate/index.html (active) - data/navigate/next.html - data/navigate/prev.html """ Scenario: no stacking tabs opening tab with tabs.new_position.related prev When I set tabs.new_position.related to prev And I set tabs.new_position.stacking to false And I set tabs.background to true And I open about:blank And I open data/navigate/index.html in a new tab And I hint with args "all tab-bg" and follow a And I hint with args "all tab-bg" and follow s And I wait until data/navigate/prev.html is loaded And I wait until data/navigate/next.html is loaded Then the following tabs should be open: """ - about:blank - data/navigate/prev.html - data/navigate/next.html - data/navigate/index.html (active) """ # :tab-select Scenario: :tab-select without args or count When I run :tab-select Then qute://tabs should be loaded Scenario: :tab-select with a matching title When I open data/title.html And I open data/search.html in a new tab And I open data/scroll/simple.html in a new tab And I run :tab-select Searching text And I wait for "Current tab changed, focusing " in the log Then the following tabs should be open: """ - data/title.html - data/search.html (active) - data/scroll/simple.html """ Scenario: :tab-select with no matching title When I run :tab-select invalid title Then the error "No matching tab for: invalid title" should be shown @flaky Scenario: :tab-select with matching title and two windows When I open data/title.html And I open data/search.html in a new tab And I open data/scroll/simple.html in a new tab And I open data/caret.html in a new window And I open data/paste_primary.html in a new tab And I run :tab-select Scrolling And I wait for "Focus object changed: *" in the log Then the session should look like: """ windows: - active: true tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - history: - url: http://localhost:*/data/search.html - active: true history: - url: http://localhost:*/data/scroll/simple.html - tabs: - history: - url: http://localhost:*/data/caret.html - active: true history: - url: http://localhost:*/data/paste_primary.html """ Scenario: :tab-select with no matching index When I open data/title.html And I run :tab-select 666 Then the error "There's no tab with index 666!" should be shown Scenario: :tab-select with no matching window index When I open data/title.html And I run :tab-select 99/1 Then the error "There's no window with id 99!" should be shown @skip # Too flaky Scenario: :tab-select with matching window index Given I have a fresh instance When I open data/title.html And I open data/search.html in a new tab And I open data/scroll/simple.html in a new tab And I run :open -w http://localhost:(port)/data/caret.html And I open data/paste_primary.html in a new tab And I wait until data/caret.html is loaded And I run :tab-select 0/2 And I wait for "Focus object changed: *" in the log Then the session should look like: """ windows: - active: true tabs: - history: - url: about:blank - url: http://localhost:*/data/title.html - active: true history: - url: http://localhost:*/data/search.html - history: - url: http://localhost:*/data/scroll/simple.html - tabs: - history: - url: http://localhost:*/data/caret.html - active: true history: - url: http://localhost:*/data/paste_primary.html """ Scenario: :tab-select with wrong argument (-1) When I open data/title.html And I run :tab-select -1 Then the error "There's no tab with index -1!" should be shown Scenario: :tab-select with wrong argument (/) When I open data/title.html And I run :tab-select / Then the following tabs should be open: """ - data/title.html (active) """ Scenario: :tab-select with wrong argument (//) When I open data/title.html And I run :tab-select // Then the following tabs should be open: """ - data/title.html (active) """ Scenario: :tab-select with wrong argument (0/x) When I open data/title.html And I run :tab-select 0/x Then the error "No matching tab for: 0/x" should be shown Scenario: :tab-select with wrong argument (1/2/3) When I open data/title.html And I run :tab-select 1/2/3 Then the error "No matching tab for: 1/2/3" should be shown # :tab-take @xfail_norun # Needs qutewm Scenario: Take a tab from another window Given I have a fresh instance When I open data/numbers/1.txt And I open data/numbers/2.txt in a new window And I run :tab-take 0/1 Then the session should look like: """ windows: - tabs: - history: - url: about:blank - tabs: - history: - url: http://localhost:*/data/numbers/2.txt - history: - url: http://localhost:*/data/numbers/1.txt """ Scenario: Take a tab from the same window Given I have a fresh instance When I open data/numbers/1.txt And I run :tab-take 0/1 Then the error "Can't take a tab from the same window" should be shown Scenario: Take a tab while using tabs_are_windows When I open data/numbers/1.txt And I open data/numbers/2.txt in a new window And I set tabs.tabs_are_windows to true And I run :tab-take 0/1 Then the error "Can't take tabs when using windows as tabs" should be shown @windows_skip @no_offscreen Scenario: Close the last tab of a window when taken by another window Given I have a fresh instance When I open data/numbers/1.txt And I run :tab-only And I open data/numbers/2.txt in a new window And I set tabs.last_close to ignore And I run :tab-take 1/1 And I wait until data/numbers/2.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt - active: true history: - url: http://localhost:*/data/numbers/2.txt """ # :tab-give @xfail_norun # Needs qutewm Scenario: Give a tab to another window Given I have a fresh instance When I open data/numbers/1.txt And I open data/numbers/2.txt in a new window And I run :tab-give 0 Then the session should look like: """ windows: - tabs: - history: - url: http://localhost:*/data/numbers/1.txt - history: - url: http://localhost:*/data/numbers/2.txt - tabs: - history: - url: about:blank """ Scenario: Give a tab to the same window Given I have a fresh instance When I open data/numbers/1.txt And I run :tab-give 0 Then the error "Can't give a tab to the same window" should be shown Scenario: Give a tab to a new window When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-give And I wait until data/numbers/2.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt - tabs: - history: - url: http://localhost:*/data/numbers/2.txt """ Scenario: Give a tab from window with only one tab When I open data/hello.txt And I run :tab-give Then the error "Cannot detach from a window with only one tab" should be shown Scenario: Give a tab to a window ID that does not exist When I open data/hello.txt And I run :tab-give 99 Then the error "There's no window with id 99!" should be shown Scenario: Give a tab while using tabs_are_windows When I open data/numbers/1.txt And I open data/numbers/2.txt in a new window And I set tabs.tabs_are_windows to true And I run :tab-give 0 Then the error "Can't give tabs when using windows as tabs" should be shown @windows_skip @no_offscreen Scenario: Close the last tab of a window when given to another window Given I have a fresh instance When I open data/numbers/1.txt And I run :tab-only And I open data/numbers/2.txt in a new window And I set tabs.last_close to ignore And I run :tab-give 1 And I wait until data/numbers/1.txt is loaded Then the session should look like: """ windows: - tabs: - active: true history: - url: http://localhost:*/data/numbers/2.txt - history: - url: http://localhost:*/data/numbers/1.txt """ # Other Scenario: Using :tab-next after closing last tab (#1448) When I set tabs.last_close to close And I run :tab-only And I run :tab-close ;; tab-next Then the error "No WebView available yet!" should be shown And qutebrowser should quit And no crash should happen Scenario: Using :tab-prev after closing last tab (#1448) When I set tabs.last_close to close And I run :tab-only And I run :tab-close ;; tab-prev Then the error "No WebView available yet!" should be shown And qutebrowser should quit And no crash should happen Scenario: Opening link with tabs_are_windows set (#2162) When I set tabs.tabs_are_windows to true And I open data/hints/html/simple.html And I hint with args "all tab-fg" and follow a And I wait until data/hello.txt is loaded Then the session should look like: """ windows: - tabs: - history: - url: about:blank - url: http://localhost:*/data/hints/html/simple.html - tabs: - history: - url: http://localhost:*/data/hello.txt """ Scenario: Closing tab with tabs_are_windows When I set tabs.tabs_are_windows to true And I set tabs.last_close to ignore And I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-close And I wait for "removed: tabbed-browser" in the log Then the session should look like: """ windows: - tabs: - active: true history: - url: about:blank - url: http://localhost:*/data/numbers/1.txt """ # :tab-pin Scenario: :tab-pin command When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-pin Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) (pinned) """ Scenario: :tab-pin unpin When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-pin And I run :tab-pin Then the following tabs should be open: """ - data/numbers/1.txt (pinned) - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: :tab-pin to index 2 When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-pin with count 2 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (pinned) - data/numbers/3.txt (active) """ Scenario: :tab-pin with an invalid count When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I open data/numbers/3.txt in a new tab And I run :tab-pin with count 23 Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) """ Scenario: Pinned :tab-close prompt yes When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-close And I wait for "*want to close a pinned tab*" in the log And I run :prompt-accept yes Then the following tabs should be open: """ - data/numbers/1.txt (active) (pinned) """ Scenario: Pinned :tab-close prompt no When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-close And I wait for "*want to close a pinned tab*" in the log And I run :prompt-accept no Then the following tabs should be open: """ - data/numbers/1.txt (pinned) - data/numbers/2.txt (active) (pinned) """ Scenario: Pinned :tab-only prompt yes When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-next And I run :tab-only And I wait for "*want to close pinned tabs*" in the log And I run :prompt-accept yes Then the following tabs should be open: """ - data/numbers/1.txt (active) (pinned) """ Scenario: Pinned :tab-only prompt no When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-next And I run :tab-only And I wait for "*want to close pinned tabs*" in the log And I run :prompt-accept no Then the following tabs should be open: """ - data/numbers/1.txt (active) (pinned) - data/numbers/2.txt (pinned) """ Scenario: Pinned :tab-only close all but pinned tab When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-only Then the following tabs should be open: """ - data/numbers/2.txt (active) (pinned) """ Scenario: Pinned :tab-only --pinned close When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-next And I run :tab-only --pinned close Then the following tabs should be open: """ - data/numbers/1.txt (active) (pinned) """ Scenario: Pinned :tab-only --pinned keep When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-next And I run :tab-only --pinned keep Then the following tabs should be open: """ - data/numbers/1.txt (active) (pinned) - data/numbers/2.txt (pinned) """ Scenario: Pinned :tab-only --pinned prompt When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-next And I run :tab-only --pinned prompt Then "*want to close pinned tabs*" should be logged Scenario: :tab-pin open url When I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt Then the message "Tab is pinned! Opening in new tab." should be shown And the following tabs should be open: """ - data/numbers/1.txt (active) (pinned) - data/numbers/2.txt """ Scenario: :tab-pin open url with tabs.pinned.frozen = false When I set tabs.pinned.frozen to false And I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt Then the following tabs should be open: """ - data/numbers/2.txt (active) (pinned) """ Scenario: :home on a pinned tab When I open data/numbers/1.txt And I run :tab-pin And I run :home Then the message "Tab is pinned!" should be shown And the following tabs should be open: """ - data/numbers/1.txt (active) (pinned) """ Scenario: :home on a pinned tab with tabs.pinned.frozen = false When I set url.start_pages to ["http://localhost:(port)/data/numbers/2.txt"] And I set tabs.pinned.frozen to false And I open data/numbers/1.txt And I run :tab-pin And I run :home Then data/numbers/2.txt should be loaded And the following tabs should be open: """ - data/numbers/2.txt (active) (pinned) """ Scenario: Cloning a pinned tab When I open data/numbers/1.txt And I run :tab-pin And I run :tab-clone And I wait until data/numbers/1.txt is loaded Then the following tabs should be open: """ - data/numbers/1.txt (pinned) - data/numbers/1.txt (pinned) (active) """ Scenario: Undo a pinned tab When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-close --force And I run :undo And I wait until data/numbers/2.txt is loaded Then the following tabs should be open: """ - data/numbers/1.txt - data/numbers/2.txt (pinned) (active) """ Scenario: Focused webview after clicking link in bg When I open data/hints/link_input.html And I run :click-element id qute-input-existing And I wait for "Entering mode KeyMode.insert *" in the log And I run :mode-leave And I hint with args "all tab-bg" and follow a And I wait until data/hello.txt is loaded And I run :mode-enter insert And I run :fake-key -g new Then the javascript message "contents: existingnew" should be logged Scenario: Focused webview after opening link in bg When I open data/hints/link_input.html And I run :click-element id qute-input-existing And I wait for "Entering mode KeyMode.insert *" in the log And I run :mode-leave And I open data/hello.txt in a new background tab And I run :mode-enter insert And I run :fake-key -g new Then the javascript message "contents: existingnew" should be logged @skip # Too flaky Scenario: Focused prompt after opening link in bg When I open data/hints/link_input.html When I run :cmd-set-text -s :message-info And I open data/hello.txt in a new background tab And I run :fake-key -g hello-world Then the message "hello-world" should be shown @skip # Too flaky Scenario: Focused prompt after opening link in fg When I open data/hints/link_input.html When I run :cmd-set-text -s :message-info And I open data/hello.txt in a new tab And I run :fake-key -g hello-world Then the message "hello-world" should be shown Scenario: Undo after changing tabs_are_windows When I open data/hello.txt And I open data/hello.txt in a new tab And I set tabs.tabs_are_windows to true And I run :tab-close And I run :undo And I run :message-info "Still alive!" Then the message "Still alive!" should be shown Scenario: Passthrough mode override When I run :set -u localhost:*/data/numbers/1.txt input.mode_override 'passthrough' And I open data/numbers/1.txt Then "Entering mode KeyMode.passthrough (reason: mode_override)" should be logged Scenario: Insert mode override When I run :set -u localhost:*/data/numbers/1.txt input.mode_override 'insert' And I open data/numbers/1.txt Then "Entering mode KeyMode.insert (reason: mode_override)" should be logged Scenario: Mode override on tab switch When I run :set -u localhost:*/data/numbers/1.txt input.mode_override 'insert' And I open data/numbers/1.txt And I wait for "Entering mode KeyMode.insert (reason: mode_override)" in the log And I run :fake-key -g And I open data/numbers/2.txt in a new tab And I run :tab-prev Then "Entering mode KeyMode.insert (reason: mode_override)" should be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_backforward_bdd.py0000644000175100017510000000027415102145205024555 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('backforward.feature') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_caret_bdd.py0000644000175100017510000000043415102145205023364 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd # pylint: disable=unused-import from end2end.features.test_yankpaste_bdd import init_fake_clipboard bdd.scenarios('caret.feature') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_completion_bdd.py0000644000175100017510000000070315102145205024436 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('completion.feature') @bdd.then(bdd.parsers.parse("the completion model should be {model}")) def check_model(quteproc, model): """Make sure the completion model was set to something.""" pattern = "Starting {} completion *".format(model) quteproc.wait_for(message=pattern) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_downloads_bdd.py0000644000175100017510000001226515102145205024265 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import os import sys import shlex import pytest import pytest_bdd as bdd from qutebrowser.qt.network import QSslSocket bdd.scenarios('downloads.feature') PROMPT_MSG = ("Asking question option=None " "text=* title='Save file to:'>, *") @pytest.fixture def download_dir(tmpdir): downloads = tmpdir / 'downloads' downloads.ensure(dir=True) (downloads / 'subdir').ensure(dir=True) try: os.mkfifo(downloads / 'fifo') except AttributeError: pass unwritable = downloads / 'unwritable' unwritable.ensure(dir=True) unwritable.chmod(0) yield downloads unwritable.chmod(0o755) @bdd.given("I set up a temporary download dir") def temporary_download_dir(quteproc, download_dir): quteproc.set_setting('downloads.location.prompt', 'false') quteproc.set_setting('downloads.location.remember', 'false') quteproc.set_setting('downloads.location.directory', str(download_dir)) @bdd.given("I clean old downloads") def clean_old_downloads(quteproc): quteproc.send_cmd(':download-cancel --all') quteproc.send_cmd(':download-clear') @bdd.when("SSL is supported") def check_ssl(): if not QSslSocket.supportsSsl(): pytest.skip("QtNetwork SSL not supported") @bdd.when("I download an SSL redirect page") def download_ssl_redirect(server, ssl_server, quteproc): path = "data/downloads/download.bin" url = f"https://localhost:{ssl_server.port}/redirect-http/{path}?port={server.port}" quteproc.send_cmd(f":download {url}") @bdd.when("the unwritable dir is unwritable") def check_unwritable(tmpdir): unwritable = tmpdir / 'downloads' / 'unwritable' if os.access(unwritable, os.W_OK): # Docker container or similar pytest.skip("Unwritable dir was writable") @bdd.when("I wait until the download is finished") def wait_for_download_finished(quteproc): quteproc.wait_for(category='downloads', message='Download * finished') @bdd.when(bdd.parsers.parse("I wait until the download {name} is finished")) def wait_for_download_finished_name(quteproc, name): quteproc.wait_for(category='downloads', message='Download {} finished'.format(name)) @bdd.when(bdd.parsers.parse('I wait for the download prompt for "{path}"')) def wait_for_download_prompt(tmpdir, quteproc, path): full_path = path.replace('(tmpdir)', str(tmpdir)).replace('/', os.sep) quteproc.wait_for(message=PROMPT_MSG.format(full_path)) quteproc.wait_for(message="Entering mode KeyMode.prompt " "(reason: question asked)") @bdd.then(bdd.parsers.parse("The downloaded file {filename} should not exist")) def download_should_not_exist(filename, tmpdir): path = tmpdir / 'downloads' / filename assert not path.check() @bdd.then(bdd.parsers.parse("The downloaded file {filename} should exist")) def download_should_exist(filename, tmpdir): path = tmpdir / 'downloads' / filename assert path.check() @bdd.then(bdd.parsers.parse("The downloaded file {filename} should be " "{size} bytes big")) def download_size(filename, size, tmpdir): path = tmpdir / 'downloads' / filename assert path.size() == int(size) @bdd.then(bdd.parsers.parse("The downloaded file {filename} should contain " "{text}")) def download_contents(filename, text, tmpdir): path = tmpdir / 'downloads' / filename assert text in path.read() @bdd.then(bdd.parsers.parse('The download prompt should be shown with ' '"{path}"')) def download_prompt(tmpdir, quteproc, path): full_path = path.replace('(tmpdir)', str(tmpdir)).replace('/', os.sep) quteproc.wait_for(message=PROMPT_MSG.format(full_path)) quteproc.send_cmd(':mode-leave') @bdd.when("I set a test python open_dispatcher") def default_open_dispatcher_python(quteproc, tmpdir): cmd = '{} -c "import sys; print(sys.argv[1])"'.format( shlex.quote(sys.executable)) quteproc.set_setting('downloads.open_dispatcher', cmd) @bdd.when("I open the download") def download_open(quteproc): cmd = '{} -c "import sys; print(sys.argv[1])"'.format( shlex.quote(sys.executable)) quteproc.send_cmd(':download-open {}'.format(cmd)) @bdd.when("I open the download with a placeholder") def download_open_placeholder(quteproc): cmd = '{} -c "import sys; print(sys.argv[1])"'.format( shlex.quote(sys.executable)) quteproc.send_cmd(':download-open {} {{}}'.format(cmd)) @bdd.when("I directly open the download") def download_open_with_prompt(quteproc): cmd = '{} -c pass'.format(shlex.quote(sys.executable)) quteproc.send_cmd(':prompt-open-download {}'.format(cmd)) @bdd.when(bdd.parsers.parse("I delete the downloaded file {filename}")) def delete_file(tmpdir, filename): (tmpdir / 'downloads' / filename).remove() @bdd.then("the FIFO should still be a FIFO") def fifo_should_be_fifo(tmpdir): download_dir = tmpdir / 'downloads' assert download_dir.exists() assert not os.path.isfile(download_dir / 'fifo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_editor_bdd.py0000644000175100017510000001254215102145205023557 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import sys import json import textwrap import os import signal import time import pytest import pytest_bdd as bdd from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QFileSystemWatcher bdd.scenarios('editor.feature') from qutebrowser.utils import utils @bdd.when(bdd.parsers.parse('I setup a fake editor replacing "{text}" by ' '"{replacement}"')) def set_up_editor_replacement(quteproc, server, tmpdir, text, replacement): """Set up editor.command to a small python script doing a replacement.""" text = text.replace('(port)', str(server.port)) script = tmpdir / 'script.py' script.write(textwrap.dedent(""" import sys with open(sys.argv[1], encoding='utf-8') as f: data = f.read() data = data.replace("{text}", "{replacement}") with open(sys.argv[1], 'w', encoding='utf-8') as f: f.write(data) """.format(text=text, replacement=replacement))) editor = json.dumps([sys.executable, str(script), '{}']) quteproc.set_setting('editor.command', editor) @bdd.when(bdd.parsers.parse('I setup a fake editor returning "{text}"')) def set_up_editor(quteproc, tmpdir, text): """Set up editor.command to a small python script inserting a text.""" script = tmpdir / 'script.py' script.write(textwrap.dedent(""" import sys with open(sys.argv[1], 'w', encoding='utf-8') as f: f.write({text!r}) """.format(text=text))) editor = json.dumps([sys.executable, str(script), '{}']) quteproc.set_setting('editor.command', editor) @bdd.when(bdd.parsers.parse('I setup a fake editor returning empty text')) def set_up_editor_empty(quteproc, tmpdir): """Set up editor.command to a small python script inserting empty text.""" set_up_editor(quteproc, tmpdir, "") class EditorPidWatcher(QObject): appeared = pyqtSignal() def __init__(self, directory, parent=None): super().__init__(parent) self._pidfile = directory / 'editor_pid' self._watcher = QFileSystemWatcher(self) self._watcher.addPath(str(directory)) self._watcher.directoryChanged.connect(self._check_update) self.has_pidfile = False self._check_update() @pyqtSlot() def _check_update(self): if self.has_pidfile: return if self._pidfile.check(): if self._pidfile.read(): self.has_pidfile = True self.appeared.emit() else: self._watcher.addPath(str(self._pidfile)) def manual_check(self): return self._pidfile.check() @pytest.fixture def editor_pid_watcher(tmpdir): return EditorPidWatcher(tmpdir) @bdd.when(bdd.parsers.parse('I setup a fake editor that writes "{text}" on ' 'save')) def set_up_editor_wait(quteproc, tmpdir, text, editor_pid_watcher): """Set up editor.command to a small python script inserting a text.""" assert not utils.is_windows pidfile = tmpdir / 'editor_pid' script = tmpdir / 'script.py' script.write(textwrap.dedent(""" import os import sys import time import signal def handle(sig, _frame): filename = sys.argv[1] old_mtime = new_mtime = os.stat(filename).st_mtime while old_mtime == new_mtime: time.sleep(0.1) with open(filename, 'w', encoding='utf-8') as f: f.write({text!r}) new_mtime = os.stat(filename).st_mtime if sig == signal.SIGUSR1: sys.exit(0) signal.signal(signal.SIGUSR1, handle) signal.signal(signal.SIGUSR2, handle) with open(r'{pidfile}', 'w') as f: f.write(str(os.getpid())) time.sleep(100) """.format(pidfile=pidfile, text=text))) editor = json.dumps([sys.executable, str(script), '{}']) quteproc.set_setting('editor.command', editor) @bdd.when("I wait until the editor has started") def wait_editor(qtbot, editor_pid_watcher): if not editor_pid_watcher.has_pidfile: with qtbot.wait_signal(editor_pid_watcher.appeared, raising=False): pass if not editor_pid_watcher.manual_check(): pytest.fail("Editor pidfile failed to appear!") @bdd.when(bdd.parsers.parse('I kill the waiting editor')) def kill_editor_wait(tmpdir): """Kill the waiting editor.""" pidfile = tmpdir / 'editor_pid' pid = int(pidfile.read()) # windows has no SIGUSR1, but we don't run this on windows anyways # for posix, there IS a member so we need to ignore useless-suppression # pylint: disable=no-member,useless-suppression os.kill(pid, signal.SIGUSR1) @bdd.when(bdd.parsers.parse('I save without exiting the editor')) def save_editor_wait(tmpdir): """Trigger the waiting editor to write without exiting.""" pidfile = tmpdir / 'editor_pid' # give the "editor" process time to write its pid for _ in range(10): if pidfile.check(): break time.sleep(1) pid = int(pidfile.read()) # windows has no SIGUSR2, but we don't run this on windows anyways # for posix, there IS a member so we need to ignore useless-suppression # pylint: disable=no-member,useless-suppression os.kill(pid, signal.SIGUSR2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_hints_bdd.py0000644000175100017510000000113615102145205023413 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import textwrap import pytest import pytest_bdd as bdd bdd.scenarios('hints.feature') @pytest.fixture(autouse=True) def set_up_word_hints(tmpdir, quteproc): dict_file = tmpdir / 'dict' dict_file.write(textwrap.dedent(""" one two three four five six seven eight nine ten eleven twelve thirteen """)) quteproc.set_setting('hints.dictionary', str(dict_file)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_history_bdd.py0000644000175100017510000000353115102145205023770 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import json import logging import re import pytest import pytest_bdd as bdd bdd.scenarios('history.feature') @pytest.fixture(autouse=True) def turn_on_sql_history(quteproc): """Make sure SQL writing is enabled for tests in this module.""" cmd = ":debug-pyeval objects.debug_flags.remove('no-sql-history')" quteproc.send_cmd(cmd) quteproc.wait_for_load_finished_url('qute://pyeval') quteproc.wait_for(message='INSERT INTO History *', category='sql') @bdd.then(bdd.parsers.parse("the query parameter {name} should be set to " "{value}")) def check_query(quteproc, name, value): """Check if a given query is set correctly. This assumes we're on the server query page. """ content = quteproc.get_content() data = json.loads(content) print(data) assert data[name] == value @bdd.then(bdd.parsers.parse("the history should contain:")) def check_history(quteproc, server, tmpdir, docstring): quteproc.wait_for(message='INSERT INTO History *', category='sql') path = tmpdir / 'history' quteproc.send_cmd(':debug-dump-history "{}"'.format(path)) quteproc.wait_for(category='message', loglevel=logging.INFO, message='Dumped history to {}'.format(path)) with path.open('r', encoding='utf-8') as f: # ignore access times, they will differ in each run actual = '\n'.join(re.sub('^\\d+-?', '', line).strip() for line in f) expected = docstring.replace('(port)', str(server.port)) assert actual == expected @bdd.then("the history should be empty") def check_history_empty(quteproc, server, tmpdir): quteproc.wait_for(message='DELETE FROM History', category='sql') check_history(quteproc, server, tmpdir, '') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_invoke_bdd.py0000644000175100017510000000105715102145205023563 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('invoke.feature') @bdd.when(bdd.parsers.parse("I spawn a new window")) def invoke_with(quteproc): """Spawn a new window via IPC call.""" quteproc.log_summary("Create a new window") quteproc.send_ipc([], target_arg='window') quteproc.wait_for(category='init', module='app', function='_open_startpage', message='Opening start pages') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_javascript_bdd.py0000644000175100017510000000333115102145205024433 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import os.path import pytest_bdd as bdd bdd.scenarios('javascript.feature') @bdd.then("the window sizes should be the same") def check_window_sizes(quteproc): hidden = quteproc.wait_for_js('hidden window size: *') quteproc.send_cmd(':jseval --world main updateText("visible")') visible = quteproc.wait_for_js('visible window size: *') hidden_size = hidden.message.split()[-1] visible_size = visible.message.split()[-1] assert hidden_size == visible_size test_gm_script = r""" // ==UserScript== // @name qutebrowser test userscript // @namespace invalid.org // @include http://localhost:*/data/hints/iframe.html // @include http://localhost:*/data/hints/html/wrapped.html // @exclude ??? // @run-at {stage} // {frames} // ==/UserScript== console.log("Script is running on " + window.location.pathname); """ @bdd.when(bdd.parsers.parse("I have a GreaseMonkey file saved for {stage} " "with noframes {frameset}")) def create_greasemonkey_file(quteproc, stage, frameset): script_path = os.path.join(quteproc.basedir, 'data', 'greasemonkey') try: os.mkdir(script_path) except FileExistsError: pass file_path = os.path.join(script_path, 'test.user.js') if frameset == "set": frames = "@noframes" elif frameset == "unset": frames = "" else: raise ValueError("noframes can only be set or unset, " "not {}".format(frameset)) with open(file_path, 'w', encoding='utf-8') as f: f.write(test_gm_script.format(stage=stage, frames=frames)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_keyinput_bdd.py0000644000175100017510000000027115102145205024135 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('keyinput.feature') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_marks_bdd.py0000644000175100017510000000112415102145205023400 0ustar00runnerrunner# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest import pytest_bdd as bdd bdd.scenarios('marks.feature') @pytest.fixture(autouse=True) def turn_on_scroll_logging(quteproc): quteproc.turn_on_scroll_logging(no_scroll_filtering=True) @bdd.then(bdd.parsers.parse("the page should be scrolled to {x} {y}")) def check_y(request, quteproc, x, y): data = quteproc.get_session() pos = data['windows'][0]['tabs'][0]['history'][-1]['scroll-pos'] assert int(x) == pos['x'] assert int(y) == pos['y'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_misc_bdd.py0000644000175100017510000000167015102145205023224 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('misc.feature') @bdd.when("I load a third-party iframe") def load_iframe(quteproc, server, ssl_server): quteproc.set_setting('content.tls.certificate_errors', 'load-insecurely') quteproc.open_path(f'https-iframe/{ssl_server.port}', port=server.port) msg = quteproc.wait_for(message="Certificate error: *") msg.expected = True msg = quteproc.wait_for(message="Certificate error: *") msg.expected = True @bdd.when("I turn on scroll logging") def turn_on_scroll_logging(quteproc): quteproc.turn_on_scroll_logging(no_scroll_filtering=True) @bdd.then(bdd.parsers.parse('the PDF {filename} should exist in the tmpdir')) def pdf_exists(quteproc, tmpdir, filename): path = tmpdir / filename data = path.read_binary() assert data.startswith(b'%PDF') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_navigate_bdd.py0000644000175100017510000000027115102145205024063 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('navigate.feature') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_notifications_bdd.py0000644000175100017510000000605615102145205025145 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest import pytest_bdd as bdd bdd.scenarios('notifications.feature') pytestmark = [ pytest.mark.usefixtures('notification_server'), pytest.mark.qtwebkit_skip, ] @bdd.given("the notification server supports body markup") def supports_body_markup(notification_server, quteproc): notification_server.supports_body_markup = True quteproc.send_cmd( ":debug-pyeval -q __import__('qutebrowser').browser.webengine.notification." "bridge._drop_adapter()") @bdd.given("the notification server doesn't support body markup") def doesnt_support_body_markup(notification_server, quteproc): notification_server.supports_body_markup = False quteproc.send_cmd( ":debug-pyeval -q __import__('qutebrowser').browser.webengine.notification." "bridge._drop_adapter()") @bdd.given('I clean up the notification server') def cleanup_notification_server(notification_server): notification_server.cleanup() @bdd.then('1 notification should be presented') def notification_presented_single(notification_server): assert len(notification_server.messages) == 1 @bdd.then(bdd.parsers.cfparse('{count:d} notifications should be presented')) def notification_presented_count(notification_server, count): assert len(notification_server.messages) == count @bdd.then(bdd.parsers.parse('the notification should have body "{body}"')) def notification_body(notification_server, body): msg = notification_server.last_msg() assert msg.body == body @bdd.then(bdd.parsers.parse('the notification should have title "{title}"')) def notification_title(notification_server, title): msg = notification_server.last_msg() assert msg.title == title @bdd.then(bdd.parsers.cfparse( 'the notification should have image dimensions {width:d}x{height:d}')) def notification_image_dimensions(notification_server, width, height): msg = notification_server.last_msg() assert (msg.img_width, msg.img_height) == (width, height) @bdd.then('the notification should be closed via web') def notification_closed(notification_server): msg = notification_server.last_msg() assert msg.closed_via_web @bdd.when('I close the notification') def close_notification(notification_server): notification_server.close(notification_server.last_id) @bdd.when(bdd.parsers.cfparse('I close the notification with id {id_:d}')) def close_notification_id(notification_server, id_): notification_server.close(id_) @bdd.when('I click the notification') def click_notification(notification_server): notification_server.click(notification_server.last_id) @bdd.when(bdd.parsers.cfparse('I click the notification with id {id_:d}')) def click_notification_id(notification_server, id_): notification_server.click(id_) @bdd.when(bdd.parsers.cfparse( 'I trigger a {name} action on the notification with id {id_:d}')) def custom_notification_action(notification_server, id_, name): notification_server.action(id_, name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_open_bdd.py0000644000175100017510000000221315102145205023224 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import logging import pytest import pytest_bdd as bdd bdd.scenarios('open.feature') @pytest.mark.parametrize('scheme', ['http://', '']) def test_open_s(request, quteproc, ssl_server, scheme): """Test :open with -s.""" quteproc.set_setting('content.tls.certificate_errors', 'load-insecurely') quteproc.send_cmd(':open -s {}localhost:{}/' .format(scheme, ssl_server.port)) if scheme == 'http://' or not request.config.webengine: # Error is only logged on the first error with QtWebEngine quteproc.mark_expected(category='message', loglevel=logging.ERROR, message="Certificate error: *") quteproc.wait_for_load_finished('/', port=ssl_server.port, https=True, load_status='warn') def test_open_s_non_http(quteproc, ssl_server): """Test :open with -s and a qute:// page.""" quteproc.send_cmd(':open -s qute://version') quteproc.wait_for_load_finished('qute://version') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_private_bdd.py0000644000175100017510000000176415102145205023747 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import json import pytest_bdd as bdd bdd.scenarios('private.feature') @bdd.then(bdd.parsers.parse('the cookie {name} should be set to {value}')) def check_cookie(quteproc, name, value): """Check if a given cookie is set correctly. This assumes we're on the server cookies page. """ content = quteproc.get_content() data = json.loads(content) print(data) assert data['cookies'][name] == value @bdd.then(bdd.parsers.parse('the cookie {name} should not be set')) def check_cookie_not_set(quteproc, name): """Check if a given cookie is not set.""" content = quteproc.get_content() data = json.loads(content) print(data) assert name not in data['cookies'] @bdd.then(bdd.parsers.parse('the file {name} should not contain "{text}"')) def check_not_contain(tmpdir, name, text): path = tmpdir / name assert text not in path.read() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_prompts_bdd.py0000644000175100017510000000742615102145205024002 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import logging import pytest_bdd as bdd bdd.scenarios('prompts.feature') from qutebrowser.utils import qtutils try: from qutebrowser.qt.webenginecore import PYQT_WEBENGINE_VERSION except ImportError: PYQT_WEBENGINE_VERSION = None @bdd.when("I load an SSL page") def load_ssl_page(quteproc, ssl_server): # We don't wait here as we can get an SSL question. quteproc.open_path('/', port=ssl_server.port, https=True, wait=False, new_tab=True) @bdd.when("I wait until the SSL page finished loading") def wait_ssl_page_finished_loading(quteproc, ssl_server): quteproc.wait_for_load_finished('/', port=ssl_server.port, https=True, load_status='warn') @bdd.when("I load an SSL resource page") def load_ssl_resource_page(quteproc, server, ssl_server): # We don't wait here as we can get an SSL question. quteproc.open_path(f'https-script/{ssl_server.port}', port=server.port, wait=False) @bdd.when("I wait until the SSL resource page finished loading") def wait_ssl_resource_page_finished_loading(quteproc, server, ssl_server): quteproc.wait_for_load_finished(f'https-script/{ssl_server.port}', port=server.port) @bdd.when("I wait for a prompt") def wait_for_prompt(quteproc): quteproc.wait_for(message='Asking question *') @bdd.given("I may need a fresh instance") def fresh_instance(quteproc): """Restart qutebrowser to bypass webengine's permission persistance.""" # Qt6.8 by default will remember feature grants or denies. When we are # on PyQt6.8 we disable that with the new API, otherwise restart the # browser to make it forget previous prompts. # # Qt 6.10 Beta 4 accidentally persists some permissions; # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-140194 if ( qtutils.version_check("6.8", compiled=False) and PYQT_WEBENGINE_VERSION and PYQT_WEBENGINE_VERSION < 0x60800 ) or qtutils.version_check("6.10", compiled=False, exact=True): quteproc.terminate() quteproc.start() @bdd.then("no prompt should be shown") def no_prompt_shown(quteproc): quteproc.ensure_not_logged(message='Entering mode KeyMode.* (reason: ' 'question asked)') @bdd.then("a SSL error page should be shown") def ssl_error_page(request, quteproc): if request.config.webengine: quteproc.wait_for(message="Certificate error: *") msg = quteproc.wait_for(message="Load error: *") msg.expected = True assert msg.message == 'Load error: ERR_CERT_AUTHORITY_INVALID' else: line = quteproc.wait_for(message='Error while loading *: SSL handshake failed') line.expected = True quteproc.wait_for(message="Changing title for idx * to 'Error loading page: *'") content = quteproc.get_content().strip() assert "Unable to load page" in content def test_certificate_error_load_status(request, quteproc, ssl_server): """If we load the same page twice, we should get a 'warn' status twice.""" quteproc.set_setting('content.tls.certificate_errors', 'load-insecurely') for i in range(2): quteproc.open_path('/', port=ssl_server.port, https=True, wait=False, new_tab=True) if i == 0 or not request.config.webengine: # Error is only logged on the first error with QtWebEngine quteproc.mark_expected(category='message', loglevel=logging.ERROR, message="Certificate error: *") quteproc.wait_for_load_finished('/', port=ssl_server.port, https=True, load_status='warn') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_qutescheme_bdd.py0000644000175100017510000000326415102145205024435 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('qutescheme.feature') @bdd.then(bdd.parsers.parse("the {kind} request should be blocked")) def request_blocked(request, quteproc, kind): blocking_csrf_msg = ( "Blocking malicious request from " "http://localhost:*/data/misc/qutescheme_csrf.html to " "qute://settings/set?*") blocking_js_msg = ( "[http://localhost:*/data/misc/qutescheme_csrf.html:0] Not allowed to " "load local resource: qute://settings/set?*" ) unsafe_redirect_msg = "Load error: ERR_UNSAFE_REDIRECT" webkit_error_invalid = ( "Error while loading qute://settings/set?*: Invalid qute://settings " "request") webkit_error_unsupported = ( "Error while loading qute://settings/set?*: Unsupported request type") if request.config.webengine: # We mark qute:// as a local scheme, causing most requests being blocked # by Chromium internally (logging to the JS console). expected_messages = { 'img': [blocking_js_msg], 'link': [blocking_js_msg], 'redirect': [unsafe_redirect_msg], 'form': [blocking_js_msg], } else: # QtWebKit expected_messages = { 'img': [blocking_csrf_msg], 'link': [blocking_csrf_msg, webkit_error_invalid], 'redirect': [blocking_csrf_msg, webkit_error_invalid], 'form': [webkit_error_unsupported], } for pattern in expected_messages[kind]: msg = quteproc.wait_for(message=pattern) msg.expected = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_scroll_bdd.py0000644000175100017510000000051215102145205023561 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest import pytest_bdd as bdd bdd.scenarios('scroll.feature') @pytest.fixture(autouse=True) def turn_on_scroll_logging(quteproc): quteproc.turn_on_scroll_logging(no_scroll_filtering=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_search_bdd.py0000644000175100017510000000170515102145205023535 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import json import pytest import pytest_bdd as bdd @pytest.fixture(autouse=True) def init_fake_clipboard(quteproc): """Make sure the fake clipboard will be used.""" quteproc.send_cmd(':debug-set-fake-clipboard') @bdd.then(bdd.parsers.parse('"{text}" should be found')) def check_found_text(request, quteproc, text): if request.config.webengine: # WORKAROUND # This probably should work with Qt 5.9: # https://codereview.qt-project.org/#/c/192920/ # https://codereview.qt-project.org/#/c/192921/ # https://bugreports.qt.io/browse/QTBUG-53134 # FIXME: Doesn't actually work, investigate why. return quteproc.send_cmd(':yank selection') quteproc.wait_for(message='Setting fake clipboard: {}'.format( json.dumps(text))) bdd.scenarios('search.feature') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_sessions_bdd.py0000644000175100017510000000357415102145205024144 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import os.path import logging import pytest import pytest_bdd as bdd bdd.scenarios('sessions.feature') @pytest.fixture(autouse=True) def turn_on_scroll_logging(quteproc): quteproc.turn_on_scroll_logging() @bdd.when(bdd.parsers.parse('I have a "{name}" session file:')) def create_session_file(quteproc, name, docstring): filename = os.path.join(quteproc.basedir, 'data', 'sessions', name + '.yml') with open(filename, 'w', encoding='utf-8') as f: f.write(docstring) @bdd.when(bdd.parsers.parse('I replace "{pattern}" by "{replacement}" in the ' '"{name}" session file')) def session_replace(quteproc, server, pattern, replacement, name): # First wait until the session was actually saved quteproc.wait_for(category='message', loglevel=logging.INFO, message='Saved session {}.'.format(name)) filename = os.path.join(quteproc.basedir, 'data', 'sessions', name + '.yml') replacement = replacement.replace('(port)', str(server.port)) # yo dawg with open(filename, 'r', encoding='utf-8') as f: data = f.read() with open(filename, 'w', encoding='utf-8') as f: f.write(data.replace(pattern, replacement)) @bdd.then(bdd.parsers.parse("the session {name} should exist")) def session_should_exist(quteproc, name): filename = os.path.join(quteproc.basedir, 'data', 'sessions', name + '.yml') assert os.path.exists(filename) @bdd.then(bdd.parsers.parse("the session {name} should not exist")) def session_should_not_exist(quteproc, name): filename = os.path.join(quteproc.basedir, 'data', 'sessions', name + '.yml') assert not os.path.exists(filename) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_spawn_bdd.py0000644000175100017510000000026615102145205023421 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('spawn.feature') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_tabs_bdd.py0000644000175100017510000000026515102145205023221 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('tabs.feature') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_urlmarks_bdd.py0000644000175100017510000000443715102145205024135 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import os.path import pytest import pytest_bdd as bdd from helpers import testutils bdd.scenarios('urlmarks.feature') @pytest.fixture(autouse=True) def clear_marks(quteproc): """Clear all existing marks between tests.""" yield quteproc.send_cmd(':quickmark-del --all') quteproc.wait_for(message="Quickmarks cleared.") quteproc.send_cmd(':bookmark-del --all') quteproc.wait_for(message="Bookmarks cleared.") def _check_marks(quteproc, quickmarks, expected, contains): """Make sure the given line does (not) exist in the bookmarks. Args: quickmarks: True to check the quickmarks file instead of bookmarks. expected: The line to search for. contains: True if the line should be there, False otherwise. """ if quickmarks: mark_file = os.path.join(quteproc.basedir, 'config', 'quickmarks') else: mark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks', 'urls') quteproc.clear_data() # So we don't match old messages quteproc.send_cmd(':save') quteproc.wait_for(message='Saved to {}'.format(mark_file)) with open(mark_file, 'r', encoding='utf-8') as f: lines = f.readlines() matched_line = any( testutils.pattern_match(pattern=expected, value=line.rstrip('\n')) for line in lines) assert matched_line == contains, lines @bdd.then(bdd.parsers.parse('the bookmark file should contain "{line}"')) def bookmark_file_contains(quteproc, line): _check_marks(quteproc, quickmarks=False, expected=line, contains=True) @bdd.then(bdd.parsers.parse('the bookmark file should not contain "{line}"')) def bookmark_file_does_not_contain(quteproc, line): _check_marks(quteproc, quickmarks=False, expected=line, contains=False) @bdd.then(bdd.parsers.parse('the quickmark file should contain "{line}"')) def quickmark_file_contains(quteproc, line): _check_marks(quteproc, quickmarks=True, expected=line, contains=True) @bdd.then(bdd.parsers.parse('the quickmark file should not contain "{line}"')) def quickmark_file_does_not_contain(quteproc, line): _check_marks(quteproc, quickmarks=True, expected=line, contains=False) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_utilcmds_bdd.py0000644000175100017510000000046415102145205024115 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest import pytest_bdd as bdd bdd.scenarios('utilcmds.feature') @pytest.fixture(autouse=True) def turn_on_scroll_logging(quteproc): quteproc.turn_on_scroll_logging() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_yankpaste_bdd.py0000644000175100017510000000114215102145205024262 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest import pytest_bdd as bdd bdd.scenarios('yankpaste.feature') @pytest.fixture(autouse=True) def init_fake_clipboard(quteproc): """Make sure the fake clipboard will be used.""" quteproc.send_cmd(':debug-set-fake-clipboard') @bdd.when(bdd.parsers.parse('I insert "{value}" into the text field')) def set_text_field(quteproc, value): quteproc.send_cmd(":jseval --world=0 set_text('{}')".format(value)) quteproc.wait_for_js('textarea set to: ' + value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/test_zoom_bdd.py0000644000175100017510000000073615102145205023257 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest_bdd as bdd bdd.scenarios('zoom.feature') @bdd.then(bdd.parsers.parse("the zoom should be {zoom}%")) def check_zoom(quteproc, zoom): data = quteproc.get_session() histories = data['windows'][0]['tabs'][0]['history'] value = next(h for h in histories if 'zoom' in h)['zoom'] * 100 assert abs(value - float(zoom)) < 0.0001 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/urlmarks.feature0000644000175100017510000003114315102145205023262 0ustar00runnerrunnerFeature: quickmarks and bookmarks ## bookmarks Scenario: Saving a bookmark When I open data/title.html And I run :bookmark-add Then the message "Bookmarked http://localhost:*/data/title.html" should be shown And the bookmark file should contain "http://localhost:*/data/title.html Test title" Scenario: Saving a bookmark with a provided url and title When I run :bookmark-add http://example.com "some example title" Then the message "Bookmarked http://example.com" should be shown And the bookmark file should contain "http://example.com some example title" Scenario: Saving a bookmark with a url but no title When I run :bookmark-add http://example.com Then the error "Title must be provided if url has been provided" should be shown Scenario: Saving a bookmark with an invalid url When I set url.auto_search to never And I run :bookmark-add foo! "some example title" Then the error "Invalid URL" should be shown Scenario: Saving a duplicate bookmark Given I have a fresh instance When I open data/title.html And I run :bookmark-add And I run :bookmark-add Then the error "Bookmark already exists!" should be shown Scenario: Loading a bookmark When I run :tab-only And I run :bookmark-load http://localhost:(port)/data/numbers/1.txt Then data/numbers/1.txt should be loaded And the following tabs should be open: """ - data/numbers/1.txt (active) """ Scenario: Loading a bookmark in a new tab Given I open about:blank When I run :tab-only And I run :bookmark-load -t http://localhost:(port)/data/numbers/2.txt Then data/numbers/2.txt should be loaded And the following tabs should be open: """ - about:blank - data/numbers/2.txt (active) """ Scenario: Loading a bookmark in a background tab Given I open about:blank When I run :tab-only And I run :bookmark-load -b http://localhost:(port)/data/numbers/3.txt Then data/numbers/3.txt should be loaded And the following tabs should be open: """ - about:blank (active) - data/numbers/3.txt """ Scenario: Loading a bookmark in a new window Given I open about:blank When I run :tab-only And I run :bookmark-load -w http://localhost:(port)/data/numbers/4.txt And I wait until data/numbers/4.txt is loaded Then the session should look like: """ windows: - tabs: - active: true history: - active: true url: about:blank - tabs: - active: true history: - active: true url: http://localhost:*/data/numbers/4.txt """ Scenario: Loading a bookmark with -t and -b When I run :bookmark-load -t -b about:blank Then the error "Only one of -t/-b/-w/-p can be given!" should be shown Scenario: Deleting a bookmark which does not exist When I run :bookmark-del doesnotexist Then the error "Bookmark 'doesnotexist' not found!" should be shown Scenario: Deleting a bookmark When I open data/numbers/5.txt And I run :bookmark-add And I run :bookmark-del http://localhost:(port)/data/numbers/5.txt Then the bookmark file should not contain "http://localhost:*/data/numbers/5.txt *" Scenario: Deleting all bookmarks When I open data/numbers/1.txt And I run :bookmark-add And I open data/numbers/2.txt And I run :bookmark-add And I run :bookmark-del --all Then the message "Bookmarks cleared." should be shown And the bookmark file should not contain "http://localhost:*/data/numbers/1.txt *" And the bookmark file should not contain "http://localhost:*/data/numbers/2.txt *" Scenario: Deleting all bookmarks with url When I open data/numbers/1.txt And I run :bookmark-add And I run :bookmark-del --all https://example.org Then the error "Cannot specify url and --all" should be shown And the bookmark file should contain "http://localhost:*/data/numbers/1.txt *" Scenario: Deleting the current page's bookmark if it doesn't exist When I open data/hello.txt And I run :bookmark-del Then the error "Bookmark 'http://localhost:(port)/data/hello.txt' not found!" should be shown Scenario: Deleting the current page's bookmark When I open data/numbers/6.txt And I run :bookmark-add And I run :bookmark-del Then the bookmark file should not contain "http://localhost:*/data/numbers/6.txt *" Scenario: Toggling a bookmark When I open data/numbers/7.txt And I run :bookmark-add And I run :bookmark-add --toggle Then the bookmark file should not contain "http://localhost:*/data/numbers/7.txt *" Scenario: Loading a bookmark with --delete When I run :bookmark-add http://localhost:(port)/data/numbers/8.txt "eight" And I run :bookmark-load -d http://localhost:(port)/data/numbers/8.txt Then the bookmark file should not contain "http://localhost:*/data/numbers/8.txt *" ## quickmarks Scenario: Saving a quickmark (:quickmark-add) When I run :quickmark-add http://localhost:(port)/data/numbers/9.txt nine Then the quickmark file should contain "nine http://localhost:*/data/numbers/9.txt" @flaky Scenario: Saving a quickmark (:quickmark-save) When I open data/numbers/10.txt And I run :quickmark-save And I wait for "Entering mode KeyMode.prompt (reason: question asked)" in the log And I press the keys "ten" And I press the keys "" Then the quickmark file should contain "ten http://localhost:*/data/numbers/10.txt" Scenario: Saving a duplicate quickmark (without override) When I run :quickmark-add http://localhost:(port)/data/numbers/11.txt eleven And I run :quickmark-add http://localhost:(port)/data/numbers/11_2.txt eleven And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log And I run :prompt-accept no Then the quickmark file should contain "eleven http://localhost:*/data/numbers/11.txt" Scenario: Saving a duplicate quickmark (with override) When I run :quickmark-add http://localhost:(port)/data/numbers/12.txt twelve And I run :quickmark-add http://localhost:(port)/data/numbers/12_2.txt twelve And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log And I run :prompt-accept yes Then the quickmark file should contain "twelve http://localhost:*/data/numbers/12_2.txt" Scenario: Adding a quickmark with an empty name When I run :quickmark-add about:blank "" Then the error "Can't set mark with empty name!" should be shown Scenario: Adding a quickmark with an empty URL When I run :quickmark-add "" foo Then the error "Can't set mark with empty URL!" should be shown Scenario: Loading a quickmark Given I have a fresh instance When I run :quickmark-add http://localhost:(port)/data/numbers/13.txt thirteen And I run :quickmark-load thirteen Then data/numbers/13.txt should be loaded And the following tabs should be open: """ - data/numbers/13.txt (active) """ Scenario: Loading a quickmark in a new tab Given I open about:blank When I run :tab-only And I run :quickmark-add http://localhost:(port)/data/numbers/14.txt fourteen And I run :quickmark-load -t fourteen Then data/numbers/14.txt should be loaded And the following tabs should be open: """ - about:blank - data/numbers/14.txt (active) """ Scenario: Loading a quickmark in a background tab Given I open about:blank When I run :tab-only And I run :quickmark-add http://localhost:(port)/data/numbers/15.txt fifteen And I run :quickmark-load -b fifteen Then data/numbers/15.txt should be loaded And the following tabs should be open: """ - about:blank (active) - data/numbers/15.txt """ Scenario: Loading a quickmark in a new window Given I open about:blank When I run :tab-only And I run :quickmark-add http://localhost:(port)/data/numbers/16.txt sixteen And I run :quickmark-load -w sixteen And I wait until data/numbers/16.txt is loaded Then the session should look like: """ windows: - tabs: - active: true history: - active: true url: about:blank - tabs: - active: true history: - active: true url: http://localhost:*/data/numbers/16.txt """ Scenario: Loading a quickmark which does not exist When I run :quickmark-load -b doesnotexist Then the error "Quickmark 'doesnotexist' does not exist!" should be shown Scenario: Loading a quickmark with -t and -b When I run :quickmark-add http://localhost:(port)/data/numbers/17.txt seventeen When I run :quickmark-load -t -b seventeen Then the error "Only one of -t/-b/-w/-p can be given!" should be shown Scenario: Deleting a quickmark which does not exist When I run :quickmark-del doesnotexist Then the error "Quickmark 'doesnotexist' not found!" should be shown Scenario: Deleting a quickmark When I run :quickmark-add http://localhost:(port)/data/numbers/18.txt eighteen And I run :quickmark-del eighteen Then the quickmark file should not contain "eighteen http://localhost:*/data/numbers/18.txt " Scenario: Deleting all quickmarks When I run :quickmark-add http://localhost:(port)/data/numbers/1.txt one When I run :quickmark-add http://localhost:(port)/data/numbers/2.txt two And I run :quickmark-del --all Then the message "Quickmarks cleared." should be shown And the quickmark file should not contain "one http://localhost:*/data/numbers/1.txt" And the quickmark file should not contain "two http://localhost:*/data/numbers/2.txt" Scenario: Deleting all quickmarks with name When I run :quickmark-add http://localhost:(port)/data/numbers/1.txt one And I run :quickmark-del --all invalid Then the error "Cannot specify name and --all" should be shown And the quickmark file should contain "one http://localhost:*/data/numbers/1.txt" Scenario: Deleting the current page's quickmark if it has none When I open data/hello.txt And I run :quickmark-del Then the error "Quickmark for 'http://localhost:(port)/data/hello.txt' not found!" should be shown Scenario: Deleting the current page's quickmark When I open data/numbers/19.txt And I run :quickmark-add http://localhost:(port)/data/numbers/19.txt nineteen And I run :quickmark-del Then the quickmark file should not contain "nineteen http://localhost:*/data/numbers/19.txt" Scenario: Listing quickmarks When I run :quickmark-add http://localhost:(port)/data/numbers/20.txt twenty And I run :quickmark-add http://localhost:(port)/data/numbers/21.txt twentyone And I open qute://bookmarks Then the page should contain the plaintext "twenty" And the page should contain the plaintext "twentyone" Scenario: Listing bookmarks When I open data/title.html#unique-url in a new tab And I run :bookmark-add And I open qute://bookmarks Then the page should contain the plaintext "Test title" Scenario: Following a bookmark When I open data/numbers/1.txt in a new tab And I run :bookmark-add And I open qute://bookmarks And I hint with args "links current" and follow a Then data/numbers/1.txt should be loaded Scenario: Following a bookmark and going back/forward When I open data/numbers/1.txt in a new tab And I run :bookmark-add And I open qute://bookmarks And I hint with args "links current" and follow a And I wait until data/numbers/1.txt is loaded And I run :back And I wait until qute://bookmarks is loaded And I run :forward Then data/numbers/1.txt should be loaded ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/utilcmds.feature0000644000175100017510000001531515102145205023251 0ustar00runnerrunnerFeature: Miscellaneous utility commands exposed to the user. Background: Given I open data/scroll/simple.html And I run :tab-only And I run :window-only ## :cmd-later Scenario: :cmd-later before When I run :cmd-later 500 scroll down Then the page should not be scrolled # wait for scroll to execute so we don't ruin our future And the page should be scrolled vertically Scenario: :cmd-later after When I run :cmd-later 500 scroll down And I wait 0.6s Then the page should be scrolled vertically # for some reason, argparser gives us the error instead, see #2046 @xfail Scenario: :cmd-later with negative delay When I run :cmd-later -1 scroll down Then the error "I can't run something in the past!" should be shown Scenario: :cmd-later with humongous delay When I run :cmd-later 36893488147419103232 scroll down Then the error "Numeric argument is too large for internal int representation." should be shown ## :cmd-repeat Scenario: :cmd-repeat simple When I run :cmd-repeat 2 message-info repeat-test Then the message "repeat-test" should be shown And the message "repeat-test" should be shown Scenario: :cmd-repeat zero times When I run :cmd-repeat 0 message-error "repeat-test 2" # If we have an error, the test will fail Then no crash should happen Scenario: :cmd-repeat with count When I run :cmd-repeat 3 message-info "repeat-test 3" with count 2 Then the message "repeat-test 3" should be shown And the message "repeat-test 3" should be shown And the message "repeat-test 3" should be shown And the message "repeat-test 3" should be shown And the message "repeat-test 3" should be shown And the message "repeat-test 3" should be shown ## :cmd-run-with-count Scenario: :cmd-run-with-count When I run :cmd-run-with-count 2 message-info "run-with-count test" Then the message "run-with-count test" should be shown And the message "run-with-count test" should be shown Scenario: :cmd-run-with-count with count When I run :cmd-run-with-count 2 message-info "run-with-count test 2" with count 2 Then the message "run-with-count test 2" should be shown And the message "run-with-count test 2" should be shown And the message "run-with-count test 2" should be shown And the message "run-with-count test 2" should be shown ## :message-* Scenario: :message-error When I run :message-error "Hello World" Then the error "Hello World" should be shown Scenario: :message-info When I run :message-info "Hello World" Then the message "Hello World" should be shown Scenario: :message-warning When I run :message-warning "Hello World" Then the warning "Hello World" should be shown # argparser again @xfail Scenario: :cmd-repeat negative times When I run :cmd-repeat -4 scroll-px 10 0 Then the error "A negative count doesn't make sense." should be shown And the page should not be scrolled ## :debug-all-objects Scenario: :debug-all-objects When I run :debug-all-objects Then "*Qt widgets - *Qt objects - *" should be logged ## :debug-cache-stats Scenario: :debug-cache-stats When I run :debug-cache-stats Then "is_valid_prefix: CacheInfo(*)" should be logged And "_render_stylesheet: CacheInfo(*)" should be logged ## :debug-console @no_xvfb Scenario: :debug-console smoke test When I run :debug-console And I wait for "Focus object changed: " in the log And I run :debug-console And I wait for "Focus object changed: *" in the log Then "initializing debug console" should be logged And "showing debug console" should be logged And "hiding debug console" should be logged And no crash should happen ## :cmd-repeat-last Scenario: :cmd-repeat-last When I run :message-info test1 And I run :cmd-repeat-last Then the message "test1" should be shown And the message "test1" should be shown Scenario: :cmd-repeat-last with count When I run :message-info test2 And I run :cmd-repeat-last with count 2 Then the message "test2" should be shown And the message "test2" should be shown And the message "test2" should be shown Scenario: :cmd-repeat-last with not-normal command in between When I run :message-info test3 And I run :prompt-accept And I run :cmd-repeat-last Then the message "test3" should be shown And the error "prompt-accept: This command is only allowed in prompt/yesno mode, not normal." should be shown And the error "prompt-accept: This command is only allowed in prompt/yesno mode, not normal." should be shown Scenario: :cmd-repeat-last with mode-switching command When I open data/hints/link_blank.html And I run :tab-only And I hint with args "all tab-fg" And I run :mode-leave And I run :cmd-repeat-last And I wait for "hints: *" in the log And I run :hint-follow a And I wait until data/hello.txt is loaded Then the following tabs should be open: """ - data/hints/link_blank.html - data/hello.txt (active) """ ## :debug-log-capacity Scenario: Using :debug-log-capacity When I run :debug-log-capacity 100 And I run :message-info oldstuff And I run :cmd-repeat 20 message-info otherstuff And I run :message-info newstuff And I open qute://log Then the page should contain the plaintext "newstuff" And the page should not contain the plaintext "oldstuff" Scenario: Using :debug-log-capacity with negative capacity When I run :debug-log-capacity -1 Then the error "Can't set a negative log capacity!" should be shown ## :debug-log-level / :debug-log-filter # Other :debug-log-{level,filter} features are tested in # unit/utils/test_log.py as using them would break end2end tests. Scenario: Using debug-log-filter with invalid filter When I run :debug-log-filter blah Then the error "Invalid log category blah - valid categories: statusbar, *" should be shown Scenario: Using debug-log-filter When I run :debug-log-filter commands,ipc,webview And I run :mode-enter insert And I run :debug-log-filter none And I run :mode-leave Then "Entering mode KeyMode.insert *" should not be logged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/yankpaste.feature0000644000175100017510000003565415102145205023434 0ustar00runnerrunnerFeature: Yanking and pasting. :yank, {clipboard} and {primary} can be used to copy/paste the URL or title from/to the clipboard and primary selection. Background: Given I clean up open tabs #### :yank Scenario: Yanking URLs to clipboard When I open data/title.html And I run :yank Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title.html" should be shown And the clipboard should contain "http://localhost:(port)/data/title.html" Scenario: Yanking URLs to primary selection When selection is supported And I open data/title.html And I run :yank --sel Then the message "Yanked URL to primary selection: http://localhost:(port)/data/title.html" should be shown And the primary selection should contain "http://localhost:(port)/data/title.html" Scenario: Yanking URLs with ref and UTM parameters When I open data/title.html?utm_source=kikolani&utm_medium=320banner&utm_campaign=bpp&ref=facebook And I run :yank Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title.html" should be shown And the clipboard should contain "http://localhost:(port)/data/title.html" Scenario: Yanking URLs with ref and UTM parameters and some other parameters When I open data/title.html?stype=models&utm_source=kikolani&utm_medium=320banner&utm_campaign=bpp&ref=facebook And I run :yank Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title.html?stype=models" should be shown And the clipboard should contain "http://localhost:(port)/data/title.html?stype=models" Scenario: Yanking title to clipboard When I open data/title.html And I wait for regex "Changing title for idx \d to 'Test title'" in the log And I run :yank title Then the message "Yanked title to clipboard: Test title" should be shown And the clipboard should contain "Test title" Scenario: Yanking inline to clipboard When I open data/title.html And I run :yank inline '[[{url:yank}][qutebrowser" And I press the key "" And I press the key "" And I run :insert-text Hello world # Compare Then the javascript message "textarea contents: onHello worlde two three four" should be logged Scenario: Inserting text into a text field with undo When I open data/paste_primary.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log # Paste and undo And I run :insert-text This text should be undone And I wait for the javascript message "textarea contents: This text should be undone" And I press the key "" And I wait for the javascript message "textarea contents: " # Paste final text And I run :insert-text This text should stay # Compare Then the javascript message "textarea contents: This text should stay" should be logged Scenario: Inserting text without a focused field When I open data/paste_primary.html And I run :mode-enter insert And I run :insert-text test Then the error "No element focused!" should be shown Scenario: Inserting text with a read-only field When I open data/paste_primary.html And I run :click-element id qute-textarea-noedit And I wait for "Clicked non-editable element!" in the log And I run :mode-enter insert And I run :insert-text test Then the error "Element is not editable!" should be shown ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/features/zoom.feature0000644000175100017510000001043115102145205022403 0ustar00runnerrunnerFeature: Zooming in and out Background: Given I open data/hello.txt And I set zoom.levels to [50%, 90%, 100%, 110%, 120%] And I run :tab-only Scenario: Zooming in When I run :zoom-in Then the message "Zoom level: 110%" should be shown And the zoom should be 110% Scenario: Zooming out When I run :zoom-out Then the message "Zoom level: 90%" should be shown And the zoom should be 90% Scenario: Zooming in with count When I run :zoom-in with count 2 Then the message "Zoom level: 120%" should be shown And the zoom should be 120% # https://github.com/qutebrowser/qutebrowser/issues/1118 Scenario: Zooming in with very big count When I run :zoom-in with count 99999999999 Then the message "Zoom level: 120%" should be shown And the zoom should be 120% # https://github.com/qutebrowser/qutebrowser/issues/1118 Scenario: Zooming out with very big count When I run :zoom-out with count 99999999999 Then the message "Zoom level: 50%" should be shown And the zoom should be 50% # https://github.com/qutebrowser/qutebrowser/issues/1118 Scenario: Zooming in with very big count and snapping in When I run :zoom-in with count 99999999999 And I run :zoom-out Then the message "Zoom level: 110%" should be shown And the zoom should be 110% Scenario: Zooming out with count When I run :zoom-out with count 2 Then the message "Zoom level: 50%" should be shown And the zoom should be 50% Scenario: Setting zoom When I run :zoom 50 Then the message "Zoom level: 50%" should be shown And the zoom should be 50% Scenario: Setting zoom with trailing % When I run :zoom 50% Then the message "Zoom level: 50%" should be shown And the zoom should be 50% Scenario: Setting zoom with count When I run :zoom with count 40 Then the message "Zoom level: 40%" should be shown And the zoom should be 40% Scenario: Resetting zoom When I set zoom.default to 42% And I run :zoom 50 And I run :zoom Then the message "Zoom level: 42%" should be shown And the zoom should be 42% Scenario: Setting zoom to invalid value When I run :zoom -1 Then the error "Can't zoom -1%!" should be shown Scenario: Setting zoom with very big count When I run :zoom with count 99999999999 Then the message "Zoom level: 99999999999%" should be shown Scenario: Setting zoom with argument and count When I run :zoom 50 with count 60 Then the message "Zoom level: 60%" should be shown And the zoom should be 60% # https://github.com/qutebrowser/qutebrowser/issues/2507 # Using 127.0.0.1 because separate domain is required to reproduce Scenario: Qutebrowser enforces correct zoom level When I run :zoom 150% And I open data/search.html And I run :open http://127.0.0.1:(port)/data/long_load.html And I wait until http://127.0.0.1:(port)/data/long_load.html is loaded And I run :back And I wait until data/search.html is loaded Then the zoom should be 150% # Fixed in QtWebEngine branch @xfail Scenario: Zooming in with cloned tab When I set zoom.default to 100% And I run :zoom-in And I wait for "Zoom level: 110%" in the log And I run :tab-clone And I wait until data/hello.txt is loaded And I run :zoom-in Then the message "Zoom level: 120%" should be shown And the zoom should be 120% # https://github.com/qutebrowser/qutebrowser/issues/2183 @qtwebengine_flaky Scenario: Setting a default zoom When I set zoom.default to 200% And I open data/hello.txt in a new tab And I run :tab-only Then the zoom should be 200% Scenario: Zooming in with --quiet When I run :zoom-in --quiet Then "Zoom level: *" should not be logged Scenario: Zooming out with --quiet When I run :zoom-out --quiet Then "Zoom level: *" should not be logged Scenario: Zooming with --quiet When I run :zoom --quiet Then "Zoom level: *" should not be logged ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6026382 qutebrowser-3.6.1/tests/end2end/fixtures/0000755000175100017510000000000015102145351020100 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/notificationserver.py0000644000175100017510000002125715102145205024374 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import dataclasses import itertools from qutebrowser.qt.core import QObject, QByteArray, QUrl, pyqtSlot from qutebrowser.qt.gui import QImage from qutebrowser.qt.dbus import QDBusConnection, QDBusMessage import pytest from qutebrowser.browser.webengine import notification from qutebrowser.utils import utils from tests.helpers import testutils @dataclasses.dataclass class NotificationProperties: title: str body: str replaces_id: int img_width: int img_height: int closed_via_web: bool = False class TestNotificationServer(QObject): """A libnotify notification server used for testing.""" def __init__(self, service: str): """Constructs a new server. This is safe even if there is no DBus daemon; we don't check whether the connection is successful until register(). """ # Note that external users should call get() instead. super().__init__() self._service = service # Trying to connect to the bus doesn't fail if there's no bus. self._bus = QDBusConnection.sessionBus() self._message_id_gen = itertools.count(1) # A dict mapping notification IDs to currently-displayed notifications. self.messages: dict[int, NotificationProperties] = {} self.supports_body_markup = True self.last_id = None def cleanup(self) -> None: self.messages = {} def last_msg(self) -> NotificationProperties: return self.messages[self.last_id] def register(self) -> bool: """Try to register to DBus. If no bus is available, returns False. If a bus is available but registering fails, raises an AssertionError. If registering succeeded, returns True. """ if not self._bus.isConnected(): return False assert self._bus.registerService(self._service) assert self._bus.registerObject( notification.DBusNotificationAdapter.PATH, notification.DBusNotificationAdapter.INTERFACE, self, QDBusConnection.RegisterOption.ExportAllSlots, ) return True def unregister(self) -> None: self._bus.unregisterObject(notification.DBusNotificationAdapter.PATH) assert self._bus.unregisterService(self._service) def _parse_notify_args(self, appname, replaces_id, icon, title, body, actions, hints, timeout) -> NotificationProperties: """Parse a Notify dbus reply. Checks all constant values and returns a NotificationProperties object for values being checked inside test cases. """ assert appname == "qutebrowser" assert icon == '' # using icon data assert actions == ['default', 'Activate'] assert timeout == -1 assert hints.keys() == { "x-qutebrowser-origin", "x-kde-origin-name", "desktop-entry", "image-data", } for key in 'x-qutebrowser-origin', 'x-kde-origin-name': value = hints[key] url = QUrl(value) assert url.isValid(), value assert url.scheme() == 'http', value assert url.host() == 'localhost', value assert hints['desktop-entry'] == 'org.qutebrowser.qutebrowser' img = self._parse_image(*hints["image-data"]) if replaces_id != 0: assert replaces_id in self.messages return NotificationProperties(title=title, body=body, replaces_id=replaces_id, img_width=img.width(), img_height=img.height()) def _parse_image( self, width: int, height: int, bytes_per_line: int, has_alpha: bool, bits_per_color: int, channel_count: int, data: QByteArray, ) -> QImage: """Make sure the given image data is valid and return a QImage.""" # Chromium limit? assert 0 < width <= 320 assert 0 < height <= 320 # Based on dunst: # https://github.com/dunst-project/dunst/blob/v1.6.1/src/icon.c#L336-L348 # (A+7)/8 rounds up A to the next byte boundary pixelstride = (channel_count * bits_per_color + 7) // 8 expected_len = (height - 1) * bytes_per_line + width * pixelstride assert len(data) == expected_len assert bits_per_color == 8 assert channel_count == (4 if has_alpha else 3) assert bytes_per_line >= width * channel_count qimage_format = QImage.Format.Format_RGBA8888 if has_alpha else QImage.Format.Format_RGB888 img = QImage(data, width, height, bytes_per_line, qimage_format) assert not img.isNull() assert img.width() == width assert img.height() == height return img def close(self, notification_id: int) -> None: """Sends a close notification for the given ID.""" message = QDBusMessage.createSignal( notification.DBusNotificationAdapter.PATH, notification.DBusNotificationAdapter.INTERFACE, "NotificationClosed") # The 2 here is the notification removal reason ("dismissed by the user") # it's effectively arbitrary as we don't use that information message.setArguments([ notification._as_uint32(notification_id), notification._as_uint32(2), ]) if not self._bus.send(message): raise OSError("Could not send close notification") def click(self, notification_id: int) -> None: """Sends a click (default action) notification for the given ID.""" self.action(notification_id, "default") def action(self, notification_id: int, name: str) -> None: """Sends an action notification for the given ID.""" message = QDBusMessage.createSignal( notification.DBusNotificationAdapter.PATH, notification.DBusNotificationAdapter.INTERFACE, "ActionInvoked") message.setArguments([notification._as_uint32(notification_id), name]) if not self._bus.send(message): raise OSError("Could not send action notification") # Everything below is exposed via DBus # pylint: disable=invalid-name @pyqtSlot(QDBusMessage, result="uint") def Notify(self, dbus_message: QDBusMessage) -> int: assert dbus_message.signature() == 'susssasa{sv}i' assert dbus_message.type() == QDBusMessage.MessageType.MethodCallMessage message = self._parse_notify_args(*dbus_message.arguments()) if message.replaces_id == 0: message_id = next(self._message_id_gen) else: message_id = message.replaces_id self.messages[message_id] = message self.last_id = message_id return message_id @pyqtSlot(QDBusMessage, result="QStringList") def GetCapabilities(self, message: QDBusMessage) -> list[str]: assert not message.signature() assert not message.arguments() assert message.type() == QDBusMessage.MessageType.MethodCallMessage capabilities = ["actions", "x-kde-origin-name"] if self.supports_body_markup: capabilities.append("body-markup") return capabilities @pyqtSlot(QDBusMessage) def GetServerInformation(self, message: QDBusMessage) -> None: name = "test notification server" vendor = "qutebrowser" version = "v0.0.1" spec_version = "1.2" self._bus.send(message.createReply([name, vendor, version, spec_version])) @pyqtSlot(QDBusMessage) def CloseNotification(self, dbus_message: QDBusMessage) -> None: assert dbus_message.signature() == 'u' assert dbus_message.type() == QDBusMessage.MessageType.MethodCallMessage message_id = dbus_message.arguments()[0] self.messages[message_id].closed_via_web = True @pytest.fixture(scope='module') def notification_server(qapp, quteproc_process): if utils.is_windows: # The QDBusConnection destructor seems to cause error messages (and potentially # segfaults) on Windows, so we bail out early in that case. We still try to get # a connection on macOS, since it's theoretically possible to run DBus there. pytest.skip("Skipping DBus on Windows") qb_pid = quteproc_process.proc.processId() server = TestNotificationServer( f"{notification.DBusNotificationAdapter.TEST_SERVICE}{qb_pid}") registered = server.register() if not registered: assert not (utils.is_linux and testutils.ON_CI), "Expected DBus on Linux CI" pytest.skip("No DBus server available") yield server server.unregister() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/quteprocess.py0000644000175100017510000012401315102145205023026 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Fixtures to run qutebrowser in a QProcess and communicate.""" import pathlib import os import re import sys import time import datetime import logging import tempfile import contextlib import itertools import collections import json import yaml import pytest from PIL.ImageGrab import grab from qutebrowser.qt.core import pyqtSignal, QUrl, QPoint from qutebrowser.qt.gui import QImage, QColor from qutebrowser.misc import ipc from qutebrowser.utils import log, utils, javascript from helpers import testutils from end2end.fixtures import testprocess instance_counter = itertools.count() def is_ignored_qt_message(pytestconfig, message): """Check if the message is listed in qt_log_ignore.""" regexes = pytestconfig.getini('qt_log_ignore') return any(re.search(regex, message) for regex in regexes) def is_ignored_lowlevel_message(message): """Check if we want to ignore a lowlevel process output.""" ignored_messages = [ # Qt 6.2 / 6.3 'Fontconfig error: Cannot load default config file: No such file: (null)', 'Fontconfig error: Cannot load default config file', # Qt 6.4, from certificate error below, but on separate lines '----- Certificate i=0 (*,CN=localhost,O=qutebrowser test certificate) -----', 'ERROR: No matching issuer found', # Qt 6.5 debug, overflow/linebreak from a JS message... # IGNORED: [678403:678403:0315/203342.008878:INFO:CONSOLE(65)] "Refused # to apply inline style because it violates the following Content # Security Policy directive: "default-src none". Either the # 'unsafe-inline' keyword, a hash # ('sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='), or a nonce # ('nonce-...') is required to enable inline execution. Note also that # 'style-src' was not explicitly set, so 'default-src' is used as a # fallback. # INVALID: ", source: userscript:_qute_stylesheet (65) '", source: userscript:_qute_stylesheet (*)', # Randomly started showing up on Qt 5.15.2 'QPaintDevice: Cannot destroy paint device that is being painted', # Qt 6.6 on GitHub Actions ( 'libva error: vaGetDriverNameByIndex() failed with unknown libva error, ' 'driver_name = (null)' ), 'libva error: vaGetDriverNames() failed with unknown libva error', # Mesa 23.3 # See https://gitlab.freedesktop.org/mesa/mesa/-/issues/10293 'MESA: error: ZINK: vkCreateInstance failed (VK_ERROR_INCOMPATIBLE_DRIVER)', 'glx: failed to create drisw screen', 'failed to load driver: zink', 'DRI3 not available', # Webkit on arch with a newer mesa 'MESA: error: ZINK: failed to load libvulkan.so.1', # GitHub Actions with Archlinux unstable packages 'libEGL warning: DRI3: Screen seems not DRI3 capable', 'libEGL warning: egl: failed to create dri2 screen', 'libEGL warning: DRI3 error: Could not get DRI3 device', 'libEGL warning: Activate DRI3 at Xorg or build mesa with DRI2', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) def is_ignored_chromium_message(line): msg_re = re.compile(r""" \[ (\d+:\d+:)? # Process/Thread ID \d{4}/[\d.]+: # MMDD/Time (?P[A-Z]+): # Log level [^ :]+ # filename / line \] \ (?P.*) # message """, re.VERBOSE) match = msg_re.fullmatch(line) if match is None: return False if match.group('loglevel') == 'INFO': return True message = match.group('message') ignored_messages = [ # GitHub Actions with Qt 5.15.2 'SharedImageManager::ProduceGLTexture: Trying to produce a representation from a non-existent mailbox. *', ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : ' 'DoCreateAndTexStorage2DSharedImageINTERNAL: invalid mailbox name'), ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : ' 'DoBeginSharedImageAccessCHROMIUM: bound texture is not a shared image'), ('[.DisplayCompositor]RENDER WARNING: texture bound to texture unit 0 is ' 'not renderable. It might be non-power-of-2 or have incompatible texture ' 'filtering (maybe)?'), ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : ' 'DoEndSharedImageAccessCHROMIUM: bound texture is not a shared image'), # [916:934:1213/080738.912432:ERROR:address_tracker_linux.cc(214)] Could not bind NETLINK socket: Address already in use (98) 'Could not bind NETLINK socket: Address already in use (98)', # Flatpak with data/crashers/webrtc.html (Qt 6.2) # [9044:9113:0512/012126.284773:ERROR:mdns_responder.cc(868)] mDNS responder manager failed to start. # [9044:9113:0512/012126.284818:ERROR:mdns_responder.cc(885)] The mDNS responder manager is not started yet. 'mDNS responder manager failed to start.', 'The mDNS responder manager is not started yet.', # Qt 6.2: # [503633:503650:0509/185222.442798:ERROR:ssl_client_socket_impl.cc(959)] handshake failed; returned -1, SSL error code 1, net_error -202 'handshake failed; returned -1, SSL error code 1, net_error -202', # Qt 6.8 + Python 3.14 'handshake failed; returned -1, SSL error code 1, net_error -101', # Qt 6.2: # [2432160:7:0429/195800.168435:ERROR:command_buffer_proxy_impl.cc(140)] ContextResult::kTransientFailure: Failed to send GpuChannelMsg_CreateCommandBuffer. # Qt 6.3: # [2435381:7:0429/200014.168057:ERROR:command_buffer_proxy_impl.cc(125)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. 'ContextResult::kTransientFailure: Failed to send *CreateCommandBuffer.', # Qt 6.3: # [4919:8:0530/170658.033287:ERROR:command_buffer_proxy_impl.cc(328)] GPU state invalid after WaitForGetOffsetInRange. 'GPU state invalid after WaitForGetOffsetInRange.', # [5469:5503:0621/183219.878182:ERROR:backend_impl.cc(1414)] Unable to map Index file 'Unable to map Index file', # Qt 6.4: # [2456284:2456339:0715/110322.570154:ERROR:cert_verify_proc_builtin.cc(681)] CertVerifyProcBuiltin for localhost failed: # ----- Certificate i=0 (1.2.840.113549.1.9.1=#6D61696C407175746562726F777365722E6F7267,CN=localhost,O=qutebrowser test certificate) ----- # ERROR: No matching issuer found # (Note, subsequent lines above in is_ignored_lowlevel_message) 'CertVerifyProcBuiltin for localhost failed:', # [320667:320667:1124/135621.718232:ERROR:interface_endpoint_client.cc(687)] Message 4 rejected by interface blink.mojom.WidgetHost 'Message 4 rejected by interface blink.mojom.WidgetHost', # Qt 5.15.1 debug build (Chromium 83) # '[314297:7:0929/214605.491958:ERROR:context_provider_command_buffer.cc(145)] # GpuChannelHost failed to create command buffer.' # Still present on Qt 6.5 'GpuChannelHost failed to create command buffer.', # Qt 6.5 debug build # [640812:640865:0315/200415.708148:WARNING:important_file_writer.cc(185)] # Failed to create temporary file to update # /tmp/qutebrowser-basedir-3kvto2eq/data/webengine/user_prefs.json: No # such file or directory (2) # [640812:640864:0315/200415.715398:WARNING:important_file_writer.cc(185)] # Failed to create temporary file to update # /tmp/qutebrowser-basedir-3kvto2eq/data/webengine/Network Persistent # State: No such file or directory (2) 'Failed to create temporary file to update *user_prefs.json: No such file or directory (2)', 'Failed to create temporary file to update *Network Persistent State: No such file or directory (2)', # Qt 6.5 debug build # [645145:645198:0315/200704.324733:WARNING:simple_synchronous_entry.cc(1438)] "Could not open platform files for entry.", # Qt 6.5 debug build # [664320:664320:0315/202235.943899:ERROR:node_channel.cc(861)] # Dropping message on closed channel. "Dropping message on closed channel.", # Qt 6.5 debug build # tests/end2end/features/test_misc_bdd.py::test_webrtc_renderer_process_crash # [679056:14:0315/203418.631075:WARNING:media_session.cc(949)] RED # codec red is missing an associated payload type. "RED codec red is missing an associated payload type.", # Qt 6.5 debug build # tests/end2end/features/test_javascript_bdd.py::test_error_pages_without_js_enabled # and others using internal URL schemes? # [677785:1:0315/203308.328104:ERROR:html_media_element.cc(4874)] # SetError: {code=4, message="MEDIA_ELEMENT_ERROR: Media load rejected # by URL safety check"} 'SetError: {code=4, message="MEDIA_ELEMENT_ERROR: Media load rejected by URL safety check"}', # Qt 6.5 debug build # [714871:715010:0315/205751.155681:ERROR:surface_manager.cc(419)] # Old/orphaned temporary reference to SurfaceId(FrameSinkId[](14, 3), # LocalSurfaceId(3, 1, 5D04...)) "Old/orphaned temporary reference to SurfaceId(FrameSinkId[](*, *), LocalSurfaceId(*, *, *...))", # Qt 6.5 debug build # [758352:758352:0315/212511.747791:WARNING:render_widget_host_impl.cc(280)] # Input request on unbound interface "Input request on unbound interface", # Qt 6.5 debug build # [1408271:1408418:0317/201633.360945:ERROR:http_cache_transaction.cc(3622)] # ReadData failed: 0 "ReadData failed: 0", # Qt 6.{4,5}, possibly relates to a lifecycle mismatch between Qt and # Chromium but no issues have been concretely linked to it yet. # [5464:5464:0318/024215.821650:ERROR:interface_endpoint_client.cc(687)] Message 6 rejected by interface blink.mojom.WidgetHost # [5718:5718:0318/031330.803863:ERROR:interface_endpoint_client.cc(687)] Message 3 rejected by interface blink.mojom.Widget "Message * rejected by interface blink.mojom.Widget*", # GitHub Actions, Qt 6.6 # [9895:9983:0904/043039.500565:ERROR:gpu_memory_buffer_support_x11.cc(49)] # dri3 extension not supported. "dri3 extension not supported.", # Qt 6.7 debug build # [44513:44717:0325/173456.146759:WARNING:render_message_filter.cc(144)] # Could not find tid "Could not find tid", # [127693:127748:0325/230155.835421:WARNING:discardable_shared_memory_manager.cc(438)] # Some MojoDiscardableSharedMemoryManagerImpls are still alive. They # will be leaked. "Some MojoDiscardableSharedMemoryManagerImpls are still alive. They will be leaked.", # Qt 6.7 on GitHub Actions # [3456:5752:1111/103609.929:ERROR:block_files.cc(443)] Failed to open # C:\Users\RUNNER~1\AppData\Local\Temp\qutebrowser-basedir-ruvn1lys\data\webengine\DawnCache\data_0 "Failed to open *webengine*Dawn*Cache*data_*", # Qt 6.8 on GitHub Actions # [7072:3412:1209/220659.527:ERROR:simple_index_file.cc(322)] Failed to # write the temporary index file "Failed to write the temporary index file", # Qt 6.9 Beta 3 on GitHub Actions # [978:1041:0311/070551.759339:ERROR:bus.cc(407)] "Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory", # Qt 6.9 on GitHub Actions with Windows Server 2025 # [4348:7828:0605/123815.402:ERROR:shared_image_manager.cc(356)] "SharedImageManager::ProduceMemory: Trying to Produce a Memory representation from a non-existent mailbox.", # Qt 6.10 debug build # "[453900:453973:0909/000324.265214:WARNING:viz_main_impl.cc(85)]" "VizNullHypothesis is disabled (not a warning)", # Qt 6.10 on Windows + GitHub Actions # [1784:7100:1022/150433.690:ERROR:direct_composition_support.cc(225)] "GetGpuDriverOverlayInfo: Failed to retrieve video device", # [1784:7100:1022/150434.202:ERROR:direct_composition_support.cc(1122)] "QueryInterface to IDCompositionDevice4 failed: No such interface supported (0x80004002)", ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) class LogLine(testprocess.Line): """A parsed line from the qutebrowser log output. Attributes: timestamp/loglevel/category/module/function/line/message/levelname: Parsed from the log output. expected: Whether the message was expected or not. """ def __init__(self, pytestconfig, data): super().__init__(data) try: line = json.loads(data) except ValueError: raise testprocess.InvalidLine(data) if not isinstance(line, dict): raise testprocess.InvalidLine(data) self.timestamp = datetime.datetime.fromtimestamp(line['created']) self.msecs = line['msecs'] self.loglevel = line['levelno'] self.levelname = line['levelname'] self.category = line['name'] self.module = line['module'] self.function = line['funcName'] self.line = line['lineno'] if self.function is None and self.line == 0: self.line = None self.traceback = line.get('traceback') self.message = line['message'] self.expected = is_ignored_qt_message(pytestconfig, self.message) self.use_color = False def __str__(self): return self.formatted_str(colorized=self.use_color) def formatted_str(self, colorized=True): """Return a formatted colorized line. This returns a line like qute without --json-logging would produce. Args: colorized: If True, ANSI color codes will be embedded. """ r = logging.LogRecord(self.category, self.loglevel, '', self.line, self.message, (), None) # Patch some attributes of the LogRecord if self.line is None: r.line = 0 r.created = self.timestamp.timestamp() r.msecs = self.msecs r.module = self.module r.funcName = self.function format_str = log.EXTENDED_FMT format_str = format_str.replace('{asctime:8}', '{asctime:8}.{msecs:03.0f}') # Mark expected errors with (expected) so it's less confusing for tests # which expect errors but fail due to other errors. if self.expected and self.loglevel > logging.INFO: new_color = '{' + log.LOG_COLORS['DEBUG'] + '}' format_str = format_str.replace('{log_color}', new_color) format_str = re.sub(r'{levelname:(\d*)}', # Leave away the padding because (expected) is # longer anyway. r'{levelname} (expected)', format_str) formatter = log.ColoredFormatter(format_str, log.DATEFMT, '{', use_colors=colorized) result = formatter.format(r) # Manually append the stringified traceback if one is present if self.traceback is not None: result += '\n' + self.traceback return result class QuteProc(testprocess.Process): """A running qutebrowser process used for tests. Attributes: _ipc_socket: The IPC socket of the started instance. _webengine: Whether to use QtWebEngine basedir: The base directory for this instance. request: The request object for the current test. _instance_id: A unique ID for this QuteProc instance _run_counter: A counter to get a unique ID for each run. Signals: got_error: Emitted when there was an error log line. """ got_error = pyqtSignal() KEYS = ['timestamp', 'loglevel', 'category', 'module', 'function', 'line', 'message'] def __init__(self, request, *, parent=None): super().__init__(request, parent) self._ipc_socket = None self.basedir = None self._instance_id = next(instance_counter) self._run_counter = itertools.count() self._screenshot_counters = collections.defaultdict(itertools.count) def _process_line(self, log_line): """Check if the line matches any initial lines we're interested in.""" start_okay_message = ( "load status for : LoadStatus.success") if (log_line.category == 'ipc' and log_line.message.startswith("Listening as ")): self._ipc_socket = log_line.message.split(' ', maxsplit=2)[2] elif (log_line.category == 'webview' and testutils.pattern_match(pattern=start_okay_message, value=log_line.message)): log_line.waited_for = True self.ready.emit() elif (log_line.category == 'init' and log_line.module == 'standarddir' and log_line.function == 'init' and log_line.message.startswith('Base directory:')): self.basedir = log_line.message.split(':', maxsplit=1)[1].strip() elif self._is_error_logline(log_line): self.got_error.emit() def _parse_line(self, line): try: log_line = LogLine(self.request.config, line) except testprocess.InvalidLine: if not line.strip(): return None elif (is_ignored_qt_message(self.request.config, line) or is_ignored_lowlevel_message(line) or is_ignored_chromium_message(line) or list(self.request.node.iter_markers('no_invalid_lines'))): self._log("IGNORED: {}".format(line)) return None else: raise log_line.use_color = self.request.config.getoption('--color') != 'no' verbose = self.request.config.getoption('--verbose') if log_line.loglevel > logging.VDEBUG or verbose: self._log(log_line) self._process_line(log_line) return log_line def _executable_args(self): profile = self.request.config.getoption('--qute-profile-subprocs') strace = self.request.config.getoption('--qute-strace-subprocs') if hasattr(sys, 'frozen'): if profile or strace: raise RuntimeError("Can't profile/strace with sys.frozen!") executable = str(pathlib.Path(sys.executable).parent / 'qutebrowser') args = [] else: if strace: executable = 'strace' args = [ "-o", "qb-strace", "--output-separately", # create .PID files "--write=2", # dump full stderr data (qb JSON logs) sys.executable, ] else: executable = sys.executable args = [] if profile: profile_dir = pathlib.Path.cwd() / 'prof' profile_id = '{}_{}'.format(self._instance_id, next(self._run_counter)) profile_file = profile_dir / '{}.pstats'.format(profile_id) profile_dir.mkdir(exist_ok=True) args += [str(pathlib.Path('scripts') / 'dev' / 'run_profile.py'), '--profile-tool', 'none', '--profile-file', str(profile_file)] else: args += ['-bb', '-m', 'qutebrowser'] return executable, args def _default_args(self): backend = 'webengine' if self.request.config.webengine else 'webkit' args = ['--debug', '--no-err-windows', '--temp-basedir', '--json-logging', '--loglevel', 'vdebug', '--backend', backend, '--debug-flag', 'no-sql-history', '--debug-flag', 'werror', '--debug-flag', 'test-notification-service', '--debug-flag', 'caret', '--qt-flag', 'disable-features=PaintHoldingCrossOrigin', '--qt-arg', 'geometry', '800x600+0+0'] if self.request.config.webengine: if testutils.disable_seccomp_bpf_sandbox(): args += testutils.DISABLE_SECCOMP_BPF_ARGS if testutils.use_software_rendering(): args += testutils.SOFTWARE_RENDERING_ARGS args.append('about:blank') return args def path_to_url(self, path, *, port=None, https=False): """Get a URL based on a filename for the localhost webserver. URLs like about:... and qute:... are handled specially and returned verbatim. """ special_schemes = ['about:', 'qute:', 'chrome:', 'view-source:', 'data:', 'http:', 'https:', 'file:'] server = self.request.getfixturevalue('server') server_port = server.port if port is None else port if any(path.startswith(scheme) for scheme in special_schemes): path = path.replace('(port)', str(server_port)) return path else: return '{}://localhost:{}/{}'.format( 'https' if https else 'http', server_port, path if path != '/' else '') def wait_for_js(self, message): """Wait for the given javascript console message. Return: The LogLine. """ line = self.wait_for(category='js', message='[*] {}'.format(message)) line.expected = True return line def wait_scroll_pos_changed(self, x=None, y=None): """Wait until a "Scroll position changed" message was found. With QtWebEngine, on older Qt versions which lack QWebEnginePage.scrollPositionChanged, this also skips the test. """ __tracebackhide__ = (lambda e: e.errisinstance(testprocess.WaitForTimeout)) if (x is None and y is not None) or (y is None and x is not None): raise ValueError("Either both x/y or neither must be given!") if x is None and y is None: point = 'Py*.QtCore.QPoint(*, *)' # not counting 0/0 here elif x == '0' and y == '0': point = 'Py*.QtCore.QPoint()' else: point = 'Py*.QtCore.QPoint({}, {})'.format(x, y) self.wait_for(category='webview', message='Scroll position changed to ' + point) def wait_for(self, timeout=None, **kwargs): """Extend wait_for to add divisor if a test is xfailing.""" __tracebackhide__ = (lambda e: e.errisinstance(testprocess.WaitForTimeout)) xfail = self.request.node.get_closest_marker('xfail') if xfail and (not xfail.args or xfail.args[0]): kwargs['divisor'] = 10 else: kwargs['divisor'] = 1 return super().wait_for(timeout=timeout, **kwargs) def _is_error_logline(self, msg): """Check if the given LogLine is some kind of error message.""" is_js_error = (msg.category == 'js' and testutils.pattern_match(pattern='[*] [FAIL] *', value=msg.message)) # Try to complain about the most common mistake when accidentally # loading external resources. is_ddg_load = testutils.pattern_match( pattern="load status for <* tab_id=* url='*duckduckgo*'>: *", value=msg.message) is_log_error = (msg.loglevel > logging.INFO and not msg.message.startswith("Ignoring world ID") and not msg.message.startswith( "Could not initialize QtNetwork SSL support.")) return is_log_error or is_js_error or is_ddg_load def _maybe_skip(self): """Skip the test if [SKIP] lines were logged.""" skip_texts = [] for msg in self._data: if (msg.category == 'js' and testutils.pattern_match(pattern='[*] [SKIP] *', value=msg.message)): skip_texts.append(msg.message.partition(' [SKIP] ')[2]) if skip_texts: pytest.skip(', '.join(skip_texts)) def before_test(self): """Clear settings before every test.""" super().before_test() self.send_cmd(':clear-messages') self.send_cmd(':config-clear') self._init_settings() self.clear_data() def _init_settings(self): """Adjust some qutebrowser settings after starting.""" settings = [ ('messages.timeout', '0'), ('auto_save.interval', '0'), ('new_instance_open_target_window', 'last-opened') ] for opt, value in settings: self.set_setting(opt, value) def after_test(self): """Handle unexpected/skip logging and clean up after each test.""" __tracebackhide__ = lambda e: e.errisinstance(pytest.fail.Exception) bad_msgs = [msg for msg in self._data if self._is_error_logline(msg) and not msg.expected] try: call = self.request.node.rep_call except AttributeError: pass else: if call.failed: self._take_x11_screenshot_of_failed_test() if call.failed or hasattr(call, 'wasxfail') or call.skipped: super().after_test() return try: if bad_msgs: text = 'Logged unexpected errors:\n\n' + '\n'.join( str(e) for e in bad_msgs) pytest.fail(text, pytrace=False) else: self._maybe_skip() finally: super().after_test() def _wait_for_ipc(self): """Wait for an IPC message to arrive.""" self.wait_for(category='ipc', module='ipc', function='on_ready_read', message='Read from socket *') @contextlib.contextmanager def disable_capturing(self): capmanager = self.request.config.pluginmanager.getplugin("capturemanager") with capmanager.global_and_fixture_disabled(): yield def _after_start(self): """Wait before continuing if requested, e.g. for debugger attachment.""" delay = self.request.config.getoption('--qute-delay-start') if delay: with self.disable_capturing(): print(f"- waiting {delay}ms for quteprocess " f"(PID: {self.proc.processId()})...") time.sleep(delay / 1000) def send_ipc(self, commands, target_arg=''): """Send a raw command to the running IPC socket.""" delay = self.request.config.getoption('--qute-delay') time.sleep(delay / 1000) assert self._ipc_socket is not None ipc.send_to_running_instance(self._ipc_socket, commands, target_arg) try: self._wait_for_ipc() except testprocess.WaitForTimeout: # Sometimes IPC messages seem to get lost on Windows CI? # Retry a second time as this shouldn't make tests fail. ipc.send_to_running_instance(self._ipc_socket, commands, target_arg) self._wait_for_ipc() def start(self, *args, **kwargs): try: super().start(*args, **kwargs) except testprocess.ProcessExited: is_dl_inconsistency = str(self.captured_log[-1]).endswith( "_dl_allocate_tls_init: Assertion " "`listp->slotinfo[cnt].gen <= GL(dl_tls_generation)' failed!") if testutils.ON_CI and is_dl_inconsistency: # WORKAROUND for https://sourceware.org/bugzilla/show_bug.cgi?id=19329 self.captured_log = [] self._log("NOTE: Restarted after libc DL inconsistency!") self.clear_data() super().start(*args, **kwargs) else: raise def send_cmd(self, command, count=None, invalid=False, *, escape=True): """Send a command to the running qutebrowser instance. Args: count: The count to pass to the command. invalid: If True, we don't wait for "command called: ..." in the log and return None. escape: Escape backslashes in the command Return: The parsed log line with "command called: ..." or None. """ __tracebackhide__ = lambda e: e.errisinstance(testprocess.WaitForTimeout) summary = command if count is not None: summary += ' (count {})'.format(count) self.log_summary(summary) if escape: command = command.replace('\\', r'\\') if count is not None: command = ':cmd-run-with-count {} {}'.format(count, command.lstrip(':')) self.send_ipc([command]) if invalid: return None else: return self.wait_for(category='commands', module='command', function='run', message='command called: *') def get_setting(self, opt, pattern=None): """Get the value of a qutebrowser setting.""" if pattern is None: cmd = ':set {}?'.format(opt) else: cmd = ':set -u {} {}?'.format(pattern, opt) self.send_cmd(cmd) msg = self.wait_for(loglevel=logging.INFO, category='message', message='{} = *'.format(opt)) if pattern is None: return msg.message.split(' = ')[1] else: return msg.message.split(' = ')[1].split(' for ')[0] def set_setting(self, option, value): # \ and " in a value should be treated literally, so escape them value = value.replace('\\', r'\\') value = value.replace('"', '\\"') self.send_cmd(':set -t "{}" "{}"'.format(option, value), escape=False) self.wait_for(category='config', message='Config option changed: *') @contextlib.contextmanager def temp_setting(self, opt, value): """Context manager to set a setting and reset it on exit.""" old_value = self.get_setting(opt) self.set_setting(opt, value) yield self.set_setting(opt, old_value) def open_path(self, path, *, new_tab=False, new_bg_tab=False, new_window=False, private=False, as_url=False, port=None, https=False, wait=True): """Open the given path on the local webserver in qutebrowser.""" url = self.path_to_url(path, port=port, https=https) self.open_url(url, new_tab=new_tab, new_bg_tab=new_bg_tab, new_window=new_window, private=private, as_url=as_url, wait=wait) def open_url(self, url, *, new_tab=False, new_bg_tab=False, new_window=False, private=False, as_url=False, wait=True): """Open the given url in qutebrowser.""" if sum(1 for opt in [new_tab, new_bg_tab, new_window, private, as_url] if opt) > 1: raise ValueError("Conflicting options given!") if as_url: self.send_cmd(url, invalid=True) line = None elif new_tab: line = self.send_cmd(':open -t ' + url) elif new_bg_tab: line = self.send_cmd(':open -b ' + url) elif new_window: line = self.send_cmd(':open -w ' + url) elif private: line = self.send_cmd(':open -p ' + url) else: line = self.send_cmd(':open ' + url) if wait: self.wait_for_load_finished_url(url, after=line) def mark_expected(self, category=None, loglevel=None, message=None): """Mark a given logging message as expected.""" line = self.wait_for(category=category, loglevel=loglevel, message=message) line.expected = True def wait_for_load_finished_url(self, url, *, timeout=None, load_status='success', after=None): """Wait until a URL has finished loading.""" __tracebackhide__ = (lambda e: e.errisinstance( testprocess.WaitForTimeout)) if timeout is None: if testutils.ON_CI: timeout = 15000 else: timeout = 5000 qurl = QUrl(url) if not qurl.isValid(): raise ValueError("Invalid URL {}: {}".format(url, qurl.errorString())) # We really need the same representation that the webview uses in # its __repr__ url = utils.elide(qurl.toDisplayString(QUrl.ComponentFormattingOption.EncodeUnicode), 100) assert url pattern = re.compile( r"(load status for : LoadStatus\.{load_status}|fetch: " r"Py.*\.QtCore\.QUrl\('{url}'\) -> .*)".format( load_status=re.escape(load_status), url=re.escape(url))) try: self.wait_for(message=pattern, timeout=timeout, after=after) except testprocess.WaitForTimeout: raise testprocess.WaitForTimeout("Timed out while waiting for {} " "to be loaded".format(url)) def wait_for_load_finished(self, path, *, port=None, https=False, timeout=None, load_status='success'): """Wait until a path has finished loading.""" __tracebackhide__ = (lambda e: e.errisinstance( testprocess.WaitForTimeout)) url = self.path_to_url(path, port=port, https=https) self.wait_for_load_finished_url(url, timeout=timeout, load_status=load_status) def get_session(self, flags="--with-private"): """Save the session and get the parsed session data.""" with tempfile.TemporaryDirectory() as tdir: session = pathlib.Path(tdir) / 'session.yml' self.send_cmd(f':session-save {flags} "{session}"') self.wait_for(category='message', loglevel=logging.INFO, message=f'Saved session {session}.') data = session.read_text(encoding='utf-8') self._log('\nCurrent session data:\n' + data) return utils.yaml_load(data) def get_content(self, plain=True): """Get the contents of the current page.""" with tempfile.TemporaryDirectory() as tdir: path = pathlib.Path(tdir) / 'page' if plain: self.send_cmd(':debug-dump-page --plain "{}"'.format(path)) else: self.send_cmd(':debug-dump-page "{}"'.format(path)) self.wait_for(category='message', loglevel=logging.INFO, message='Dumped page to {}.'.format(path)) return path.read_text(encoding='utf-8') def get_screenshot( self, *, probe_pos: QPoint = None, probe_color: QColor = testutils.Color(0, 0, 0), ) -> QImage: """Get a screenshot of the current page. Arguments: probe_pos: If given, only continue if the pixel at the given position isn't black (or whatever is specified by probe_color). """ for _ in range(5): tmp_path = self.request.getfixturevalue('tmp_path') counter = self._screenshot_counters[self.request.node.nodeid] path = tmp_path / f'screenshot-{next(counter)}.png' self.send_cmd(f':screenshot {path}') screenshot_msg = f'Screenshot saved to {path}' self.wait_for(message=screenshot_msg) print(screenshot_msg) img = QImage(str(path)) assert not img.isNull() if probe_pos is None: return img probed_color = testutils.Color(img.pixelColor(probe_pos)) if probed_color == probe_color: return img # Rendering might not be completed yet... time.sleep(0.5) # Using assert again for pytest introspection assert probed_color == probe_color, "Color probing failed, values on last try:" raise utils.Unreachable() def press_keys(self, keys): """Press the given keys using :fake-key.""" self.send_cmd(':fake-key -g "{}"'.format(keys)) def click_element_by_text(self, text): """Click the element with the given text.""" # Use Javascript and XPath to find the right element, use console.log # to return an error (no element found, ambiguous element) script = ( 'var _es = document.evaluate(\'//*[text()={text}]\', document, ' 'null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);' 'if (_es.snapshotLength == 0) {{ console.log("qute:no elems"); }} ' 'else if (_es.snapshotLength > 1) {{ console.log("qute:ambiguous ' 'elems") }} ' 'else {{ console.log("qute:okay"); _es.snapshotItem(0).click() }}' ).format(text=javascript.string_escape(_xpath_escape(text))) self.send_cmd(':jseval ' + script, escape=False) message = self.wait_for_js('qute:*').message if message.endswith('qute:no elems'): raise ValueError('No element with {!r} found'.format(text)) if message.endswith('qute:ambiguous elems'): raise ValueError('Element with {!r} is not unique'.format(text)) if not message.endswith('qute:okay'): raise ValueError('Invalid response from qutebrowser: {}' .format(message)) def compare_session(self, expected, *, flags="--with-private"): """Compare the current sessions against the given template. partial_compare is used, which means only the keys/values listed will be compared. """ __tracebackhide__ = lambda e: e.errisinstance(pytest.fail.Exception) data = self.get_session(flags=flags) expected = yaml.load(expected, Loader=YamlLoader) outcome = testutils.partial_compare(data, expected) if not outcome: msg = "Session comparison failed: {}".format(outcome.error) msg += '\nsee stdout for details' pytest.fail(msg) def turn_on_scroll_logging(self, no_scroll_filtering=False): """Make sure all scrolling changes are logged.""" cmd = ":debug-pyeval -q objects.debug_flags.add('{}')" if no_scroll_filtering: self.send_cmd(cmd.format('no-scroll-filtering')) self.send_cmd(cmd.format('log-scroll-pos')) def _take_x11_screenshot_of_failed_test(self): fixture = self.request.getfixturevalue('take_x11_screenshot') fixture() class YamlLoader(yaml.SafeLoader): """Custom YAML loader used in compare_session.""" # Translate ... to ellipsis in YAML. YamlLoader.add_constructor('!ellipsis', lambda loader, node: ...) YamlLoader.add_implicit_resolver('!ellipsis', re.compile(r'\.\.\.'), None) def _xpath_escape(text): """Escape a string to be used in an XPath expression. The resulting string should still be escaped with javascript.string_escape, to prevent javascript from interpreting the quotes. This function is needed because XPath does not provide any character escaping mechanisms, so to get the string "I'm back", he said you have to use concat like concat('"I', "'m back", '", he said') Args: text: Text to escape Return: The string "escaped" as a concat() call. """ # Shortcut if at most a single quoting style is used if "'" not in text or '"' not in text: return repr(text) parts = re.split('([\'"])', text) # Python's repr() of strings will automatically choose the right quote # type. Since each part only contains one "type" of quote, no escaping # should be necessary. parts = [repr(part) for part in parts if part] return 'concat({})'.format(', '.join(parts)) @pytest.fixture def screenshot_dir(request, tmp_path_factory): """Return the path of a directory to save e2e screenshots in.""" path = tmp_path_factory.getbasetemp() if "PYTEST_XDIST_WORKER" in os.environ: # If we are running under xdist remove the per-worker directory # (like "popen-gw0") so the user doesn't have to search through # multiple folders for the screenshot they are looking for. path = path.parent path /= "pytest-screenshots" path.mkdir(exist_ok=True) return path @pytest.fixture def take_x11_screenshot(request, screenshot_dir, record_property, xvfb): """Take a screenshot of the current pytest-xvfb display. Screenshots are saved to the location of the `screenshot_dir` fixture. """ def doit(): if not xvfb: # Likely we are being run with --no-xvfb return img = grab(xdisplay=f":{xvfb.display}") fpath = screenshot_dir / f"{request.node.name}.png" img.save(fpath) record_property("screenshot", str(fpath)) return doit @pytest.fixture(scope='module') def quteproc_process(qapp, server, request): """Fixture for qutebrowser process which is started once per file.""" # Passing request so it has an initial config proc = QuteProc(request) proc.start() yield proc proc.terminate() @pytest.fixture def quteproc(quteproc_process, server, request, take_x11_screenshot): """Per-test qutebrowser fixture which uses the per-file process.""" request.node._quteproc_log = quteproc_process.captured_log quteproc_process.before_test() quteproc_process.request = request yield quteproc_process quteproc_process.after_test() @pytest.fixture def quteproc_new(qapp, server, request): """Per-test qutebrowser process to test invocations.""" proc = QuteProc(request) request.node._quteproc_log = proc.captured_log # Not calling before_test here as that would start the process yield proc proc.after_test() proc.terminate() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/test_quteprocess.py0000644000175100017510000003130415102145205024065 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test the quteproc fixture used for tests.""" import logging import datetime import json import pytest from end2end.fixtures import quteprocess, testprocess from qutebrowser.utils import log class FakeRepCall: """Fake for request.node.rep_call.""" def __init__(self): self.failed = False self.skipped = False class FakeConfig: """Fake for request.config.""" ARGS = { '--qute-delay': 0, '--color': True, '--verbose': False, '--capture': None, } INI = { 'qt_log_ignore': [], } def __init__(self): self.webengine = False def getoption(self, name): return self.ARGS[name] def getini(self, name): return self.INI[name] class FakeNode: """Fake for request.node.""" def __init__(self, call): self.rep_call = call def get_closest_marker(self, _name): return None class FakeRequest: """Fake for request.""" def __init__(self, node, config, server): self.node = node self.config = config self._server = server def getfixturevalue(self, name): assert name == 'server' return self._server @pytest.fixture def request_mock(quteproc, monkeypatch, server): """Patch out a pytest request.""" fake_call = FakeRepCall() fake_config = FakeConfig() fake_node = FakeNode(fake_call) fake_request = FakeRequest(fake_node, fake_config, server) assert not hasattr(fake_request.node.rep_call, 'wasxfail') monkeypatch.setattr(quteproc, 'request', fake_request) return fake_request @pytest.mark.parametrize('cmd', [ ':message-error test', ':jseval console.log("[FAIL] test");' ]) def test_quteproc_error_message(qtbot, quteproc, cmd, request_mock): """Make sure the test fails with an unexpected error message.""" with qtbot.wait_signal(quteproc.got_error): quteproc.send_cmd(cmd) # Usually we wouldn't call this from inside a test, but here we force the # error to occur during the test rather than at teardown time. with pytest.raises(pytest.fail.Exception): quteproc.after_test() def test_quteproc_error_message_did_fail(qtbot, quteproc, request_mock, monkeypatch): """Make sure the test does not fail on teardown if the main test failed.""" monkeypatch.setattr(quteproc, "_take_x11_screenshot_of_failed_test", lambda: None) request_mock.node.rep_call.failed = True with qtbot.wait_signal(quteproc.got_error): quteproc.send_cmd(':message-error test') # Usually we wouldn't call this from inside a test, but here we force the # error to occur during the test rather than at teardown time. quteproc.after_test() def test_quteproc_screenshot_on_fail(qtbot, quteproc, request_mock, monkeypatch, mocker): """Make sure we call the method to take a screenshot to test failure.""" take_screenshot_spy = mocker.Mock() monkeypatch.setattr( quteproc, "_take_x11_screenshot_of_failed_test", take_screenshot_spy ) request_mock.node.rep_call.failed = True quteproc.after_test() take_screenshot_spy.assert_called_once() def test_quteproc_skip_via_js(qtbot, quteproc): with pytest.raises(pytest.skip.Exception, match='test'): quteproc.send_cmd(':jseval console.log("[SKIP] test");') quteproc.wait_for_js('[SKIP] test') # Usually we wouldn't call this from inside a test, but here we force # the error to occur during the test rather than at teardown time. quteproc.after_test() def test_quteproc_skip_and_wait_for(qtbot, quteproc): """This test will skip *again* during teardown, but we don't care.""" with pytest.raises(pytest.skip.Exception): quteproc.send_cmd(':jseval console.log("[SKIP] foo");') quteproc.wait_for_js("[SKIP] foo") quteproc.wait_for(message='This will not match') def test_qt_log_ignore(qtbot, quteproc): """Make sure the test passes when logging a qt_log_ignore message.""" with qtbot.wait_signal(quteproc.got_error): quteproc.send_cmd( ':message-error "QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to blabla"') def test_quteprocess_quitting(qtbot, quteproc_process): """When qutebrowser quits, after_test should fail.""" with qtbot.wait_signal(quteproc_process.proc.finished, timeout=15000): quteproc_process.send_cmd(':quit') with pytest.raises(testprocess.ProcessExited): quteproc_process.after_test() @pytest.mark.parametrize('data, attrs', [ pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "init", ' '"module": "earlyinit", "funcName": "init_log", "lineno": 280, ' '"levelno": 10, "message": "Log initialized."}', { 'timestamp': datetime.datetime.fromtimestamp(86400), 'loglevel': logging.DEBUG, 'category': 'init', 'module': 'earlyinit', 'function': 'init_log', 'line': 280, 'message': 'Log initialized.', 'expected': False, }, id='normal'), pytest.param( '{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", ' '"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, ' '"message": ""}', {'loglevel': log.VDEBUG_LEVEL}, id='vdebug'), pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", ' '"module": null, "funcName": null, "lineno": 0, "levelno": 10, ' '"message": "test"}', {'module': None, 'function': None, 'line': None}, id='unknown module'), pytest.param( '{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", ' '"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, ' '"message": "QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to blabla"}', {'expected': True}, id='expected message'), pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", ' '"module": "qnetworkreplyhttpimpl", "funcName": ' '"void QNetworkReplyHttpImplPrivate::error(' 'QNetworkReply::NetworkError, const QString&)", "lineno": 1929, ' '"levelno": 10, "message": "QNetworkReplyImplPrivate::error: ' 'Internal problem, this method must only be called once."}', { 'module': 'qnetworkreplyhttpimpl', 'function': 'void QNetworkReplyHttpImplPrivate::error(' 'QNetworkReply::NetworkError, const QString&)', 'line': 1929 }, id='weird Qt location'), pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", ' '"module": "qxcbxsettings", "funcName": "QXcbXSettings::QXcbXSettings(' 'QXcbScreen*)", "lineno": 233, "levelno": 10, "message": ' '"QXcbXSettings::QXcbXSettings(QXcbScreen*) Failed to get selection ' 'owner for XSETTINGS_S atom"}', { 'module': 'qxcbxsettings', 'function': 'QXcbXSettings::QXcbXSettings(QXcbScreen*)', 'line': 233, }, id='QXcbXSettings'), pytest.param( '{"created": 86400, "msecs": 0, "levelname": "WARNING", ' '"name": "py.warnings", "module": "app", "funcName": "qt_mainloop", ' '"lineno": 121, "levelno": 30, "message": ' '".../app.py:121: ResourceWarning: unclosed file <_io.TextIOWrapper ' 'name=18 mode=\'r\' encoding=\'UTF-8\'>"}', {'category': 'py.warnings'}, id='resourcewarning'), ]) def test_log_line_parse(pytestconfig, data, attrs): line = quteprocess.LogLine(pytestconfig, data) for name, expected in attrs.items(): actual = getattr(line, name) assert actual == expected, name @pytest.mark.parametrize('data, colorized, expect_error, expected', [ pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux'}, False, False, '{timestamp} DEBUG foo bar:qux:10 quux', id='normal'), pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux', 'traceback': ('Traceback (most recent call ' 'last):\n here be dragons')}, False, False, '{timestamp} DEBUG foo bar:qux:10 quux\n' 'Traceback (most recent call last):\n' ' here be dragons', id='traceback'), pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux'}, True, False, '\033[32m{timestamp}\033[0m \033[37mDEBUG \033[0m \033[36mfoo ' ' bar:qux:10\033[0m \033[37mquux\033[0m', id='colored'), pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40, 'message': 'quux'}, False, True, '{timestamp} ERROR (expected) foo bar:qux:10 quux', id='expected error'), pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux'}, False, True, '{timestamp} DEBUG foo bar:qux:10 quux', id='expected other'), pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40, 'message': 'quux'}, True, True, '\033[32m{timestamp}\033[0m \033[37mERROR (expected)\033[0m ' '\033[36mfoo bar:qux:10\033[0m \033[37mquux\033[0m', id='expected error colorized'), ]) def test_log_line_formatted(pytestconfig, data, colorized, expect_error, expected): line = json.dumps(data) record = quteprocess.LogLine(pytestconfig, line) record.expected = expect_error ts = datetime.datetime.fromtimestamp(data['created']).strftime('%H:%M:%S') ts += '.{:03.0f}'.format(data['msecs']) expected = expected.format(timestamp=ts) assert record.formatted_str(colorized=colorized) == expected def test_log_line_no_match(pytestconfig): with pytest.raises(testprocess.InvalidLine): quteprocess.LogLine(pytestconfig, "Hello World!") class TestClickElementByText: @pytest.fixture(autouse=True) def open_page(self, quteproc): quteproc.open_path('data/click_element.html') def test_click_element(self, quteproc): quteproc.click_element_by_text('Test Element') quteproc.wait_for_js('click_element clicked') def test_click_special_chars(self, quteproc): quteproc.click_element_by_text('"Don\'t", he shouted') quteproc.wait_for_js('click_element special chars') def test_duplicate(self, quteproc): with pytest.raises(ValueError, match='not unique'): quteproc.click_element_by_text('Duplicate') def test_nonexistent(self, quteproc): with pytest.raises(ValueError, match='No element'): quteproc.click_element_by_text('no element exists with this text') @pytest.mark.parametrize('string, expected', [ ('Test', "'Test'"), ("Don't", '"Don\'t"'), # This is some serious string escaping madness ('"Don\'t", he said', "concat('\"', 'Don', \"'\", 't', '\"', ', he said')"), ]) def test_xpath_escape(string, expected): assert quteprocess._xpath_escape(string) == expected @pytest.mark.parametrize('value', [ 'foo', 'foo"bar', # Make sure a " is preserved ]) def test_set(quteproc, value): quteproc.set_setting('content.default_encoding', value) read_back = quteproc.get_setting('content.default_encoding') assert read_back == value @pytest.mark.parametrize('message, ignored', [ # Unparsable ('Hello World', False), # Without process/thread ID ('[0509/185222.442798:ERROR:ssl_client_socket_impl.cc(959)] handshake failed; returned -1, SSL error code 1, net_error -202', True), # Random ignored message ('[503633:503650:0509/185222.442798:ERROR:ssl_client_socket_impl.cc(959)] handshake failed; returned -1, SSL error code 1, net_error -202', True), # Not ignored ('[26598:26598:0605/191429.639416:WARNING:audio_manager.cc(317)] Test', False), ]) def test_is_ignored_chromium_message(message, ignored): assert quteprocess.is_ignored_chromium_message(message) == ignored ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/test_testprocess.py0000644000175100017510000002004515102145205024066 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test testprocess.Process.""" import sys import time import contextlib import datetime import pytest from qutebrowser.qt.core import QProcess from end2end.fixtures import testprocess pytestmark = [pytest.mark.not_frozen] @contextlib.contextmanager def stopwatch(min_ms=None, max_ms=None): if min_ms is None and max_ms is None: raise ValueError("Using stopwatch with both min_ms/max_ms None does " "nothing.") start = datetime.datetime.now() yield stop = datetime.datetime.now() delta_ms = (stop - start).total_seconds() * 1000 if min_ms is not None: assert delta_ms >= min_ms if max_ms is not None: assert delta_ms <= max_ms class PythonProcess(testprocess.Process): """A testprocess which runs the given Python code.""" def __init__(self, request): super().__init__(request) self.proc.setReadChannel(QProcess.ProcessChannel.StandardOutput) self.code = None def _parse_line(self, line): print("LINE: {}".format(line)) if line.strip() == 'ready': self.ready.emit() return testprocess.Line(line) def _executable_args(self): code = [ 'import sys, time', 'print("ready")', 'sys.stdout.flush()', self.code, 'sys.stdout.flush()', 'time.sleep(20)', ] return (sys.executable, ['-c', ';'.join(code)]) def _default_args(self): return [] class QuitPythonProcess(PythonProcess): """A testprocess which quits immediately.""" def _executable_args(self): code = [ 'import sys', 'print("ready")', 'sys.exit(0)', ] return (sys.executable, ['-c', ';'.join(code)]) class NoReadyPythonProcess(PythonProcess): """A testprocess which never emits 'ready' and quits.""" def _executable_args(self): code = [ 'import sys', 'sys.exit(0)', ] return (sys.executable, ['-c', ';'.join(code)]) @pytest.fixture def pyproc(request): proc = PythonProcess(request) yield proc proc.terminate() @pytest.fixture def quit_pyproc(request): proc = QuitPythonProcess(request) yield proc proc.terminate() @pytest.fixture def noready_pyproc(request): proc = NoReadyPythonProcess(request) yield proc proc.terminate() def test_no_ready_python_process(noready_pyproc): """When a process quits immediately, waiting for start should interrupt.""" with pytest.raises(testprocess.ProcessExited): with stopwatch(max_ms=5000): noready_pyproc.start() def test_quitting_process(qtbot, quit_pyproc): with qtbot.wait_signal(quit_pyproc.proc.finished): quit_pyproc.start() with pytest.raises(testprocess.ProcessExited): quit_pyproc.after_test() def test_quitting_process_expected(qtbot, quit_pyproc): quit_pyproc.exit_expected = True with qtbot.wait_signal(quit_pyproc.proc.finished): quit_pyproc.start() quit_pyproc.after_test() def test_process_never_started(qtbot, quit_pyproc): """Calling after_test without start should not fail.""" quit_pyproc.after_test() def test_wait_signal_raising(request, qtbot): """testprocess._wait_signal should raise by default.""" proc = testprocess.Process(request) with pytest.raises(qtbot.TimeoutError): with proc._wait_signal(proc.proc.started, timeout=0): pass def test_custom_environment(pyproc): pyproc.code = 'import os; print(os.environ["CUSTOM_ENV"])' pyproc.start(env={'CUSTOM_ENV': 'blah'}) pyproc.wait_for(data='blah') @pytest.mark.posix def test_custom_environment_system_env(monkeypatch, pyproc): """When env=... is given, the system environment should be present.""" monkeypatch.setenv('QUTE_TEST_ENV', 'blubb') pyproc.code = 'import os; print(os.environ["QUTE_TEST_ENV"])' pyproc.start(env={}) pyproc.wait_for(data='blubb') class TestWaitFor: def test_successful(self, pyproc): """Using wait_for with the expected text.""" pyproc.code = "time.sleep(0.5); print('foobar')" with stopwatch(min_ms=500): pyproc.start() pyproc.wait_for(data="foobar") def test_other_text(self, pyproc): """Test wait_for when getting some unrelated text.""" pyproc.code = "time.sleep(0.1); print('blahblah')" pyproc.start() with pytest.raises(testprocess.WaitForTimeout): pyproc.wait_for(data="foobar", timeout=500) def test_no_text(self, pyproc): """Test wait_for when getting no text at all.""" pyproc.code = "pass" pyproc.start() with pytest.raises(testprocess.WaitForTimeout): pyproc.wait_for(data="foobar", timeout=100) @pytest.mark.parametrize('message', ['foobar', 'literal [x]']) def test_existing_message(self, message, pyproc): """Test with a message which already passed when waiting.""" pyproc.code = "print('{}')".format(message) pyproc.start() time.sleep(0.5) # to make sure the message is printed pyproc.wait_for(data=message) def test_existing_message_previous_test(self, pyproc): """Make sure the message of a previous test gets ignored.""" pyproc.code = "print('foobar')" pyproc.start() line = pyproc.wait_for(data="foobar") line.waited_for = False # so we don't test what the next test does pyproc.after_test() with pytest.raises(testprocess.WaitForTimeout): pyproc.wait_for(data="foobar", timeout=100) def test_existing_message_already_waited(self, pyproc): """Make sure an existing message doesn't stop waiting twice. wait_for checks existing messages (see above), but we don't want it to automatically proceed if we already *did* use wait_for on one of the existing messages, as that makes it likely it's not what we actually want. """ pyproc.code = "time.sleep(0.1); print('foobar')" pyproc.start() pyproc.wait_for(data="foobar") with pytest.raises(testprocess.WaitForTimeout): pyproc.wait_for(data="foobar", timeout=100) def test_no_kwargs(self, pyproc): """Using wait_for without kwargs should raise an exception. Otherwise it'd match automatically because of the all(matches). """ with pytest.raises(TypeError): pyproc.wait_for() def test_do_skip(self, pyproc): """Test wait_for when getting no text at all, with do_skip.""" pyproc.code = "pass" pyproc.start() with pytest.raises(pytest.skip.Exception): pyproc.wait_for(data="foobar", timeout=100, do_skip=True) class TestEnsureNotLogged: @pytest.mark.parametrize('message, pattern', [ ('blacklisted', 'blacklisted'), ('bl[a]cklisted', 'bl[a]cklisted'), ('blacklisted', 'black*'), ]) def test_existing_message(self, pyproc, message, pattern): pyproc.code = "print('{}')".format(message) pyproc.start() with stopwatch(max_ms=1000): with pytest.raises(testprocess.BlacklistedMessageError): pyproc.ensure_not_logged(data=pattern, delay=2000) def test_late_message(self, pyproc): pyproc.code = "time.sleep(0.5); print('blacklisted')" pyproc.start() with pytest.raises(testprocess.BlacklistedMessageError): pyproc.ensure_not_logged(data='blacklisted', delay=5000) def test_no_matching_message(self, pyproc): pyproc.code = "print('blacklisted... nope!')" pyproc.start() pyproc.ensure_not_logged(data='blacklisted', delay=100) def test_wait_for_and_blacklist(self, pyproc): pyproc.code = "print('blacklisted')" pyproc.start() pyproc.wait_for(data='blacklisted') with pytest.raises(testprocess.BlacklistedMessageError): pyproc.ensure_not_logged(data='blacklisted', delay=0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/test_webserver.py0000644000175100017510000000614715102145205023523 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test the server webserver used for tests.""" import json import urllib.request import urllib.error from http import HTTPStatus import pytest @pytest.mark.parametrize('path, content, expected', [ ('/', 'qutebrowser test webserver', True), # https://github.com/Runscope/server/issues/245 ('/', 'www.google-analytics.com', False), ('/data/hello.txt', 'Hello World!', True), ]) def test_server(server, qtbot, path, content, expected): with qtbot.wait_signal(server.new_request, timeout=100): url = 'http://localhost:{}{}'.format(server.port, path) try: with urllib.request.urlopen(url) as response: data = response.read().decode('utf-8') except urllib.error.HTTPError as e: # "Though being an exception (a subclass of URLError), an HTTPError # can also function as a non-exceptional file-like return value # (the same thing that urlopen() returns)." # ...wat print(e.read().decode('utf-8')) raise assert server.get_requests() == [server.ExpectedRequest('GET', path)] assert (content in data) == expected @pytest.mark.parametrize('line, verb, path, equal', [ ({'verb': 'GET', 'path': '/', 'status': HTTPStatus.OK}, 'GET', '/', True), ({'verb': 'GET', 'path': '/foo/', 'status': HTTPStatus.OK}, 'GET', '/foo', True), ({'verb': 'GET', 'path': '/relative-redirect', 'status': HTTPStatus.FOUND}, 'GET', '/relative-redirect', True), ({'verb': 'GET', 'path': '/absolute-redirect', 'status': HTTPStatus.FOUND}, 'GET', '/absolute-redirect', True), ({'verb': 'GET', 'path': '/redirect-to', 'status': HTTPStatus.FOUND}, 'GET', '/redirect-to', True), ({'verb': 'GET', 'path': '/redirect-self', 'status': HTTPStatus.FOUND}, 'GET', '/redirect-self', True), ({'verb': 'GET', 'path': '/content-size', 'status': HTTPStatus.OK}, 'GET', '/content-size', True), ({'verb': 'GET', 'path': '/twenty-mb', 'status': HTTPStatus.OK}, 'GET', '/twenty-mb', True), ({'verb': 'GET', 'path': '/500-inline', 'status': HTTPStatus.INTERNAL_SERVER_ERROR}, 'GET', '/500-inline', True), ({'verb': 'GET', 'path': '/basic-auth/user1/password1', 'status': HTTPStatus.UNAUTHORIZED}, 'GET', '/basic-auth/user1/password1', True), ({'verb': 'GET', 'path': '/drip', 'status': HTTPStatus.OK}, 'GET', '/drip', True), ({'verb': 'GET', 'path': '/404', 'status': HTTPStatus.NOT_FOUND}, 'GET', '/404', True), ({'verb': 'GET', 'path': '/', 'status': HTTPStatus.OK}, 'GET', '/foo', False), ({'verb': 'POST', 'path': '/', 'status': HTTPStatus.OK}, 'GET', '/', False), ({'verb': 'GET', 'path': '/basic-auth/user/password', 'status': HTTPStatus.UNAUTHORIZED}, 'GET', '/basic-auth/user/passwd', False), ]) def test_expected_request(server, line, verb, path, equal): expected = server.ExpectedRequest(verb, path) request = server.Request(json.dumps(line)) assert (expected == request) == equal ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/testprocess.py0000644000175100017510000004123315102145205023031 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Base class for a subprocess run for tests.""" import re import time import warnings import dataclasses import pytest import pytestqt.wait_signal from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer, QProcessEnvironment) from qutebrowser.qt.test import QSignalSpy from helpers import testutils from qutebrowser.utils import utils as quteutils class InvalidLine(Exception): """Raised when the process prints a line which is not parsable.""" class ProcessExited(Exception): """Raised when the child process did exit.""" class WaitForTimeout(Exception): """Raised when wait_for didn't get the expected message.""" class BlacklistedMessageError(Exception): """Raised when ensure_not_logged found a message.""" @dataclasses.dataclass class Line: """Container for a line of data the process emits. Attributes: data: The raw data passed to the constructor. waited_for: If Process.wait_for was used on this line already. """ data: str waited_for: bool = False def _render_log(data, *, verbose, threshold=100): """Shorten the given log without -v and convert to a string.""" data = [str(d) for d in data] is_exception = any('Traceback (most recent call last):' in line or 'Uncaught exception' in line for line in data) if (len(data) > threshold and not verbose and not is_exception and not testutils.ON_CI): msg = '[{} lines suppressed, use -v to show]'.format( len(data) - threshold) data = [msg] + data[-threshold:] if testutils.ON_CI: data = [testutils.gha_group_begin('Log')] + data + [testutils.gha_group_end()] return '\n'.join(data) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): """Add qutebrowser/server sections to captured output if a test failed.""" outcome = yield if call.when not in ['call', 'teardown']: return report = outcome.get_result() if report.passed: return quteproc_log = getattr(item, '_quteproc_log', None) server_logs = getattr(item, '_server_logs', []) if not hasattr(report.longrepr, 'addsection'): # In some conditions (on macOS and Windows it seems), report.longrepr # is actually a tuple. This is handled similarly in pytest-qt too. return if item.config.getoption('--capture') == 'no': # Already printed live return verbose = item.config.getoption('--verbose') if quteproc_log is not None: report.longrepr.addsection( "qutebrowser output", _render_log(quteproc_log, verbose=verbose)) for name, content in server_logs: report.longrepr.addsection( f"{name} output", _render_log(content, verbose=verbose)) class Process(QObject): """Abstraction over a running test subprocess process. Reads the log from its stdout and parses it. Attributes: _invalid: A list of lines which could not be parsed. _data: A list of parsed lines. _started: Whether the process was ever started. proc: The QProcess for the underlying process. exit_expected: Whether the process is expected to quit. request: The request object for the current test. Signals: ready: Emitted when the server finished starting up. new_data: Emitted when a new line was parsed. """ ready = pyqtSignal() new_data = pyqtSignal(object) KEYS = ['data'] def __init__(self, request, parent=None): super().__init__(parent) self.request = request self.captured_log = [] self._started = False self._invalid = [] self._data = [] self.proc = QProcess() self.proc.setReadChannel(QProcess.ProcessChannel.StandardError) self.exit_expected = None # Not started at all yet def _log(self, line): """Add the given line to the captured log output.""" if self.request.config.getoption('--capture') == 'no': print(line) self.captured_log.append(line) def log_summary(self, text): """Log the given line as summary/title.""" text = '\n{line} {text} {line}\n'.format(line='='*30, text=text) self._log(text) def _parse_line(self, line): """Parse the given line from the log. Return: A self.ParseResult member. """ raise NotImplementedError def _executable_args(self): """Get the executable and necessary arguments as a tuple.""" raise NotImplementedError def _default_args(self): """Get the default arguments to use if none were passed to start().""" raise NotImplementedError def _get_data(self): """Get the parsed data for this test. Also waits for 0.5s to make sure any new data is received. Subprocesses are expected to alias this to a public method with a better name. """ self.proc.waitForReadyRead(500) self.read_log() return self._data def _wait_signal(self, signal, timeout=5000, raising=True): """Wait for a signal to be emitted. Should be used in a contextmanager. """ blocker = pytestqt.wait_signal.SignalBlocker(timeout=timeout, raising=raising) blocker.connect(signal) return blocker @pyqtSlot() def read_log(self): """Read the log from the process' stdout.""" if not hasattr(self, 'proc'): # I have no idea how this happens, but it does... return while self.proc.canReadLine(): line = self.proc.readLine() line = bytes(line).decode('utf-8', errors='ignore').rstrip('\r\n') try: parsed = self._parse_line(line) except InvalidLine: self._invalid.append(line) self._log("INVALID: {}".format(line)) continue if parsed is None: if self._invalid: self._log("IGNORED: {}".format(line)) else: self._data.append(parsed) self.new_data.emit(parsed) def start(self, args=None, *, env=None): """Start the process and wait until it started.""" self._start(args, env=env) self._started = True verbose = self.request.config.getoption('--verbose') timeout = 60 if testutils.ON_CI else 20 for _ in range(timeout): with self._wait_signal(self.ready, timeout=1000, raising=False) as blocker: pass if not self.is_running(): if self.exit_expected: return # _start ensures it actually started, but it might quit shortly # afterwards raise ProcessExited('\n' + _render_log(self.captured_log, verbose=verbose)) if blocker.signal_triggered: self._after_start() return raise WaitForTimeout("Timed out while waiting for process start.\n" + _render_log(self.captured_log, verbose=verbose)) def _start(self, args, env): """Actually start the process.""" executable, exec_args = self._executable_args() if args is None: args = self._default_args() procenv = QProcessEnvironment.systemEnvironment() if env is not None: for k, v in env.items(): procenv.insert(k, v) self.proc.readyRead.connect(self.read_log) self.proc.setProcessEnvironment(procenv) self.proc.start(executable, exec_args + args) ok = self.proc.waitForStarted() assert ok assert self.is_running() def _after_start(self): """Do things which should be done immediately after starting.""" def before_test(self): """Restart process before a test if it exited before.""" self._invalid = [] if not self.is_running(): self.start() def after_test(self): """Clean up data after each test. Also checks self._invalid so the test counts as failed if there were unexpected output lines earlier. """ __tracebackhide__ = lambda e: e.errisinstance(ProcessExited) self.captured_log = [] if self._invalid: # Wait for a bit so the full error has a chance to arrive time.sleep(1) # Exit the process to make sure we're in a defined state again self.terminate() self.clear_data() raise InvalidLine('\n' + '\n'.join(self._invalid)) self.clear_data() if not self.is_running() and not self.exit_expected and self._started: raise ProcessExited self.exit_expected = False def clear_data(self): """Clear the collected data.""" self._data.clear() def terminate(self): """Clean up and shut down the process.""" if not self.is_running(): return if quteutils.is_windows: self.proc.kill() else: self.proc.terminate() ok = self.proc.waitForFinished(5000) if not ok: cmdline = ' '.join([self.proc.program()] + self.proc.arguments()) warnings.warn(f"Test process {cmdline} with PID {self.proc.processId()} " "failed to terminate!") self.proc.kill() self.proc.waitForFinished() def is_running(self): """Check if the process is currently running.""" return self.proc.state() == QProcess.ProcessState.Running def _match_data(self, value, expected): """Helper for wait_for to match a given value. The behavior of this method is slightly different depending on the types of the filtered values: - If expected is None, the filter always matches. - If the value is a string or bytes object and the expected value is too, the pattern is treated as a glob pattern (with only * active). - If the value is a string or bytes object and the expected value is a compiled regex, it is used for matching. - If the value is any other type, == is used. Return: A bool """ regex_type = type(re.compile('')) if expected is None: return True elif isinstance(expected, regex_type): return expected.search(value) elif isinstance(value, (bytes, str)): return testutils.pattern_match(pattern=expected, value=value) else: return value == expected def _wait_for_existing(self, override_waited_for, after, **kwargs): """Check if there are any line in the history for wait_for. Return: either the found line or None. """ for line in self._data: matches = [] for key, expected in kwargs.items(): value = getattr(line, key) matches.append(self._match_data(value, expected)) if after is None: too_early = False else: too_early = ((line.timestamp, line.msecs) < (after.timestamp, after.msecs)) if (all(matches) and (not line.waited_for or override_waited_for) and not too_early): # If we waited for this line, chances are we don't mean the # same thing the next time we use wait_for and it matches # this line again. line.waited_for = True self._log("\n----> Already found {!r} in the log: {}".format( kwargs.get('message', 'line'), line)) return line return None def _wait_for_new(self, timeout, do_skip, **kwargs): """Wait for a log message which doesn't exist yet. Called via wait_for. """ __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout) message = kwargs.get('message', None) if message is not None: elided = quteutils.elide(repr(message), 100) self._log("\n----> Waiting for {} in the log".format(elided)) spy = QSignalSpy(self.new_data) elapsed_timer = QElapsedTimer() elapsed_timer.start() while True: # Skip if there are pending messages causing a skip self._maybe_skip() got_signal = spy.wait(timeout) if not got_signal or elapsed_timer.hasExpired(timeout): msg = "Timed out after {}ms waiting for {!r}.".format( timeout, kwargs) if do_skip: pytest.skip(msg) else: raise WaitForTimeout(msg) match = self._wait_for_match(spy, kwargs) if match is not None: if message is not None: self._log(f"----> found it: {match.formatted_str()}") return match raise quteutils.Unreachable def _wait_for_match(self, spy, kwargs): """Try matching the kwargs with the given QSignalSpy.""" for args in spy: assert len(args) == 1 line = args[0] matches = [] for key, expected in kwargs.items(): value = getattr(line, key) matches.append(self._match_data(value, expected)) if all(matches): # If we waited for this line, chances are we don't mean the # same thing the next time we use wait_for and it matches # this line again. line.waited_for = True return line return None def _maybe_skip(self): """Can be overridden by subclasses to skip on certain log lines. We can't run pytest.skip directly while parsing the log, as that would lead to a pytest.skip.Exception error in a virtual Qt method, which means pytest-qt fails the test. Instead, we check for skip messages periodically in QuteProc._maybe_skip, and call _maybe_skip after every parsed message in wait_for (where it's most likely that new messages arrive). """ def wait_for(self, timeout=None, *, override_waited_for=False, do_skip=False, divisor=1, after=None, **kwargs): """Wait until a given value is found in the data. Keyword arguments to this function get interpreted as attributes of the searched data. Every given argument is treated as a pattern which the attribute has to match against. Args: timeout: How long to wait for the message. override_waited_for: If set, gets triggered by previous messages again. do_skip: If set, call pytest.skip on a timeout. divisor: A factor to decrease the timeout by. after: If it's an existing line, ensure it's after the given one. Return: The matched line. """ __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout) if timeout is None: if do_skip: timeout = 2000 elif testutils.ON_CI: timeout = 15000 else: timeout = 5000 timeout //= divisor if not kwargs: raise TypeError("No keyword arguments given!") for key in kwargs: assert key in self.KEYS existing = self._wait_for_existing(override_waited_for, after, **kwargs) if existing is not None: return existing else: return self._wait_for_new(timeout=timeout, do_skip=do_skip, **kwargs) def ensure_not_logged(self, delay=500, **kwargs): """Make sure the data matching the given arguments is not logged. If nothing is found in the log, we wait for delay ms to make sure nothing arrives. """ __tracebackhide__ = lambda e: e.errisinstance(BlacklistedMessageError) try: line = self.wait_for(timeout=delay, override_waited_for=True, **kwargs) except WaitForTimeout: return else: raise BlacklistedMessageError(line) def wait_for_quit(self): """Wait until the process has quit.""" self.exit_expected = True with self._wait_signal(self.proc.finished, timeout=15000): pass assert not self.is_running() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/webserver.py0000644000175100017510000001624015102145205022457 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Fixtures for the server webserver.""" import re import sys import json import pathlib import socket import dataclasses from http import HTTPStatus import pytest from qutebrowser.qt.core import pyqtSignal, QUrl from end2end.fixtures import testprocess from helpers import testutils class Request(testprocess.Line): """A parsed line from the flask log output. Attributes: verb/path/status: Parsed from the log output. """ def __init__(self, data): super().__init__(data) try: parsed = json.loads(data) except ValueError: raise testprocess.InvalidLine(data) assert isinstance(parsed, dict) assert set(parsed.keys()) == {'path', 'verb', 'status'} self.verb = parsed['verb'] path = parsed['path'] self.path = '/' if path == '/' else path.rstrip('/') self.status = parsed['status'] self._check_status() def _check_status(self): """Check if the http status is what we expected.""" path_to_statuses = { '/favicon.ico': [ HTTPStatus.OK, HTTPStatus.PARTIAL_CONTENT, HTTPStatus.NOT_MODIFIED, ], '/does-not-exist': [HTTPStatus.NOT_FOUND], '/does-not-exist-2': [HTTPStatus.NOT_FOUND], '/404': [HTTPStatus.NOT_FOUND], '/redirect-later': [HTTPStatus.FOUND], '/redirect-self': [HTTPStatus.FOUND], '/redirect-to': [HTTPStatus.FOUND], '/relative-redirect': [HTTPStatus.FOUND], '/absolute-redirect': [HTTPStatus.FOUND], '/redirect-http/data/downloads/download.bin': [HTTPStatus.FOUND], '/cookies/set': [HTTPStatus.FOUND], '/cookies/set-custom': [HTTPStatus.FOUND], '/500-inline': [HTTPStatus.INTERNAL_SERVER_ERROR], '/500': [HTTPStatus.INTERNAL_SERVER_ERROR], } for i in range(25): path_to_statuses['/redirect/{}'.format(i)] = [HTTPStatus.FOUND] for suffix in ['', '1', '2', '3', '4', '5', '6']: key = ('/basic-auth/user{suffix}/password{suffix}' .format(suffix=suffix)) path_to_statuses[key] = [HTTPStatus.UNAUTHORIZED, HTTPStatus.OK] default_statuses = [HTTPStatus.OK, HTTPStatus.NOT_MODIFIED] sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo expected_statuses = path_to_statuses.get(sanitized, default_statuses) if self.status not in expected_statuses: raise AssertionError( "{} loaded with status {} but expected {}".format( sanitized, self.status, ' / '.join(repr(e) for e in expected_statuses))) def __eq__(self, other): return NotImplemented @dataclasses.dataclass(frozen=True) class ExpectedRequest: """Class to compare expected requests easily.""" verb: str path: int @classmethod def from_request(cls, request): """Create an ExpectedRequest from a Request.""" return cls(request.verb, request.path) def __eq__(self, other): if isinstance(other, (Request, ExpectedRequest)): return self.verb == other.verb and self.path == other.path else: return NotImplemented def is_ignored_webserver_message(line: str) -> bool: return testutils.pattern_match( pattern=( "Client ('127.0.0.1', *) lost * peer dropped the TLS connection suddenly, " "during handshake: (1, '[SSL: SSLV3_ALERT_CERTIFICATE_UNKNOWN] * " "alert certificate unknown (_ssl.c:*)')" ), value=line, ) class WebserverProcess(testprocess.Process): """Abstraction over a running Flask server process. Reads the log from its stdout and parses it. Signals: new_request: Emitted when there's a new request received. """ new_request = pyqtSignal(Request) Request = Request # So it can be used from the fixture easily. ExpectedRequest = ExpectedRequest KEYS = ['verb', 'path'] def __init__(self, request, script, parent=None): super().__init__(request, parent) self._script = script self.port = self._random_port() self.new_data.connect(self.new_request) def _random_port(self) -> int: """Get a random free port.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(('localhost', 0)) return sock.getsockname()[1] def get_requests(self): """Get the requests to the server during this test.""" requests = self._get_data() return [r for r in requests if r.path != '/favicon.ico'] def _parse_line(self, line): self._log(line) started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/? ' r'\(Press CTRL\+C to quit\)'.format(self.port)) if started_re.fullmatch(line): self.ready.emit() return None try: return Request(line) except testprocess.InvalidLine: if is_ignored_webserver_message(line): return None raise def _executable_args(self): if hasattr(sys, 'frozen'): executable = str(pathlib.Path(sys.executable).parent / self._script) args = [] else: executable = sys.executable py_file = (pathlib.Path(__file__).parent / self._script).with_suffix('.py') args = [str(py_file)] return executable, args def _default_args(self): return [str(self.port)] @pytest.fixture(scope='session', autouse=True) def server(qapp, request): """Fixture for an server object which ensures clean setup/teardown.""" server = WebserverProcess(request, 'webserver_sub') server.start() yield server server.terminate() @pytest.fixture(autouse=True) def server_per_test(server, request): """Fixture to clean server request list after each test.""" if not hasattr(request.node, '_server_logs'): request.node._server_logs = [] request.node._server_logs.append(('server', server.captured_log)) server.before_test() yield server.after_test() @pytest.fixture def server2(qapp, request): """Fixture for a second server object for cross-origin tests.""" server = WebserverProcess(request, 'webserver_sub') if not hasattr(request.node, '_server_logs'): request.node._server_logs = [] request.node._server_logs.append(('secondary server', server.captured_log)) server.start() yield server server.terminate() @pytest.fixture def ssl_server(request, qapp): """Fixture for a webserver with a self-signed SSL certificate. This needs to be explicitly used in a test. """ server = WebserverProcess(request, 'webserver_sub_ssl') if not hasattr(request.node, '_server_logs'): request.node._server_logs = [] request.node._server_logs.append(('SSL server', server.captured_log)) server.start() yield server server.after_test() server.terminate() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/webserver_sub.py0000644000175100017510000002612015102145205023326 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Web server for end2end tests. This script gets called as a QProcess from end2end/conftest.py. Some of the handlers here are inspired by the server project, but simplified for qutebrowser's needs. Note that it probably doesn't handle e.g. multiple parameters or headers with the same name properly. """ import sys import errno import json import time import threading import mimetypes import pathlib from http import HTTPStatus import cheroot.wsgi import flask app = flask.Flask(__name__) _redirect_later_event = None END2END_DIR = pathlib.Path(__file__).resolve().parents[1] @app.route('/') def root(): """Show simple text.""" return flask.Response(b'qutebrowser test webserver, ' b'user agent') @app.route('/data/') @app.route('/data2/') # for per-URL settings def send_data(path): """Send a given data file to qutebrowser. If a directory is requested, its index.html is sent. """ data_dir = END2END_DIR / 'data' if (data_dir / path).is_dir(): path += '/index.html' return flask.send_from_directory(data_dir, path) @app.route('/redirect-later') def redirect_later(): """302 redirect to / after the given delay. If delay is -1, wait until a request on redirect-later-continue is done. """ global _redirect_later_event delay = float(flask.request.args.get('delay', '1')) if delay == -1: _redirect_later_event = threading.Event() ok = _redirect_later_event.wait(timeout=30 * 1000) assert ok _redirect_later_event = None else: time.sleep(delay) x = flask.redirect('/') return x @app.route('/redirect-later-continue') def redirect_later_continue(): """Continue a redirect-later request.""" if _redirect_later_event is None: return flask.Response(b'Timed out or no redirect pending.') else: _redirect_later_event.set() return flask.Response(b'Continued redirect.') @app.route('/redirect-self') def redirect_self(): """302 Redirects to itself.""" return app.make_response(flask.redirect(flask.url_for('redirect_self'))) @app.route('/redirect/') def redirect_n_times(n): """302 Redirects n times.""" assert n > 0 return flask.redirect(flask.url_for('redirect_n_times', n=n-1)) @app.route('/relative-redirect') def relative_redirect(): """302 Redirect once.""" response = app.make_response('') response.status_code = HTTPStatus.FOUND response.headers['Location'] = flask.url_for('root') return response @app.route('/absolute-redirect') def absolute_redirect(): """302 Redirect once.""" response = app.make_response('') response.status_code = HTTPStatus.FOUND response.headers['Location'] = flask.url_for('root', _external=True) return response @app.route('/redirect-to') def redirect_to(): """302/3XX Redirects to the given URL.""" # We need to build the response manually and convert to UTF-8 to prevent # werkzeug from "fixing" the URL. This endpoint should set the Location # header to the exact string supplied. response = app.make_response('') response.status_code = HTTPStatus.FOUND response.headers['Location'] = flask.request.args['url'] return response @app.route('/content-size') def content_size(): """Send two bytes of data without a content-size.""" def generate_bytes(): yield b'*' time.sleep(0.2) yield b'*' response = flask.Response(generate_bytes(), headers={ "Content-Type": "application/octet-stream", }) response.status_code = HTTPStatus.OK return response @app.route('/twenty-mb') def twenty_mb(): """Send 20MB of data.""" def generate_bytes(): yield b'*' * 20 * 1024 * 1024 response = flask.Response(generate_bytes(), headers={ "Content-Type": "application/octet-stream", "Content-Length": str(20 * 1024 * 1024), }) response.status_code = HTTPStatus.OK return response @app.route('/500-inline') def internal_error_attachment(): """A 500 error with Content-Disposition: inline.""" response = flask.Response(b"", headers={ "Content-Type": "application/octet-stream", "Content-Disposition": 'inline; filename="attachment.jpg"', }) response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR return response @app.route('/500') def internal_error(): """A normal 500 error.""" r = flask.make_response() r.status_code = HTTPStatus.INTERNAL_SERVER_ERROR return r @app.route('/cookies') def view_cookies(): """Show cookies.""" return flask.jsonify(cookies=flask.request.cookies) @app.route('/cookies/set') def set_cookies(): """Set cookie(s) as provided by the query string.""" r = app.make_response(flask.redirect(flask.url_for('view_cookies'))) for key, value in flask.request.args.items(): r.set_cookie(key=key, value=value) return r @app.route('/cookies/set-custom') def set_custom_cookie(): """Set a cookie with a custom max_age/expires.""" r = app.make_response(flask.redirect(flask.url_for('view_cookies'))) max_age = flask.request.args.get('max_age') r.set_cookie(key='cookie', value='value', max_age=int(max_age) if max_age else None) return r @app.route('/basic-auth//') def basic_auth(user='user', passwd='passwd'): """Prompt the user for authorization using HTTP Basic Auth.""" auth = flask.request.authorization if not auth or auth.username != user or auth.password != passwd: r = flask.make_response() r.status_code = HTTPStatus.UNAUTHORIZED r.headers = {'WWW-Authenticate': 'Basic realm="Fake Realm"'} return r return flask.jsonify(authenticated=True, user=user) @app.route('/drip') def drip(): """Drip data over a duration.""" duration = float(flask.request.args.get('duration')) numbytes = int(flask.request.args.get('numbytes')) pause = duration / numbytes def generate_bytes(): for _ in range(numbytes): yield b"*" time.sleep(pause) response = flask.Response(generate_bytes(), headers={ "Content-Type": "application/octet-stream", "Content-Length": str(numbytes), }) response.status_code = HTTPStatus.OK return response @app.route('/404') def status_404(): r = flask.make_response() r.status_code = HTTPStatus.NOT_FOUND return r @app.route('/headers') def view_headers(): """Return HTTP headers.""" return flask.jsonify(headers=dict(flask.request.headers)) @app.route('/headers-link/') def headers_link(port): """Get a (possibly cross-origin) link to /headers.""" return flask.render_template('headers-link.html', port=port) @app.route('/https-script/') def https_script(port): """Get a script loaded via HTTPS.""" return flask.render_template('https-script.html', port=port) @app.route('/https-iframe/') def https_iframe(port): """Get an iframe loaded via HTTPS.""" return flask.render_template('https-iframe.html', port=port) @app.route('/response-headers') def response_headers(): """Return a set of response headers from the query string.""" headers = flask.request.args response = flask.jsonify(headers) response.headers.extend(headers) response = flask.jsonify(dict(response.headers)) response.headers.extend(headers) return response @app.route('/query') def query(): return flask.jsonify(flask.request.args) @app.route('/user-agent') def view_user_agent(): """Return User-Agent.""" return flask.jsonify({'user-agent': flask.request.headers['user-agent']}) @app.route('/restrictive-csp') def restrictive_csp(): csp = "img-src 'self'; default-src none" # allow favicon.ico return flask.Response(b"", headers={"Content-Security-Policy": csp}) @app.route('/favicon.ico') def favicon(): # WORKAROUND for https://github.com/PyCQA/pylint/issues/5783 # pylint: disable-next=no-member,useless-suppression icon_dir = END2END_DIR.parents[1] / 'qutebrowser' / 'icons' return flask.send_from_directory( icon_dir, 'qutebrowser.ico', mimetype='image/vnd.microsoft.icon') @app.after_request def log_request(response): """Log a webserver request.""" request = flask.request data = { 'verb': request.method, 'path': request.full_path if request.query_string else request.path, 'status': response.status_code, } print(json.dumps(data), file=sys.stderr, flush=True) return response class WSGIServer(cheroot.wsgi.Server): """A custom WSGIServer that prints a line on stderr when it's ready. Attributes: _ready: Internal state for the 'ready' property. _printed_ready: Whether the initial ready message was printed. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._ready = False self._printed_ready = False @property def ready(self): return self._ready @ready.setter def ready(self, value): if value and not self._printed_ready: port = self.bind_addr[1] scheme = 'http' if self.ssl_adapter is None else 'https' print(f' * Running on {scheme}://127.0.0.1:{port}/ (Press CTRL+C to quit)', file=sys.stderr, flush=True) self._printed_ready = True self._ready = value def unraisable_hook(unraisable: "sys.UnraisableHookArgs") -> None: if ( sys.version_info[:2] >= (3, 13) and isinstance(unraisable.exc_value, OSError) and ( unraisable.exc_value.errno == errno.EBADF or ( sys.platform == "win32" # pylint: disable-next=no-member and unraisable.exc_value.winerror == errno.WSAENOTSOCK ) ) and ( ( # Python 3.14 unraisable.object is None and unraisable.err_msg.startswith( "Exception ignored while calling deallocator None: sys.unraisablehook = unraisable_hook def main(): init_unraisable_hook() app.template_folder = END2END_DIR / 'templates' assert app.template_folder.is_dir(), app.template_folder if mimetypes.guess_type('worker.js')[0] == 'text/plain': # WORKAROUND for https://github.com/pallets/flask/issues/1045 # Needed for Windows on GitHub Actions for some reason... mimetypes.add_type('application/javascript', '.js') port = int(sys.argv[1]) server = WSGIServer(('127.0.0.1', port), app) server.start() if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/fixtures/webserver_sub_ssl.py0000644000175100017510000000256215102145205024213 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Minimal flask webserver serving a Hello World via SSL. This script gets called as a QProcess from end2end/conftest.py. """ import sys import flask import webserver_sub import cheroot.ssl.builtin app = flask.Flask(__name__) @app.route('/') def hello_world(): return "Hello World via SSL!" @app.route('/data/') def send_data(path): return webserver_sub.send_data(path) @app.route('/redirect-http/') def redirect_http(path): """Redirect to the given (plaintext) HTTP port on localhost.""" host, _orig_port = flask.request.server port = flask.request.args["port"] return flask.redirect(f"http://{host}:{port}/{path}") @app.route('/favicon.ico') def favicon(): return webserver_sub.favicon() @app.after_request def log_request(response): return webserver_sub.log_request(response) def main(): webserver_sub.init_unraisable_hook() port = int(sys.argv[1]) server = webserver_sub.WSGIServer(('127.0.0.1', port), app) ssl_dir = webserver_sub.END2END_DIR / 'data' / 'ssl' server.ssl_adapter = cheroot.ssl.builtin.BuiltinSSLAdapter( certificate=ssl_dir / 'cert.pem', private_key=ssl_dir / 'key.pem', ) server.start() if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6026382 qutebrowser-3.6.1/tests/end2end/misc/0000755000175100017510000000000015102145351017162 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/misc/test_runners_e2e.py0000644000175100017510000000450415102145205023023 0ustar00runnerrunner# SPDX-FileCopyrightText: Jay Kamat # # SPDX-License-Identifier: GPL-3.0-or-later """Tests for runners.""" import logging import pytest def command_expansion_base( quteproc, send_msg, recv_msg, url="data/hello.txt"): quteproc.open_path(url) quteproc.send_cmd(':message-info ' + send_msg) quteproc.mark_expected(category='message', loglevel=logging.INFO, message=recv_msg) @pytest.mark.parametrize('send_msg, recv_msg', [ # escaping by double-quoting ('foo{{url}}bar', 'foo{url}bar'), ('foo{url}', 'foohttp://localhost:*/hello.txt'), ('foo{url:pretty}', 'foohttp://localhost:*/hello.txt'), ('foo{url:domain}', 'foohttp://localhost:*'), # test {url:auth} on a site with no auth ('foo{url:auth}', 'foo'), ('foo{url:scheme}', 'foohttp'), ('foo{url:host}', 'foolocalhost'), ('foo{url:path}', 'foo*/hello.txt'), ]) def test_command_expansion(quteproc, send_msg, recv_msg): command_expansion_base(quteproc, send_msg, recv_msg) @pytest.mark.parametrize('send_msg, recv_msg, url', [ ('foo{title}', 'fooTest title', 'data/title.html'), ('foo{url:query}', 'fooq=bar', 'data/hello.txt?q=bar'), ('foo{url:yank}', 'foohttp://localhost:*/hello.txt', 'data/hello.txt?ref=test'), # multiple variable expansion ('{title}bar{url}', 'Test titlebarhttp://localhost:*/title.html', 'data/title.html'), ]) def test_command_expansion_complex( quteproc, send_msg, recv_msg, url): command_expansion_base(quteproc, send_msg, recv_msg, url) def test_command_expansion_basic_auth(quteproc, server): url = ('http://user1:password1@localhost:{port}/basic-auth/user1/password1' .format(port=server.port)) quteproc.open_url(url) quteproc.send_cmd(':message-info foo{url:auth}') quteproc.mark_expected( category='message', loglevel=logging.INFO, message='foouser1:password1@') def test_command_expansion_clipboard(quteproc): quteproc.send_cmd(':debug-set-fake-clipboard "foo"') command_expansion_base( quteproc, '{clipboard}bar{url}', "foobarhttp://localhost:*/hello.txt") quteproc.send_cmd(':debug-set-fake-clipboard "{{url}}"') command_expansion_base( quteproc, '{clipboard}bar{url}', "{url}barhttp://localhost:*/hello.txt") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6036382 qutebrowser-3.6.1/tests/end2end/templates/0000755000175100017510000000000015102145351020225 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/templates/headers-link.html0000644000175100017510000000034315102145205023457 0ustar00runnerrunner Link to header page headers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/templates/https-iframe.html0000644000175100017510000000031615102145205023514 0ustar00runnerrunner HTTPS iframe ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/templates/https-script.html0000644000175100017510000000041715102145205023557 0ustar00runnerrunner HTTPS script

Script not loaded.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/test_adblock_e2e.py0000644000175100017510000000266115102145205021775 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """End to end tests for adblocking.""" import pytest try: import adblock except ImportError: adblock = None needs_adblock_lib = pytest.mark.skipif( adblock is None, reason="Needs 'adblock' library") @pytest.mark.parametrize('method', [ 'auto', 'hosts', pytest.param('adblock', marks=needs_adblock_lib), pytest.param('both', marks=needs_adblock_lib), ]) def test_adblock(method, quteproc, server): for kind in ['hosts', 'adblock']: quteproc.set_setting( f'content.blocking.{kind}.lists', f"['http://localhost:{server.port}/data/blocking/qutebrowser-{kind}']" ) quteproc.set_setting('content.blocking.method', method) quteproc.send_cmd(':adblock-update') quteproc.wait_for(message="hostblock: Read 1 hosts from 1 sources.") if adblock is not None: quteproc.wait_for( message="braveadblock: Filters successfully read from 1 sources.") quteproc.open_path('data/blocking/external_logo.html') if method in ['hosts', 'both'] or (method == 'auto' and adblock is None): message = "Request to qutebrowser.org blocked by host blocker." else: message = ("Request to https://qutebrowser.org/icons/qutebrowser.svg blocked " "by ad blocker.") quteproc.wait_for(message=message) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/test_dirbrowser.py0000644000175100017510000001316615102145205022027 0ustar00runnerrunner# SPDX-FileCopyrightText: Daniel Schadt # SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test the built-in directory browser.""" import pathlib import dataclasses import pytest import bs4 from qutebrowser.qt.core import QUrl from qutebrowser.utils import urlutils from helpers import testutils pytestmark = pytest.mark.qtwebengine_skip("Title is empty when parsing for " "some reason?") class DirLayout: """Provide a fake directory layout to test dirbrowser.""" LAYOUT = [ 'folder0/file00', 'folder0/file01', 'folder1/folder10/file100', 'folder1/file10', 'folder1/file11', 'file0', 'file1', ] @classmethod def layout_folders(cls): """Return all folders in the root directory of the layout.""" folders = set() for path in cls.LAYOUT: parts = path.split('/') if len(parts) > 1: folders.add(parts[0]) folders = list(folders) folders.sort() return folders @classmethod def get_folder_content(cls, name): """Return (folders, files) for the given folder in the root dir.""" folders = set() files = set() for path in cls.LAYOUT: if not path.startswith(name + '/'): continue parts = path.split('/') if len(parts) == 2: files.add(parts[1]) else: folders.add(parts[1]) folders = list(folders) folders.sort() files = list(files) files.sort() return (folders, files) def __init__(self, factory): self._factory = factory self.base = factory.getbasetemp() self.layout = factory.mktemp('layout') self._mklayout() def _mklayout(self): for filename in self.LAYOUT: path = self.layout / filename path.parent.mkdir(exist_ok=True, parents=True) path.touch() def file_url(self): """Return a file:// link to the directory.""" return urlutils.file_url(str(self.layout)) def path(self, *parts): """Return the path to the given file inside the layout folder.""" return self.layout.joinpath(*parts) def base_path(self): """Return the path of the base temporary folder.""" return self.base @dataclasses.dataclass class Parsed: path: str parent: str folders: list[str] files: list[str] @dataclasses.dataclass class Item: path: str link: str text: str def parse(quteproc): """Parse the dirbrowser content from the given quteproc. Args: quteproc: The quteproc fixture. """ html = quteproc.get_content(plain=False) soup = bs4.BeautifulSoup(html, 'html.parser') with testutils.ignore_bs4_warning(): print(soup.prettify()) title_prefix = 'Browse directory: ' # Strip off the title prefix to obtain the path of the folder that # we're browsing path = pathlib.Path(soup.title.string.removeprefix(title_prefix)) container = soup('div', id='dirbrowserContainer')[0] parent_elem = container('ul', class_='parent') if not parent_elem: parent = None else: parent = pathlib.Path(QUrl(parent_elem[0].li.a['href']).toLocalFile()) folders = [] files = [] for css_class, list_ in [('folders', folders), ('files', files)]: for li in container('ul', class_=css_class)[0]('li'): item_path = pathlib.Path(QUrl(li.a['href']).toLocalFile()) list_.append(Item(path=item_path, link=li.a['href'], text=str(li.a.string))) return Parsed(path=path, parent=parent, folders=folders, files=files) @pytest.fixture(scope='module') def dir_layout(tmp_path_factory): return DirLayout(tmp_path_factory) def test_parent_folder(dir_layout, quteproc): quteproc.open_url(dir_layout.file_url()) page = parse(quteproc) assert page.parent == dir_layout.base_path() def test_parent_with_slash(dir_layout, quteproc): """Test the parent link with a URL that has a trailing slash.""" quteproc.open_url(dir_layout.file_url() + '/') page = parse(quteproc) assert page.parent == dir_layout.base_path() def test_parent_in_root_dir(dir_layout, quteproc): # This actually works on windows urlstr = urlutils.file_url(str(pathlib.Path('/'))) quteproc.open_url(urlstr) page = parse(quteproc) assert page.parent is None def test_enter_folder_smoke(dir_layout, quteproc): quteproc.open_url(dir_layout.file_url()) quteproc.send_cmd(':hint all normal') # a is the parent link, s is the first listed folder/file quteproc.send_cmd(':hint-follow s') expected_url = urlutils.file_url(str(dir_layout.path('folder0'))) quteproc.wait_for_load_finished_url(expected_url) page = parse(quteproc) assert page.path == dir_layout.path('folder0') @pytest.mark.parametrize('folder', DirLayout.layout_folders()) def test_enter_folder(dir_layout, quteproc, folder): quteproc.open_url(dir_layout.file_url()) quteproc.click_element_by_text(text=folder) expected_url = urlutils.file_url(str(dir_layout.path(folder))) quteproc.wait_for_load_finished_url(expected_url) page = parse(quteproc) assert page.path == dir_layout.path(folder) assert page.parent == dir_layout.path() folders, files = DirLayout.get_folder_content(folder) foldernames = [item.text for item in page.folders] assert foldernames == folders filenames = [item.text for item in page.files] assert filenames == files ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/test_hints_html.py0000644000175100017510000001114515102145205022011 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test hints based on html files with special comments.""" import pathlib import textwrap import dataclasses from typing import Optional import pytest import bs4 from qutebrowser.utils import utils def collect_tests(): basedir = pathlib.Path(__file__).parent datadir = basedir / 'data' / 'hints' / 'html' files = [f.name for f in datadir.iterdir() if f.name != 'README.md'] return files @dataclasses.dataclass class ParsedFile: target: Optional[str] qtwebengine_todo: Optional[str] class InvalidFile(Exception): def __init__(self, test_name, msg): super().__init__("Invalid comment found in {}, please read " "tests/end2end/data/hints/html/README.md - {}".format( test_name, msg)) def _parse_file(test_name): """Parse the given HTML file.""" file_path = (pathlib.Path(__file__).parent.resolve() / 'data' / 'hints' / 'html' / test_name) with file_path.open('r', encoding='utf-8') as html: soup = bs4.BeautifulSoup(html, 'html.parser') comment = str(soup.find(string=lambda text: isinstance(text, bs4.Comment))) if comment is None: raise InvalidFile(test_name, "no comment found") data = utils.yaml_load(comment) if not isinstance(data, dict): raise InvalidFile(test_name, "expected yaml dict but got {}".format( type(data).__name__)) allowed_keys = {'target', 'qtwebengine_todo'} if not set(data.keys()).issubset(allowed_keys): raise InvalidFile(test_name, "expected keys {} but found {}".format( ', '.join(allowed_keys), ', '.join(set(data.keys())))) if 'target' not in data: raise InvalidFile(test_name, "'target' key not found") qtwebengine_todo = data.get('qtwebengine_todo', None) return ParsedFile(target=data['target'], qtwebengine_todo=qtwebengine_todo) @pytest.mark.parametrize('test_name', collect_tests()) @pytest.mark.parametrize('zoom_text_only', [True, False]) @pytest.mark.parametrize('zoom_level', [100, 66, 33]) @pytest.mark.parametrize('find_implementation', ['javascript', 'python']) def test_hints(test_name, zoom_text_only, zoom_level, find_implementation, quteproc, request): if zoom_text_only and request.config.webengine: pytest.skip("QtWebEngine doesn't have zoom.text_only") if find_implementation == 'python' and request.config.webengine: pytest.skip("QtWebEngine doesn't have a python find implementation") parsed = _parse_file(test_name) if parsed.qtwebengine_todo is not None and request.config.webengine: pytest.xfail("QtWebEngine TODO: {}".format(parsed.qtwebengine_todo)) url_path = 'data/hints/html/{}'.format(test_name) quteproc.open_path(url_path) # setup if not request.config.webengine: quteproc.set_setting('zoom.text_only', str(zoom_text_only)) quteproc.set_setting('hints.find_implementation', find_implementation) quteproc.send_cmd(':zoom {}'.format(zoom_level)) # follow hint quteproc.send_cmd(':hint all normal') if parsed.target is None: msg = quteproc.wait_for(message='No elements found.', category='message') msg.expected = True else: quteproc.wait_for(message='hints: a', category='hints') quteproc.send_cmd(':hint-follow a') quteproc.wait_for_load_finished('data/' + parsed.target) # reset quteproc.send_cmd(':zoom 100') if not request.config.webengine: quteproc.set_setting('zoom.text_only', 'false') quteproc.set_setting('hints.find_implementation', 'javascript') @pytest.mark.skip # Too flaky def test_word_hints_issue1393(quteproc, tmp_path): dict_file = tmp_path / 'dict' dict_file.write_text(textwrap.dedent(""" alph beta gamm delt epsi """)) targets = [ ('words', 'words.txt'), ('smart', 'smart.txt'), ('hinting', 'hinting.txt'), ('alph', 'l33t.txt'), ('beta', 'l33t.txt'), ('gamm', 'l33t.txt'), ('delt', 'l33t.txt'), ('epsi', 'l33t.txt'), ] quteproc.set_setting('hints.mode', 'word') quteproc.set_setting('hints.dictionary', str(dict_file)) for hint, target in targets: quteproc.open_path('data/hints/issue1393.html') quteproc.send_cmd(':hint') quteproc.wait_for(message='hints: *', category='hints') quteproc.send_cmd(':hint-follow {}'.format(hint)) quteproc.wait_for_load_finished('data/{}'.format(target)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/test_insert_mode.py0000644000175100017510000001005015102145205022142 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test insert mode settings on html files.""" import pytest @pytest.mark.parametrize('file_name, elem_id, source, input_text', [ ('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser'), ('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser'), ('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser'), ('input.html', 'qute-input', 'keypress', 'awesomequtebrowser'), pytest.param('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser', marks=pytest.mark.flaky), ]) @pytest.mark.parametrize('zoom', [100, 125, 250]) def test_insert_mode(file_name, elem_id, source, input_text, zoom, quteproc, request): url_path = 'data/insert_mode_settings/html/{}'.format(file_name) quteproc.open_path(url_path) quteproc.send_cmd(':zoom {}'.format(zoom)) quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id)) quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)') quteproc.send_cmd(':debug-set-fake-clipboard') if source == 'keypress': quteproc.press_keys(input_text) elif source == 'clipboard': quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(input_text)) quteproc.send_cmd(':insert-text {clipboard}') else: raise ValueError("Invalid source {!r}".format(source)) quteproc.wait_for_js('contents: {}'.format(input_text)) quteproc.send_cmd(':mode-leave') @pytest.mark.parametrize('auto_load, background, insert_mode', [ (False, False, False), # auto_load disabled (True, False, True), # enabled and foreground tab (True, True, False), # background tab ]) def test_auto_load(quteproc, auto_load, background, insert_mode): quteproc.set_setting('input.insert_mode.auto_load', str(auto_load)) url_path = 'data/insert_mode_settings/html/autofocus.html' quteproc.open_path(url_path, new_bg_tab=background) log_message = 'Entering mode KeyMode.insert (reason: *)' if insert_mode: quteproc.wait_for(message=log_message) quteproc.send_cmd(':mode-leave') else: quteproc.ensure_not_logged(message=log_message) def test_auto_load_delayed_tab_close(quteproc): """We shouldn't try to run JS on dead tabs async. Triggering the bug is pretty timing-dependent, so this test might still pass even if a bug is present. Howevber, with those timings, it triggers consistently on my machine. """ quteproc.set_setting('input.insert_mode.auto_load', "true") quteproc.send_cmd(":cmd-later 50 open -t about:blank") quteproc.send_cmd(":cmd-later 110 tab-close") quteproc.wait_for(message="command called: tab-close") def test_auto_leave_insert_mode(quteproc): quteproc.set_setting('input.insert_mode.auto_load', 'true') url_path = 'data/insert_mode_settings/html/autofocus.html' quteproc.open_path(url_path) quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)') quteproc.set_setting('input.insert_mode.auto_leave', 'true') quteproc.send_cmd(':zoom 100') quteproc.press_keys('abcd') quteproc.send_cmd(':hint all') quteproc.wait_for(message='hints: *') # Select the disabled input box to leave insert mode quteproc.send_cmd(':hint-follow s') quteproc.wait_for(message='Clicked non-editable element!') @pytest.mark.parametrize('leave_on_load', [True, False]) def test_auto_leave_insert_mode_reload(quteproc, leave_on_load): url_path = 'data/hello.txt' quteproc.open_path(url_path) quteproc.set_setting('input.insert_mode.leave_on_load', str(leave_on_load).lower()) quteproc.send_cmd(':mode-enter insert') quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)') quteproc.send_cmd(':reload') if leave_on_load: quteproc.wait_for(message='Leaving mode KeyMode.insert (reason: *)') else: quteproc.wait_for( message='Ignoring leave_on_load request due to setting.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/test_invocations.py0000644000175100017510000011200315102145205022167 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test starting qutebrowser with special arguments/environments.""" import os import signal import configparser import subprocess import sys import logging import importlib import re import json import platform from contextlib import nullcontext as does_not_raise from unittest.mock import ANY import pytest from qutebrowser.qt.core import QProcess, QPoint from helpers import testutils from qutebrowser.utils import qtutils, utils, version ascii_locale = pytest.mark.skipif(sys.hexversion >= 0x03070000, reason="Python >= 3.7 doesn't force ASCII " "locale with LC_ALL=C") # For some reason (some floating point rounding differences?), color values are # slightly different (and wrong!) on ARM machines. We adjust our expected values # accordingly, since we don't really care about the exact value, we just want to # know that the underlying Chromium is respecting our preferences. # FIXME what to do about 32-bit ARM? IS_ARM = platform.machine() == 'aarch64' def _base_args(config): """Get the arguments to pass with every invocation.""" args = ['--debug', '--json-logging', '--no-err-windows'] if config.webengine: args += ['--backend', 'webengine'] else: args += ['--backend', 'webkit'] if config.webengine: if testutils.disable_seccomp_bpf_sandbox(): args += testutils.DISABLE_SECCOMP_BPF_ARGS if testutils.use_software_rendering(): args += testutils.SOFTWARE_RENDERING_ARGS args.append('about:blank') return args @pytest.fixture def runtime_tmpdir(short_tmpdir): """A directory suitable for XDG_RUNTIME_DIR.""" runtime_dir = short_tmpdir / 'rt' runtime_dir.ensure(dir=True) runtime_dir.chmod(0o700) return runtime_dir @pytest.fixture def temp_basedir_env(tmp_path, runtime_tmpdir): """Return a dict of environment variables that fakes --temp-basedir. We can't run --basedir or --temp-basedir for some tests, so we mess with XDG_*_DIR to get things relocated. """ data_dir = tmp_path / 'data' config_dir = tmp_path / 'config' cache_dir = tmp_path / 'cache' lines = [ '[general]', 'quickstart-done = 1', 'backend-warning-shown = 1', 'webkit-warning-shown = 1', ] state_file = data_dir / 'qutebrowser' / 'state' state_file.parent.mkdir(parents=True) state_file.write_text('\n'.join(lines), encoding='utf-8') env = { 'XDG_DATA_HOME': str(data_dir), 'XDG_CONFIG_HOME': str(config_dir), 'XDG_RUNTIME_DIR': str(runtime_tmpdir), 'XDG_CACHE_HOME': str(cache_dir), } return env @pytest.mark.linux @ascii_locale def test_downloads_with_ascii_locale(request, server, tmp_path, quteproc_new): """Test downloads with LC_ALL=C set. https://github.com/qutebrowser/qutebrowser/issues/908 https://github.com/qutebrowser/qutebrowser/issues/1726 """ args = ['--temp-basedir'] + _base_args(request.config) quteproc_new.start(args, env={'LC_ALL': 'C'}) quteproc_new.set_setting('downloads.location.directory', str(tmp_path)) # Test a normal download quteproc_new.set_setting('downloads.location.prompt', 'false') url = 'http://localhost:{port}/data/downloads/ä-issue908.bin'.format( port=server.port) quteproc_new.send_cmd(':download {}'.format(url)) quteproc_new.wait_for(category='downloads', message='Download ?-issue908.bin finished') # Test :prompt-open-download quteproc_new.set_setting('downloads.location.prompt', 'true') quteproc_new.send_cmd(':download {}'.format(url)) quteproc_new.send_cmd(':prompt-open-download "{}" -c pass' .format(sys.executable)) quteproc_new.wait_for(category='downloads', message='Download ä-issue908.bin finished') quteproc_new.wait_for(category='misc', message='Opening * with [*python*]') assert len(list(tmp_path.iterdir())) == 1 assert (tmp_path / '?-issue908.bin').exists() @pytest.mark.linux @pytest.mark.parametrize('url', ['/föö.html', 'file:///föö.html']) @ascii_locale def test_open_with_ascii_locale(request, server, tmp_path, quteproc_new, url): """Test opening non-ascii URL with LC_ALL=C set. https://github.com/qutebrowser/qutebrowser/issues/1450 """ args = ['--temp-basedir'] + _base_args(request.config) quteproc_new.start(args, env={'LC_ALL': 'C'}) quteproc_new.set_setting('url.auto_search', 'never') # Test opening a file whose name contains non-ascii characters. # No exception thrown means test success. quteproc_new.send_cmd(':open {}'.format(url)) if not request.config.webengine: line = quteproc_new.wait_for(message="Error while loading *: Error " "opening /*: No such file or directory") line.expected = True quteproc_new.wait_for(message="load status for <* tab_id=* " "url='*/f%C3%B6%C3%B6.html'>: LoadStatus.error") if request.config.webengine: line = quteproc_new.wait_for(message='Load error: ERR_FILE_NOT_FOUND') line.expected = True @pytest.mark.linux @ascii_locale def test_open_command_line_with_ascii_locale(request, server, tmp_path, quteproc_new): """Test opening file via command line with a non-ascii name with LC_ALL=C. https://github.com/qutebrowser/qutebrowser/issues/1450 """ # The file does not actually have to exist because the relevant checks will # all be called. No exception thrown means test success. args = (['--temp-basedir'] + _base_args(request.config) + ['/home/user/föö.html']) quteproc_new.start(args, env={'LC_ALL': 'C'}) if not request.config.webengine: line = quteproc_new.wait_for(message="Error while loading *: Error " "opening /*: No such file or directory") line.expected = True quteproc_new.wait_for(message="load status for <* tab_id=* " "url='*/f*.html'>: LoadStatus.error") if request.config.webengine: line = quteproc_new.wait_for(message="Load error: ERR_FILE_NOT_FOUND") line.expected = True @pytest.mark.linux def test_misconfigured_user_dirs(request, server, temp_basedir_env, tmp_path, quteproc_new): """Test downloads with a misconfigured XDG_DOWNLOAD_DIR. https://github.com/qutebrowser/qutebrowser/issues/866 https://github.com/qutebrowser/qutebrowser/issues/1269 """ home = tmp_path / 'home' home.mkdir() temp_basedir_env['HOME'] = str(home) config_userdir_dir = tmp_path / 'config' config_userdir_dir.mkdir(parents=True) config_userdir_file = tmp_path / 'config' / 'user-dirs.dirs' config_userdir_file.touch() assert temp_basedir_env['XDG_CONFIG_HOME'] == str(tmp_path / 'config') config_userdir_file.write_text('XDG_DOWNLOAD_DIR="relative"') quteproc_new.start(_base_args(request.config), env=temp_basedir_env) quteproc_new.set_setting('downloads.location.prompt', 'false') url = 'http://localhost:{port}/data/downloads/download.bin'.format( port=server.port) quteproc_new.send_cmd(':download {}'.format(url)) line = quteproc_new.wait_for( loglevel=logging.ERROR, category='message', message='XDG_DOWNLOAD_DIR points to a relative path - please check ' 'your ~/.config/user-dirs.dirs. The download is saved in your ' 'home directory.') line.expected = True quteproc_new.wait_for(category='downloads', message='Download download.bin finished') assert (home / 'download.bin').exists() def test_no_loglines(request, quteproc_new): """Test qute://log with --loglines=0.""" quteproc_new.start(args=['--temp-basedir', '--loglines=0'] + _base_args(request.config)) quteproc_new.open_path('qute://log') assert quteproc_new.get_content() == 'Log output was disabled.' @pytest.mark.not_frozen @pytest.mark.parametrize('level', ['1', '2']) def test_optimize(request, quteproc_new, capfd, level): quteproc_new.start(args=['--temp-basedir'] + _base_args(request.config), env={'PYTHONOPTIMIZE': level}) if level == '2': msg = ("Running on optimize level higher than 1, unexpected behavior " "may occur.") line = quteproc_new.wait_for(message=msg) line.expected = True # Waiting for quit to make sure no other warning is emitted quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() @pytest.mark.not_frozen @pytest.mark.flaky # Fails sometimes with empty output... def test_version(request): """Test invocation with --version argument.""" args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) # can't use quteproc_new here because it's confused by # early process termination proc = QProcess() proc.setProcessChannelMode(QProcess.ProcessChannelMode.SeparateChannels) proc.start(sys.executable, args) ok = proc.waitForStarted(2000) assert ok ok = proc.waitForFinished(10000) stdout = bytes(proc.readAllStandardOutput()).decode('utf-8') print(stdout) stderr = bytes(proc.readAllStandardError()).decode('utf-8') print(stderr) assert ok assert proc.exitStatus() == QProcess.ExitStatus.NormalExit match = re.search(r'^qutebrowser\s+v\d+(\.\d+)', stdout, re.MULTILINE) assert match is not None def test_qt_arg(request, quteproc_new, tmp_path): """Test --qt-arg.""" args = (['--temp-basedir', '--qt-arg', 'stylesheet', str(tmp_path / 'does-not-exist')] + _base_args(request.config)) quteproc_new.start(args) msg = 'QCss::Parser - Failed to load file "*does-not-exist"' line = quteproc_new.wait_for(message=msg) line.expected = True quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() @pytest.mark.linux def test_webengine_download_suffix(request, quteproc_new, tmp_path): """Make sure QtWebEngine does not add a suffix to downloads.""" if not request.config.webengine: pytest.skip() download_dir = tmp_path / 'downloads' download_dir.mkdir() (tmp_path / 'user-dirs.dirs').write_text( 'XDG_DOWNLOAD_DIR={}'.format(download_dir)) env = {'XDG_CONFIG_HOME': str(tmp_path)} args = ['--temp-basedir'] + _base_args(request.config) quteproc_new.start(args, env=env) quteproc_new.set_setting('downloads.location.prompt', 'false') quteproc_new.set_setting('downloads.location.directory', str(download_dir)) quteproc_new.open_path('data/downloads/download.bin', wait=False) quteproc_new.wait_for(category='downloads', message='Download * finished') quteproc_new.open_path('data/downloads/download.bin', wait=False) quteproc_new.wait_for(message='Entering mode KeyMode.yesno *') quteproc_new.send_cmd(':prompt-accept yes') quteproc_new.wait_for(category='downloads', message='Download * finished') files = list(download_dir.iterdir()) assert len(files) == 1 assert files[0].name == 'download.bin' def test_command_on_start(request, quteproc_new): """Make sure passing a command on start works. See https://github.com/qutebrowser/qutebrowser/issues/2408 """ args = (['--temp-basedir'] + _base_args(request.config) + [':quickmark-add https://www.example.com/ example']) quteproc_new.start(args) quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() @pytest.mark.parametrize('python', ['python2', 'python3.6', 'python3.7']) def test_launching_with_old_python(python): try: proc = subprocess.run( [python, '-m', 'qutebrowser', '--no-err-windows'], stderr=subprocess.PIPE, check=False) except FileNotFoundError: pytest.skip(f"{python} not found") assert proc.returncode == 1 error = "At least Python 3.9 is required to run qutebrowser" assert proc.stderr.decode('ascii').startswith(error) def test_initial_private_browsing(request, quteproc_new): """Make sure the initial window is private when the setting is set.""" args = (_base_args(request.config) + ['--temp-basedir', '-s', 'content.private_browsing', 'true']) quteproc_new.start(args) quteproc_new.compare_session(""" windows: - private: True tabs: - history: - url: about:blank """) quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() def test_loading_empty_session(tmp_path, request, quteproc_new): """Make sure loading an empty session opens a window.""" session = tmp_path / 'session.yml' session.write_text('windows: []') args = _base_args(request.config) + ['--temp-basedir', '-r', str(session)] quteproc_new.start(args) quteproc_new.compare_session(""" windows: - tabs: - history: - url: about:blank """) quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): """Make sure settings from qute://settings are persistent.""" args = _base_args(request.config) + ['--basedir', str(short_tmpdir)] quteproc_new.start(args) quteproc_new.open_path('qute://settings/') quteproc_new.send_cmd(':jseval --world main ' 'cset("search.ignore_case", "always")') quteproc_new.wait_for(message='No output or error') quteproc_new.wait_for(category='config', message='Config option changed: ' 'search.ignore_case = always') assert quteproc_new.get_setting('search.ignore_case') == 'always' quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() quteproc_new.start(args) assert quteproc_new.get_setting('search.ignore_case') == 'always' quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() @pytest.mark.parametrize('value, expected', [ # https://chromium-review.googlesource.com/c/chromium/src/+/2545444 pytest.param( 'always', 'http://localhost:(port2)/headers-link/(port)', marks=pytest.mark.qt5_only, ), pytest.param( 'always', 'http://localhost:(port2)/', marks=pytest.mark.qt6_only, ), ('never', None), ('same-domain', 'http://localhost:(port2)/'), # None with QtWebKit ]) def test_referrer(quteproc_new, server, server2, request, value, expected): """Check referrer settings.""" args = _base_args(request.config) + [ '--temp-basedir', '-s', 'content.headers.referer', value, ] quteproc_new.start(args) quteproc_new.open_path(f'headers-link/{server.port}', port=server2.port) quteproc_new.send_cmd(':click-element id link') quteproc_new.wait_for_load_finished('headers') content = quteproc_new.get_content() data = json.loads(content) print(data) headers = data['headers'] if not request.config.webengine and value == 'same-domain': # With QtWebKit and same-domain, we don't send a referer at all. expected = None if expected is not None: for key, val in [('(port)', server.port), ('(port2)', server2.port)]: expected = expected.replace(key, str(val)) assert headers.get('Referer') == expected def test_preferred_colorscheme_unsupported(request, quteproc_new): """Test versions without preferred-color-scheme support.""" if request.config.webengine: pytest.skip("preferred-color-scheme is supported") args = _base_args(request.config) + ['--temp-basedir'] quteproc_new.start(args) quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') content = quteproc_new.get_content() assert content == "Preference support missing." @pytest.mark.qtwebkit_skip @pytest.mark.parametrize('value', ["dark", "light", "auto", None]) def test_preferred_colorscheme(request, quteproc_new, value): """Make sure the the preferred colorscheme is set.""" if not request.config.webengine: pytest.skip("Skipped with QtWebKit") args = _base_args(request.config) + ['--temp-basedir'] if value is not None: args += ['-s', 'colors.webpage.preferred_color_scheme', value] quteproc_new.start(args) dark_text = "Dark preference detected." light_text = "Light preference detected." expected_values = { "dark": [dark_text], "light": [light_text], # Depends on the environment the test is running in. "auto": [dark_text, light_text], None: [dark_text, light_text], } xfail = False if qtutils.version_check('5.15.2', exact=True, compiled=False): # Test the WORKAROUND https://bugreports.qt.io/browse/QTBUG-89753 # With that workaround, we should always get the light preference. for key in ["auto", None]: expected_values[key].remove(dark_text) xfail = value in ["auto", None] quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') content = quteproc_new.get_content() assert content in expected_values[value] if xfail: # Unsatisfactory result, but expected based on a Qt bug. pytest.xfail("QTBUG-89753") def test_preferred_colorscheme_with_dark_mode( request, quteproc_new, webengine_versions): """Test interaction between preferred-color-scheme and dark mode. We would actually expect a color of 34, 34, 34 and 'Dark preference detected.'. That was the behavior on Qt 5.14 and 5.15.0/.1. """ if not request.config.webengine: pytest.skip("Skipped with QtWebKit") args = _base_args(request.config) + [ '--temp-basedir', '-s', 'colors.webpage.preferred_color_scheme', 'dark', '-s', 'colors.webpage.darkmode.enabled', 'true', '-s', 'colors.webpage.darkmode.algorithm', 'brightness-rgb', ] if webengine_versions.webengine == utils.VersionNumber(6, 9): # WORKAROUND: For unknown reasons, dark mode colors are wrong with # Qt 6.9 + hardware rendering + Xvfb. args += testutils.SOFTWARE_RENDERING_ARGS quteproc_new.start(args) quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') content = quteproc_new.get_content() if webengine_versions.webengine == utils.VersionNumber(5, 15, 2): # Our workaround breaks when dark mode is enabled... # Also, for some reason, dark mode doesn't work on that page either! expected_text = 'No preference detected.' expected_color = testutils.Color(0, 170, 0) # green xfail = "QTBUG-89753" elif webengine_versions.webengine < utils.VersionNumber(6, 4): # https://bugs.chromium.org/p/chromium/issues/detail?id=1177973 # No workaround known. expected_text = 'Light preference detected.' # light website color, inverted by darkmode if webengine_versions.webengine >= utils.VersionNumber(6): expected_color = (testutils.Color(148, 146, 148) if IS_ARM else testutils.Color(144, 144, 144)) else: expected_color = (testutils.Color(123, 125, 123) if IS_ARM else testutils.Color(127, 127, 127)) xfail = "Chromium bug 1177973" else: # Correct behavior on QtWebEngine 6.4 (and 5.14/5.15.0/5.15.1 in the past) expected_text = 'Dark preference detected.' expected_color = (testutils.Color(33, 32, 33) if IS_ARM else testutils.Color(34, 34, 34)) # dark website color xfail = False pos = QPoint(0, 0) img = quteproc_new.get_screenshot(probe_pos=pos, probe_color=expected_color) color = testutils.Color(img.pixelColor(pos)) assert content == expected_text assert color == expected_color if xfail: # We still do some checks, but we want to mark the test outcome as xfail. pytest.xfail(xfail) @pytest.mark.qtwebkit_skip @pytest.mark.parametrize('reason', [ 'Explicitly enabled', 'Qt version changed', None, ]) def test_service_worker_workaround( request, server, quteproc_new, short_tmpdir, reason): """Make sure we remove the QtWebEngine Service Worker directory if configured.""" args = _base_args(request.config) + ['--basedir', str(short_tmpdir)] if reason == 'Explicitly enabled': settings_args = ['-s', 'qt.workarounds.remove_service_workers', 'true'] else: settings_args = [] service_worker_dir = short_tmpdir / 'data' / 'webengine' / 'Service Worker' # First invocation: Create directory quteproc_new.start(args) quteproc_new.open_path('data/service-worker/index.html') server.wait_for(verb='GET', path='/data/service-worker/data.json') quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() assert service_worker_dir.exists() # Edit state file if needed state_file = short_tmpdir / 'data' / 'state' if reason == 'Qt version changed': parser = configparser.ConfigParser() parser.read(state_file) del parser['general']['qt_version'] with state_file.open('w', encoding='utf-8') as f: parser.write(f) # Second invocation: Directory gets removed (if workaround enabled) quteproc_new.start(args + settings_args) if reason is not None: quteproc_new.wait_for( message=(f'Removing service workers at {service_worker_dir} ' f'(reason: {reason})')) quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() if reason is None: assert service_worker_dir.exists() quteproc_new.ensure_not_logged(message='Removing service workers at *') else: assert not service_worker_dir.exists() @pytest.mark.parametrize('store', [True, False]) def test_cookies_store(quteproc_new, request, short_tmpdir, store): # Start test process args = _base_args(request.config) + [ '--basedir', str(short_tmpdir), '-s', 'content.cookies.store', str(store), ] quteproc_new.start(args) # Set cookie and ensure it's set quteproc_new.open_path('cookies/set-custom?max_age=30', wait=False) quteproc_new.wait_for_load_finished('cookies') content = quteproc_new.get_content() data = json.loads(content) assert data == {'cookies': {'cookie': 'value'}} # Restart quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() quteproc_new.start(args) # Check cookies quteproc_new.open_path('cookies') content = quteproc_new.get_content() data = json.loads(content) expected_cookies = {'cookie': 'value'} if store else {} assert data == {'cookies': expected_cookies} quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() def test_permission_prompt_across_restart(quteproc_new, request, short_tmpdir): # Start test process args = _base_args(request.config) + [ '--basedir', str(short_tmpdir), '-s', 'content.notifications.enabled', 'ask', ] quteproc_new.start(args) def notification_prompt(answer): quteproc_new.open_path('data/prompt/notifications.html') quteproc_new.send_cmd(':click-element id button') quteproc_new.wait_for(message='Asking question *') quteproc_new.send_cmd(f':prompt-accept {answer}') # Make sure we are prompted the first time we are opened in this basedir notification_prompt('yes') quteproc_new.wait_for_js('notification permission granted') # Restart with same basedir quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() quteproc_new.start(args) # We should be re-prompted in the new instance notification_prompt('no') quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() # The 'colors' dictionaries in the parametrize decorator below have (QtWebEngine # version, CPU architecture) as keys. Either of those (or both) can be None to # say "on all other Qt versions" or "on all other CPU architectures". @pytest.mark.parametrize('filename, algorithm, colors', [ ( 'blank', 'lightness-cielab', { (None, None): testutils.Color(18, 18, 18), (None, 'aarch64'): testutils.Color(16, 16, 16), } ), ( 'blank', 'lightness-hsl', { ('5.15', None): testutils.Color(0, 0, 0), ('6.2', None): testutils.Color(0, 0, 0), # Qt 6.3+ (why #121212 rather than #000000?) (None, None): testutils.Color(18, 18, 18), } ), ( 'blank', 'brightness-rgb', { ('5.15', None): testutils.Color(0, 0, 0), ('6.2', None): testutils.Color(0, 0, 0), # Qt 6.3+ (why #121212 rather than #000000?) (None, None): testutils.Color(18, 18, 18), } ), ( 'yellow', 'lightness-cielab', { (None, None): testutils.Color(35, 34, 0), (None, 'aarch64'): testutils.Color(33, 32, 0), } ), ( 'yellow', 'lightness-hsl', { (None, None): testutils.Color(215, 215, 0), (None, 'aarch64'): testutils.Color(214, 215, 0), ('5.15', None): testutils.Color(204, 204, 0), ('5.15', 'aarch64'): testutils.Color(206, 207, 0), }, ), ( 'yellow', 'brightness-rgb', { (None, None): testutils.Color(0, 0, 215), (None, 'aarch64'): testutils.Color(0, 0, 214), ('5.15', None): testutils.Color(0, 0, 204), ('5.15', 'aarch64'): testutils.Color(0, 0, 206), } ), ]) def test_dark_mode(webengine_versions, quteproc_new, request, filename, algorithm, colors): if not request.config.webengine: pytest.skip("Skipped with QtWebKit") args = _base_args(request.config) + [ '--temp-basedir', '-s', 'colors.webpage.darkmode.enabled', 'true', '-s', 'colors.webpage.darkmode.algorithm', algorithm, ] if webengine_versions.webengine == utils.VersionNumber(6, 9): # WORKAROUND: For unknown reasons, dark mode colors are wrong with # Qt 6.9 + hardware rendering + Xvfb. args += testutils.SOFTWARE_RENDERING_ARGS quteproc_new.start(args) minor_version = str(webengine_versions.webengine.strip_patch()) arch = platform.machine() for key in [ (minor_version, arch), (minor_version, None), (None, arch), (None, None), ]: if key in colors: expected = colors[key] break quteproc_new.open_path(f'data/darkmode/{filename}.html') # Position chosen by fair dice roll. # https://xkcd.com/221/ quteproc_new.get_screenshot( probe_pos=QPoint(4, 4), probe_color=expected, ) @pytest.mark.parametrize("suffix", ["inline", "display"]) def test_dark_mode_mathml(webengine_versions, quteproc_new, request, qtbot, suffix): if not request.config.webengine: pytest.skip("Skipped with QtWebKit") args = _base_args(request.config) + [ '--temp-basedir', '-s', 'colors.webpage.darkmode.enabled', 'true', '-s', 'colors.webpage.darkmode.algorithm', 'brightness-rgb', ] if webengine_versions.webengine == utils.VersionNumber(6, 9): # WORKAROUND: For unknown reasons, dark mode colors are wrong with # Qt 6.9 + hardware rendering + Xvfb. args += testutils.SOFTWARE_RENDERING_ARGS quteproc_new.start(args) quteproc_new.open_path(f'data/darkmode/mathml-{suffix}.html') quteproc_new.wait_for_js('Image loaded') # First make sure loading finished by looking outside of the image if webengine_versions.webengine >= utils.VersionNumber(6): expected = testutils.Color(0, 0, 214) if IS_ARM else testutils.Color(0, 0, 215) else: expected = testutils.Color(0, 0, 206) if IS_ARM else testutils.Color(0, 0, 204) quteproc_new.get_screenshot( probe_pos=QPoint(105, 0), probe_color=expected, ) # Then get the actual formula color, probing again in case it's not displayed yet... quteproc_new.get_screenshot( probe_pos=QPoint(4, 4), probe_color=testutils.Color(255, 255, 255), ) @pytest.mark.parametrize('value, preference', [ ('true', 'Reduced motion'), ('false', 'No'), ]) @pytest.mark.skipif( utils.is_windows, reason="Outcome on Windows depends on system settings", ) def test_prefers_reduced_motion(quteproc_new, request, value, preference): if not request.config.webengine: pytest.skip("Skipped with QtWebKit") args = _base_args(request.config) + [ '--temp-basedir', '-s', 'content.prefers_reduced_motion', value, ] quteproc_new.start(args) quteproc_new.open_path('data/prefers_reduced_motion.html') content = quteproc_new.get_content() assert content == f"{preference} preference detected." def test_unavailable_backend(request, quteproc_new): """Test starting with a backend which isn't available. If we use --qute-bdd-webengine, we test with QtWebKit here; otherwise we test with QtWebEngine. If both are available, the test is skipped. This ensures that we don't accidentally use backend-specific code before checking that the chosen backend is actually available - i.e., that the error message is properly printed, rather than an unhandled exception. """ qtwe_module = "qutebrowser.qt.webenginewidgets" qtwk_module = "qutebrowser.qt.webkitwidgets" # Note we want to try the *opposite* backend here. if request.config.webengine: pytest.importorskip(qtwe_module) module = qtwk_module backend = 'webkit' else: pytest.importorskip(qtwk_module) module = qtwe_module backend = 'webengine' try: importlib.import_module(module) except ImportError: pass else: pytest.skip(f"{module} is available") args = [ '--debug', '--json-logging', '--no-err-windows', '--backend', backend, '--temp-basedir' ] quteproc_new.exit_expected = True quteproc_new.start(args) line = quteproc_new.wait_for( message=('*qutebrowser tried to start with the Qt* backend but failed ' 'because * could not be imported.*')) line.expected = True def test_json_logging_without_debug(request, quteproc_new, runtime_tmpdir): args = _base_args(request.config) + ['--temp-basedir', ':quit'] args.remove('--debug') args.remove('about:blank') # interferes with :quit at the end quteproc_new.exit_expected = True quteproc_new.start(args, env={'XDG_RUNTIME_DIR': str(runtime_tmpdir)}) assert not quteproc_new.is_running() @pytest.mark.qtwebkit_skip @pytest.mark.parametrize( 'sandboxing, has_namespaces, has_seccomp, has_yama, expected_result', [ ('enable-all', True, True, True, "You are adequately sandboxed."), ('disable-seccomp-bpf', True, False, True, "You are NOT adequately sandboxed."), ('disable-all', False, False, False, "You are NOT adequately sandboxed."), ] ) def test_sandboxing( request, quteproc_new, sandboxing, has_namespaces, has_seccomp, has_yama, expected_result, ): # https://github.com/qutebrowser/qutebrowser/issues/8424 userns_restricted = testutils.is_userns_restricted() if not request.config.webengine: pytest.skip("Skipped with QtWebKit") elif sandboxing == "enable-all" and testutils.disable_seccomp_bpf_sandbox(): pytest.skip("Full sandboxing not supported") elif version.is_flatpak() or userns_restricted: # https://github.com/flathub/io.qt.qtwebengine.BaseApp/pull/66 has_namespaces = False expected_result = "You are NOT adequately sandboxed." has_yama_non_broker = has_yama else: has_yama_non_broker = False args = _base_args(request.config) + [ '--temp-basedir', '-s', 'qt.chromium.sandboxing', sandboxing, ] quteproc_new.start(args) quteproc_new.open_url('chrome://sandbox') text = quteproc_new.get_content() print(text) not_found_msg = ("The webpage at chrome://sandbox/ might be temporarily down or " "it may have moved permanently to a new web address.") if not_found_msg in text.split("\n"): line = quteproc_new.wait_for(message='Load error: ERR_INVALID_URL') line.expected = True pytest.skip("chrome://sandbox/ not supported") if len(text.split("\n")) == 1: # Try again, maybe the JS hasn't run yet? text = quteproc_new.get_content() print(text) bpf_text = "Seccomp-BPF sandbox" yama_text = "Ptrace Protection with Yama LSM" if not utils.is_windows: header, *lines, empty, result = text.split("\n") assert not empty expected_status = { "Layer 1 Sandbox": "Namespace" if has_namespaces else "None", "PID namespaces": "Yes" if has_namespaces else "No", "Network namespaces": "Yes" if has_namespaces else "No", bpf_text: "Yes" if has_seccomp else "No", f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No", f"{yama_text} (Broker)": "Yes" if has_yama else "No", # pylint: disable-next=used-before-assignment f"{yama_text} (Non-broker)": "Yes" if has_yama_non_broker else "No", } assert header == "Sandbox Status" assert result == expected_result status = dict(line.split("\t") for line in lines) assert status == expected_status else: # utils.is_windows # The sandbox page on Windows if different that Linux and macOS. It's # a lot more complex. There is a table up top with lots of columns and # a row per tab and helper process then a json object per row down # below with even more detail (which we ignore). # https://www.chromium.org/Home/chromium-security/articles/chrome-sandbox-diagnostics-for-windows/ # We're not getting full coverage of the table and there doesn't seem # to be a simple summary like for linux. The "Sandbox" and "Lockdown" # column are probably the key ones. # We are looking at all the rows in the table for the sake of # completeness, but I expect there will always be just one row with a # renderer process in it for this test. If other helper processes pop # up we might want to exclude them. lines = text.split("\n") assert lines.pop(0) == "Sandbox Status" header = lines.pop(0).split("\t") rows = [] current_line = lines.pop(0) while current_line.strip(): if lines[0].startswith("\t"): # Continuation line. Not sure how to 100% identify them # but new rows should start with a process ID. current_line += lines.pop(0) continue columns = current_line.split("\t") assert len(header) == len(columns) rows.append(dict(zip(header, columns))) current_line = lines.pop(0) assert rows # I'm using has_namespaces as a proxy for "should be sandboxed" here, # which is a bit lazy but its either that or match on the text # "sandboxing" arg. The seccomp-bpf arg does nothing on windows, so # we only have the off and on states. for row in rows: assert row == { "Process": ANY, "Type": "Renderer", "Name": "", "Sandbox": "Renderer" if has_namespaces else "Not Sandboxed", "Lockdown": "Lockdown" if has_namespaces else "", "Integrity": ANY if has_namespaces else "", "Mitigations": ANY if has_namespaces else "", "Component Filter": ANY if has_namespaces else "", "Lowbox/AppContainer": "", } @pytest.mark.not_frozen def test_logfilter_arg_does_not_crash(request, quteproc_new): args = ['--temp-basedir', '--debug', '--logfilter', 'commands, init, ipc, webview'] with does_not_raise(): quteproc_new.start(args=args + _base_args(request.config)) # Waiting for quit to make sure no other warning is emitted quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() def test_restart(request, quteproc_new): args = _base_args(request.config) + ['--temp-basedir'] quteproc_new.start(args) quteproc_new.send_cmd(':restart') prefix = "New process PID: " line = quteproc_new.wait_for(message=f"{prefix}*") quteproc_new.wait_for_quit() assert line.message.startswith(prefix) pid = int(line.message.removeprefix(prefix)) os.kill(pid, signal.SIGTERM) # This often hangs on Windows for unknown reasons if not utils.is_windows: try: # If the new process hangs, this will hang too. # Still better than just ignoring it, so we can fix it if something is broken. os.waitpid(pid, 0) # pid, options... positional-only :( except (ChildProcessError, PermissionError): # Already gone. Even if not documented, Windows seems to raise PermissionError # here... pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/end2end/test_mhtml_e2e.py0000644000175100017510000001033415102145205021513 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test mhtml downloads based on sample files.""" import pathlib import re import collections import pytest def collect_tests(): basedir = pathlib.Path(__file__).parent datadir = basedir / 'data' / 'downloads' / 'mhtml' files = [x.name for x in datadir.iterdir()] return files def normalize_line(line): line = line.rstrip('\n') line = re.sub('boundary="---=_qute-[0-9a-f-]+"', 'boundary="---=_qute-UUID"', line) line = re.sub('^-----+=_qute-[0-9a-f-]+$', '-----=_qute-UUID', line) line = re.sub(r'localhost:\d{1,5}', 'localhost:(port)', line) # Depending on Python's mimetypes module/the system's mime files, .js # files could be either identified as x-javascript or just javascript line = line.replace('Content-Type: application/x-javascript', 'Content-Type: application/javascript') # With QtWebKit and newer Werkzeug versions, we also get an encoding # specified. line = line.replace('javascript; charset=utf-8', 'javascript') return line class DownloadDir: """Abstraction over a download directory.""" def __init__(self, tmp_path, config): self._tmp_path = tmp_path self._config = config self.location = str(tmp_path) def read_file(self): files = list(self._tmp_path.iterdir()) assert len(files) == 1 return files[0].read_text(encoding='utf-8').splitlines() def sanity_check_mhtml(self): assert 'Content-Type: multipart/related' in '\n'.join(self.read_file()) def compare_mhtml(self, filename): with open(filename, 'r', encoding='utf-8') as f: expected_data = '\n'.join(normalize_line(line) for line in f if normalize_line(line) is not None) actual_data = '\n'.join(normalize_line(line) for line in self.read_file()) assert actual_data == expected_data @pytest.fixture def download_dir(tmp_path, pytestconfig): return DownloadDir(tmp_path, pytestconfig) def _test_mhtml_requests(test_dir, test_path, server): with (test_dir / 'requests').open(encoding='utf-8') as f: expected_requests = [] for line in f: if line.startswith('#'): continue path = '/{}/{}'.format(test_path, line.strip()) expected_requests.append(server.ExpectedRequest('GET', path)) actual_requests = server.get_requests() # Requests are not hashable, we need to convert to ExpectedRequests actual_requests = [server.ExpectedRequest.from_request(req) for req in actual_requests] assert (collections.Counter(actual_requests) == collections.Counter(expected_requests)) @pytest.mark.parametrize('test_name', collect_tests()) def test_mhtml(request, test_name, download_dir, quteproc, server): quteproc.set_setting('downloads.location.directory', download_dir.location) quteproc.set_setting('downloads.location.prompt', 'false') test_dir = (pathlib.Path(__file__).parent.resolve() / 'data' / 'downloads' / 'mhtml' / test_name) test_path = 'data/downloads/mhtml/{}'.format(test_name) url_path = '{}/{}.html'.format(test_path, test_name) quteproc.open_path(url_path) download_dest = (pathlib.Path(download_dir.location) / '{}-downloaded.mht'.format(test_name)) # Wait for favicon.ico to be loaded if there is one if (test_dir / 'favicon.png').exists(): server.wait_for(path='/{}/favicon.png'.format(test_path)) # Discard all requests that were necessary to display the page server.clear_data() quteproc.send_cmd(':download --mhtml --dest "{}"'.format(download_dest)) quteproc.wait_for(category='downloads', message='File successfully written.') if request.config.webengine: download_dir.sanity_check_mhtml() return filename = test_name + '.mht' expected_file = test_dir / filename download_dir.compare_mhtml(expected_file) _test_mhtml_requests(test_dir, test_path, server) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1762183912.604638 qutebrowser-3.6.1/tests/helpers/0000755000175100017510000000000015102145351016352 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/fixtures.py0000644000175100017510000005356715102145205020613 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=invalid-name """pytest fixtures used by the whole testsuite. See https://pytest.org/latest/fixture.html """ import sys import tempfile import itertools import textwrap import unittest.mock import types import mimetypes import os.path import dataclasses import pytest import py.path from qutebrowser.qt.core import QSize, Qt from qutebrowser.qt.widgets import QWidget, QHBoxLayout, QVBoxLayout from qutebrowser.qt.network import QNetworkCookieJar import helpers.stubs as stubsmod import qutebrowser from qutebrowser.config import (config, configdata, configtypes, configexc, configfiles, configcache, stylesheet) from qutebrowser.api import config as configapi from qutebrowser.utils import objreg, standarddir, utils, usertypes, version from qutebrowser.browser import greasemonkey, history, qutescheme from qutebrowser.browser.webkit import cookies, cache from qutebrowser.misc import savemanager, sql, objects, sessions from qutebrowser.keyinput import modeman from qutebrowser.qt import sip _qute_scheme_handler = None class WidgetContainer(QWidget): """Container for another widget.""" def __init__(self, qtbot, parent=None): super().__init__(parent) self._qtbot = qtbot self.vbox = QVBoxLayout(self) qtbot.add_widget(self) self._widget = None def set_widget(self, widget): self.vbox.addWidget(widget) widget.container = self self._widget = widget def expose(self): with self._qtbot.wait_exposed(self): self.show() self._widget.setFocus() @pytest.fixture def widget_container(qtbot): return WidgetContainer(qtbot) class WinRegistryHelper: """Helper class for win_registry.""" @dataclasses.dataclass class FakeWindow: """A fake window object for the registry.""" registry: objreg.ObjectRegistry def windowTitle(self): return 'window title - qutebrowser' @property def tabbed_browser(self): return self.registry['tabbed-browser'] def __init__(self): self._ids = [] def add_window(self, win_id): assert win_id not in objreg.window_registry registry = objreg.ObjectRegistry() window = self.FakeWindow(registry) objreg.window_registry[win_id] = window self._ids.append(win_id) def cleanup(self): for win_id in self._ids: del objreg.window_registry[win_id] class FakeStatusBar(QWidget): """Fake statusbar to test progressbar sizing.""" def __init__(self, parent=None): super().__init__(parent) self.hbox = QHBoxLayout(self) self.hbox.addStretch() self.hbox.setContentsMargins(0, 0, 0, 0) self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) self.setStyleSheet('background-color: red;') def minimumSizeHint(self): return QSize(1, self.fontMetrics().height()) @pytest.fixture def fake_statusbar(widget_container): """Fixture providing a statusbar in a container window.""" widget_container.vbox.addStretch() statusbar = FakeStatusBar(widget_container) widget_container.set_widget(statusbar) return statusbar @pytest.fixture def win_registry(): """Fixture providing a window registry for win_id 0 and 1.""" helper = WinRegistryHelper() helper.add_window(0) yield helper helper.cleanup() @pytest.fixture def tab_registry(win_registry): """Fixture providing a tab registry for win_id 0.""" registry = objreg.ObjectRegistry() objreg.register('tab-registry', registry, scope='window', window=0) yield registry objreg.delete('tab-registry', scope='window', window=0) @pytest.fixture def fake_web_tab(stubs, tab_registry, mode_manager, qapp): """Fixture providing the FakeWebTab *class*.""" return stubs.FakeWebTab @pytest.fixture def greasemonkey_manager(monkeypatch, data_tmpdir, config_tmpdir): gm_manager = greasemonkey.GreasemonkeyManager() monkeypatch.setattr(greasemonkey, 'gm_manager', gm_manager) @pytest.fixture(scope='session') def testdata_scheme(qapp): try: global _qute_scheme_handler from qutebrowser.browser.webengine import webenginequtescheme from qutebrowser.qt.webenginecore import QWebEngineProfile webenginequtescheme.init() _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler( parent=qapp) _qute_scheme_handler.install(QWebEngineProfile.defaultProfile()) except ImportError: pass @qutescheme.add_handler('testdata') def handler(url): file_abs = os.path.abspath(os.path.dirname(__file__)) filename = os.path.join(file_abs, os.pardir, 'end2end', url.path().lstrip('/')) with open(filename, 'rb') as f: data = f.read() mimetype, _encoding = mimetypes.guess_type(filename) return mimetype, data @pytest.fixture def web_tab_setup(qtbot, tab_registry, session_manager_stub, greasemonkey_manager, fake_args, config_stub, testdata_scheme): """Shared setup for webkit_tab/webengine_tab.""" # Make sure error logging via JS fails tests config_stub.val.content.javascript.log = { 'info': 'info', 'error': 'error', 'unknown': 'error', 'warning': 'error', } @pytest.fixture def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager, widget_container, download_stub, webpage, monkeypatch): webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab') monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit) tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager, private=False) tab.backend = usertypes.Backend.QtWebKit widget_container.set_widget(tab) yield tab # Make sure the tab shuts itself down properly tab.private_api.shutdown() @pytest.fixture def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data, tabbed_browser_stubs, mode_manager, widget_container, monkeypatch): monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) tabwidget = tabbed_browser_stubs[0].widget tabwidget.current_index = 0 tabwidget.index_of = 0 webenginetab = pytest.importorskip( 'qutebrowser.browser.webengine.webenginetab') tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager, private=False) tab.backend = usertypes.Backend.QtWebEngine widget_container.set_widget(tab) yield tab # If a page is still loading here, _on_load_finished could get called # during teardown when session_manager_stub is already deleted. tab.stop() # Make sure the tab shuts itself down properly tab.private_api.shutdown() # If we wait for the GC to clean things up, there's a segfault inside # QtWebEngine sometimes (e.g. if we only run # tests/unit/browser/test_caret.py). sip.delete(tab._widget) @pytest.fixture(params=['webkit', 'webengine']) def web_tab(request): """A WebKitTab/WebEngineTab.""" if request.param == 'webkit': pytest.importorskip('qutebrowser.browser.webkit.webkittab') return request.getfixturevalue('webkit_tab') elif request.param == 'webengine': pytest.importorskip('qutebrowser.browser.webengine.webenginetab') return request.getfixturevalue('webengine_tab') else: raise utils.Unreachable def _generate_cmdline_tests(): """Generate testcases for test_split_binding.""" @dataclasses.dataclass class TestCase: cmd: str valid: bool separators = [';;', ' ;; ', ';; ', ' ;;'] invalid = ['foo', ''] valid = ['mode-leave', 'hint all'] # Valid command only -> valid for item in valid: yield TestCase(''.join(item), True) # Invalid command only -> invalid for item in invalid: yield TestCase(''.join(item), False) # Invalid command combined with invalid command -> invalid for item in itertools.product(invalid, separators, invalid): yield TestCase(''.join(item), False) # Valid command combined with valid command -> valid for item in itertools.product(valid, separators, valid): yield TestCase(''.join(item), True) # Valid command combined with invalid command -> invalid for item in itertools.product(valid, separators, invalid): yield TestCase(''.join(item), False) # Invalid command combined with valid command -> invalid for item in itertools.product(invalid, separators, valid): yield TestCase(''.join(item), False) # Command with no_cmd_split combined with an "invalid" command -> valid for item in itertools.product(['bind x open'], separators, invalid): yield TestCase(''.join(item), True) # Partial command yield TestCase('message-i', False) @pytest.fixture(params=_generate_cmdline_tests(), ids=lambda e: e.cmd) def cmdline_test(request): """Fixture which generates tests for things validating commandlines.""" return request.param @pytest.fixture(scope='session') def configdata_init(): """Initialize configdata if needed.""" if configdata.DATA is None: configdata.init() @pytest.fixture def yaml_config_stub(config_tmpdir): """Fixture which provides a YamlConfig object.""" return configfiles.YamlConfig() @pytest.fixture def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub, qapp): """Fixture which provides a fake config object.""" conf = config.Config(yaml_config=yaml_config_stub) monkeypatch.setattr(config, 'instance', conf) container = config.ConfigContainer(conf) monkeypatch.setattr(config, 'val', container) monkeypatch.setattr(configapi, 'val', container) cache = configcache.ConfigCache() monkeypatch.setattr(config, 'cache', cache) try: configtypes.FontBase.set_defaults(None, '10pt') except configexc.NoOptionError: # Completion tests patch configdata so fonts.default_family is # unavailable. pass conf.val = container # For easier use in tests stylesheet.init() return conf @pytest.fixture def key_config_stub(config_stub, monkeypatch): """Fixture which provides a fake key config object.""" keyconf = config.KeyConfig(config_stub) monkeypatch.setattr(config, 'key_instance', keyconf) return keyconf @pytest.fixture def quickmark_manager_stub(stubs): """Fixture which provides a fake quickmark manager object.""" stub = stubs.QuickmarkManagerStub() objreg.register('quickmark-manager', stub) yield stub objreg.delete('quickmark-manager') @pytest.fixture def bookmark_manager_stub(stubs): """Fixture which provides a fake bookmark manager object.""" stub = stubs.BookmarkManagerStub() objreg.register('bookmark-manager', stub) yield stub objreg.delete('bookmark-manager') @pytest.fixture def session_manager_stub(stubs, monkeypatch): """Fixture which provides a fake session-manager object.""" stub = stubs.SessionManagerStub() monkeypatch.setattr(sessions, 'session_manager', stub) return stub @pytest.fixture def tabbed_browser_stubs(qapp, stubs, win_registry): """Fixture providing a fake tabbed-browser object on win_id 0 and 1.""" win_registry.add_window(1) stubs = [stubs.TabbedBrowserStub(), stubs.TabbedBrowserStub()] objreg.register('tabbed-browser', stubs[0], scope='window', window=0) objreg.register('tabbed-browser', stubs[1], scope='window', window=1) yield stubs objreg.delete('tabbed-browser', scope='window', window=0) objreg.delete('tabbed-browser', scope='window', window=1) @pytest.fixture def status_command_stub(stubs, qtbot, win_registry): """Fixture which provides a fake status-command object.""" cmd = stubs.StatusBarCommandStub() objreg.register('status-command', cmd, scope='window', window=0) qtbot.add_widget(cmd) yield cmd objreg.delete('status-command', scope='window', window=0) @pytest.fixture(scope='session') def stubs(): """Provide access to stub objects useful for testing.""" return stubsmod @pytest.fixture(scope='session') def unicode_encode_err(): """Provide a fake UnicodeEncodeError exception.""" return UnicodeEncodeError('ascii', # codec '', # object 0, # start 2, # end 'fake exception') # reason @pytest.fixture(scope='session') def qnam(qapp): """Session-wide QNetworkAccessManager.""" from qutebrowser.qt.network import QNetworkAccessManager nam = QNetworkAccessManager() try: nam.setNetworkAccessible(QNetworkAccessManager.NetworkAccessibility.NotAccessible) except AttributeError: # Qt 5 only, deprecated seemingly without replacement. pass return nam @pytest.fixture def webengineview(qtbot, monkeypatch, web_tab_setup): """Get a QWebEngineView if QtWebEngine is available.""" QtWebEngineWidgets = pytest.importorskip('qutebrowser.qt.webenginewidgets') monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) view = QtWebEngineWidgets.QWebEngineView() qtbot.add_widget(view) return view @pytest.fixture def webpage(qnam, monkeypatch): """Get a new QWebPage object.""" QtWebKitWidgets = pytest.importorskip('qutebrowser.qt.webkitwidgets') monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit) class WebPageStub(QtWebKitWidgets.QWebPage): """QWebPage with default error pages disabled.""" def supportsExtension(self, _ext): """No extensions needed.""" return False page = WebPageStub() page.networkAccessManager().deleteLater() page.setNetworkAccessManager(qnam) from qutebrowser.browser.webkit import webkitsettings webkitsettings._init_user_agent() return page @pytest.fixture def webview(qtbot, webpage): """Get a new QWebView object.""" QtWebKitWidgets = pytest.importorskip('qutebrowser.qt.webkitwidgets') view = QtWebKitWidgets.QWebView() qtbot.add_widget(view) view.page().deleteLater() view.setPage(webpage) view.resize(640, 480) return view @pytest.fixture def webframe(webpage): """Convenience fixture to get a mainFrame of a QWebPage.""" return webpage.mainFrame() @pytest.fixture def cookiejar_and_cache(stubs, monkeypatch): """Fixture providing a fake cookie jar and cache.""" monkeypatch.setattr(cookies, 'cookie_jar', QNetworkCookieJar()) monkeypatch.setattr(cookies, 'ram_cookie_jar', cookies.RAMCookieJar()) monkeypatch.setattr(cache, 'diskcache', stubs.FakeNetworkCache()) @pytest.fixture def py_proc(tmp_path): """Get a python executable and args list which executes the given code.""" if getattr(sys, 'frozen', False): pytest.skip("Can't be run when frozen") def func(code): code = textwrap.dedent(code.strip('\n')) if '\n' in code: py_file = tmp_path / 'py_proc.py' py_file.write_text(code) return (sys.executable, [str(py_file)]) else: return (sys.executable, ['-c', code]) return func @pytest.fixture def fake_save_manager(): """Create a mock of save-manager and register it into objreg.""" fake_save_manager = unittest.mock.Mock(spec=savemanager.SaveManager) objreg.register('save-manager', fake_save_manager) yield fake_save_manager objreg.delete('save-manager') @pytest.fixture def fake_args(request, monkeypatch): ns = types.SimpleNamespace() ns.backend = 'webengine' if request.config.webengine else 'webkit' ns.debug_flags = [] monkeypatch.setattr(objects, 'args', ns) return ns @pytest.fixture def mode_manager(win_registry, config_stub, key_config_stub, qapp): mm = modeman.init(win_id=0, parent=qapp) yield mm objreg.delete('mode-manager', scope='window', window=0) mm.deleteLater() def standarddir_tmpdir(folder, monkeypatch, tmpdir): """Set tmpdir/config as the configdir. Use this to avoid creating a 'real' config dir (~/.config/qute_test). """ confdir = tmpdir / folder confdir.ensure(dir=True) if hasattr(standarddir, folder): monkeypatch.setattr(standarddir, folder, lambda **_kwargs: str(confdir)) return confdir @pytest.fixture def download_tmpdir(monkeypatch, tmpdir): """Set tmpdir/download as the downloaddir. Use this to avoid creating a 'real' download dir (~/.config/qute_test). """ return standarddir_tmpdir('download', monkeypatch, tmpdir) @pytest.fixture def config_tmpdir(monkeypatch, tmpdir): """Set tmpdir/config as the configdir. Use this to avoid creating a 'real' config dir (~/.config/qute_test). """ monkeypatch.setattr( standarddir, 'config_py', lambda **_kwargs: str(tmpdir / 'config' / 'config.py')) return standarddir_tmpdir('config', monkeypatch, tmpdir) @pytest.fixture def config_py_arg(tmpdir, monkeypatch): """Set the config_py arg with a custom value for init.""" f = tmpdir / 'temp_config.py' monkeypatch.setattr( standarddir, 'config_py', lambda **_kwargs: str(f)) return f @pytest.fixture def data_tmpdir(monkeypatch, tmpdir): """Set tmpdir/data as the datadir. Use this to avoid creating a 'real' data dir (~/.local/share/qute_test). """ return standarddir_tmpdir('data', monkeypatch, tmpdir) @pytest.fixture def runtime_tmpdir(monkeypatch, tmpdir): """Set tmpdir/runtime as the runtime dir. Use this to avoid creating a 'real' runtime dir. """ return standarddir_tmpdir('runtime', monkeypatch, tmpdir) @pytest.fixture def cache_tmpdir(monkeypatch, tmpdir): """Set tmpdir/cache as the cachedir. Use this to avoid creating a 'real' cache dir (~/.cache/qute_test). """ return standarddir_tmpdir('cache', monkeypatch, tmpdir) @pytest.fixture def redirect_webengine_data(data_tmpdir, monkeypatch): """Set XDG_DATA_HOME and HOME to a temp location. While data_tmpdir covers most cases by redirecting standarddir.data(), this is not enough for places QtWebEngine references the data dir internally. For these, we need to set the environment variable to redirect data access. We also set HOME as in some places, the home directory is used directly... """ monkeypatch.setenv('XDG_DATA_HOME', str(data_tmpdir)) monkeypatch.setenv('HOME', str(data_tmpdir)) @pytest.fixture def short_tmpdir(): """A short temporary directory for a XDG_RUNTIME_DIR.""" with tempfile.TemporaryDirectory() as tdir: yield py.path.local(tdir) class ModelValidator: """Validates completion models.""" def __init__(self, modeltester): self._model = None self._modeltester = modeltester def set_model(self, model): self._model = model self._modeltester.check(model) def validate(self, expected): assert self._model.rowCount() == len(expected) for row, items in enumerate(expected): for col, item in enumerate(items): assert self._model.data(self._model.index(row, col)) == item @pytest.fixture def model_validator(qtmodeltester): return ModelValidator(qtmodeltester) @pytest.fixture def download_stub(win_registry, tmpdir, stubs): """Register a FakeDownloadManager.""" stub = stubs.FakeDownloadManager(tmpdir) objreg.register('qtnetwork-download-manager', stub) yield stub objreg.delete('qtnetwork-download-manager') @pytest.fixture def database(data_tmpdir): """Create a Database object.""" db = sql.Database(str(data_tmpdir / 'test.db')) yield db db.close() @pytest.fixture def web_history(fake_save_manager, tmpdir, database, config_stub, stubs, monkeypatch): """Create a WebHistory object.""" config_stub.val.completion.timestamp_format = '%Y-%m-%d' config_stub.val.completion.web_history.max_items = -1 web_history = history.WebHistory(database, stubs.FakeHistoryProgress()) monkeypatch.setattr(history, 'web_history', web_history) return web_history @pytest.fixture def blue_widget(qtbot): widget = QWidget() widget.setStyleSheet('background-color: blue;') qtbot.add_widget(widget) return widget @pytest.fixture def red_widget(qtbot): widget = QWidget() widget.setStyleSheet('background-color: red;') qtbot.add_widget(widget) return widget @pytest.fixture def state_config(data_tmpdir, monkeypatch): state = configfiles.StateConfig() monkeypatch.setattr(configfiles, 'state', state) return state @pytest.fixture def unwritable_tmp_path(tmp_path): tmp_path.chmod(0) if os.access(tmp_path, os.W_OK): # Docker container or similar pytest.skip("Directory was still writable") yield tmp_path # Make sure pytest can clean up the tmp_path tmp_path.chmod(0o755) @pytest.fixture def webengine_versions(testdata_scheme): """Get QtWebEngine version numbers. Calling qtwebengine_versions() initializes QtWebEngine, so we depend on testdata_scheme here, to make sure that happens before. """ pytest.importorskip('qutebrowser.qt.webenginewidgets') return version.qtwebengine_versions() @pytest.fixture(params=[True, False]) def freezer(request, monkeypatch): if request.param and not getattr(sys, 'frozen', False): monkeypatch.setattr(sys, 'frozen', True, raising=False) monkeypatch.setattr(sys, 'executable', qutebrowser.__file__) elif not request.param and getattr(sys, 'frozen', False): # Want to test unfrozen tests, but we are frozen pytest.skip("Can't run with sys.frozen = True!") return request.param @pytest.fixture def fake_flatpak(monkeypatch): app_id = 'org.qutebrowser.qutebrowser' monkeypatch.setenv('FLATPAK_ID', app_id) assert version.is_flatpak() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/logfail.py0000644000175100017510000000243515102145205020343 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Logging handling for the tests.""" import logging import pytest class LogFailHandler(logging.Handler): """A logging handler which makes tests fail on unexpected messages.""" def __init__(self, level=logging.NOTSET, min_level=logging.WARNING): self._min_level = min_level super().__init__(level) def emit(self, record): logger = logging.getLogger(record.name) root_logger = logging.getLogger() if logger.name == 'messagemock': return if record.levelno in (logger.level, root_logger.level): # caplog.at_level(...) was used with the level of this message, # i.e. it was expected. return if record.levelno < self._min_level: return pytest.fail("Got logging message on logger {} with level {}: " "{}!".format(record.name, record.levelname, record.getMessage())) @pytest.fixture(scope='session', autouse=True) def fail_on_logging(): handler = LogFailHandler() logging.getLogger().addHandler(handler) yield logging.getLogger().removeHandler(handler) handler.close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/messagemock.py0000644000175100017510000000534615102145205021230 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """pytest helper to monkeypatch the message module.""" import logging import pytest from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QObject from qutebrowser.utils import usertypes, message class MessageMock(QObject): """Helper object for message_mock. Attributes: messages: A list of Message objects. questions: A list of Question objects. _logger: The logger to use for messages/questions. """ got_message = pyqtSignal(message.MessageInfo) got_question = pyqtSignal(usertypes.Question) def __init__(self, parent=None): super().__init__(parent) self.messages = [] self.questions = [] self._logger = logging.getLogger('messagemock') @pyqtSlot(message.MessageInfo) def _record_message(self, info): self.got_message.emit(info) log_levels = { usertypes.MessageLevel.error: logging.ERROR, usertypes.MessageLevel.info: logging.INFO, usertypes.MessageLevel.warning: logging.WARNING, } log_level = log_levels[info.level] self._logger.log(log_level, info.text) self.messages.append(info) @pyqtSlot(usertypes.Question) def _record_question(self, question): self.got_question.emit(question) self._logger.debug(question) self.questions.append(question) def getmsg(self, level=None): """Get the only message in self.messages. Raises AssertionError if there are multiple or no messages. Args: level: The message level to check against, or None. """ assert len(self.messages) == 1 msg = self.messages[0] if level is not None: assert msg.level == level return msg def get_question(self): """Get the only question in self.questions. Raises AssertionError if there are multiple or no questions. """ assert len(self.questions) == 1 return self.questions[0] def connect(self): """Start recording messages / questions.""" message.global_bridge.show_message.connect(self._record_message) message.global_bridge.ask_question.connect(self._record_question) message.global_bridge._connected = True def disconnect(self): """Stop recording messages/questions.""" message.global_bridge.show_message.disconnect(self._record_message) message.global_bridge.ask_question.disconnect(self._record_question) @pytest.fixture def message_mock(): """Fixture to get a MessageMock.""" mmock = MessageMock() mmock.connect() yield mmock mmock.disconnect() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/stubs.py0000644000175100017510000004647115102145205020076 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later # pylint: disable=abstract-method """Fake objects/stubs.""" from typing import Any from collections.abc import Callable from unittest import mock import contextlib import shutil import dataclasses import builtins import importlib import types from qutebrowser.qt.core import pyqtSignal, QPoint, QProcess, QObject, QUrl, QByteArray from qutebrowser.qt.gui import QIcon from qutebrowser.qt.network import (QNetworkRequest, QAbstractNetworkCache, QNetworkCacheMetaData) from qutebrowser.qt.widgets import QCommonStyle, QLineEdit, QWidget, QTabBar from qutebrowser.browser import browsertab, downloads from qutebrowser.utils import usertypes from qutebrowser.commands import runners class FakeNetworkCache(QAbstractNetworkCache): """Fake cache with no data.""" def cacheSize(self): return 0 def data(self, _url): return None def insert(self, _dev): pass def metaData(self, _url): return QNetworkCacheMetaData() def prepare(self, _metadata): return None def remove(self, _url): return False def updateMetaData(self, _url): pass class FakeKeyEvent: """Fake QKeyPressEvent stub.""" def __init__(self, key, modifiers=0, text=''): self.key = mock.Mock(return_value=key) self.text = mock.Mock(return_value=text) self.modifiers = mock.Mock(return_value=modifiers) class FakeWebFrame: """A stub for QWebFrame.""" def __init__(self, geometry=None, *, scroll=None, plaintext=None, html=None, parent=None, zoom=1.0): """Constructor. Args: geometry: The geometry of the frame as QRect. scroll: The scroll position as QPoint. plaintext: Return value of toPlainText html: Return value of tohtml. zoom: The zoom factor. parent: The parent frame. """ if scroll is None: scroll = QPoint(0, 0) self.geometry = mock.Mock(return_value=geometry) self.scrollPosition = mock.Mock(return_value=scroll) self.parentFrame = mock.Mock(return_value=parent) self.toPlainText = mock.Mock(return_value=plaintext) self.toHtml = mock.Mock(return_value=html) self.zoomFactor = mock.Mock(return_value=zoom) class FakeChildrenFrame: """A stub for QWebFrame to test get_child_frames.""" def __init__(self, children=None): if children is None: children = [] self.childFrames = mock.Mock(return_value=children) class FakeQApplication: """Stub to insert as QApplication module.""" UNSET = object() def __init__(self, *, style=None, all_widgets=None, active_window=None, arguments=None, platform_name=None): self.style = mock.Mock(spec=QCommonStyle) self.style().metaObject().className.return_value = style self.allWidgets = lambda: all_widgets self.activeWindow = lambda: active_window self.arguments = lambda: arguments self.platformName = lambda: platform_name class FakeNetworkReply: """QNetworkReply stub which provides a Content-Disposition header.""" KNOWN_HEADERS = { QNetworkRequest.KnownHeaders.ContentTypeHeader: 'Content-Type', } def __init__(self, headers=None, url=None): if url is None: url = QUrl() if headers is None: self.headers = {} else: self.headers = headers self.url = mock.Mock(return_value=url) def hasRawHeader(self, name): """Check if the reply has a certain header. Args: name: The name of the header as ISO-8859-1 encoded bytes object. Return: True if the header is present, False if not. """ return name.decode('iso-8859-1') in self.headers def rawHeader(self, name): """Get the raw header data of a header. Args: name: The name of the header as ISO-8859-1 encoded bytes object. Return: The header data, as ISO-8859-1 encoded bytes() object. """ name = name.decode('iso-8859-1') return self.headers[name].encode('iso-8859-1') def header(self, known_header): """Get a known header. Args: known_header: A QNetworkRequest::KnownHeaders member. """ key = self.KNOWN_HEADERS[known_header] try: return self.headers[key] except KeyError: return None def setHeader(self, known_header, value): """Set a known header. Args: known_header: A QNetworkRequest::KnownHeaders member. value: The value to set. """ key = self.KNOWN_HEADERS[known_header] self.headers[key] = value class FakeProcess(QProcess): def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self.start = mock.Mock(spec=QProcess.start) self.startDetached = mock.Mock(spec=QProcess.startDetached) self.readAllStandardOutput = mock.Mock( spec=QProcess.readAllStandardOutput, return_value=QByteArray(b'')) self.readAllStandardError = mock.Mock( spec=QProcess.readAllStandardError, return_value=QByteArray(b'')) self.terminate = mock.Mock(spec=QProcess.terminate) self.kill = mock.Mock(spec=QProcess.kill) class FakeWebTabScroller(browsertab.AbstractScroller): """Fake AbstractScroller to use in tests.""" def __init__(self, tab, pos_perc): super().__init__(tab) self._pos_perc = pos_perc def pos_perc(self): return self._pos_perc class FakeWebTabHistory(browsertab.AbstractHistory): """Fake for Web{Kit,Engine}History.""" def __init__(self, tab, *, can_go_back, can_go_forward): super().__init__(tab) self._can_go_back = can_go_back self._can_go_forward = can_go_forward def can_go_back(self): assert self._can_go_back is not None return self._can_go_back def can_go_forward(self): assert self._can_go_forward is not None return self._can_go_forward class FakeWebTabAudio(browsertab.AbstractAudio): def is_muted(self): return False def is_recently_audible(self): return False class FakeWebTabPrivate(browsertab.AbstractTabPrivate): def shutdown(self): pass class FakeWebTab(browsertab.AbstractTab): """Fake AbstractTab to use in tests.""" def __init__(self, url=QUrl(), title='', tab_id=0, *, scroll_pos_perc=(0, 0), load_status=usertypes.LoadStatus.success, progress=0, can_go_back=None, can_go_forward=None): super().__init__(win_id=0, mode_manager=None, private=False) self._load_status = load_status self._title = title self._url = url self._progress = progress self.history = FakeWebTabHistory(self, can_go_back=can_go_back, can_go_forward=can_go_forward) self.scroller = FakeWebTabScroller(self, scroll_pos_perc) self.audio = FakeWebTabAudio(self) self.private_api = FakeWebTabPrivate(tab=self, mode_manager=None) wrapped = QWidget() self._layout.wrap(self, wrapped) def url(self, *, requested=False): assert not requested return self._url def title(self): return self._title def progress(self): return self._progress def load_status(self): return self._load_status def icon(self): return QIcon() def renderer_process_pid(self): return None def load_url(self, url): self._url = url class FakeSignal: """Fake pyqtSignal stub which does nothing. Attributes: signal: The name of the signal, like pyqtSignal. """ def __init__(self, name='fake'): self.signal = '2{}(int, int)'.format(name) def connect(self, slot): """Connect the signal to a slot. Currently does nothing, but could be improved to do some sanity checking on the slot. """ def disconnect(self, slot=None): """Disconnect the signal from a slot. Currently does nothing, but could be improved to do some sanity checking on the slot and see if it actually got connected. """ def emit(self, *args): """Emit the signal. Currently does nothing, but could be improved to do type checking based on a signature given to __init__. """ @dataclasses.dataclass(frozen=True) class FakeCommand: """A simple command stub which has a description.""" name: str = '' desc: str = '' hide: bool = False debug: bool = False deprecated: bool = False completion: Any = None maxsplit: int = None takes_count: Callable[[], bool] = lambda: False modes: tuple[usertypes.KeyMode] = (usertypes.KeyMode.normal, ) class FakeTimer(QObject): """Stub for a usertypes.Timer.""" timeout_signal = pyqtSignal() def __init__(self, parent=None, name=None): super().__init__(parent) self.timeout = mock.Mock(spec=['connect', 'disconnect', 'emit']) self.timeout.connect.side_effect = self.timeout_signal.connect self.timeout.disconnect.side_effect = self.timeout_signal.disconnect self.timeout.emit.side_effect = self._emit self._started = False self._singleshot = False self._interval = 0 self._name = name def __repr__(self): return '<{} name={!r}>'.format(self.__class__.__name__, self._name) def _emit(self): """Called when the timeout "signal" gets emitted.""" if self._singleshot: self._started = False self.timeout_signal.emit() def setInterval(self, interval): self._interval = interval def interval(self): return self._interval def setSingleShot(self, singleshot): self._singleshot = singleshot def isSingleShot(self): return self._singleshot def start(self, interval=None): if interval: self._interval = interval self._started = True def stop(self): self._started = False def isActive(self): return self._started class InstaTimer(QObject): """Stub for a QTimer that fires instantly on start(). Useful to test a time-based event without inserting an artificial delay. """ timeout = pyqtSignal() def start(self, interval=None): self.timeout.emit() def setSingleShot(self, yes): pass def setInterval(self, interval): pass @staticmethod def singleShot(_interval, fun): fun() class StatusBarCommandStub(QLineEdit): """Stub for the statusbar command prompt.""" got_cmd = pyqtSignal(str) clear_completion_selection = pyqtSignal() hide_completion = pyqtSignal() update_completion = pyqtSignal() show_cmd = pyqtSignal() hide_cmd = pyqtSignal() def prefix(self): return self.text()[0] class UrlMarkManagerStub(QObject): """Stub for the quickmark-manager or bookmark-manager object.""" added = pyqtSignal(str, str) removed = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.marks = {} def delete(self, key): del self.marks[key] self.removed.emit(key) class BookmarkManagerStub(UrlMarkManagerStub): """Stub for the bookmark-manager object.""" class QuickmarkManagerStub(UrlMarkManagerStub): """Stub for the quickmark-manager object.""" def quickmark_del(self, key): self.delete(key) class SessionManagerStub: """Stub for the session-manager object.""" def __init__(self): self.sessions = [] def list_sessions(self): return self.sessions def save_autosave(self): pass class TabbedBrowserStub(QObject): """Stub for the tabbed-browser object.""" def __init__(self, parent=None): super().__init__(parent) self.widget = TabWidgetStub() self.is_shutting_down = False self.loaded_url = None self.cur_url = None self.undo_stack = None def on_tab_close_requested(self, idx): del self.widget.tabs[idx] def widgets(self): return self.widget.tabs def tabopen(self, url): self.loaded_url = url def load_url(self, url, *, newtab): self.loaded_url = url def current_url(self): if self.current_url is None: raise ValueError("current_url got called with cur_url None!") return self.cur_url class TabWidgetStub(QObject): """Stub for the tab-widget object.""" new_tab = pyqtSignal(browsertab.AbstractTab, int) def __init__(self, parent=None): super().__init__(parent) self.tabs = [] self._qtabbar = QTabBar() self.index_of = None self.current_index = None def count(self): return len(self.tabs) def widget(self, i): return self.tabs[i] def page_title(self, i): return self.tabs[i].title() def tabBar(self): return self._qtabbar def indexOf(self, _tab): if self.index_of is None: raise ValueError("indexOf got called with index_of None!") if self.index_of is RuntimeError: raise RuntimeError return self.index_of def currentIndex(self): if self.current_index is None: raise ValueError("currentIndex got called with current_index " "None!") return self.current_index def currentWidget(self): idx = self.currentIndex() if idx == -1: return None return self.tabs[idx - 1] class HTTPPostStub(QObject): """A stub class for HTTPClient. Attributes: url: the last url send by post() data: the last data send by post() """ success = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.url = None self.data = None def post(self, url, data=None): self.url = url self.data = data class FakeDownloadItem(QObject): """Mock browser.downloads.DownloadItem.""" finished = pyqtSignal() def __init__(self, fileobj, name, parent=None): super().__init__(parent) self.fileobj = fileobj self.name = name self.successful = False class FakeDownloadManager: """Mock browser.downloads.DownloadManager.""" def __init__(self, tmpdir): self._tmpdir = tmpdir self.downloads = [] @contextlib.contextmanager def _open_fileobj(self, target): """Ensure a DownloadTarget's fileobj attribute is available.""" if isinstance(target, downloads.FileDownloadTarget): target.fileobj = open(target.filename, 'wb') try: yield target.fileobj finally: target.fileobj.close() else: yield target.fileobj def get(self, url, target, **kwargs): """Return a FakeDownloadItem instance with a fileobj. The content is copied from the file the given url links to. """ with self._open_fileobj(target): download_item = FakeDownloadItem(target.fileobj, name=url.path()) with (self._tmpdir / url.path()).open('rb') as fake_url_file: shutil.copyfileobj(fake_url_file, download_item.fileobj) self.downloads.append(download_item) return download_item def has_downloads_with_nam(self, _nam): """Needed during WebView.shutdown().""" return False class FakeHistoryTick(Exception): pass class FakeHistoryProgress: """Fake for a WebHistoryProgress object.""" def __init__(self, *, raise_on_tick=False): self._started = False self._finished = False self._value = 0 self._raise_on_tick = raise_on_tick def start(self, _text): self._started = True def set_maximum(self, _maximum): pass def tick(self): if self._raise_on_tick: raise FakeHistoryTick('tick-tock') self._value += 1 def finish(self): self._finished = True class FakeCommandRunner(runners.AbstractCommandRunner): def __init__(self, parent=None): super().__init__(parent) self.commands = [] def run(self, text, count=None, *, safely=False): self.commands.append((text, count)) class FakeHintManager: def __init__(self): self.keystr = None def handle_partial_key(self, keystr): self.keystr = keystr def current_mode(self): return 'letter' class FakeWebEngineProfile: def __init__(self, cookie_store): self.cookieStore = lambda: cookie_store class FakeCookieStore: def __init__(self): self.cookie_filter = None def setCookieFilter(self, func): self.cookie_filter = func class ImportFake: """A fake for __import__ which is used by the import_fake fixture. Attributes: modules: A dict mapping module names to bools. If True, the import will succeed. If an exception is given, it will be raised. Otherwise, it'll fail with a fake ImportError. version_attribute: The name to use in the fake modules for the version attribute. version: The version to use for the modules. _real_import: Saving the real __import__ builtin so the imports can be done normally for modules not in self. modules. """ def __init__(self, modules, monkeypatch): self._monkeypatch = monkeypatch self.modules = modules self.version_attribute = '__version__' self.version = '1.2.3' self._real_import = builtins.__import__ self._real_importlib_import = importlib.import_module def patch(self): """Patch import functions.""" self._monkeypatch.setattr(builtins, '__import__', self.fake_import) self._monkeypatch.setattr( importlib, 'import_module', self.fake_importlib_import) def _do_import(self, name): """Helper for fake_import and fake_importlib_import to do the work. Return: The imported fake module, or None if normal importing should be used. """ if name not in self.modules: # Not one of the modules to test -> use real import return None elif isinstance(self.modules[name], Exception): raise self.modules[name] elif self.modules[name]: ns = types.SimpleNamespace() if self.version_attribute is not None: setattr(ns, self.version_attribute, self.version) return ns else: raise ImportError("Fake ImportError for {}.".format(name)) def fake_import(self, name, *args, **kwargs): """Fake for the builtin __import__.""" module = self._do_import(name) if module is not None: return module else: return self._real_import(name, *args, **kwargs) def fake_importlib_import(self, name): """Fake for importlib.import_module.""" module = self._do_import(name) if module is not None: return module else: return self._real_importlib_import(name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/test_helper_utils.py0000644000175100017510000000443115102145205022462 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later import pytest from helpers import testutils from qutebrowser.qt.widgets import QFrame @pytest.mark.parametrize('val1, val2', [ ({'a': 1}, {'a': 1}), ({'a': 1, 'b': 2}, {'a': 1}), ({'a': [1, 2, 3]}, {'a': [1]}), ({'a': [1, 2, 3]}, {'a': [..., 2]}), (1.0, 1.00000001), ("foobarbaz", "foo*baz"), ]) def test_partial_compare_equal(val1, val2): assert testutils.partial_compare(val1, val2) @pytest.mark.parametrize('val1, val2, error', [ ({'a': 1}, {'a': 2}, "1 != 2"), ({'a': 1}, {'b': 1}, "Key 'b' is in second dict but not in first!"), ({'a': 1, 'b': 2}, {'a': 2}, "1 != 2"), ({'a': [1]}, {'a': [1, 2, 3]}, "Second list is longer than first list"), ({'a': [1]}, {'a': [2, 3, 4]}, "Second list is longer than first list"), ([1], {1: 2}, "Different types (list, dict) -> False"), ({1: 1}, {1: [1]}, "Different types (int, list) -> False"), ({'a': [1, 2, 3]}, {'a': [..., 3]}, "2 != 3"), ("foo*baz", "foobarbaz", "'foo*baz' != 'foobarbaz' (pattern matching)"), (23.42, 13.37, "23.42 != 13.37 (float comparison)"), ]) def test_partial_compare_not_equal(val1, val2, error): outcome = testutils.partial_compare(val1, val2) assert not outcome assert isinstance(outcome, testutils.PartialCompareOutcome) assert outcome.error == error @pytest.mark.parametrize('pattern, value, expected', [ ('foo', 'foo', True), ('foo', 'bar', False), ('foo', 'Foo', False), ('foo', 'foobar', False), ('foo', 'barfoo', False), ('foo*', 'foobarbaz', True), ('*bar', 'foobar', True), ('foo*baz', 'foobarbaz', True), ('foo[b]ar', 'foobar', False), ('foo[b]ar', 'foo[b]ar', True), ('foo?ar', 'foobar', False), ('foo?ar', 'foo?ar', True), ]) def test_pattern_match(pattern, value, expected): assert testutils.pattern_match(pattern=pattern, value=value) == expected def test_nop_contextmanager(): with testutils.nop_contextmanager(): pass def test_enum_members(): expected = { "Plain": QFrame.Shadow.Plain, "Raised": QFrame.Shadow.Raised, "Sunken": QFrame.Shadow.Sunken, } assert testutils.enum_members(QFrame, QFrame.Shadow) == expected ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/test_logfail.py0000644000175100017510000000235515102145205021403 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Tests for the LogFailHandler test helper.""" import logging import pytest def test_log_debug(): logging.debug('foo') def test_log_warning(): with pytest.raises(pytest.fail.Exception): logging.warning('foo') def test_log_expected(caplog): with caplog.at_level(logging.ERROR): logging.error('foo') def test_log_expected_logger(caplog): logger = 'logfail_test_logger' with caplog.at_level(logging.ERROR, logger): logging.getLogger(logger).error('foo') def test_log_expected_wrong_level(caplog): with pytest.raises(pytest.fail.Exception): with caplog.at_level(logging.ERROR): logging.critical('foo') def test_log_expected_logger_wrong_level(caplog): logger = 'logfail_test_logger' with pytest.raises(pytest.fail.Exception): with caplog.at_level(logging.ERROR, logger): logging.getLogger(logger).critical('foo') def test_log_expected_wrong_logger(caplog): logger = 'logfail_test_logger' with pytest.raises(pytest.fail.Exception): with caplog.at_level(logging.ERROR, logger): logging.error('foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/test_stubs.py0000644000175100017510000000422015102145205021117 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Test test stubs.""" from unittest import mock import pytest @pytest.fixture def timer(stubs): return stubs.FakeTimer() def test_timeout(timer): """Test whether timeout calls the functions.""" func = mock.Mock() func2 = mock.Mock() timer.timeout.connect(func) timer.timeout.connect(func2) func.assert_not_called() func2.assert_not_called() timer.timeout.emit() func.assert_called_once_with() func2.assert_called_once_with() def test_disconnect_all(timer): """Test disconnect without arguments.""" func = mock.Mock() timer.timeout.connect(func) timer.timeout.disconnect() timer.timeout.emit() func.assert_not_called() def test_disconnect_one(timer): """Test disconnect with a single argument.""" func = mock.Mock() timer.timeout.connect(func) timer.timeout.disconnect(func) timer.timeout.emit() func.assert_not_called() def test_disconnect_all_invalid(timer): """Test disconnecting with no connections.""" with pytest.raises(TypeError): timer.timeout.disconnect() def test_disconnect_one_invalid(timer): """Test disconnecting with an invalid connection.""" func1 = mock.Mock() func2 = mock.Mock() timer.timeout.connect(func1) with pytest.raises(TypeError): timer.timeout.disconnect(func2) func1.assert_not_called() func2.assert_not_called() timer.timeout.emit() func1.assert_called_once_with() def test_singleshot(timer): """Test setting singleShot.""" assert not timer.isSingleShot() timer.setSingleShot(True) assert timer.isSingleShot() timer.start() assert timer.isActive() timer.timeout.emit() assert not timer.isActive() def test_active(timer): """Test isActive.""" assert not timer.isActive() timer.start() assert timer.isActive() timer.stop() assert not timer.isActive() def test_interval(timer): """Test setting an interval.""" assert timer.interval() == 0 timer.setInterval(1000) assert timer.interval() == 1000 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/helpers/testutils.py0000644000175100017510000002635415102145205020774 0ustar00runnerrunner# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later """Various utilities used inside tests.""" import io import re import enum import gzip import pprint import platform import os.path import contextlib import pathlib import subprocess import importlib.util import importlib.machinery from typing import Optional import pytest from qutebrowser.qt.gui import QColor from qutebrowser.utils import log, utils, version ON_CI = 'CI' in os.environ class Color(QColor): """A QColor with a nicer repr().""" def __repr__(self): return utils.get_repr(self, constructor=True, red=self.red(), green=self.green(), blue=self.blue(), alpha=self.alpha()) class PartialCompareOutcome: """Storage for a partial_compare error. Evaluates to False if an error was found. Attributes: error: A string describing an error or None. """ def __init__(self, error=None): self.error = error def __bool__(self): return self.error is None def __repr__(self): return 'PartialCompareOutcome(error={!r})'.format(self.error) def __str__(self): return 'true' if self.error is None else 'false' def print_i(text, indent, error=False): if error: text = '| ****** {} ******'.format(text) for line in text.splitlines(): print('| ' * indent + line) def _partial_compare_dict(val1, val2, *, indent): for key in val2: if key not in val1: outcome = PartialCompareOutcome( "Key {!r} is in second dict but not in first!".format(key)) print_i(outcome.error, indent, error=True) return outcome outcome = partial_compare(val1[key], val2[key], indent=indent + 1) if not outcome: return outcome return PartialCompareOutcome() def _partial_compare_list(val1, val2, *, indent): if len(val1) < len(val2): outcome = PartialCompareOutcome( "Second list is longer than first list") print_i(outcome.error, indent, error=True) return outcome for item1, item2 in zip(val1, val2): outcome = partial_compare(item1, item2, indent=indent + 1) if not outcome: return outcome return PartialCompareOutcome() def _partial_compare_float(val1, val2, *, indent): if val1 == pytest.approx(val2): return PartialCompareOutcome() return PartialCompareOutcome("{!r} != {!r} (float comparison)".format( val1, val2)) def _partial_compare_str(val1, val2, *, indent): if pattern_match(pattern=val2, value=val1): return PartialCompareOutcome() return PartialCompareOutcome("{!r} != {!r} (pattern matching)".format( val1, val2)) def _partial_compare_eq(val1, val2, *, indent): if val1 == val2: return PartialCompareOutcome() return PartialCompareOutcome("{!r} != {!r}".format(val1, val2)) def gha_group_begin(name): """Get a string to begin a GitHub Actions group. Should only be called on CI. """ assert ON_CI return '::group::' + name def gha_group_end(): """Get a string to end a GitHub Actions group. Should only be called on CI. """ assert ON_CI return '::endgroup::' def partial_compare(val1, val2, *, indent=0): """Do a partial comparison between the given values. For dicts, keys in val2 are checked, others are ignored. For lists, entries at the positions in val2 are checked, others ignored. For other values, == is used. This happens recursively. """ if ON_CI and indent == 0: print(gha_group_begin('Comparison')) print_i("Comparing", indent) print_i(pprint.pformat(val1), indent + 1) print_i("|---- to ----", indent) print_i(pprint.pformat(val2), indent + 1) if val2 is Ellipsis: print_i("Ignoring ellipsis comparison", indent, error=True) return PartialCompareOutcome() elif type(val1) is not type(val2): outcome = PartialCompareOutcome( "Different types ({}, {}) -> False".format(type(val1).__name__, type(val2).__name__)) print_i(outcome.error, indent, error=True) return outcome handlers = { dict: _partial_compare_dict, list: _partial_compare_list, float: _partial_compare_float, str: _partial_compare_str, } for typ, handler in handlers.items(): if isinstance(val2, typ): print_i("|======= Comparing as {}".format(typ.__name__), indent) outcome = handler(val1, val2, indent=indent) break else: print_i("|======= Comparing via ==", indent) outcome = _partial_compare_eq(val1, val2, indent=indent) print_i("---> {}".format(outcome), indent) if ON_CI and indent == 0: print(gha_group_end()) return outcome def pattern_match(*, pattern, value): """Do fnmatch.fnmatchcase like matching, but only with * active. Return: True on a match, False otherwise. """ re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*')) return re.fullmatch(re_pattern, value, flags=re.DOTALL) is not None def abs_datapath(): """Get the absolute path to the end2end data directory.""" path = pathlib.Path(__file__).parent / '..' / 'end2end' / 'data' return path.resolve() def substitute_testdata(path): r"""Replace the (testdata) placeholder in path with `abs_datapath()`. If path is starting with file://, return path as an URI with file:// removed. This is useful if path is going to be inserted into an URI: >>> path = substitute_testdata("C:\Users\qute") >>> f"file://{path}/slug # results in valid URI 'file:///C:/Users/qute/slug' """ if path.startswith('file://'): testdata_path = abs_datapath().as_uri().replace('file://', '') else: testdata_path = str(abs_datapath()) return path.replace('(testdata)', testdata_path) @contextlib.contextmanager def nop_contextmanager(): yield @contextlib.contextmanager def change_cwd(path): """Use a path as current working directory.""" old_cwd = pathlib.Path.cwd() os.chdir(path) try: yield finally: os.chdir(old_cwd) @contextlib.contextmanager def ignore_bs4_warning(): """WORKAROUND for https://bugs.launchpad.net/beautifulsoup/+bug/1847592.""" with log.py_warning_filter( category=DeprecationWarning, message="Using or importing the ABCs from 'collections' instead " "of from 'collections.abc' is deprecated", module='bs4.element'): yield def _decompress_gzip_datafile(filename): path = os.path.join(abs_datapath(), filename) yield from io.TextIOWrapper(gzip.open(path), encoding="utf-8") def blocked_hosts(): return _decompress_gzip_datafile("blocked-hosts.gz") def adblock_dataset_tsv(): return _decompress_gzip_datafile("brave-adblock/ublock-matches.tsv.gz") def easylist_txt(): return _decompress_gzip_datafile("easylist.txt.gz") def easyprivacy_txt(): return _decompress_gzip_datafile("easyprivacy.txt.gz") def _has_qtwebengine() -> bool: """Check whether QtWebEngine is available.""" try: from qutebrowser.qt import webenginecore # pylint: disable=unused-import except ImportError: return False return True DISABLE_SECCOMP_BPF_FLAG = "--disable-seccomp-filter-sandbox" DISABLE_SECCOMP_BPF_ARGS = ["-s", "qt.chromium.sandboxing", "disable-seccomp-bpf"] def _needs_map_discard_workaround(qtwe_version: utils.VersionNumber) -> bool: """Check if this system needs the glibc 2.41+ MAP_DISCARD workaround. WORKAROUND for https://bugreports.qt.io/browse/QTBUG-134631 See https://bugs.gentoo.org/show_bug.cgi?id=949654 """ if not utils.is_posix: return False libc_name, libc_version_str = platform.libc_ver() if libc_name != "glibc": return False libc_version = utils.VersionNumber.parse(libc_version_str) kernel_version = utils.VersionNumber.parse(os.uname().release) # https://sourceware.org/git/?p=glibc.git;a=commit;h=461cab1 affected_glibc = utils.VersionNumber(2, 41) affected_kernel = utils.VersionNumber(6, 11) return ( libc_version >= affected_glibc and kernel_version >= affected_kernel and not ( # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/631749 # -> Fixed in QtWebEngine 5.15.9 utils.VersionNumber(5, 15, 19) <= qtwe_version < utils.VersionNumber(6) # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/631750 # -> Fixed in QtWebEngine 6.8.4 or utils.VersionNumber(6, 8, 4) <= qtwe_version < utils.VersionNumber(6, 9) # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/631348 # -> Fixed in QtWebEngine 6.9.1 or utils.VersionNumber(6, 9, 1) <= qtwe_version ) ) def disable_seccomp_bpf_sandbox() -> bool: """Check whether we need to disable the seccomp BPF sandbox. This is needed for some QtWebEngine setups, with older Qt versions but newer kernels. """ if not _has_qtwebengine(): return False versions = version.qtwebengine_versions(avoid_init=True) return ( versions.webengine == utils.VersionNumber(5, 15, 2) or _needs_map_discard_workaround(versions.webengine) ) SOFTWARE_RENDERING_FLAG = "--disable-gpu" SOFTWARE_RENDERING_ARGS = ["-s", "qt.force_software_rendering", "chromium"] def offscreen_plugin_enabled() -> bool: """Check whether offscreen rendering is enabled.""" # FIXME allow configuring via custom CLI flag? return os.environ.get("QT_QPA_PLATFORM") == "offscreen" def use_software_rendering() -> bool: """Check whether to enforce software rendering for tests.""" return _has_qtwebengine() and offscreen_plugin_enabled() def import_userscript(name): """Import a userscript via importlib. This is needed because userscripts don't have a .py extension and violate Python's module naming convention. """ repo_root = pathlib.Path(__file__).resolve().parents[2] script_path = repo_root / 'misc' / 'userscripts' / name module_name = name.replace('-', '_') loader = importlib.machinery.SourceFileLoader( module_name, str(script_path)) spec = importlib.util.spec_from_loader(module_name, loader) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def enum_members(base, enumtype): """Get all members of a Qt enum.""" if issubclass(enumtype, enum.Enum): # PyQt 6 return {m.name: m for m in enumtype} else: # PyQt 5 return { name: value for name, value in vars(base).items() if isinstance(value, enumtype) } def is_userns_restricted() -> Optional[bool]: if not utils.is_linux: return None try: proc = subprocess.run( ["sysctl", "-n", "kernel.apparmor_restrict_unprivileged_userns"], capture_output=True, text=True, check=True, ) except (FileNotFoundError, subprocess.CalledProcessError): return None return proc.stdout.strip() == "1" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1762183912.604638 qutebrowser-3.6.1/tests/manual/0000755000175100017510000000000015102145351016165 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1762183912.604638 qutebrowser-3.6.1/tests/manual/completion/0000755000175100017510000000000015102145351020336 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/completion/changing_title.html0000644000175100017510000000052015102145205024176 0ustar00runnerrunner Old title

This page should change its title after 3s.

When opening the :tab-select completion ("gt"), the title should update while it's open.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/files.html0000644000175100017510000000055315102145205020156 0ustar00runnerrunner Selecting files

Single file:

Multiple files:

With accept-attribute:

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6056383 qutebrowser-3.6.1/tests/manual/hints/0000755000175100017510000000000015102145351017312 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/hints/find_implementation.html0000644000175100017510000000150415102145205024223 0ustar00runnerrunner Different hint implementations

When setting hints.find_implementation to python, the label for the wrapped hint should be drawn at the wrong position.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/hints/hide_unmatched_rapid_hints.html0000644000175100017510000000237115102145205025526 0ustar00runnerrunner Hide unmatched rapid hints

When hints.hide_unmatched_rapid_hints is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see #1799).

Note that when hinting in number mode, the hints.hide_unmatched_rapid_hints option affects typing the hint string (number), but not the filter (letters).

Here is couple of invalid links to test the behavior:

one

two

three

four

five

six

seven

eight

nine

ten

eleven

twelve

thirteen

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/hints/issue824.html0000644000175100017510000000074215102145205021567 0ustar00runnerrunner Issue 824

When using hints (f) on this page, the hint should be drawn over the link.

See #824.

This was fixed by #1433.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/hints/issue925.html0000644000175100017510000000065015102145205021567 0ustar00runnerrunner Issue 925

When using hints (f) on this page, the hint should have a normal size.

See #925.

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/hints/other.html0000644000175100017510000000502015102145205021314 0ustar00runnerrunner Unknown hint issues

Hint issues without minimal reproducers yet:

  • Links should be correctly positioned on metafilter.com - see #824 (comment).
    Current state: good
  • Links should be correctly positioned on xkcd.org - see #824.
    Current state: good - fixed in #1433
  • links should be correctly positioned on this exherbo.org page - see #1003.
    current state: bad
  • links should be correctly positioned on this ctl.io page - see #824 (comment).
    Current state: good - fixed in #1433
  • When clicking titles under the images on etsy, the correct item should be selected (sometimes the one on the right is selected instead) - see #1005.
    Current state: good - fixed in #1433
  • When clicking titles on Geizhals, the correct item should be selected (one of the sub-titles is selected instead) - see #1514.
    Current state: good - fixed in #1433
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/hints/zoom.html0000644000175100017510000000052215102145205021161 0ustar00runnerrunner Drawing hints with zoom

When you press 2+ then f on this page, the hint should be drawn at the correct position. link.

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1762183912.6066382 qutebrowser-3.6.1/tests/manual/history/0000755000175100017510000000000015102145351017666 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1762183813.0 qutebrowser-3.6.1/tests/manual/history/visited.html0000644000175100017510000000140515102145205022221 0ustar00runnerrunner Visited/Unvisited links