Repository: parkouss/webmacs Branch: master Commit: c42d89fcc41e Files: 113 Total size: 777.4 KB Directory structure: gitextract_3q3a7_u6/ ├── .flake8 ├── .gitattributes ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── COPYING ├── README-nix.org ├── README.org ├── c/ │ └── adblock.c ├── docs/ │ ├── Makefile │ ├── advanced_topics.rst │ ├── api.rst │ ├── basic_usage.rst │ ├── concepts.rst │ ├── conf.py │ ├── ext/ │ │ └── webmacs_sphinx_ext.py │ ├── faq.rst │ ├── glossary.rst │ ├── index.rst │ ├── make.bat │ └── user_configuration.rst ├── git_archive_all.py ├── pytest.ini ├── setup.py ├── test-requirements.txt ├── tests/ │ ├── integration/ │ │ ├── conftest.py │ │ ├── iframe_follow/ │ │ │ ├── index.html │ │ │ └── my_iframe.html │ │ ├── javascript_prompt/ │ │ │ └── index.html │ │ ├── navigation/ │ │ │ ├── index.html │ │ │ └── page1.html │ │ ├── test_copy_link.py │ │ ├── test_iframe_navigation.py │ │ ├── test_javascript_prompt.py │ │ ├── test_navigation.py │ │ └── test_user_download_dir.py │ ├── test_prompt_history.py │ └── test_variables.py └── webmacs/ ├── __init__.py ├── adblock.py ├── application.py ├── bookmarks.py ├── clipboard.py ├── commands/ │ ├── __init__.py │ ├── buffer_history.py │ ├── caret_browsing.py │ ├── content_edit.py │ ├── follow.py │ ├── global.py │ ├── isearch.py │ ├── minibuffer.py │ ├── webbuffer.py │ └── webjump.py ├── content_handler.py ├── default_webjumps.py ├── download_manager/ │ ├── __init__.py │ └── prompts.py ├── egrid.py ├── external_editor.py ├── features.py ├── filter_webengine_output.py ├── hooks.py ├── ignore_certificates.py ├── ipc.py ├── keyboardhandler.py ├── keymaps/ │ ├── __init__.py │ ├── caret_browsing.py │ ├── content_edit.py │ ├── fullscreen.py │ ├── global.py │ ├── hints.py │ ├── isearch.py │ ├── minibuffer.py │ └── webbuffer.py ├── killed_buffers.py ├── main.py ├── minibuffer/ │ ├── __init__.py │ ├── prompt.py │ └── right_label.py ├── mode.py ├── password_manager/ │ ├── __init__.py │ └── password_store.py ├── profile.py ├── scheme_handlers/ │ ├── __init__.py │ └── webmacs/ │ ├── __init__.py │ ├── js/ │ │ └── vue.js │ └── templates/ │ ├── base.html │ ├── bindings.html │ ├── command.html │ ├── commands.html │ ├── downloads.html │ ├── key.html │ ├── keymap.html │ ├── variable.html │ ├── variables.html │ └── version.html ├── scripts/ │ ├── caret_browsing.js │ ├── hint.js │ ├── password_manager.js │ ├── setup.js │ ├── textedit.js │ └── textzoom.js ├── session.py ├── spell_checking.py ├── task.py ├── url_opener.py ├── variables.py ├── version.py ├── visited_links.py ├── webbuffer.py ├── webview.py └── window.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .flake8 ================================================ [flake8] exclude = ./vendor/, ./build/, ./dist/, ./webmacs.egg-info/, # exclude virtual envs ./venv*, ================================================ FILE: .gitattributes ================================================ **/.gitignore export-ignore **/.gitmodules export-ignore **/.travis.yml export-ignore vendor/*/test/** export-ignore vendor/*/vendor/depot_tools/** export-ignore vendor/*/.npmignore export-ignore git_archive_all.py export-ignore ================================================ FILE: .gitignore ================================================ __pycache__/ venv/ ================================================ FILE: .gitmodules ================================================ [submodule "vendor/hashset-cpp"] path = vendor/hashset-cpp url = https://github.com/bbondy/hashset-cpp [submodule "vendor/bloom-filter-cpp"] path = vendor/bloom-filter-cpp url = https://github.com/bbondy/bloom-filter-cpp [submodule "vendor/ad-block"] path = vendor/ad-block url = https://github.com/brave/ad-block ================================================ FILE: .travis.yml ================================================ sudo: true language: python dist: xenial python: 3.7 matrix: include: - os: linux env: PYQT_VERSION=5.7.1 - os: linux env: PYQT_VERSION=5.8.2 - os: linux env: PYQT_VERSION=5.9.2 - os: linux env: PYQT_VERSION=5.10.1 - os: linux env: PYQT_VERSION=5.11.2 GENERATE_GIT_ARCHIVE=true fast_finish: true cache: directories: - $HOME/.cache/pip install: - pip install PyQt5==$PYQT_VERSION sphinx - pip install -r test-requirements.txt - sudo apt-get --yes install xvfb herbstluftwm script: - flake8 - pytest - cd docs && READTHEDOCS=1 make html && cd .. before_deploy: - python git_archive_all.py webmacs-${TRAVIS_TAG}.tar.gz deploy: provider: releases api_key: secure: MZSDuOdwUlHn55GfMy4MwkT9ZgDhd5/09gKTH2futOWtF90oIhbIO2a1ZfH8QXPB2AZtISzPFgPgMgFtdCIBkmv7ic5yD0AC+hfb6F8iWsxVaUCBQh6B7g4vUomoAEkr7+pUzjapOuJFSlaIWajSL43WcZb7Ep6gdy8M1bBnj8HsyBug5g5uV0GEFmk55Jwlg1EsXF9Jx23EstEBW1HC/wdywa3vkYZsaCzKSco+L1XU2lVtbrRBuY8SbYyvadeFPkyu315+cFNT+HkdD5yvFjRNFoaSYF4AMskv8LR7cCQ6Z7Hug1huFBNgr+LPVFxfflb+zaEETimLV5BN52pB2udv+RDJu0Xmc2hGcCb3eRrHC/w7DkxXWuldF/g8b2aCmoCUDrca1Dh+ipeXzWKo5yEFgrP4dp8l/q5ClbpH3Qc/o6IUhFvVS7YH8udO7A7bAHcZisDGuSfasQeHSDq28maU0pSH3sB0zcTvcphIzkDUjT7+9peXhn56MWUMKlBRX2JU9eoUCz1dBuxK/XCuRZ0YPHjV3xrDvEIbMBny3kdKHCTkc7rcZZs1QfZgnMmySLbM6uUXpmwxBhH5eIEqdZ5isQn/oCt+G3L99Lp7/sNr+gXJtNuocEzWcryEIeJ2Vi/hL9+Mu/I4TlgyLv6f1RS74Ndp4cPpgCCwGS2yVwY= file: webmacs-${TRAVIS_TAG}.tar.gz skip_cleanup: true on: tags: true repo: parkouss/webmacs branch: master # limit to only one element in the matrix condition: $GENERATE_GIT_ARCHIVE = true ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) ## [Unreleased] ### Fixed - Cookies and persistent data is now really saved on disk, and used on restart. - Fixed a possible security issue of the webmacs.ipc file that was readable and writable by others. - adblock url fetching and parsing is now more verbose on error and more resilient. - Fixed text selection within iframes. - Fixed minibuffer line input redo binding. - Fixed exiting webmacs as fast as possible by rewriting long-running tasks asynchronously. ### Added - Added passwordstore support for storing passwords (the linux 'pass' command line tool) - Added **content-edit-select-all** in content edit mode, bound to **C-x h**. - Added **minibuffer-select-all** in minibuffer keymap, bound to **C-x h**. - Added bindings currently attached to commands when using **M-x** command. - Added support for an off-the-record (private) mode. It is enabled by starting webmacs using **--off-the-record** flag, or using the command **open-off-the-record**. ### Changed - Moved codebase to PyQt6. PyQt5 is no longer supported. - Removed support for an internal database to store passwords. - Moved path to the spell checking data (to ~/.webmacs/spell_checking/) ## [0.8] - 2019-09-15 ### Fixed - autocompletion for duckduckgo now uses duckduckgo servers - Fixed displaying long lines in the minibuffer popup (such as long urls in history) - Fixed regression, unable to revive a buffer. - webmacs command line now handle correctly opening relative local file paths. - Fixed scrolling top or bottom when switching view, and losing focus sometimes. - Use one session file per webmacs instance. ### Added - Added **where-is** command, bound to **C-h w**, to look up what keys a command is bound to, if any. - Added **describe-key-briefly** command, bound to **C-h c**, as a less verbose alternative to **describe-key**. - Added **print-buffer** command, bound to **C-x p** to print the current buffer. - Improved customization of key bindings for incremental search, hinting and minibuffer - Added key bindings **C-/** and **C-?** (resp. undo and redo) as minibuffer input key bindings - Added a **clipboard-copy** variable to be able to copy to primary clipboard (still the default), mouse selection clipboard or both. - Added a **--list-instances** command line flag, allowing to list current running instances. - Passing an empty string to the **--instance** command line flag will generate a new unique instance name. - Added a **raise-instance** command, to raise the current window of another webmacs instance. - Added a **current-instance** command that show the name of the current instance name. - Added **C-u C-u** prefix argument for command opening urls. Using it will open the chosen url in a new window. - Added the variable **visited-links-display-limit**, to limit the number of elements displayed in the **visited-links-history** command. Defaults to 2000. - Added **--profile** command line option to allow using more than one profile. ## Changed - **C-h c** is now bound to **describe-key-briefly** instead of **describe-command** which has in turn been moved to **C-h f**, in accordance with Emacs default keybindings. ## [0.7] - 2018-09-20 ### Fixed - Fix refreshing buffer count in the minibuffer right label when a buffer is closed. - Fix reviving buffers that were not loaded (after restoring a session). - Fix hinting urls in iframes when opening in a new buffer. - Fixed support for Qt 5.7 - Fixed requiring escaping of raw % signs in custom webjumps. - Fixed zoom-normal was bound to **0** instead of **=**. ### Added - Added the **switch-buffer-current-color** variable that customize the color of the current buffer row in the **switch-buffer** displayed list. The color defaults to a light blue. Set to an empty string to get the old behavior (no specific color). - **switch-buffer** now list buffers using the internal buffer order. - Buffers now have numbers and the current buffer number is displayed in the minibuffer right label as well as in the **switch-buffer** and **switch-recent-buffer** lists. - **M-n** and **M-p** are bound respectively to the new commands **next-buffer** and **previous-buffer**, allowing to cycle through buffers. - **M-<**, **M->**, **C-v**, **M-v** bindings in the minibuffer lists for navigation. - New commands and bindings added to copy stuff to clipboard. **copy-current-link** bound to **c c**, **copy-current-buffer-title** bound to **c t**, and copy-current-buffer-url bound to **c u** (all in the webbuffer keymap) - Added a new method for hinting: alphabet. This allow to navigate only using the home row keys, and without using Enter. Can be enabled by setting the new variable **hint-method** to "alphabet". - Added a new variable **hint-alphabet-characters** to specify which characters to use with the alphabet hinting. - Added a new variable **hint-node-style** to change the style of the hint div nodes. - Added a **close-buffer-close-window** variable to be able to close a window when a buffer is closed. - **C-g** in webbuffer is now bound to **buffer-escape** instead of **buffer-unselect**, which does the same thing and send the Escape too (which closes popup and other things). - Added two variables, **default-download-dir** and **keep-temporary-download-dir**. ### Changed - The old **switch-buffer** behavior is now offered with the **switch-recent-buffer** command. The latter is now bound to **C-x b** and **C-x C-b** so there is no visible change using those keybindings. - The **c** (copy-link) binding is now available using **c l** (think about Copy Link). - The css style of the hints has been changed. If you prefer the old style, just set the **hint-node-style** variable to {"background": "red", "color": "white"}. - The **go-to** command (bounds to **g**) now set the current url in the minibuffer input and select it. - The **go-to-selected-url** and **go-to-selected-url-new-buffer** commands are renamed to **go-to-alternate-url** and **go-to-alternate-url-new-buffer**. Also they do not anymore select the current url, but only set the cursor at the end of the url. ## [0.6] - 2018-08-20 ### Fixed - crash when opening in a new window (from right-click menu on a link), in qt 5.11.1. - crash when reviving closed buffers in some cases. - crash when calling switch-buffer and closing buffer (including the current one) using C-k. ### Added - added a basic navigation toolbar, that can be shown using the command **toggle-toolbar**. Also added a new variable, **window-toolbar-on-startup** that can be set to True to show the toolbar automatically. - added a database to keep feature permissions (geolocation, camera, ...) on a per-url basis (thanks to Patrick Lafrance) - the allow permission for feature dialog now ask for Always/Never, and save that in the database. (thanks to Patrick Lafrance) - it is now possible to answer Never when webmacs ask to save a password. (thanks to Patrick Lafrance) - it is now possible to answer Always to bypass certificate errors. (thanks to Patrick Lafrance) - when opening a download, there is now a prompt to ask to download or to open the file with an external command. ### Changed - the functions **webmacs.keymaps.global_keymap()**, **webmacs.keymaps.webbuffer_keymap()**, **webmacs.keymaps.content_edit_keymap()** have been deprecated in favor of **webmacs.keymaps.keymap()** (respectively with the argument "global", "webbuffer" and "webcontent-edit"). - loading page information is now displayed in the minibuffer right label, using the **loading** key in the **minibuffer-right-label** variable (default value of this variable has changed) ## [0.5] - 2018-07-08 ### Fixed - focus is not lost anymore in the minibuffer input on page loading - adblock is fully disabled when the variable **adblock-urls-rules** is set to an empty list. - adblock cache is rebuilt when the variable **adblock-urls-rules** has changed. - Fixed the **copy-link** command (**c**) when used with the argument 0. - Added a space after the default webjump when calling **search-default**. - Mouse events are now propagated to the minibuffer input and popup. - Fixed a bug that prevented to use multi-modifiers keybindings (e.g., C-M-a) - Fixed regression in **close-other-buffers** command. - The keyboard is not anymore lost when a new buffer is opened from javascript. - The **follow** command is now working in cross-origin iframes. - Text edition in web pages is now working in cross-origin iframes. - Text zoom in web pages is now working in cross-origin iframes. - Caret navigation is now working in cross-origin iframes. ### Changed - **scroll-page-down**, **scroll-page-up**, **scroll-bottom** and **scroll-top** are now implemented by sending the PageDown, PageUp and End and Home key presses. - **search-default** now defaults to google. ### Added - The minibuffer input now flashes under some circumstances to grab user's attention. - Added **minibuffer-flash-duration**, **minibuffer-flash-color**, and **minibuffer-flash-count** variables to customize the flash animation. ## [0.4] - 2018-05-04 ### Fixed - Fixed closing buffer in some circumstances using C-k from the switch-buffer command. - Improved position of the minibuffer popup, removing empty pixels between the popup and the input. - Fixed using i-search when caret browsing is enabled - improve using multiple views, fixing a lot of bugs around that (keyboard focus lost, crash using switch-buffer on an already displayed buffer, ...) ### Changed - **breaking change**. The completion\_fn argument in define\_webjump has changed, see the documentation about that. - improved webjump completions in multiple ways. - switching buffers now tries its best to keep the current scroll and cursor position, so that coming back to a previous buffer feels more natural. This is in part implemented by keeping one internal qt webengineview per buffer. - improved the visibility of the current view when there are multiple views. There is now a border on each side of the view, with one pixel red and one black. ### Added - added a **revive-buffer** command, bound to **C-x r** in the global keymap. This allow to reopen a previously closed buffer. - added the **revive-buffers-limit** variable, to specify how many buffers might be revived. This defaults to 10. - added basic handling of web features to enable video, audio from javascript. - added two variables to customize how webmacs starts: **home-page** and **home-page-in-new-window**. - added a command to restore previous session (windows and buffers), **restore-session**. - Under X11, if the --instance is passed at the command line, the WM_CLASS property is set to "webmacs-{instance}". - added basic support for multiple windows. New commands added: **make-window**, **other-window**, **close-window**, **close-other-window**. - saving and restoring web views in session - saving and restoring window position and state in session - added a variable **webview-stylesheet** to customize the above view style. - added a command **buffer-unselect** to clear selection in the current buffer. - bound **buffer-unselect** to **C-g** in the webbuffer keymap. ## [0.3] ### Added - keymaps can now have a name and associated documentation - added command **describe-commands** to list all named webmacs commands - added command **describe-bindings** to list named commands in named keymaps - added command **describe-variables** to list named webmacs variables - added command **downloads** to open a buffer to see downloads of the session - added command **version** to open a version buffer. - added command **describe-variable** to describe a variable (bound to C-h v) - added command **describe-command** to describe a command (bound to C-h c) - added command **describe-key** to describe a keychord (bound to C-h k) - added python dependency *pygments* to render source code. - added a command line flag **--instance** to run named instances of webmacs ### Fixed - If webmacs has crashed, the local socket used for ipc is now cleaned, so other commands for this webmacs instance are forwarded and does not anymore create a new instance. - do not set the webbuffer active keymap as the current local keymap if the minibuffer input is currently opened. ## [0.2] The distinction between 0.1 and 0.2 version is not clear unfortunately - patches were just going in the main git branch. To highlight some features, let's start the version 0.2 from the commit cb0cea39eaab6a01ee74ca16261c2b467b4af5a3. ### Added - buffers loading from last session are delayed until they are displayed - added caret browsing support, with a specific keymap and its set of commands. The **C** binding in a web buffer enter the caret browsing mode - added new variable **adblock-urls-rules** to list rules url for the ad-blocker - added new variable **webjump-default** - better prompt completion - added information in the minibuffer - added new variable **minibuffer-right-label** for the format of the displayed information in the minibuffer - support for bookmarks (see **bookmark-add** and **bookmark-open** commands, bound to respectively **M** and **m** in the webbuffer keymap). - support for zoom and text zoom. See the **zoom-\*** and **text-zoom-\*** commands. - added undefine_key method on Keymaps to unbind keys - added command **close-other-buffers** - added basic notion of mode to a web buffer, normal usage being "standard-mode" and a new mode "no-keybindings" - added new variable **auto-buffer-modes** to set up rules for settings web buffer mode based on urls - added new command **content-edit-open-external-editor** to open a text editor to edit web content, bound to **Cx e** and **C-x C-e** in the webcontent-edit keymap. The external command to run is stored in the variable **external-editor-command**. - added **content-edit-undo** and **content-edit-redo** commands, bound respectively to **C-/** and **C-?** in webcontent-edit keymap. - added spell check support, configurable with the **spell-checking-dictionaries** variable. ### Fixed - fixed segfault with some graphic cards - retrieving ad-block rules and compiling them is now done in a thread, so webmacs is not slow at startup anymore - added a warning when using opengl with the nouveau driver. - default qt shortcuts in webviews are removed, so webmacs bindings are working without side-effect anymore (e.g., C-a was sometimes selecting the text) - changed implementation of the webcontent-edit movement text commands, so now undo redo works better and it also mostly works in contenteditable fields ================================================ FILE: COPYING ================================================ 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 . ================================================ FILE: README-nix.org ================================================ * Installation with Nix There are two recommended ways of installing webmacs: 1. Nix 2. pip/virtualenv ** Nix Currently, the easiest way to install webmacs is via the [[https://nixos.org/nix/][Nix package manager]]: #+BEGIN_SRC bash nix-env -i webmacs #+END_SRC *** Need more help with nix? Nix is available for Linux, macOS and other Unix-like systems. Rest assured that removing Nix (along with any packages installed using Nix) is as easy as =rm /nix -rf=. If you do not have Nix, install it. For details see https://nixos.org/nix/manual/#chap-installation, but this step approximates to #+BEGIN_SRC bash bash <(curl https://nixos.org/nix/install) #+END_SRC and will require you to provide a sudo password. Look out for, and follow the instructions which will appear once nix is installed, and which will look something like this: #+BEGIN_SRC text Installation finished! To ensure that the necessary environment variables are set, either log in again, or type . /home/yourusername/.nix-profile/etc/profile.d/nix.sh in your shell. #+END_SRC If you don't spot this, the installation will appear to have failed. Now you can use =nix-env= to install webmacs: #+BEGIN_SRC bash nix-env -i webmacs #+END_SRC For further details, see https://nixos.org/nix/manual/#chap-quick-start. *** working on webmacs with nix The command #+BEGIN_SRC bash nix-shell -p webmacs #+END_SRC will drop you into a shell which makes available all the compilers and libraries required to build and run webmacs, thus =nix-shell= plays the role of =virtualenv= in the pip/virtualenv approach described below. Unfortunately, some of the libraries required to run the tests, are not yet available in this shell. ================================================ FILE: README.org ================================================ * webmacs *webmacs* is yet another browser for keyboard-based web navigation. It mainly target emacs-like navigation, and started as a clone (in terms of features) of [[http://conkeror.org/][conkeror]]. See the documentation manual: https://webmacs.readthedocs.io/en/latest/ webmacs is based on qt webengine and written mainly in Python (version 3). #+html:

* Features Short list of features: - keyboard navigation everywhere (including basic emacs movements in editable web content) - Integrated, fast ad-blocker - [[https://webmacs.readthedocs.io/en/latest/basic_usage.html#live-documentation][live documentation]] - [[https://webmacs.readthedocs.io/en/latest/user_configuration.html][highly customizable using Python]] * Installation (... and development) ** Using Nix See the [[./.README-nix.org][dedicated page]]. ** Using pip/virtualenv Be prepared to have a working c and c++ compiler with python development library. Note I only have tested on linux. You will also need the PyQt6 library, as I believe it can't be installed through pip. It's easy to install using any package manager though. Then you have to check out the repository (do not forget the *recursive* flag): #+BEGIN_SRC bash git clone --recursive https://github.com/parkouss/webmacs #+END_SRC To test it, or work on it, I recommend virtualenv: #+BEGIN_SRC bash virtualenv --system-site-packages -p python3 venv # activate the virtualenv source venv/bin/activate # install webmacs in there pip install -e # and now to run webmacs python -m webmacs.main #+END_SRC Then you can create a system alias to run it: #+BEGIN_SRC bash sudo ln -s /bin/webmacs /usr/local/bin/webmacs # now you can use the webmacs command on your system, given that # /usr/local/bin is in your PATH. #+END_SRC * Running tests To run the tests, you will need a few more dependencies (the virtualenv needs to be activated): #+BEGIN_SRC bash # install test dependencies pip install -r /test-requirements.txt # also install the herbstluftwm window manager, using your package manager. # Example on fedora: sudo dnf install herbstluftwm #+END_SRC Then you can run the tests (the virtualenv needs to be activated): #+BEGIN_SRC bash python -m pytest /tests # you can run them with the windows visible: python -m pytest /tests --no-xvfb #+END_SRC * Qt versions support Every stable Qt version from (and including) 6.0 should work with webmacs. * Contributions Contributions are much welcome! Writing this browser is exciting and I love that, though I don't have many time to spend on it, having a family life and a job; And anyway the more we are to work on it and use the tool, the better! ================================================ FILE: c/adblock.c ================================================ // This file is part of webmacs. // // webmacs 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. // // webmacs 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 webmacs. If not, see . #include #include "structmember.h" #include #include #include "ad_block_client.h" using namespace std; typedef struct { PyObject_HEAD AdBlockClient * client; char * data; } AdBlock; static void AdBlock_dealloc(AdBlock* self) { delete self->client; if (self->data) delete[] self->data; Py_TYPE(self)->tp_free((PyObject*)self); } static PyObject * AdBlock_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { AdBlock *self; self = (AdBlock *)type->tp_alloc(type, 0); return (PyObject *)self; } static int AdBlock_init(AdBlock *self, PyObject *args, PyObject *kwds) { self->client = new AdBlockClient; self->data = NULL; return 0; } static PyObject * AdBlock_parse(AdBlock* self, PyObject *args) { const char *data; if (!PyArg_ParseTuple(args, "s", &data)) return NULL; Py_BEGIN_ALLOW_THREADS self->client->parse(data); Py_END_ALLOW_THREADS Py_RETURN_NONE; } static PyObject * AdBlock_matches(AdBlock* self, PyObject *args) { const char *url, *domain; bool result; if (!PyArg_ParseTuple(args, "ss", &url, &domain)) return NULL; /* I suspect that allowing threads here does create deadlocks, but anyway I am not sure it would be useful to allow them. */ /* Py_BEGIN_ALLOW_THREADS */ result = self->client->matches(url, FONoFilterOption, domain); /* Py_END_ALLOW_THREADS */ if (result) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } static PyObject * AdBlock_save(AdBlock* self, PyObject *args) { const char *path; if (!PyArg_ParseTuple(args, "s", &path)) return NULL; int size; ofstream outFile(path, ios::out | ios::binary); if (!outFile) { Py_RETURN_FALSE; } Py_BEGIN_ALLOW_THREADS char * buffer = self->client->serialize(&size); outFile.write(buffer, size); outFile.close(); Py_END_ALLOW_THREADS Py_RETURN_TRUE; } static PyObject * AdBlock_load(AdBlock* self, PyObject *args) { const char *path; bool result = false; if (!PyArg_ParseTuple(args, "s", &path)) return NULL; ifstream file(path, ios::binary | ios::ate); if (!file) { Py_RETURN_FALSE; } Py_BEGIN_ALLOW_THREADS streamsize size = file.tellg(); file.seekg(0, ios::beg); if (self->data) {delete[] self->data;} self->data = new char[size]; if (file.read(self->data, size)) { self->client->deserialize(self->data); result = true; } Py_END_ALLOW_THREADS if (result) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } static PyMethodDef AdBlock_methods[] = { {"parse", (PyCFunction)AdBlock_parse, METH_VARARGS, "Parse adblock data string, like the content of an easylist." }, {"matches", (PyCFunction)AdBlock_matches, METH_VARARGS, "matches an url, returns True if it should be filtered." }, {"save", (PyCFunction)AdBlock_save, METH_VARARGS, "Save serialized data into a file." }, {"load", (PyCFunction)AdBlock_load, METH_VARARGS, "Load serialized data from a file." }, {NULL} /* Sentinel */ }; static PyTypeObject AdBlockType = { PyVarObject_HEAD_INIT(NULL, 0) "adblock.AdBlock", /* tp_name */ sizeof(AdBlock), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)AdBlock_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "Adblock objects", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ AdBlock_methods, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ (initproc)AdBlock_init, /* tp_init */ 0, /* tp_alloc */ AdBlock_new, /* tp_new */ }; static PyModuleDef adblockmodule = { PyModuleDef_HEAD_INIT, "adblock", "Module to speed up ad filtering.", -1, NULL, NULL, NULL, NULL, NULL }; PyMODINIT_FUNC PyInit__adblock(void) { PyObject* m; AdBlockType.tp_new = PyType_GenericNew; if (PyType_Ready(&AdBlockType) < 0) return NULL; m = PyModule_Create(&adblockmodule); if (m == NULL) return NULL; Py_INCREF(&AdBlockType); PyModule_AddObject(m, "AdBlock", (PyObject *)&AdBlockType); return m; } ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -W SPHINXBUILD = sphinx-build SPHINXPROJ = webmacs SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/advanced_topics.rst ================================================ Advanced topics =============== .. current-keymap:: global .. _managing_views: Managing views ************** It is possible to manage multiple :term:`views` in a webmacs window: - :key:`C-x 2` split the current view in two horizontally. - :key:`C-x 3` split the current view in two vertically. - :key:`C-x o` navigate in current window views. - :key:`C-x 0` close the current view. - :key:`C-x 1` maximize the current view, closing every other view. Managing windows **************** You can also manage multiple windows: - :cmd:`make-window` to create a new window. - :cmd:`other-window` to navigate through windows. - :cmd:`close-window` to close the current window. - :cmd:`close-other-windows` to close all but the current window. Caret browsing ************** .. current-keymap:: caret-browsing Caret browsing allows to navigate in a web page using a caret. It is mainly useful for copying text inside a web page without using the mouse. It is enabled by pressing :key:`C (webbuffer)`. Then you can navigate using arrow keys or standard Emacs bindings: - :key:`C-n` to go to the next line. - :key:`C-p` to go to the previous line. - :key:`C-f` to go to the next character. - :key:`C-b` to go to the previous character. - :key:`M-f` to go to the next word. - :key:`M-b` to go to the previous word. - :key:`C-e` to go to the end of the line. - :key:`C-a` to go to the beginning of the line. You can select some text and copy it using: - :key:`C-Space` to toggle the mark - :key:`M-w` to copy the current selection to the clipboard. .. current-keymap:: webbuffer .. note:: Incremental search can be used when in caret browsing, to allow easier navigation. It is also great to start caret browsing after an incremental search, as the caret will be at the beginning of the current web selection. Bookmarks ********* Bookmarks are like a dictionary of URLs. Each bookmark must have a unique name. Bookmarks are stored in the profile, and hence are persistent across sessions. It is possible to manage bookmarks using: - :key:`M` to create a bookmark. - :key:`m` to open the bookmark list. When in the bookmark list, you can: - :key:`Return (bookmarks-list)` to open the bookmark URL in the current buffer - :key:`C-k (bookmarks-list)` to remove the highlighted bookmark. ================================================ FILE: docs/api.rst ================================================ Public api ========== Initialisation ************** .. autofunction:: webmacs.main.init Keymaps ******* .. autofunction:: webmacs.keymaps.keymap .. autoclass:: webmacs.keymaps.Keymap :members: define_key, undefine_key Webjumps ******** .. autofunction:: webmacs.commands.webjump.define_webjump .. autoclass:: webmacs.commands.webjump.WebJumpCompleter :members: .. autoclass:: webmacs.commands.webjump.WebJumpRequestCompleter .. autoclass:: webmacs.commands.webjump.SyncWebJumpCompleter .. autofunction:: webmacs.commands.webjump.define_webjump_alias Variables ********* .. autofunction:: webmacs.variables.set .. autofunction:: webmacs.variables.get .. autoexception:: webmacs.variables.VariableConditionError ================================================ FILE: docs/basic_usage.rst ================================================ Basic usage =========== Don't panic *********** When you are stuck in some interactive :term:`command` or text field, and you are unsure what to do, press **C-g**. This :term:`key binding` usually lets you out of the current action - you may have to press it more than once. **C-g** is the universal *get me out of there* command. .. note:: Usually, pressing **C-g** enough times lets you focus on the current :term:`web buffer`, and so activates the :keymap:`webbuffer` :term:`keymap`. .. current-keymap:: global Running a command using its name ******************************** It is always possible to run a :term:`command` using its name. Some commands does not have default :term:`key binding`, and requires to be called this way. To call a command using its name, use the :key:`M-x` keybinding, then select in the list (or type) the command you want to run, followed by **Return** (the Enter key). For example, :key:`M-x` toggle-toolbar will toggle the webmac's toolbar. Live documentation ****************** webmacs is self-documenting. You can easily access the documentation by running the following commands: - :cmd:`describe-commands` to see all available commands. - :cmd:`describe-command` (bound to :key:`C-h f`) to choose one command, and get a detailed description. - :cmd:`describe-variables` to see all the available :term:`variables`. - :cmd:`describe-variable` (bound to :key:`C-h v`) to choose one variable and get a detailed description. - :cmd:`describe-key` (bound to :key:`C-h k`) or :cmd:`describe-key-briefly` (bound to :key:`C-h c`) to discover what command a key binding would trigger. - :cmd:`where-is` (bound to: :key:`C-h w`) to quickly find what key(s) a command is bound to. - :cmd:`describe-bindings` to see the list of all keymaps, with the bindings and commands they contain. .. note:: Self-documentation is super useful for many things. If you want, for example, to define a custom binding for a command, but don't know its name, you can always use :key:`C-h k` to help you. Also, do not hesitate to use :key:`C-h v` to see the description of a :term:`variable`. .. current-keymap:: webbuffer Visiting urls ************* An easy way to go to a new URL is to type :key:`g`. This calls the :cmd:`go-to` command, that lets you type a URL or a :term:`webjump`. Pressing **Return** will then open it in the current web buffer. For example, try typing: **g g webmacs **. This should open a new Google page with the query 'webmacs'. .. important:: Typing **C-u** before :key:`g` will open the url or webjump in a new buffer. .. _link_hinting: Link hinting ************ Link hinting is used to navigate through visible links of the current web buffer's page, using the keyboard only. Press :key:`f`. You should see the :term:`minibuffer` right label displaying that you are in the :keymap:`hint` keymap, and the links on the page highlighted. .. current-keymap:: hint Hinting in webmacs can be done using two methods: filter (the default) and alphabet. You can use the :term:`variable` :var:`hint-method` to change it. filter ------ There is one active hint. Typing text will narrow down the hint selection by fuzzy matching against the link's texts. It is also possible to directly type the number of the link to activate it, and to cycle the visible hints (next, previous) to change the active hint. Keybindings are as follows: - :key:`C-n` activate next visible hint - :key:`C-p` activate previous visible hint Note that to validate hinting, :key:`Return` has to be pressed. alphabet -------- This is the method used by default in vimium, for example. There is no active hint, and each link is associated with some characters: they must all be entered to validate hinting. Note that the hinting characters are usually randomly picked up from the home row of the keyboard. This behavior is configured with the :term:`variable` :var:`hint-alphabet-characters`, defaulting to the home row characters of a QWERTY keyboard. .. current-keymap:: webbuffer .. _managing_buffers: Managing buffers **************** You can switch to a buffer using :key:`C-x b (global)`, which opens a list on top of the :term:`minibuffer`. Select the buffer you want to switch to by fuzzy-matching text of the url or title page, or just use the arrow keys (or better, standard Emacs bindings such as **C-n**, **C-p**, **C-v**, **M-v**, etc). Finally, validate with **Return**. .. important:: Most of the lists displayed in the :term:`minibuffer` work in this same way, and have the same basic bindings. The command is called :cmd:`switch-recent-buffer`. .. note:: The above command orders the buffers so that the most recently used is on top. If you want the buffers to be ordeded by their number, you can call the command :cmd:`switch-buffer`. You can also navigate to the next or previous buffer by using respectively :key:`M-n (global)` and :key:`M-p (global)`. A buffer can be closed by just pressing :key:`q`. When you are running :cmd:`switch-buffer` or :cmd:`switch-recent-buffer`, pressing :key:`C-k (buffer-list)` will also kill the buffer currently highlighted in the list. .. important:: If you killed a buffer by accident, no worries! Just use :key:`C-x r (global)` to resurrect it. Navigating through buffer history ********************************* - :key:`B` goes backward in the buffer history - :key:`F` goes forward in the buffer history - :key:`b` shows the current buffer's history as a list in the :term:`minibuffer`, and allows to easily navigate it. Navigating through global history ********************************* Type :key:`h` to display a list of every visited URL (these are saved in a database file and are persistent in your profile). Select one to open it in the current buffer. .. note:: Use **C-u** before :key:`h` to open the URL in a new buffer. Scrolling in current web buffer ******************************* - :key:`C-n` or :key:`n` scrolls the current buffer down a bit. - :key:`C-p` or :key:`p` scrolls the current buffer up a bit. - :key:`C-b` scrolls the current buffer left a bit. - :key:`C-f` scrolls the current buffer right a bit. - :key:`C-v` scrolls the current buffer down for one visible page. - :key:`M-v` scrolls the current buffer up for one visible page. - :key:`M-<` lets you go to the top of the page. - :key:`M->` lets you go to the bottom of the page. Searching in current web buffer ******************************* Type :key:`C-s` to start incremental search. Then you can type the text you are looking for. Press :key:`C-s` again to go to the next match, or :key:`C-r` to go to the previous match. .. note:: :key:`C-r` can also be used to start incremental search. Copying links ************* - :key:`c u` to copy the URL of the current buffer. - :key:`c l` to copy a visible link in the buffer (by :term:`hinting`). - :key:`c c` to copy the currently selected link. - :key:`c t` to copy the current buffer page title. Downloading *********** A download can be started by clicking a link or button or :term:`hinting`. When a download is about to be started, the :term:`minibuffer` will propose to either **download** or **open** it. - **download** will start downloading, and save the file to your hard drive. - **open** will download to a temporary directory, then open the file with the given command. A list of available commands is shown in the minibuffer completion list. Note that when the command exits, the file will be automatically deleted from your hard drive. .. note:: open is useful for viewing PDF files for example, as you can use your favorite PDF file viewer to read it. The list of downloads can be accessed using the :cmd:`downloads` command. .. seealso:: See the :var:`default-download-dir` and :var:`keep-temporary-download-dir` variables. Zooming ******* - :key:`+` zoom in. - :key:`-` zoom out. - :key:`=` reset the zoom to its default value. .. note:: There are variants for the zoom, using the Control modifier (:key:`C-+`, :key:`C--`, and :key:`C-=`) that are used for text zoom only. Printing ******** - :key:`C-x p` to print the current buffer. ================================================ FILE: docs/concepts.rst ================================================ Concepts ======== Please make sure to understand the following basic webmacs concepts before further reading the documentation. .. _concept_commands: Commands, key bindings and keymaps ********************************** These are quite similar to the definitions found in the Emacs manual. - A :term:`command` is a named action which can be done in the browser. For example, :cmd:`follow` is the command that allows to start hinting links to navigate. - A :term:`key binding` is a combination of key presses used to trigger commands. Key bindings are represented as in Emacs, for example **C-x C-b** means "holding the Control key while pressing x, then b on the keyboard." .. note:: The control key is called a modifier. There are three keyboard modifiers: - **C** represents the Control key. - **M** represents the Alt key. - **S** represents the Super key (often called the Windows key) .. note:: .. current-keymap:: webbuffer A key binding can also be a single key press. For example, pressing :key:`f` while in the :keymap:`webbuffer` keymap will trigger the :cmd:`follow` command. - A :term:`keymap` is an object holding a mapping between key bindings and commands, so that a command can be triggered by pressing keyboard keys. Usually, there is one global keymap, and one active local keymap activated at the same time - the local keymap changes interactively depending on the context. Some important keymaps: .. webmacs-keymaps:: :only: global, webbuffer, webcontent-edit, caret-browsing Web buffers *********** A :term:`web buffer` is like an Emacs buffer, but applying to a Web page. Buffers are like tabs in other browsers, except that they are not bound to any view or window. Windows, views ************** Differing from Emacs terminology, a window actually is what we nowadays call a window, and :term:`views` (sometimes called frames) correspond to the content of a window. ================================================ FILE: docs/conf.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # webmacs documentation build configuration file, created by # sphinx-quickstart on Sat Dec 23 08:47:03 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.join(os.path.abspath("."), "ext")) sys.path.insert(0, os.path.abspath('..')) # Generating doc don't work without that flag. That could be fixed but anyway # on readthedocs we can't install binary package so we are stuck for now trying # to mock everything. if True or "READTHEDOCS" in os.environ: # We can not install webmacs on readthedocs, as it requires to # buid some C extensions (from dateparser, PyQt6, ...). The # alternative is to mock any dependency used by webmacs. class Mock(object): def __init__(self, *a, **kw): pass def __getattr__(self, name): # noqa: E301 return Mock() def __call__(self, *a, **kw): # noqa: E301 return Mock() def __iter__(self): # noqa: E301 return iter(()) def __instancecheck__(self, instance): # noqa: E301 return True def __subclasscheck__(self, cls): # noqa: E301 return True def __mro_entries__(self, a): # noqa: E301 return () MOCK_MODULES = ["PyQt6", "PyQt6.QtCore", "PyQt6.QtGui", "PyQt6.QtWidgets", "PyQt6.QtWebEngineWidgets", "PyQt6.QtWebEngineCore", "PyQt6.QtWebChannel", "PyQt6.QtNetwork", "PyQt6.QtPrintSupport", "_adblock", "dateparser"] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # the version number is not important, though it must be an int. sys.modules["PyQt6.QtCore"].QT_VERSION \ = sys.modules["PyQt6.QtCore"].PYQT_VERSION = 330497 import webmacs # noqa: E402 # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', "webmacs_sphinx_ext"] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'webmacs' copyright = '2017, Julien Pagès' author = 'Julien Pagès' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = webmacs.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { 'github_user': 'parkouss', 'github_repo': project, "github_button": True, 'description': "An emacs-like keyboard-driven web browser", # defaults is 940, gives a bit more so viewcode looks good. 'page_width': "1050px", } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { '**': [ 'about.html', 'navigation.html', 'relations.html', # needs 'show_related': True theme option to display 'searchbox.html', ] } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'webmacsdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'webmacs.tex', 'webmacs Documentation', 'Julien Pagès', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'webmacs', 'webmacs Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'webmacs', 'webmacs Documentation', author, 'webmacs', 'One line description of project.', 'Miscellaneous'), ] ================================================ FILE: docs/ext/webmacs_sphinx_ext.py ================================================ import re from docutils.parsers.rst import Directive from docutils.statemachine import ViewList from docutils import nodes from webmacs import COMMANDS from webmacs.commands import InteractiveCommand from webmacs.commands.webjump import WEBJUMPS from webmacs.application import _app_requires from webmacs.variables import VARIABLES from webmacs.mode import MODES from webmacs.keymaps import KEYMAPS # to include all commands, etc. _app_requires() def as_rest_table(data): numcolumns = len(data[0]) colsizes = [max(len(r[i]) for r in data) for i in range(numcolumns)] formatter = ' '.join('{:<%d}' % c for c in colsizes) rowsformatted = [formatter.format(*row) for row in data] header = formatter.format(*['=' * c for c in colsizes]) yield header yield rowsformatted[0] yield header for row in rowsformatted[1:]: yield row yield header class SimpleAutoDirective(Directive): has_content = False required_arguments = 0 optional_arguments = 0 final_argument_whitespace = False option_spec = {} def run(self): self._result = ViewList() self._run() node = nodes.paragraph() node.document = self.state.document self.state.nested_parse(self._result, 0, node) return node.children class WebmacsCommands(SimpleAutoDirective): def _run(self): result = self._result def get_doc(cmd): if isinstance(cmd, InteractiveCommand): cmd = cmd.binding return cmd.__doc__ or "No description" table = [("Command", "description")] for name in sorted(COMMANDS): table.append((name, get_doc(COMMANDS[name]))) for line in as_rest_table(table): result.append(line, "") class WebmacsWebjumps(SimpleAutoDirective): def _run(self): result = self._result table = [("Name", "url", "description")] for name in sorted(WEBJUMPS): webjump = WEBJUMPS[name] table.append((name, webjump.url, webjump.doc)) for line in as_rest_table(table): result.append(line, "") class WebmacsVariables(SimpleAutoDirective): def _run(self): result = self._result table = [("Name", "description", "default")] for name in sorted(VARIABLES): variable = VARIABLES[name] table.append((name, variable.doc, repr(variable.value))) for line in as_rest_table(table): result.append(line, "") class WebmacsModes(SimpleAutoDirective): def _run(self): result = self._result table = [("Name", "Description")] for name in sorted(MODES): mode = MODES[name] table.append((name, mode.description)) for line in as_rest_table(table): result.append(line, "") class WebmacsKeymaps(SimpleAutoDirective): option_spec = { "only": lambda a: (a or "").replace(" ", "").split(",") } def _run(self): result = self._result keys = self.options.get("only") or sorted(KEYMAPS) table = [("Name", "Description")] for name in keys: km = KEYMAPS[name] table.append((name, km.doc or "")) for line in as_rest_table(table): result.append(line, "") def webmacs_role(data): """ Create a simple role function handler that check for the text to be in the given data, and just create a strong node for the it. """ def role(name, rawtext, text, lineno, inliner, options={}, content=[]): if text not in data: inliner.reporter.error("No such %s: %s" % (name, text)) node = nodes.strong(text=text) return [node], [] return role class CurrentKeymapDirective(Directive): has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = {} def run(self): env = self.state.document.settings.env keymap = self.arguments[0].strip() if keymap not in KEYMAPS: self.state_machine.reporter.error("No such keymap: %s" % keymap) env.ref_context['webmacs:keymap'] = keymap return [] KEYMAPS_BINDINGS_CACHE = {} RE_KEY_MAP = re.compile(r"^(.*)\s+\(([\w-]+)\)$") def get_keymap_bindings(keymap_name): if keymap_name not in KEYMAPS_BINDINGS_CACHE: KEYMAPS_BINDINGS_CACHE[keymap_name] \ = {k: v for k, v in KEYMAPS[keymap_name].all_bindings(raw_fn=True)} return KEYMAPS_BINDINGS_CACHE[keymap_name] def key_in_keymap_role(name, rawtext, text, lineno, inliner, options={}, content=[]): m = RE_KEY_MAP.match(text) if m: text = m.group(1) km = m.group(2) else: env = inliner.document.settings.env try: km = env.ref_context["webmacs:keymap"] except KeyError: inliner.reporter.error( "no current keymap. Use the current-keymap directive." ) keys = get_keymap_bindings(km) if text not in keys: inliner.reporter.error("No such key: %s in keymap %s" % (text, km)) node = nodes.strong(text=text) return [node], [] def setup(app): app.add_directive("webmacs-commands", WebmacsCommands) app.add_directive("webmacs-webjumps", WebmacsWebjumps) app.add_directive("webmacs-variables", WebmacsVariables) app.add_directive("webmacs-modes", WebmacsModes) app.add_directive("webmacs-keymaps", WebmacsKeymaps) app.add_directive("current-keymap", CurrentKeymapDirective) # use them to ensure doc is not outdated, making references to things that # do not exists. app.add_role("cmd", webmacs_role(COMMANDS)) app.add_role("var", webmacs_role(VARIABLES)) app.add_role("keymap", webmacs_role(KEYMAPS)) app.add_role("key", key_in_keymap_role) ================================================ FILE: docs/faq.rst ================================================ FAQ === How do I run a new webmacs instance instead of a new buffer from the command-line? ********************************************************************************** When a webmacs instance is already running, calling `webmacs ` from a shell will open the URL in a new buffer of the running instance. To run a fresh new instance, use `webmacs --instance `. How do I run webmacs with a specific profile from the command-line? ********************************************************************************** To have webmacs use a specific profile, use `webmacs --profile `. Each profile directory will contain distinct navigation data (history, cookies, ...). Website is blocked, turn off the extensions ******************************************* This message will appear in the browser when the URL has got filtered by the ad-blocker. To overcome this, you can temporarily disable the ad-blocker with *M-x toggle-ad-block*. For a permanent change, edit the :var:`adblock-urls-rules` variable, to remove some URLs in there. Note if you set this variable to an empty list, the adblocker will be completely disabled. See the :ref:`user_conf_variables` section in the documentation. ================================================ FILE: docs/glossary.rst ================================================ Glossary ======== .. glossary:: buffer web buffer The content of a web page, not including its window or view. You can learn the basics of buffer handling in :ref:`managing_buffers`. command commands A command is a named action doable in the browser. See :ref:`concept_commands` for a detailed description. See :ref:`Commands ` for a list of commands; or better, use the :cmd:`describe-commands` command to get live documentation. hinting Hinting is used to navigate through the visible links and objects of the current web buffer's page, using the keyboard only. See :ref:`link_hinting` for more information. key binding key bindings A **key binding** is a combination of key presses used to trigger commands. See :ref:`concept_commands` for a detailed description, and :ref:`user_conf_binding_keys` for custom configuration of key bindings. keymap keymaps A **keymap** is an object holding a mapping between key bindings and commands. See :ref:`concept_commands` for a detailed description. See :ref:`Keymaps ` for a list of keymaps; or better, use the :cmd:`describe-bindings` command to get live documentation. minibuffer The minibuffer is what can be seen at the bottom of a webmacs window. It displays some information on the right, such as the currently active keymap and the number of open buffers. minibuffer input When webmacs is waiting for some information from you, the **minibuffer input** is shown: it's a text edit field in which you can type some text. Often, there also is a completion list above the minibuffer input. variable variables Some behaviors of *webmacs* can be customized using variables. See :ref:`user_conf_variables` for variables configuration. See :ref:`All variables ` to see all the variables; or better, use :cmd:`describe-variables` to get live documentation. view views A view is a part of a window displaying a buffer. There can be multiple views in one window. See :ref:`managing_views`. webjump webjumps A Webjump represents a quick way to access a URL, possibly with a variable part. A webjump name becomes a part of the webmacs :cmd:`go-to` command, so for example you can type ``google foo bar`` to execute a Google query with "foo bar" terms. See :ref:`user_conf_webjumps` to see the builtins webjumps and how to configure your owns. ================================================ FILE: docs/index.rst ================================================ .. webmacs documentation master file, created by sphinx-quickstart on Sat Dec 23 08:47:03 2017. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to webmacs's documentation! =================================== **webmacs** is yet another browser for keyboard-based web navigation. Code is hosted on a `github repository `_. .. toctree:: :maxdepth: 2 :caption: Contents: concepts basic_usage advanced_topics user_configuration api faq glossary Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=webmacs if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ================================================ FILE: docs/user_configuration.rst ================================================ User configuration ================== **webmacs** can be configured by writing Python code. The files should live in a ``~/.webmacs/init`` directory, starting with an ``__init__.py`` file. If this file exists, it will be loaded early in the application. Note that you have the full power of Python in there, and as it is loaded early, you can change and adapt nearly every aspect of the webmacs behavior. .. note:: Only the documented functions and objects here are considered stable (meaning they will not change without change notes and explanations). Any other API you can use is considered internal and might change without notification. The init function ***************** You can write a function **init** that would be called when webmacs is about to start. This function must take one parameter (usually named *opts*), that contains the result of the parsed command line. For now, there is only one useful parameter: - opts.url: the url given in the command line, or None Overriding the init function does override the default init function of webmacs, though it is still accessible with :func:`webmacs.main.init`. Hello world example: .. code-block:: python import webmacs.main def init(opts): print("hello wordl") if opts.url: print("{} was given as a command line argument".format(opts.url)) webmacs.main.init(opts) The default webmacs.main.init function is responsible for restoring the session, or opening the given URL, for example. .. note:: It is not required to create an init function in the user configuration _\_init_\_.py file. Only do so if you want to change the default webmacs initialization. Other changes can be applied early, directly at the module level, such as defining :term:`webjumps` or **binding keys**. Using more than one configuration file ************************************** It is possible to write more than one configuration file. The directory where the ``__init__.py`` file lives is a Python package, so it is possible to just use relative imports. For example: ``__init__.py`` .. code-block:: python from . import webjumps ``webjumps.py`` .. code-block:: python print("definition of my custom webjumps should go there.") .. _user_conf_variables: Variables ********* It is possible to change a variable in the configuration using :func:`webmacs.variables.set`: .. code-block:: python from webmacs import variables variables.set("webjump-default", "google") .. _user_conf_all_variables: Here is the list of the variables: .. webmacs-variables:: Modes ***** Modes are used to bind keymaps to a web buffer, by assigning the buffer a given mode. By default, all buffers use the "standard-mode". Here is the list of the pre-defined modes: .. webmacs-modes:: Automatically assign modes depending on the url ----------------------------------------------- You can use the `auto-buffer-modes` variable. Example: .. code-block:: python from webmacs import variables variables.set("auto-buffer-modes", [ (".*www.gnu.org.*", "no-keybindings"), ("https://mail.google.com/.*", "no-keybindings") ]) Binding keys ************ In webmacs, like in Emacs, it is possible to bind a key to a command on a given keymap. .. _user_conf_keymaps: Keymaps ------- Here is the list of available keymaps. Note that you can see them live (with their associated key bindings) in webmacs by running the command `describe-bindings`. .. webmacs-keymaps:: A keymap object in user configuration is retrieved with :func:`webmacs.keymaps.keymap`. .. _user_conf_commands: Commands -------- Here is the list of the currently available commands: .. webmacs-commands:: .. _user_conf_binding_keys: Binding a command to a keymap ----------------------------- You should use :meth:`webmacs.keymaps.Keymap.define_key`. Here is an example: .. code-block:: python from webmacs import keymaps global_map = keymaps.keymap("global") global_map.define_key("C-c |", "split-view-right") global_map.define_key("C-c _", "split-view-bottom") buffer_keymap = keymaps.keymap("webbuffer") buffer_keymap.define_key("x", "close-buffer") .. note:: The global buffer should not define single letter keychords, as you won't be able to type that letter in editable fields; though, this is possible in the webbuffer :term:`keymap`. .. _user_conf_webjumps: Webjumps ******** Here is the implementation of the google :term:`webjump`: .. literalinclude:: ../webmacs/default_webjumps.py :start-after: # ----------- doc example :end-before: # ----------- end of doc example The list of defined webjumps in webmacs: .. webmacs-webjumps:: You can implement your own webjumps, or override the existing ones. See :func:`webmacs.commands.webjump.define_webjump` and the example above. Pressing the ``s`` key will call the command ``search-default``, wich will, by default, use the Google webjump. To change this default, change the value of the variable *webjump-default*. It is also possible to define an alias to an existing webjump, without duplicating its implementation. .. code-block:: python from webmacs.commands.webjump import define_webjump_alias define_webjump_alias("g", "google") ================================================ FILE: git_archive_all.py ================================================ # Script to generate a git archive with submodules # From https://github.com/Kentzo/git-archive-all from os import extsep, path, readlink from shlex import quote from subprocess import CalledProcessError, Popen, PIPE from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED import re import sys import tarfile __version__ = "1.18.1" class GitArchiver(object): """ GitArchiver Scan a git repository and export all tracked files, and submodules. Checks for .gitattributes files in each directory and uses 'export-ignore' pattern entries for ignore files in the archive. >>> archiver = GitArchiver(main_repo_abspath='my/repo/path') >>> archiver.create('output.zip') """ def __init__(self, prefix='', exclude=True, force_sub=False, extra=None, main_repo_abspath=None): """ @param prefix: Prefix used to prepend all paths in the resulting archive. Extra file paths are only prefixed if they are not relative. E.g. if prefix is 'foo' and extra is ['bar', '/baz'] the resulting archive will look like this: / baz foo/ bar @type prefix: str @param exclude: Determines whether archiver should follow rules specified in .gitattributes files. @type exclude: bool @param force_sub: Determines whether submodules are initialized and updated before archiving. @type force_sub: bool @param extra: List of extra paths to include in the resulting archive. @type extra: list @param main_repo_abspath: Absolute path to the main repository (or one of subdirectories). If given path is path to a subdirectory (but not a submodule directory!) it will be replaced with abspath to top-level directory of the repository. If None, current cwd is used. @type main_repo_abspath: str """ if extra is None: extra = [] if main_repo_abspath is None: main_repo_abspath = path.abspath('') elif not path.isabs(main_repo_abspath): raise ValueError("main_repo_abspath must be an absolute path") try: main_repo_abspath = path.abspath( self.run_git_shell('git rev-parse --show-toplevel', main_repo_abspath).rstrip()) except CalledProcessError: raise ValueError("{0} is not part of a git repository" .format(main_repo_abspath)) self.prefix = prefix self.exclude = exclude self.extra = extra self.force_sub = force_sub self.main_repo_abspath = main_repo_abspath def create(self, output_path, dry_run=False, output_format=None): """ Create the archive at output_file_path. Type of the archive is determined either by extension of output_file_path or by output_format. Supported formats are: gz, zip, bz2, xz, tar, tgz, txz @param output_path: Output file path. @type output_path: str @param dry_run: Determines whether create should do nothing but print what it would archive. @type dry_run: bool @param output_format: Determines format of the output archive. If None, format is determined from extension of output_file_path. @type output_format: str """ if output_format is None: file_name, file_ext = path.splitext(output_path) output_format = file_ext[len(extsep):].lower() if not dry_run: if output_format == 'zip': archive = ZipFile(path.abspath(output_path), 'w') def add_file(file_path, arcname): if not path.islink(file_path): archive.write(file_path, arcname, ZIP_DEFLATED) else: i = ZipInfo(arcname) i.create_system = 3 i.external_attr = 0xA1ED0000 archive.writestr(i, readlink(file_path)) elif output_format in ['tar', 'bz2', 'gz', 'xz', 'tgz', 'txz']: if output_format == 'tar': t_mode = 'w' elif output_format == 'tgz': t_mode = 'w:gz' elif output_format == 'txz': t_mode = 'w:xz' else: t_mode = 'w:{0}'.format(output_format) archive = tarfile.open(path.abspath(output_path), t_mode) def add_file(file_path, arcname): archive.add(file_path, arcname) else: raise RuntimeError("unknown format: {0}".format(output_format)) def archiver(file_path, arcname): add_file(file_path, arcname) else: archive = None def archiver(file_path, arcname): print("{0} => {1}".format(file_path, arcname)) self.archive_all_files(archiver) if archive is not None: archive.close() def is_file_excluded(self, file_path): """ Checks whether file at a given path is excluded. """ out = self.run_git_shell( 'git check-attr -z export-ignore -- %s' % quote(file_path), cwd=self.main_repo_abspath ).split('\0') try: return out[2] == 'set' except IndexError: return False def archive_all_files(self, archiver): """ Archive all files using archiver. @param archiver: Callable that accepts 2 arguments: abspath to file on the system and relative path within archive. @type archiver: Callable """ for file_path in self.extra: archiver(path.abspath(file_path), path.join(self.prefix, file_path)) for file_path in self.walk_git_files(): archiver(path.join(self.main_repo_abspath, file_path), path.join(self.prefix, file_path)) def walk_git_files(self, repo_path=''): """ An iterator method that yields a file path relative to main_repo_abspath for each file that should be included in the archive. Skips those that match the exclusion patterns found in any discovered .gitattributes files along the way. Recurs into submodules as well. @param repo_path: Path to the git submodule repository relative to main_repo_abspath. @type repo_path: str @return: Iterator to traverse files under git control relative to main_repo_abspath. @rtype: Iterable """ repo_abspath = path.join(self.main_repo_abspath, repo_path) repo_file_paths = self.run_git_shell( 'git ls-files -z --cached --full-name --no-empty-directory', repo_abspath ).split('\0')[:-1] for repo_file_path in repo_file_paths: # absolute file path repo_file_abspath = path.join(repo_abspath, repo_file_path) # file path relative to the main repo main_repo_file_path = path.join(repo_path, repo_file_path) # Only list symlinks and files. if not path.islink(repo_file_abspath) \ and path.isdir(repo_file_abspath): continue if self.is_file_excluded(main_repo_file_path): continue yield main_repo_file_path if self.force_sub: self.run_git_shell('git submodule init', repo_abspath) self.run_git_shell('git submodule update', repo_abspath) try: repo_gitmodules_abspath = path.join(repo_abspath, ".gitmodules") with open(repo_gitmodules_abspath) as f: lines = f.readlines() for l in lines: m = re.match("^\\s*path\\s*=\\s*(.*)\\s*$", l) if m: repo_submodule_path = m.group(1) # relative to repo_path # relative to main_repo_abspath gen = self.walk_git_files( path.join(repo_path, repo_submodule_path)) for main_repo_submodule_fpath in gen: if self.is_file_excluded(main_repo_submodule_fpath): continue yield main_repo_submodule_fpath except IOError: pass @staticmethod def run_git_shell(cmd, cwd=None): """ Runs git shell command, reads output and decodes it into unicode string. @param cmd: Command to be executed. @type cmd: str @type cwd: str @param cwd: Working directory. @rtype: str @return: Output of the command. @raise CalledProcessError: Raises exception if return code of the command is non-zero. """ p = Popen(cmd, shell=True, stdout=PIPE, cwd=cwd) output, _ = p.communicate() output = output.decode('unicode_escape')\ .encode('raw_unicode_escape').decode('utf-8') if p.returncode: if sys.version_info > (2, 6): raise CalledProcessError(returncode=p.returncode, cmd=cmd, output=output) else: raise CalledProcessError(returncode=p.returncode, cmd=cmd) return output def main(): from optparse import OptionParser parser = OptionParser( version="%prog {0}".format(__version__) ) parser.add_option('--prefix', type='string', dest='prefix', default=None, help="""prepend PREFIX to each filename in the archive. OUTPUT_FILE name is used by default to avoid tarbomb. You can set it to '' in order to explicitly request tarbomb""") parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='enable verbose mode') parser.add_option('--no-exclude', action='store_false', dest='exclude', default=True, help="don't read .gitattributes files for patterns" " containing export-ignore attrib") parser.add_option('--force-submodules', action='store_true', dest='force_sub', help='force a git submodule init && git submodule update' " at each level before iterating submodules") parser.add_option('--extra', action='append', dest='extra', default=[], help="any additional files to include in the archive") parser.add_option('--dry-run', action='store_true', dest='dry_run', help="don't actually archive anything, just show what" " would be done") options, args = parser.parse_args() if len(args) != 1: parser.error("You must specify exactly one output file") output_file_path = args[0] if path.isdir(output_file_path): parser.error("You cannot use directory as output") # avoid tarbomb if options.prefix is not None: options.prefix = path.join(options.prefix, '') else: import re output_name = path.basename(output_file_path) output_name = re.sub( '(\\.zip|\\.tar|\\.tgz|\\.txz|\\.gz|\\.bz2|\\.xz|\\.tar\\.gz' '|\\.tar\\.bz2|\\.tar\\.xz)$', '', output_name ) or "Archive" options.prefix = path.join(output_name, '') try: archiver = GitArchiver(options.prefix, options.exclude, options.force_sub, options.extra) archiver.create(output_file_path, options.dry_run) except Exception as e: parser.exit(2, "{0}\n".format(e)) sys.exit(0) if __name__ == '__main__': main() ================================================ FILE: pytest.ini ================================================ [pytest] addopts = tests log_cli = true log_cli_level = debug log_format = %(asctime)s %(name)-10s %(levelname)s %(message)s log_date_format = %Y-%m-%d %H:%M:%S ================================================ FILE: setup.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import re import subprocess from setuptools import setup, Extension, find_packages from distutils.command.build_py import build_py as _build_py THIS_DIR = os.path.dirname(os.path.realpath(__file__)) bloom_dir = os.path.join(THIS_DIR, "vendor", "bloom-filter-cpp") hashset_dir = os.path.join(THIS_DIR, "vendor", "hashset-cpp") adblock_dir = os.path.join(THIS_DIR, "vendor", "ad-block") if "CC" not in os.environ: # force g++, not sure why but else gcc is used and the code does not # compile... os.environ["CC"] = "g++" adblocker = Extension( '_adblock', define_macros=[], language="c++", include_dirs=[bloom_dir, hashset_dir, adblock_dir], # not sure if that help for speed. Careful it strip the debug symbols extra_compile_args=["-g0", "-std=c++11"], sources=[ os.path.join(bloom_dir, "BloomFilter.cpp"), os.path.join(bloom_dir, "hashFn.cpp"), os.path.join(hashset_dir, "hash_set.cc"), os.path.join(adblock_dir, "ad_block_client.cc"), os.path.join(adblock_dir, "filter.cc"), os.path.join(adblock_dir, "cosmetic_filter.cc"), os.path.join(adblock_dir, "no_fingerprint_domain.cc"), os.path.join(adblock_dir, "protocol.cc"), os.path.join(THIS_DIR, "c", "adblock.c"), ]) def get_version(): with open(os.path.join(THIS_DIR, "webmacs", "__init__.py")) as f: version = re.findall("__version__ = '(.+)'", f.read()) return version[0] def get_revision(): # ensure we are in a git dir if not os.path.exists(os.path.join(THIS_DIR, ".git")): return None p = subprocess.Popen( ["git", "rev-parse", "HEAD"], cwd=THIS_DIR, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out, err = p.communicate() if p.returncode == 0: return out.strip().decode("utf-8") class build_py(_build_py): """ Override build to generate a revision file to install. """ def run(self): rev = get_revision() # honor the --dry-run flag if not self.dry_run and rev: target_dir = os.path.join(self.build_lib, 'webmacs') # mkpath is a distutils helper to create directories self.mkpath(target_dir) with open(os.path.join(target_dir, 'revision'), 'w') as f: f.write(rev) # distutils uses old-style classes, so no super() _build_py.run(self) setup( name='webmacs', version=get_version(), description='Keyboard driven web browser, emacs-like', author='Julien Pagès', author_email='j.parkouss@gmail.com', url='https://github.com/parkouss/webmacs', long_description=''' A browser for keyboard-based web navigation. Keybindings are emacs-friendly, most of them took from the conkeror (http://conkeror.org/) project which is not maintained anymore. It is based on qtwebengine, which in turns uses chromium for the web engine. Some of the features are: - integrated ad-blocker - emacs like navigation nearly everywhere (C-n, C-p, ...) - hinting to navigate with keyboard only - and a lot more, see the project url ''', packages=find_packages(), install_requires=["dateparser", "jinja2", "pygments"], entry_points={"console_scripts": ["webmacs = webmacs.main:main"]}, package_data={"webmacs": [ "scripts/*.js", "scheme_handlers/webmacs/js/*.js", "scheme_handlers/webmacs/templates/*.html", ]}, cmdclass={'build_py': build_py}, python_requires=">=3.3", ext_modules=[adblocker], ) ================================================ FILE: test-requirements.txt ================================================ -e . pytest pytest-qt pytest-mock pytest-xvfb flake8 ================================================ FILE: tests/integration/conftest.py ================================================ import pytest import os import time import subprocess from PyQt6.QtTest import QTest from PyQt6.QtCore import QEvent, QTimer from webmacs.application import Application, _app_requires from webmacs import (windows, buffers, WINDOWS_HANDLER, current_buffer, current_window, current_minibuffer) from webmacs import variables as wvariables from webmacs.webbuffer import create_buffer from webmacs.window import Window from webmacs.webbuffer import close_buffer from webmacs.keymaps import KeyPress THIS_DIR = os.path.dirname(os.path.realpath(__file__)) _app = None def get_test_page(name): if name.endswith(".html"): return os.path.join(THIS_DIR, name) return os.path.join(THIS_DIR, name, "index.html") @pytest.fixture(scope="session") def wm(xvfb): if xvfb is None: yield None else: if not any( os.access(os.path.join(path, 'herbstluftwm'), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) ): raise RuntimeError("herbstluftwm is not installed, can not run" " graphical tests.") env = dict(os.environ) env["DISPLAY"] = str(":%s" % xvfb.display) proc = subprocess.Popen( ["herbstluftwm"], env=env ) time.sleep(2) # wait for the wm to be active yield proc proc.kill() proc.wait() class VariablesWrapper(object): def __init__(self): self._original = {} def set(self, name, value): if name not in self._original: self._original[name] = wvariables.get(name) wvariables.set(name, value) def get(self, name): return wvariables.get(name) def restore(self): for name, value in self._original.items(): wvariables.set(name, value) self._original.clear() @pytest.fixture() def variables(): vars = VariablesWrapper() yield vars vars.restore() @pytest.fixture(scope='session') def qapp(wm, qapp_args): _app_requires() global _app # TODO FIXME use another path for tests conf_path = os.path.join(os.path.expanduser("~"), ".webmacs") _app = Application(conf_path, ["webmacs"]) return _app class TestSession(object): NAV_HIGHLIGHT_COLOR = 'rgb(136, 255, 0)' def __init__(self, qtbot, qapp, prompt_exec): self.qtbot = qtbot self.qapp = qapp self.prompt_exec = prompt_exec def set_prompt_exec(self, fn): self.prompt_exec.side_effect = fn def waiter(self): return Waiter(self) def call_next(self, fn): QTimer.singleShot(0, fn) @property def buffer(self): return current_buffer() @property def window(self): return current_window() @property def minibuffer(self): return current_minibuffer() @property def minibuffer_input(self): return self.minibuffer.input() def wait_signal(self, *args, **kwargs): return self.qtbot.wait_signal(*args, **kwargs) def wait_until(self, func, wait=2.0, delay=0.01): delay = int(delay * 1000) end = time.time() + wait while not func(): QTest.qWait(delay) if time.time() > end: return False return True def test_page_url(self, name): return "file://" + get_test_page(name) def load_page(self, name, buffer=None, wait_iframes=False): buffer = buffer or self.buffer with self.wait_signal(buffer.loadFinished): buffer.load(self.test_page_url(name)) script = ( "__webmacs_loaded = window.__webmacsHandler__ !== null;" "if (! __webmacs_loaded) {" " document.addEventListener('_webmacs_external_created'," " function() {" " __webmacs_loaded = true;" " });" " }" ) buffer.runJavaScript(script) self.check_javascript("__webmacs_loaded", True) if wait_iframes: self.wait_iframes(buffer=buffer) def check_javascript(self, script, return_value, buffer=None): buffer = buffer or self.buffer result = [None] if return_value is None: raise ValueError("return value can't be None") def ready(): if result[0] == return_value: return True buffer.runJavaScript(script, lambda r: result.__setitem__(0, r)) assert self.wait_until(ready), "javascript result was %r" % result[0] return True def wait_hints_ready(self): return self.qtbot.wait_signal( self.buffer.content_handler.browserObjectsInited, timeout=3000, raising=True ) def check_nav_highlighted(self, js_elem): self.check_javascript("%s.style.backgroundColor" % js_elem, self.NAV_HIGHLIGHT_COLOR) def wait_iframes(self, buffer=None): buffer = buffer or self.buffer script = ( "var result = true;" "for (var i = 0; i < window.frames.length; i++) { " " if (window.frames[i].document.readyState != 'complete') {" " result = false;" " }" "}" "result;" ) self.check_javascript(script, True, buffer=buffer) def keyclick(self, key, **kwargs): QTest.keyClick(self.qapp.focusWindow(), key, **kwargs) def keyclicks(self, keys, **kwargs): for key in keys: self.keyclick(key, **kwargs) def wkeyclicks(self, shortcut, widget=None): widget = widget or self.qapp.focusWindow() keys = [KeyPress.from_str(k) for k in shortcut.split()] for key in keys: evt = key.to_qevent(QEvent.Type.KeyPress) self.keyclick(key.key, modifier=evt.modifiers()) class Waiter(object): def __init__(self, session): self.session = session self.end = False def set(self): self.end = True def wait(self, wait=5, **kwargs): kwargs["wait"] = wait return self.session.wait_until(lambda: self.end, **kwargs) @pytest.yield_fixture() def session(qtbot, qapp, mocker): # do not close the application on last window closed mocker.patch("webmacs.WindowsHandler._on_last_window_closing") \ .return_value = False prompt_exec = mocker.patch("webmacs.minibuffer.prompt._prompt_exec") sess = TestSession(qtbot, qapp, prompt_exec) window = Window() WINDOWS_HANDLER.current_window = window window.current_webview().setBuffer(create_buffer()) window.show() qtbot.waitForWindowShown(window) yield sess for w in windows(): w.current_webview().setBuffer(None) w.close() w.deleteLater() for buffer in buffers(): close_buffer(buffer) qapp.processEvents() ================================================ FILE: tests/integration/iframe_follow/index.html ================================================ iframe testing
top ================================================ FILE: tests/integration/iframe_follow/my_iframe.html ================================================ inside iframe
inside link ================================================ FILE: tests/integration/javascript_prompt/index.html ================================================ inside iframe

================================================ FILE: tests/integration/navigation/index.html ================================================ page index ================================================ FILE: tests/integration/navigation/page1.html ================================================ first page ================================================ FILE: tests/integration/test_copy_link.py ================================================ from webmacs.keyboardhandler import CommandContext from webmacs.application import app from webmacs.commands import COMMANDS def clipboard_contains(text): return app().clipboard().text() == text def test_copy_current_link(session): session.load_page("iframe_follow", wait_iframes=True) link = "document.getElementById('a_top')" session.buffer.runJavaScript("%s.focus()" % link) session.check_javascript("document.activeElement == %s" % link, True) COMMANDS["copy-current-link"](CommandContext()) assert session.wait_until( lambda: clipboard_contains("https://foo/top.html") ) def test_copy_current_link_in_subframe(session): session.load_page("iframe_follow", wait_iframes=True) link = "window.frames[0].document.getElementById('a_inside')" session.buffer.runJavaScript("%s.focus()" % link) session.check_javascript( "document.activeElement.tagName", 'IFRAME' ) session.check_javascript( "window.frames[0].document.activeElement == %s" % link, True ) COMMANDS["copy-current-link"](CommandContext()) assert session.wait_until( lambda: clipboard_contains("https://foo/inside.html") ) def test_copy_current_url(session): session.load_page("iframe_follow") url = session.buffer.url().toString() COMMANDS["copy-current-buffer-url"](CommandContext()) assert session.wait_until( lambda: clipboard_contains(url) ) def test_copy_current_title(session): session.load_page("iframe_follow") COMMANDS["copy-current-buffer-title"](CommandContext()) assert session.wait_until( lambda: clipboard_contains("iframe testing") ) ================================================ FILE: tests/integration/test_iframe_navigation.py ================================================ import pytest INPUT0 = "document.getElementById('input0')" INPUT0_IFRAME = "window.frames[0].document.getElementById('input0')" def test_iframe_navigation(session): """ Webcontent-edit keymaps are used even in sub frames. """ session.load_page("iframe_follow", wait_iframes=True) session.wkeyclicks("Tab") session.check_javascript("%s === document.activeElement" % INPUT0, True) # type some text in INPUT0 and move word backward session.keyclicks("hello world") session.check_javascript("%s.value" % INPUT0, "hello world") session.check_javascript("%s.selectionEnd" % INPUT0, 11) session.wkeyclicks("M-b") session.check_javascript("%s.selectionEnd" % INPUT0, 6) session.wkeyclicks("Tab") session.check_javascript("%s === window.frames[0].document.activeElement" % INPUT0_IFRAME, True) # type some text in INPUT0 inside the iframe and move word backward session.keyclicks("hello world2") session.check_javascript("%s.value" % INPUT0_IFRAME, "hello world2") session.check_javascript("%s.selectionEnd" % INPUT0_IFRAME, 12) session.wkeyclicks("M-b") session.check_javascript("%s.selectionEnd" % INPUT0_IFRAME, 6) @pytest.mark.parametrize("hint_method", ["filter", "alphabet"]) def test_iframe_follow(session, pytestconfig, hint_method, variables): """ It is possible to hint things inside sub frames. """ variables.set("hint-method", hint_method) session.load_page("iframe_follow", wait_iframes=True) end_waiter = session.waiter() def do_check(): session.check_javascript( "%s === window.frames[0].document.activeElement" % INPUT0_IFRAME, True) session.keyclicks("youhou") session.check_javascript("%s.value" % INPUT0_IFRAME, "youhou") end_waiter.set() def prompt_follow(prompt, _): wait_hint.wait() if hint_method == "filter": session.wkeyclicks("C-n") # wait until the background color is green, the above keypress has # been taken in account. session.check_nav_highlighted(INPUT0_IFRAME) with session.wait_signal(prompt.closed): session.wkeyclicks("Enter") else: with session.wait_signal(prompt.closed): session.keyclick("d") session.call_next(do_check) session.set_prompt_exec(prompt_follow) wait_hint = session.wait_hints_ready() session.wkeyclicks("f") end_waiter.wait() ================================================ FILE: tests/integration/test_javascript_prompt.py ================================================ import pytest def check_js_result(res): def check(session): session.check_javascript("getContent();", res) return check def check_minibuffer(res): def check(session): assert session.wait_until( lambda: session.minibuffer.label.text() == res ) return check @pytest.mark.parametrize("selection,input,check", [ # first variable is what is typed in follow command, # to select the right button to click. ("pro", "y", check_js_result("true")), ("pro", "n", check_js_result("false")), ("aler", None, check_minibuffer("[js-alert] hello there")), ]) def test_confirm(session, selection, input, check): """ test javascript confirm. """ session.load_page("javascript_prompt") end_waiter = session.waiter() def prompt_follow(prompt, _): session.keyclicks(selection) session.set_prompt_exec(confirm) with session.wait_signal(prompt.closed): session.wkeyclicks("Enter") if not input: session.call_next(do_check) def confirm(prompt, _1): with session.wait_signal(prompt.closed): session.keyclicks(input) session.call_next(do_check) def do_check(): check(session) end_waiter.set() session.set_prompt_exec(prompt_follow) session.wkeyclicks("f") end_waiter.wait() ================================================ FILE: tests/integration/test_navigation.py ================================================ from webmacs import BUFFERS from webmacs.webbuffer import create_buffer def test_cycle_buffers(session): """ Webcontent-edit keymaps are used even in sub frames. """ session.load_page("navigation") session.load_page("navigation/page1.html", buffer=create_buffer()) def on_index_page(): return session.buffer.title() == "page index" def on_first_page(): return session.buffer.title() == "first page" assert len(BUFFERS) == 2 assert on_index_page() session.wkeyclicks("M-n") assert session.wait_until(on_first_page) session.wkeyclicks("M-n") assert session.wait_until(on_index_page) session.wkeyclicks("M-p") assert session.wait_until(on_first_page) session.wkeyclicks("M-p") assert session.wait_until(on_index_page) ================================================ FILE: tests/integration/test_user_download_dir.py ================================================ import pytest import os from webmacs import download_manager # should be more or less in unit test section, but uses the variables fixture. def test_get_user_download_dir(variables, tmpdir): # by default, no custom user download dir assert download_manager.get_user_download_dir() is None d1 = str(tmpdir.mkdir("d1")) d2 = str(tmpdir.mkdir("d2")) download_manager.TEMPORARY_DOWNLOAD_DIR = d2 # if default-download-_dir is set, use it variables.set("default-download-dir", d1) assert download_manager.get_user_download_dir() == d1 # if keep-temporary-download-dir is set, it takes precendence variables.set("keep-temporary-download-dir", True) assert download_manager.get_user_download_dir() == d2 variables.set("default-download-dir", "") assert download_manager.get_user_download_dir() == d2 @pytest.mark.parametrize("fname,expected", [ ("/path/to", ("/path", "to")), ("/path/to(1)", ("/path", "to")), ("/path/to(3)", ("/path", "to")), ("/path/to.txt", ("/path", "to.txt")), ("/path/to(1).txt", ("/path", "to.txt")), ("/path/to(3).tar.gz", ("/path", "to.tar.gz")), ]) def test_extract_suggested_filename(fname, expected): assert download_manager.extract_suggested_filename(fname) == expected @pytest.mark.parametrize("files, filename, expected", [ ([], "toto", "toto"), (["toto"], "toto", "toto(1)"), ([], "toto.txt", "toto.txt"), (["toto.txt"], "toto.txt", "toto(1).txt"), (["toto.tar.gz", "toto(1).tar.gz"], "toto.tar.gz", "toto(2).tar.gz"), ]) def test_find_unique_suggested_path(tmpdir, files, filename, expected): for name in files: tmpdir.join(name).ensure(file=True) dir = str(tmpdir) assert download_manager.find_unique_suggested_path(dir, filename) \ == os.path.join(dir, expected) ================================================ FILE: tests/test_prompt_history.py ================================================ from webmacs.minibuffer.prompt import PromptHistory def test_history(): p = PromptHistory(maxsize=10) # calling next or previous on empty history is alright assert p.get_next() == "" assert p.get_previous() == "" assert p.in_user_value() # we are in user value # insert some values, a bit more than what the buffer can store for i in range(12): p.push(str("test_%d" % i)) # playing around with next/previous assert p.get_previous() == "test_11" assert p.get_previous() == "test_10" assert p.get_next() == "test_11" assert not p.in_user_value() # we get back in user value when we do an equal amount of next/previous # calls assert p.get_next() == "" assert p.in_user_value() # test the custom user value p.set_user_value("foobar") assert p.get_next() == "test_2" assert not p.in_user_value() assert p.get_previous() == "foobar" assert p.in_user_value() assert p.get_next() == "test_2" assert not p.in_user_value() # resetting put the state back to initial, including the custom user value p.reset() assert p.in_user_value() assert p.get_next() == "test_2" assert p.get_previous() == "" ================================================ FILE: tests/test_variables.py ================================================ import pytest from webmacs.variables import ( VariableConditionError, String, Int, Bool, Float, List, Tuple, Dict ) def check_type_error(type, value, regex): with pytest.raises(VariableConditionError) as ei: type.validate(value) assert ei.match(regex) def test_type_string(): s = String() s.validate("") s.validate("hello") check_type_error(s, 1, r"Must be a string") s = String(choices=["aha", "hoho"]) s.validate("aha") s.validate("hoho") check_type_error(s, 1, r"Must be a string") check_type_error(s, "invalid", r"Must be one of \('aha', 'hoho'\)") def test_type_int(): i = Int() i.validate(-5) i.validate(0) i.validate(5) check_type_error(i, "1", r"Must be an integer") i = Int(min=0, max=5) i.validate(0) i.validate(1) i.validate(5) check_type_error(i, "1", r"Must be an integer") check_type_error(i, 6, r"Must be lesser or equal to 5") check_type_error(i, -1, r"Must be greater or equal to 0") def test_type_bool(): b = Bool() b.validate(True) b.validate(False) def test_type_list(): l = List(Float(min=0.0)) # noqa: E741 l.validate([]) l.validate([1.1, 6.1]) check_type_error(l, 123, "Must be a list") check_type_error(l, [2.3, "1"], r"List at position 1: Must be a float") check_type_error(l, [-2.3, 3.2], r"List at position 0: Must be greater or equal to 0.0") def test_type_dict(): d = Dict(String(), Tuple(Int(), Float(min=0.0, max=1.1))) d.validate({}) d.validate({"1": (1, 0.5)}) check_type_error(d, 123, "Must be a dict") check_type_error(d, {"1": (1,)}, "Value for key '1': Must be a tuple of size 2") ================================================ FILE: webmacs/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import importlib from PyQt6.QtCore import QObject, QEvent, QTimer from . import hooks __version__ = '0.9' # access to every opened buffers BUFFERS = [] # dictionary of all known commands COMMANDS = {} def require(module, package=__package__): return importlib.import_module(module, package) # handler for windows, to be able to list them and determine the one currently # active. class WindowsHandler(QObject): def __init__(self, parent=None): QObject.__init__(self, parent) self.windows = [] self.current_window = None def _on_last_window_closing(self): # last window is closed, do not remove it from the list but exit the # application. This is required from proper session saving. from .application import app app().quit() return True def register_window(self, window): window.installEventFilter(self) self.windows.append(window) def eventFilter(self, window, event): t = event.type() if t == QEvent.Type.WindowActivate: self.current_window = window hooks.window_activated(window) elif t == QEvent.Type.Close: if window.quit_if_last_closed and len(self.windows) == 1: if self._on_last_window_closing(): return True self.windows.remove(window) window.deleteLater() if window == self.current_window: self.current_window = None hooks.window_closed(window) return QObject.eventFilter(self, window, event) WINDOWS_HANDLER = WindowsHandler() def windows(): """ Returns the window list. Do not modify this list. """ return WINDOWS_HANDLER.windows def current_window(): """ Returns the currently activated window. """ return WINDOWS_HANDLER.current_window def current_buffer(): """ Returns the current buffer. """ w = current_window() if w: return w.current_webview().buffer() def buffers(): "Returns the list of buffers." return BUFFERS def recent_buffers(): """ Returns an iterable of buffers, most recently used first. """ return sorted(BUFFERS, key=lambda b: b.last_use, reverse=True) def current_minibuffer(): """ Returns the current minibuffer. """ w = current_window() if w: return w.minibuffer() def minibuffer_show_info(text): """ Display text information in the current minibuffer. """ minibuffer = current_minibuffer() if minibuffer: minibuffer.show_info(text) def call_later(fn, msec=0): """ Call the given function after the given time interval. If msec is 0, the function call is still delayed to the next handling of events in the qt event loop. """ QTimer.singleShot(msec, fn) class ObjRef(object): """ Maintain object references. """ __slots__ = ("__refs",) def __init__(self): self.__refs = {} def ref(self, obj, data=True): self.__refs[obj] = data def unref(self, obj): return self.__refs.pop(obj) # Sometimes we need to keep objects around with pyqt to avoid segfault; # This global object holder is designed to allow that. GLOBAL_OBJECTS = ObjRef() ================================================ FILE: webmacs/adblock.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import logging import time import json from datetime import datetime, timezone import dateparser from _adblock import AdBlock from concurrent.futures import ThreadPoolExecutor, as_completed import urllib.request from . import variables from .task import Task from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply from PyQt6.QtCore import QUrl, QThreadPool, pyqtSignal as Signal, Qt DEFAULT_EASYLIST = [ "https://easylist.to/easylist/easylist.txt", # easyprivacy blocks too much right now # "https://easylist.to/easylist/easyprivacy.txt", "https://easylist.to/easylist/fanboy-annoyance.txt" ] adblock_urls_rules = variables.define_variable( "adblock-urls-rules", "A list of urls to get rules for ad-blocking (using the Adblock format)." " The default urls are taken from the easylist site https://easylist.to.", DEFAULT_EASYLIST, type=variables.List(variables.String()), ) def cache_file(cache_path): return os.path.join(cache_path, "cache.dat") class AdBlockUpdateTask(Task): adblock_ready = Signal(AdBlock) def __init__(self, app, cache_path, ): Task.__init__(self) self.app = app if not os.path.isdir(cache_path): os.makedirs(cache_path) self._cache_path = cache_path self._cached_urls_path = os.path.join(self._cache_path, "urls.json") self._cache_file = cache_file(cache_path) self._user_urls = { url: os.path.join(self._cache_path, url.rsplit("/", 1)[-1]) for url in adblock_urls_rules.value } self.adblock_ready.connect(self._on_adblock_ready, Qt.ConnectionType.BlockingQueuedConnection) self._adblock = None self._replies = {} self._modified = False self.__thread_running = False def start(self): to_download = [(url, path) for url, path in self._user_urls.items() if not os.path.isfile(path) or (os.path.getmtime(path) + 3600) < time.time()] for url, path in to_download: reply = self.app.network_manager.get(QNetworkRequest(QUrl(url))) reply.readyRead.connect(self._dl_ready_read) reply.finished.connect(self._dl_finished) self._replies[reply] = {"path": path} self._maybe_finish() def _maybe_finish(self): if self._replies: return if not self._modified: try: with open(self._cached_urls_path) as f: cached_urls = json.load(f) except FileNotFoundError: self._modified = True except Exception: logging.exception("Could not load cached urls. Removing %s." % self._cached_urls_path) os.unlink(self._cached_urls_path) self._modified = True else: if cached_urls != self._user_urls or not os.path.exists(self._cache_file): self._modified = True self.__thread_running = True QThreadPool.globalInstance().start( self._parse_adblock_files if self._modified else self._adblock_from_cache) def _adblock_from_cache(self): adblock = AdBlock() adblock.load(self._cache_file) self.adblock_ready.emit(adblock) def _parse_adblock_files(self): adblock = AdBlock() for path in self._user_urls.values(): logging.info("parsing adblock file: %s", path) try: with open(path) as f: adblock.parse(f.read()) except Exception: logging.exception(f"Unable to parse {f.name} adblock file") adblock.save(self._cache_file) self.adblock_ready.emit(adblock) def _on_adblock_ready(self, adblock): self.__thread_running = False with open(self._cached_urls_path, "w") as f: json.dump(self._user_urls, f) self._adblock = adblock self.finished.emit() def adblock(self): return self._adblock def _dl_ready_read(self): reply = self.sender() data = self._replies[reply] if "file" not in data: headers = {bytes(k).lower(): bytes(v) for k, v in reply.rawHeaderPairs()} if os.path.isfile(data["path"]): try: last_modified = dateparser.parse( headers[b"last-modified"].decode("utf-8"), languages=["en"]) except Exception: logging.exception( "Unable to parse the last-modified header for %s", reply.url().toString()) else: file_time = datetime.fromtimestamp( os.path.getmtime(data["path"]), timezone.utc) if last_modified < file_time: logging.info("no need to download adblock rule: %s", url) # touch on the file os.utime(path, None) self._close_reply(reply) self._maybe_finish() return logging.info("downloading adblock rule: %s", reply.url().toString()) data["file"] = open(data["path"], "w") data["file"].write(bytes(reply.readAll()).decode("utf-8")) def _dl_finished(self): reply = self.sender() self._modified = True data = self._replies.pop(reply) data["file"].close() self._maybe_finish() def _close_reply(self, reply): del self._replies[reply] reply.readyRead.disconnect(self._dl_ready_read) reply.finished.disconnect(self._dl_finished) reply.close() def abort(self): for reply, data in list(self._replies.items()): self._close_reply(reply) if "file" in data: data["file"].close() os.unlink(data["path"]) # wait for any thread to join if self.__thread_running: QThreadPool.globalInstance().waitForDone(1000) ================================================ FILE: webmacs/application.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import logging from PyQt6.QtCore import pyqtSlot as Slot, Qt from PyQt6.QtWebEngineCore import QWebEngineUrlRequestInterceptor from PyQt6.QtWidgets import QApplication from PyQt6.QtNetwork import QNetworkAccessManager from . import require, version from .task import TaskRunner from .adblock import AdBlockUpdateTask, adblock_urls_rules, AdBlock from .download_manager import DownloadManager from .profile import named_profile from .minibuffer.right_label import init_minibuffer_right_labels from .keyboardhandler import LOCAL_KEYMAP_SETTER from .spell_checking import SpellCheckingTask, \ spell_checking_dictionaries from .scheme_handlers import register_schemes if version.is_linux: # workaround for a nvidia issue # see https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 import ctypes import ctypes.util ctypes.CDLL(ctypes.util.find_library("GL"), mode=ctypes.RTLD_GLOBAL) THIS_DIR = os.path.dirname(os.path.realpath(__file__)) class UrlInterceptor(QWebEngineUrlRequestInterceptor): def __init__(self, app): QWebEngineUrlRequestInterceptor.__init__(self) self._adblock = AdBlock() self._use_adblock = True @Slot(object) def update_adblock(self, adblock): self._adblock = adblock def toggle_use_adblock(self): self._use_adblock = not self._use_adblock def interceptRequest(self, request): url = request.requestUrl() url_s = url.toString() if (self._use_adblock and self._adblock.matches( url_s, request.firstPartyUrl().toString())): logging.info("filtered: %s", url_s) request.block(True) class WithoutAppEventFilter(object): def __enter__(self): app().removeEventFilter(LOCAL_KEYMAP_SETTER) def __exit__(self, type, value, traceback): app().installEventFilter(LOCAL_KEYMAP_SETTER) def app(): return Application.INSTANCE def _app_requires(): require(".commands.follow") require(".commands.buffer_history") require(".commands.global") require(".commands.isearch") require(".commands.webbuffer") require(".commands.caret_browsing") require(".commands.content_edit") require(".commands.minibuffer") require(".default_webjumps") require(".keymaps.global") require(".keymaps.caret_browsing") require(".keymaps.content_edit") require(".keymaps.fullscreen") require(".keymaps.minibuffer") require(".keymaps.hints") require(".keymaps.isearch") require(".keymaps.webbuffer") class Application(QApplication): INSTANCE = None def __init__(self, conf_path, args, instance_name="default", profile_name="default", off_the_record=False): QApplication.__init__(self, args) self.__class__.INSTANCE = self self.instance_name = instance_name self.task_runner = TaskRunner() if version.is_mac: self.setAttribute( Qt.ApplicationAttribute.AA_MacDontSwapCtrlAndMeta) register_schemes() self._conf_path = conf_path if not os.path.isdir(self.profiles_path()): os.makedirs(self.profiles_path()) self._interceptor = UrlInterceptor(self) self._download_manager = DownloadManager(self) self.profile = named_profile(profile_name, off_the_record=off_the_record) self.installEventFilter(LOCAL_KEYMAP_SETTER) self.setQuitOnLastWindowClosed(False) self.network_manager = QNetworkAccessManager(self) self.aboutToQuit.connect(self.task_runner.stop) def conf_path(self): return self._conf_path def profiles_path(self): return os.path.join(self.conf_path(), "profiles") def adblock_path(self): return os.path.join(self.conf_path(), "adblock") def visitedlinks(self): return self.profile.visitedlinks def bookmarks(self): return self.profile.bookmarks def features(self): return self.profile.features def url_interceptor(self): return self._interceptor def download_manager(self): return self._download_manager def ignored_certs(self): return self.profile.ignored_certs def adblock_update(self): if not adblock_urls_rules.value: return task = AdBlockUpdateTask(self, self.adblock_path()) def adblock_finished(): adblock = task.adblock() if adblock: self._interceptor.update_adblock(adblock) task.finished.connect(adblock_finished) self.task_runner.run(task) def update_spell_checking(self): if not bool(spell_checking_dictionaries.value): return spell_check_path = os.path.join(self._conf_path, "spell_checking") def spc_finished(*a): self.profile.update_spell_checking() task = SpellCheckingTask(self, spell_check_path) task.finished.connect(spc_finished) self.task_runner.run(task) def post_init(self): self.adblock_update() self.update_spell_checking() init_minibuffer_right_labels() ================================================ FILE: webmacs/bookmarks.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import sqlite3 class Bookmarks(object): def __init__(self, dbbath): self._conn = sqlite3.connect(dbbath) self._conn.execute(""" CREATE TABLE IF NOT EXISTS bookmarks (url TEXT PRIMARY KEY, name TEXT); """) def set(self, url, name): self._conn.execute(""" INSERT OR REPLACE INTO bookmarks (url, name) VALUES (?, ?) """, (url, name)) self._conn.commit() def list(self): return [r for r in self._conn.execute( "select url, name from bookmarks order by name" )] def remove(self, url): self._conn.execute(""" DELETE from bookmarks WHERE url = ? """, (url,)) self._conn.commit() ================================================ FILE: webmacs/clipboard.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from PyQt6.QtGui import QClipboard from . import variables, minibuffer_show_info _CLIPBOARD = None _COPY_MODE = None def _clipboard(): global _CLIPBOARD if _CLIPBOARD is None: from .application import app _CLIPBOARD = app().clipboard() return _CLIPBOARD class Mode: PRIMARY = 1 << 0 SELECTION = 1 << 1 BOTH = PRIMARY | SELECTION _from_str = { "primary": PRIMARY, "selection": SELECTION, "both": BOTH, } def _copy_mode_from_var(v): global _COPY_MODE _COPY_MODE = Mode._from_str[v.value] clipboard_copy = variables.define_variable( "clipboard-copy", "Where to copy text. Allowed values are 'primary', 'selection' or 'both'" " Defaults to primary, which is the global clipboard. selection is for" " clipboard mouse selection, and both will copy to both clipboards.", "primary", type=variables.String(choices=tuple(Mode._from_str.keys())), callbacks=(_copy_mode_from_var,) ) _copy_mode_from_var(clipboard_copy) def set_text(text, mode=None): cb = _clipboard() if mode is None: mode = _COPY_MODE if mode & Mode.PRIMARY: cb.setText(text) if mode & Mode.SELECTION: cb.setText(text, QClipboard.Selection) minibuffer_show_info("Copied: {}".format(text)) ================================================ FILE: webmacs/commands/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import inspect from .. import COMMANDS from .. import url_opener class InteractiveCommand(object): """ A command to interact with the system. A prompt class can be given to get an argument using the minibuffer prompt. :param binding: a callable to run when invoking the command. :param visible: whether or not to list the command using M-x """ __slots__ = ("binding", "visible") def __init__(self, binding, visible=True): self.binding = binding self.visible = visible def getdoc(self): return inspect.getdoc(self.binding) def __call__(self, ctx): return self.binding(ctx) def define_command(name, binding=None, **args): """ Register an interactive command. """ command = InteractiveCommand(binding, **args) COMMANDS[name] = command if binding is None: def wrapper(func): command.binding = func return func return wrapper class Opener(object): CURRENT_BUFFER = 1 NEW_BUFFER = 2 NEW_WINDOW = 3 def __init__(self, prompt_ctor): self.prompt_ctor = prompt_ctor def prompt_open(self, method, ctx): prompt = self.prompt_ctor(ctx) if method == self.NEW_BUFFER: prompt.label += " (new buffer)" elif method == self.NEW_WINDOW: prompt.label += " (new window)" return prompt def open(self, method, ctx, prompt, url): opts = {} if method == self.NEW_BUFFER: opts["new_buffer"] = True elif method == self.NEW_WINDOW: opts["new_window"] = True url_opener.url_open(ctx, url, **opts) def closed(self, method, ctx, prompt): pass def run(self, method, ctx): prompt = self.prompt_open(method, ctx) url = ctx.minibuffer.do_prompt(prompt) if url: self.open(method, ctx, prompt, url) self.closed(method, ctx, prompt) def register_prompt_opener_commands(name, opener, doc): if not isinstance(opener, Opener): opener = Opener(opener) @define_command(name + "-new-window") def open_new_window(ctx): opener.run(Opener.NEW_WINDOW, ctx) @define_command(name + "-new-buffer") def open_new_buffer(ctx): opener.run(Opener.NEW_BUFFER, ctx) @define_command(name) def open(ctx): if ctx.current_prefix_arg == (4,): return open_new_buffer(ctx) elif ctx.current_prefix_arg == (16,): return open_new_window(ctx) opener.run(Opener.CURRENT_BUFFER, ctx) open.__name__ = name.replace("-", "_") open_new_buffer.__name__ = open.__name__ + "_new_buffer" open_new_window.__name__ = open.__name__ + "_new_window" open.__doc__ = doc + "." + "\n\n You can use as a prefix to open" \ " in a new buffer, or to open in a new window." open_new_buffer.__doc__ = doc + " in a new buffer." open_new_window.__doc__ = doc + " in a new window." ================================================ FILE: webmacs/commands/buffer_history.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from PyQt6.QtCore import QAbstractTableModel, QModelIndex, Qt from PyQt6.QtGui import QImage from PyQt6.QtNetwork import QNetworkRequest from .. import current_buffer from ..minibuffer import Prompt from ..commands import define_command from ..application import app class BufferHistoryTableModel(QAbstractTableModel): def __init__(self, history): QAbstractTableModel.__init__(self) self._history = history nm = app().network_manager self._icons = {} for h in history: reply = nm.get(QNetworkRequest(h.iconUrl())) reply.finished.connect(self.icon_dl_finished) def rowCount(self, index=QModelIndex()): return len(self._history) def columnCount(self, index=QModelIndex()): return 2 def data(self, index, role=Qt.ItemDataRole.DisplayRole): hitem = index.internalPointer() if not hitem: return col = index.column() if role == Qt.ItemDataRole.DisplayRole: if col == 0: return hitem.url().toString() else: return hitem.title() elif role == Qt.ItemDataRole.DecorationRole and col == 0: return self._icons.get(hitem.iconUrl()) # hitem.iconUrl() def index(self, row, col, parent=QModelIndex()): try: return self.createIndex(row, col, self._history[row]) except IndexError: return QModelIndex() def icon_dl_finished(self): reply = self.sender() url = reply.request().url() img = QImage() img.loadFromData(reply.readAll()) if not img.isNull() and img.height() != 16 and img.width != 16: img = img.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio) reply.deleteLater() self._icons[url] = img for i, item in enumerate(self._history): if item.iconUrl() == url: index = self.index(i, 0) self.dataChanged.emit(index, index) class BufferHistoryListPrompt(Prompt): label = "buffer history:" complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } value_return_index_data = True def enable(self, minibuffer): self.page_history = current_buffer().history() # keep a python reference to the items self._items = self.page_history.items() Prompt.enable(self, minibuffer) minibuffer.input().popup().selectRow( self.page_history.currentItemIndex()) def completer_model(self): return BufferHistoryTableModel(self._items) @define_command("buffer-history") def buffer_history(ctx): """ Prompt to navigate in the local buffer history. """ prompt = BufferHistoryListPrompt(ctx) item = ctx.minibuffer.do_prompt(prompt) if item: prompt.page_history.goToItem(item) ================================================ FILE: webmacs/commands/caret_browsing.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from PyQt6.QtWebEngineCore import QWebEngineScript from . import define_command def call_js(ctx, script): ctx.buffer.runJavaScript(script, QWebEngineScript.ScriptWorldId.ApplicationWorld) @define_command("caret-browsing-init") def init(ctx): """ Init caret browsing in the current buffer. """ call_js(ctx, "CaretBrowsing.setInitialCursor();") @define_command("caret-browsing-shutdown") def shutdown(ctx): """ Shutdown caret browsing in current buffer. """ call_js(ctx, "CaretBrowsing.shutdown();") @define_command("caret-browsing-down") def down(ctx): """ Move the caret down a line. """ call_js(ctx, "CaretBrowsing.move('forward', 'line');") @define_command("caret-browsing-up") def up(ctx): """ Move the caret up a line. """ call_js(ctx, "CaretBrowsing.move('backward', 'line');") @define_command("caret-browsing-backward-char") def left_char(ctx): """ Move the caret one character backward. """ call_js(ctx, "CaretBrowsing.move('backward', 'character');") @define_command("caret-browsing-backward-word") def left_word(ctx): """ Move the caret one word backward. """ call_js(ctx, "CaretBrowsing.move('backward', 'word');") @define_command("caret-browsing-forward-char") def right_char(ctx): """ Move the caret one character forward. """ call_js(ctx, "CaretBrowsing.move('forward', 'character');") @define_command("caret-browsing-forward-word") def right_word(ctx): """ Move the caret one word forward. """ call_js(ctx, "CaretBrowsing.move('forward', 'word');") @define_command("caret-browsing-toggle-mark") def toggle_mark(ctx): """ Set or unset (toggle) the mark where the point is. """ call_js(ctx, "CaretBrowsing.toggleMark();") @define_command("caret-browsing-cut") def copy(ctx): """ Cut the current caret selection. """ call_js(ctx, "CaretBrowsing.cutSelection();") @define_command("caret-browsing-end-of-line") def end_of_line(ctx): """ Move the caret to the end of the current line. """ call_js(ctx, "CaretBrowsing.move('forward', 'lineboundary');") @define_command("caret-browsing-beginning-of-line") def beginning_of_line(ctx): """ Move the caret to the beginning of the current line. """ call_js(ctx, "CaretBrowsing.move('backward', 'lineboundary');") @define_command("caret-browsing-end-of-document") def end_of_document(ctx): """ Move the caret to the end of the document. """ call_js(ctx, "CaretBrowsing.move('forward', 'documentboundary');") @define_command("caret-browsing-beginning-of-document") def beginning_of_document(ctx): """ Move the caret to the beginning of the document. """ call_js(ctx, "CaretBrowsing.move('backward', 'documentboundary');") @define_command("caret-browsing-forward-paragraph") def forward_paragraph(ctx): """ Move the caret to the next paragraph (TODO FIXME not working yet) """ call_js(ctx, "CaretBrowsing.move('forward', 'paragraphboundary');") @define_command("caret-browsing-backward-paragraph") def backward_paragraph(ctx): """ Move the caret to the previous paragraph (TODO FIXME not working yet) """ call_js(ctx, "CaretBrowsing.move('backward', 'paragraphboundary');") ================================================ FILE: webmacs/commands/content_edit.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from PyQt6.QtWebEngineCore import QWebEngineScript from PyQt6.QtCore import Qt, QEvent from PyQt6.QtGui import QKeyEvent from ..application import app from ..webbuffer import WebBuffer from . import define_command def send_raw_key(ctx, key, with_ctrl=False, auto_shift=True): a = app() modifiers = Qt.KeyboardModifier.NoModifier if auto_shift: if ctx.buffer.hasSelection() and not ctx.buffer.text_edit_mark: ctx.buffer.set_text_edit_mark(True) if ctx.buffer.text_edit_mark: modifiers |= Qt.KeyboardModifier.ShiftModifier if with_ctrl: modifiers |= Qt.KeyboardModifier.ControlModifier w = app().focusWindow() a.postEvent(w, QKeyEvent(QEvent.Type.KeyPress, key, modifiers)) a.postEvent(w, QKeyEvent(QEvent.Type.KeyRelease, key, modifiers)) def run_js(ctx, cmd, cb=None): if cb: ctx.buffer.runJavaScript( cmd, QWebEngineScript.ScriptWorldId.ApplicationWorld, cb) else: ctx.buffer.runJavaScript( cmd, QWebEngineScript.ScriptWorldId.ApplicationWorld) @define_command("content-edit-cancel") def cancel(ctx): """ If a mark is active, clear that but keep the focus. If there is no active mark, then just unfocus the editable js object. """ if ctx.buffer.hasSelection(): run_js(ctx, "textedit.clear_mark();") else: run_js(ctx, "textedit.blur();") ctx.buffer.set_text_edit_mark(False) @define_command("content-edit-set-mark") def set_mark(ctx): """ Set or clear the mark in browser text field. """ if ctx.buffer.hasSelection(): run_js(ctx, "textedit.clear_mark();") ctx.buffer.set_text_edit_mark( not ctx.buffer.text_edit_mark ) @define_command("content-edit-forward-char") def forward_char(ctx): """ Move one character forward in browser text field. """ send_raw_key(ctx, Qt.Key.Key_Right) @define_command("content-edit-backward-char") def backward_char(ctx): """ Move one character backward in browser text field. """ send_raw_key(ctx, Qt.Key.Key_Left) @define_command("content-edit-forward-word") def forward_word(ctx): """ Move one word forward in browser text field. """ send_raw_key(ctx, Qt.Key.Key_Right, with_ctrl=True) @define_command("content-edit-backward-word") def backward_word(ctx): """ Move one word backward in browser text field. """ send_raw_key(ctx, Qt.Key.Key_Left, with_ctrl=True) @define_command("content-edit-beginning-of-line") def move_beginning_of_line(ctx): """ Move to the beginning of the line in browser text field. """ send_raw_key(ctx, Qt.Key.Key_Home) @define_command("content-edit-end-of-line") def move_end_of_line(ctx): """ Move to the end of the line in browser text field. """ send_raw_key(ctx, Qt.Key.Key_End) def delete_selection(ctx): def wrapper(_): send_raw_key(ctx, Qt.Key.Key_Backspace, auto_shift=False) ctx.buffer.set_text_edit_mark(False) return wrapper @define_command("content-edit-delete-forward-char") def delete_char(ctx): """ Delete one character forward in browser text field. """ run_js( ctx, "textedit.select_text('forward', 'character');", delete_selection(ctx), ) @define_command("content-edit-delete-forward-word") def delete_word(ctx): """ Delete one word forward in browser text field. """ run_js( ctx, "textedit.select_text('forward', 'word');", delete_selection(ctx), ) @define_command("content-edit-delete-backward-word") def delete_word_backward(ctx): """ Delete one word backward in browser text field. """ run_js( ctx, "textedit.select_text('backward', 'word');", delete_selection(ctx), ) @define_command("content-edit-copy") def copy(ctx): """ Copy browser text field selection to the clipboard. """ ctx.buffer.set_text_edit_mark(False) run_js(ctx, "textedit.copy_text(true);") @define_command("content-edit-cut") def cut(ctx): """ Cut browser text field selection to the clipboard. """ run_js(ctx, "textedit.copy_text();", delete_selection(ctx)) @define_command("content-edit-kill") def kill(ctx): """ Kill from the cursor to end of line to the clipboard. """ run_js(ctx, "textedit.select_text('forward', 'lineboundary'); \ textedit.copy_text();", delete_selection(ctx)) @define_command("content-edit-upcase-forward-word") def upcase_word(ctx): """ Upcase the word forward in browser text field. """ run_js(ctx, "textedit.upcase_word();") @define_command("content-edit-downcase-forward-word") def downcase_word(ctx): """ Downcase the word forward in browser text field. """ run_js(ctx, "textedit.downcase_word();") @define_command("content-edit-capitalize-forward-word") def capitalize_word(ctx): """ Capitalize the word forward in browser text field. """ run_js(ctx, "textedit.capitalize_word();") @define_command("content-edit-open-external-editor") def open_external_editor(ctx): """ Open an external editor to change the text field content. """ run_js(ctx, "textedit.external_editor_open()") @define_command("content-edit-undo") def undo(ctx): """ Undo the last editing action. """ ctx.buffer.triggerAction(WebBuffer.WebAction.Undo) @define_command("content-edit-redo") def redo(ctx): """ Redo the last editing action. """ ctx.buffer.triggerAction(WebBuffer.WebAction.Redo) @define_command("content-edit-select-all") def select_all(ctx): """ Select all text of the current input. """ run_js(ctx, "textedit.select_text()") ================================================ FILE: webmacs/commands/follow.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from PyQt6.QtCore import QEvent, Qt from ..minibuffer import Prompt from ..keymaps import KeyPress, HINT_KEYMAP from ..commands import define_command, register_prompt_opener_commands, \ Opener from .. import variables, clipboard HINT_METHODS = ("filter", "alphabet") hint_method = variables.define_variable( "hint-method", "Method to hint things in web buffers.", HINT_METHODS[0], type=variables.String(choices=HINT_METHODS), ) hint_alphabet_characters = variables.define_variable( "hint-alphabet-characters", "Which characters to use for alphabet hinting.", "asdfghjkl", type=variables.String(), ) hint_node_style = variables.define_variable( "hint-node-style", "The style to apply to the hint div. Note that it is a dict of JavaScript" " style property names to values. See" " https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style.", { "whiteSpace": "nowrap", "overflow": "hidden", "padding": "1px 3px 0px 3px", "background": "linear-gradient(to bottom, #fc3232 0%,#990000 100%)", "border": "solid 1px #c32222", "borderRadius": "3px", "boxShadow": "0px 3px 7px 0px rgba(0, 0, 0, 0.3)", "color": "white", "fontWeight": "bold", "fontSize": "13px", "textShadow": "1px 1px 0 rgba(0, 0, 0, 0.6)", }, type=variables.Dict(variables.String(), variables.String()), ) def hint_method_options(method): options = { "hint": hint_node_style.value, "background": "yellow", "background_active": "#88FF00", "text_color": "black", } if method == "alphabet": options["characters"] = hint_alphabet_characters.value return options # took from conkeror SELECTOR_CLICKABLE = ( "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or " "@oncommand or @role='link' or @role='button' or @role='menuitem']" " | //input[not(@type='hidden')] | //a[@href] | //area" " | //iframe | //textarea | //button | //select" " | //*[@contenteditable = 'true']" " | //xhtml:*[@onclick or @onmouseover or @onmousedown or" " @onmouseup or @oncommand or @role='link' or @role='button' or" " @role='menuitem'] | //xhtml:input[not(@type='hidden')]" " | //xhtml:a[@href] | //xhtml:area | //xhtml:iframe" " | //xhtml:textarea | //xhtml:button | //xhtml:select" " | //xhtml:*[@contenteditable = 'true'] | //svg:a" ) SELECTOR_LINK = "//a[@href] | //iframe" class HintPrompt(Prompt): keymap = HINT_KEYMAP hint_selector = "" def enable(self, minibuffer): super(HintPrompt, self).enable(minibuffer) self.page = self.ctx.buffer self.method = hint_method.value self.method_options = hint_method_options(self.method) self.page.start_select_browser_objects( self.hint_selector, method=self.method, method_options=self.method_options ) self.numbers = "" minibuffer.input().textChanged.connect(self.on_text_edited) self.browser_object_activated = {} self.page.content_handler.browserObjectActivated.connect( self.on_browser_object_activated ) minibuffer.input().installEventFilter(self) def on_browser_object_activated(self, bo): self.browser_object_activated = bo self.minibuffer.input().set_right_italic_text(bo.get("url", "")) if self.method == "alphabet": self._on_edition_finished() def on_text_edited(self, text): self.page.filter_browser_objects(text) def _update_label(self): label = self.label if self.numbers: label = label + (" #%s" % self.numbers) self.minibuffer.label.setText(label) def eventFilter(self, obj, event): numbers = ("1", "2", "3", "4", "5", "6", "7", "8", "9", "0") if event.type() == QEvent.Type.KeyPress: if self.method == "filter": text = event.text() if text in numbers: self.numbers += text self.page.select_visible_hint(self.numbers) self._update_label() return True elif not event.key() in ( Qt.Key.Key_Control, Qt.Key.Key_Shift, Qt.Key.Key_Alt, Qt.Key.Key_Meta, Qt.Key.Key_unknown, Qt.Key.Key_Return, ): self.numbers = "" self._update_label() elif self.method == "alphabet": kp = KeyPress.from_qevent(event) if kp is not None: char = kp.char() if not kp.has_any_modifier() \ and len(char) == 1 \ and char not in self.method_options["characters"]: return True return super(HintPrompt, self).eventFilter(obj, event) def value(self): return super().value() is not None class CopyLinkPrompt(HintPrompt): label = "copy link:" hint_selector = SELECTOR_LINK def eventFilter(self, obj, event): res = super(CopyLinkPrompt, self).eventFilter(obj, event) if self.numbers == "0": self.minibuffer.input().set_right_italic_text( self.page.url().toString() ) return res class FollowPrompt(HintPrompt): label = "follow:" hint_selector = SELECTOR_CLICKABLE class FollowOpener(Opener): def prompt_open(self, method, ctx): prompt = super().prompt_open(method, ctx) if method != self.CURRENT_BUFFER: prompt.hint_selector = SELECTOR_LINK return prompt def open(self, method, ctx, prompt, url): if method == self.CURRENT_BUFFER: ctx.buffer.focus_active_browser_object() elif "url" in prompt.browser_object_activated: super().open(method, ctx, prompt, prompt.browser_object_activated["url"]) ctx.buffer.stop_select_browser_objects() register_prompt_opener_commands( "follow", FollowOpener(FollowPrompt), "Hint links in the buffer and follow them on selection" ) @define_command("copy-link") def copy_link(ctx): """ Hint links in the buffer to copy them. """ prompt = CopyLinkPrompt(ctx) if not ctx.minibuffer.do_prompt(prompt): return url = None ctx.buffer.stop_select_browser_objects() if prompt.numbers == "0": # special case, copy current url url = str(ctx.buffer.url().toEncoded(), "utf-8") else: url = prompt.browser_object_activated.get("url") if url: clipboard.set_text(url) @define_command("hint-abort") def cancel(ctx): """ Abort current hint session. """ ctx.buffer.stop_select_browser_objects() ctx.minibuffer.close_prompt() @define_command("hint-next") def next_completion(ctx): """ Select the next hint. """ ctx.buffer.select_nex_browser_object() @define_command("hint-prev") def previous_completion(ctx): """ Select the previous hint. """ ctx.buffer.select_nex_browser_object(False) ================================================ FILE: webmacs/commands/global.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import itertools import os import sys from PyQt6.QtCore import QStringListModel, QModelIndex, QProcess from . import define_command, COMMANDS, register_prompt_opener_commands from ..minibuffer import Prompt from ..minibuffer.prompt import PromptTableModel, PromptHistory from ..application import app from ..webbuffer import create_buffer from ..keymaps import KeyPress, VISITEDLINKS_KEYMAP, BOOKMARKS_KEYMAP, \ KEYMAPS, GLOBAL_KEYMAP from ..keyboardhandler import send_key_event, local_keymap, KEY_EATER, \ CallHandler from .. import BUFFERS, windows, variables from ..mode import MODES from ..window import Window from ..session import session_clean, session_load from ..ipc import IpcServer from ..url_opener import url_open class CommandsListPrompt(Prompt): label = "M-x: " complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } history = PromptHistory() def __init__(self, ctx, local_keymap=None): Prompt.__init__(self, ctx) self.__local_keymap = local_keymap def completer_model(self): if self.__local_keymap: bindings = {} def add(prefix, cmd, parent): bindings[cmd] = " ".join(str(k) for k in prefix) # add bindings from currently active keymaps: global and the local # one, registered before opening the minibuffer. GLOBAL_KEYMAP.traverse_commands(add) self.__local_keymap.traverse_commands(add) data = [(k, bindings.get(k, "")) for k, v in COMMANDS.items() if v.visible] else: data = [(k,) for k, v in COMMANDS.items() if v.visible] model = PromptTableModel(data, self) return model @define_command("quit") def quit(ctx): """ Quit the application. """ app().quit() @define_command("M-x", visible=False) def commands(ctx): """ Prompt for a command name to execute. """ prompt = CommandsListPrompt(ctx, local_keymap()) value = ctx.minibuffer.do_prompt(prompt) try: COMMANDS[value](ctx) except KeyError: pass @define_command("toggle-fullscreen") def toggle_fullscreen(ctx): """ Toggle fullscreen state of the current window. """ win = ctx.window if not win: return if win.isFullScreen(): win.showNormal() else: win.showFullScreen() @define_command("toggle-maximized") def toggle_maximised(ctx): """ Toggle maximised state of the current window. """ win = ctx.window if not win: return if win.isMaximized(): win.showNormal() else: win.showMaximized() def _get_or_create_buffer(win): visible_buffers = [] for awin in windows(): for view in awin.webviews(): visible_buffers.append(view.buffer()) current_buffer = win.current_webview().buffer() buffers = [b for b in BUFFERS if b not in visible_buffers or b == current_buffer] # if there is at least one buffer not visible, use the one just # after the current one in the list if len(buffers) > 1: ibuffers = itertools.cycle(buffers) while True: buff = next(ibuffers) if buff == current_buffer: return next(ibuffers) # else create a new buffer, reusing the current buffer's url return create_buffer(url=current_buffer.url()) @define_command("split-view-right") def split_window_right(ctx): """ Create a new view on the right of the current one. """ win = ctx.window view = win.create_webview_on_right() view.setBuffer(_get_or_create_buffer(win)) win.set_current_webview(view) @define_command("split-view-bottom") def split_window_bottom(ctx): """ Create a new view below the current one. """ win = ctx.window view = win.create_webview_on_bottom() view.setBuffer(_get_or_create_buffer(win)) win.set_current_webview(view) @define_command("make-window") def create_window(ctx): """ Create a new window and focus it. """ win = Window() home_page = variables.get("home-page") win.current_webview().setBuffer( create_buffer(home_page) if home_page and variables.get("home-page-in-new-window") else _get_or_create_buffer(ctx.window) ) win.show() win.activateWindow() @define_command("other-window") def other_window(ctx): """ Switch to the next window. """ if len(windows()) <= 1: return False iterwindows = itertools.cycle(windows()) while True: win = next(iterwindows) if win == ctx.window: next(iterwindows).activateWindow() return True @define_command("close-window") def close_window(ctx): """ Close the current window, unless there is only one left. """ # first activate the next view if other_window(ctx): ctx.window.close() @define_command("close-other-windows") def close_other_windows(ctx): """ Close all windows except the current one. """ for win in windows(): if win != ctx.window: win.close() @define_command("other-view") def other_view(ctx): """ Focus on the next view. """ win = ctx.window win.other_view() @define_command("close-view") def close_view(ctx): """ Close the current view. """ window = ctx.window window.close_view(window.current_webview()) @define_command("maximise-view") def maximise_view(ctx): """ Close all the views in the current window except the current one. """ ctx.window.close_other_views() @define_command("toggle-ad-block") def toggle_ad_block(ctx): """ Toggle ad-blocking on or off. """ from .webbuffer import reload_buffer_no_cache app().url_interceptor().toggle_use_adblock() reload_buffer_no_cache(ctx) @define_command("toggle-toolbar") def toggle_toolbar(ctx): """ Toggle the main window toolbar on or off. """ ctx.window.toggle_toolbar() class VisitedLinksModel(PromptTableModel): def __init__(self, parent): visitedlinks = app().visitedlinks() PromptTableModel.__init__(self, visitedlinks.visited_urls()) self.visitedlinks = visitedlinks def remove_history_entry(self, index): self.beginRemoveRows(QModelIndex(), index.row(), index.row()) self.visitedlinks.remove(self._data.pop(index.row())[0]) self.endRemoveRows() class VisitedLinksPrompt(Prompt): label = "Find url from visited links:" complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } keymap = VISITEDLINKS_KEYMAP value_return_index_data = True def completer_model(self): return VisitedLinksModel(self) @define_command("visited-links-delete-highlighted") def visited_links_remove_entry(ctx): """ Deletes from the database the currently highlighted visited link. """ pinput = ctx.minibuffer.input() selection = pinput.popup().selectionModel().currentIndex() if not selection.isValid(): return selection = selection.model().mapToSource(selection) pinput.completer_model().remove_history_entry(selection) register_prompt_opener_commands( "visited-links-history", VisitedLinksPrompt, "Prompt to open a link previously visited", ) class BookmarksModel(VisitedLinksModel): def __init__(self, parent): bookmarks = app().bookmarks() PromptTableModel.__init__(self, bookmarks.list()) # this makes the remove_history_entry method works self.visitedlinks = bookmarks @define_command("bookmarks-delete-highlighted") def bookmarks_remove_entry(ctx): """ Deletes from the database the currently highlighted bookmark. """ # removing a bookmark is like removing a visited link visited_links_remove_entry(ctx) class BookmarksPrompt(VisitedLinksPrompt): label = "Open bookmark:" keymap = BOOKMARKS_KEYMAP history = PromptHistory() def completer_model(self): return BookmarksModel(self) register_prompt_opener_commands( "bookmark-open", BookmarksPrompt, "Prompt to open a bookmark", ) class BookmarkAddPrompt(Prompt): label = "Create a bookmark for: " def enable(self, minibuffer): Prompt.enable(self, minibuffer) url = self.ctx.buffer.url().toString() input = minibuffer.input() input.setText(url) input.setSelection(0, len(url)) class BookmarkNamePrompt(Prompt): label = "bookmark's name: " def enable(self, minibuffer): Prompt.enable(self, minibuffer) name = self.ctx.buffer.title() input = minibuffer.input() input.setText(name) input.setSelection(0, len(name)) @define_command("bookmark-add") def bookmark_add(ctx): """ Create or rename a bookmark for the current url. """ prompt = BookmarkAddPrompt(ctx) url = ctx.minibuffer.do_prompt(prompt) if not url: return otherprompt = BookmarkNamePrompt(ctx) name = ctx.minibuffer.do_prompt(otherprompt) if name: app().bookmarks().set(url, name) ctx.minibuffer.show_info("Bookmark {} created.".format(name)) class ModesPrompt(Prompt): label = "switch to mode:" complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } value_return_index_data = True def completer_model(self): return PromptTableModel([ (name, MODES[name].description) for name in sorted(MODES) ]) @define_command("buffer-set-mode") def buffer_set_mode(ctx): """ Change the mode of the current buffer. """ prompt = ModesPrompt(ctx) mode = ctx.minibuffer.do_prompt(prompt) if mode: ctx.buffer.set_mode(mode) @define_command("send-key-down") def send_down(ctx): """Send a key down event.""" send_key_event(KeyPress.from_str("Down")) @define_command("send-key-up") def send_up(ctx): """Send a key up event.""" send_key_event(KeyPress.from_str("Up")) @define_command("send-key-right") def send_right(ctx): """Send a key right event.""" send_key_event(KeyPress.from_str("Right")) @define_command("send-key-left") def send_left(ctx): """Send a key left event.""" send_key_event(KeyPress.from_str("Left")) @define_command("describe-bindings") def describe_bindings(ctx): """ Display current bindings in the current buffer or in a new buffer. """ url_open(ctx, "webmacs://bindings", new_buffer=ctx.current_prefix_arg == (4,)) @define_command("describe-commands") def describe_commands(ctx): """ Display commands in the current buffer or in a new buffer. """ url_open(ctx, "webmacs://commands", new_buffer=ctx.current_prefix_arg == (4,)) @define_command("describe-variables") def describe_variables(ctx): """ Display variables in the current buffer or in a new buffer. """ url_open(ctx, "webmacs://variables", new_buffer=ctx.current_prefix_arg == (4,)) @define_command("downloads") def downloads(ctx): """ Display information about the current downloads. """ url_open(ctx, "webmacs://downloads", new_buffer=ctx.current_prefix_arg == (4,)) @define_command("version") def version(ctx): """ Display version information. """ url_open(ctx, "webmacs://version", new_buffer=ctx.current_prefix_arg == (4,)) class VariableListPrompt(Prompt): label = "describe variable: " complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } history = PromptHistory() def completer_model(self): model = QStringListModel(self) model.setStringList(sorted(variables.VARIABLES)) return model @define_command("describe-variable") def describe_variable(ctx): """ Prompt for a variable name to describe. """ variable = ctx.minibuffer.do_prompt(VariableListPrompt(ctx)) if variable in variables.VARIABLES: buffer = create_buffer("webmacs://variable/%s" % variable) ctx.view.setBuffer(buffer) class DescribeCommandsListPrompt(CommandsListPrompt): label = "describe command: " history = PromptHistory() @define_command("describe-command") def describe_command(ctx): """ Prompt for a command name to describe. """ command = ctx.minibuffer.do_prompt(DescribeCommandsListPrompt(ctx)) if command in COMMANDS: url_open(ctx, "webmacs://command/%s" % command) class ReportCallHandler(CallHandler): def __init__(self, prompt): CallHandler.__init__(self) self.prompt = prompt self.key_presses = [] def keys_as_text(self): return " - ".join(str(k) for k in self.key_presses) def no_call(self, sender, keymap, keypress): self.key_presses.append(keypress) self.prompt.close() self.prompt.minibuffer.show_info("No such key: %s" % self.keys_as_text()) def partial_call(self, sender, keymap, keypress): self.key_presses.append(keypress) self.prompt.minibuffer.input().setText("%s -" % self.keys_as_text()) def call(self, ctx, keymap, keypress, command): self.key_presses.append(keypress) if not isinstance(command, str): command = "{}:{}".format(command.__module__, command.__name__) self.prompt.called_with = { "command": command, "key": self.keys_as_text(), "keymap": keymap.name or "unknown", } self.prompt.finished.emit() self.prompt.close() class BindingPrompt(Prompt): label = "describe key: " called_with = None def enable(self, minibuffer): self.keymap = local_keymap() Prompt.enable(self, minibuffer) self.orig_handler = KEY_EATER.call_handler KEY_EATER.set_call_handler(ReportCallHandler(self)) def close(self): KEY_EATER.set_call_handler(self.orig_handler) Prompt.close(self) def value(self): return self.called_with @define_command("describe-key") def describe_binding(ctx): """ Retrieve the command called by the given binding. """ called_with = ctx.minibuffer.do_prompt(BindingPrompt(ctx)) if called_with: url = "webmacs://key/{key}?command={command}&keymap={keymap}".format( **called_with ) url_open(ctx, url, new_buffer=True) @define_command("describe-key-briefly") def describe_binding_briefly(ctx): """ Display in the minibuffer the command name called by the given binding. """ called_with = ctx.minibuffer.do_prompt(BindingPrompt(ctx)) if called_with: ctx.minibuffer.show_info( "{key} runs the command {command} (keymap: {keymap})".format( **called_with ) ) class WhereIsCommandsListPrompt(CommandsListPrompt): label = "Where is command: " history = PromptHistory() @define_command("where-is") def where_is(ctx): """ Print short notice of where a command is bound """ command = ctx.minibuffer.do_prompt(WhereIsCommandsListPrompt(ctx)) if not command: return bindings_str = ", ".join("{} (keymap: {})".format(k, kmapname) for kmapname, kmap in KEYMAPS.items() for k, v in kmap.all_bindings() if v == command) if bindings_str: ctx.minibuffer.show_info("{} is on: {}".format(command, bindings_str)) else: ctx.minibuffer.show_info("{} is not on any key".format(command)) @define_command("restore-session") def restore_session(ctx): """ Restore windows and buffers from the previous sessions. """ session_file = app().profile.session_file if not os.path.exists(session_file): ctx.minibuffer.show_info("Error: No session file found.") session_clean() try: session_load(session_file) except Exception: w = Window() w.current_webview().setBuffer("about:blank") w.show() class InstancesListPrompt(Prompt): label = "webmacs instances: " complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } history = PromptHistory() exclude_self_instance = True def __init__(self, ctx): super().__init__(ctx) instances = IpcServer.list_all_instances(check=False) if self.exclude_self_instance: current = app().instance_name instances = [i for i in instances if i != current] self.instances = instances def completer_model(self): model = QStringListModel(self) model.setStringList(self.instances) return model @define_command("raise-instance") def raise_instance(ctx): """ Raise the current window of the selected instance. """ prompt = InstancesListPrompt(ctx) if not prompt.instances: ctx.minibuffer.show_info("There is only one instance running: %s" % app().instance_name) else: value = ctx.minibuffer.do_prompt(prompt) if value: IpcServer.instance_send(value, {}) @define_command("current-instance") def current_instance(ctx): """ Show the current instance name. """ ctx.minibuffer.show_info("Current instance name: %s" % app().instance_name) @define_command("is-off-the-record") def is_off_the_record(ctx): """ Print wether browsing is off the record. """ ctx.minibuffer.show_info( f"Is off the record: {app().profile.is_off_the_record()}") @define_command("open-off-the-record") def open_off_the_record(ctx): """ Opens a new webmacs instance with off-the-record enabled. """ proc = QProcess() proc.setProgram(sys.argv[0]) proc.setArguments(["--off-the-record", "--instance", ""]) proc.startDetached() ================================================ FILE: webmacs/commands/isearch.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from ..minibuffer import Prompt from ..keymaps import ISEARCH_KEYMAP, CARET_BROWSING_KEYMAP from ..keyboardhandler import local_keymap from ..webbuffer import WebBuffer, QWebEnginePage from ..commands import define_command from ..commands import caret_browsing as caret_browsing_commands @define_command("i-search-next") def search_next(ctx): """ Highlight next match in incremental search mode. """ if ISearchPrompt.LAST_SEARCH and not ctx.minibuffer.input().text(): ctx.minibuffer.input().setText(ISearchPrompt.LAST_SEARCH) return prompt = ctx.minibuffer.prompt() prompt.set_isearch_direction(0) prompt.find_text() @define_command("i-search-prev") def search_previous(ctx): """ Highlight previous match in incremental search mode. """ if ISearchPrompt.LAST_SEARCH and not ctx.minibuffer.input().text(): ctx.minibuffer.input().setText(ISearchPrompt.LAST_SEARCH) return prompt = ctx.minibuffer.prompt() prompt.set_isearch_direction(WebBuffer.FindFlag.FindBackward) prompt.find_text() @define_command("i-search-validate") def validate(ctx): """ Validate current match in incremental search mode. """ ISearchPrompt.LAST_SEARCH = ctx.minibuffer.input().text() ctx.buffer.findText("") # to clear the highlight ctx.minibuffer.close_prompt() @define_command("i-search-abort") def cancel(ctx): """ Abort incremental search. """ prompt = ctx.minibuffer.prompt() scroll_pos = prompt.page_scroll_pos ctx.buffer.findText("") # to clear the highlight ctx.minibuffer.close_prompt() prompt.set_page_scroll_pos(scroll_pos) class ISearchPrompt(Prompt): label = "ISearch:" keymap = ISEARCH_KEYMAP isearch_direction = 0 # forward LAST_SEARCH = None def enable(self, minibuffer): self._caret_browsing = local_keymap() == CARET_BROWSING_KEYMAP if self._caret_browsing: caret_browsing_commands.shutdown(self.ctx) Prompt.enable(self, minibuffer) self._update_label() self.page = self.ctx.buffer self.page_scroll_pos = (0, 0) self.page.async_scroll_pos( lambda p: setattr(self, "page_scroll_pos", p)) minibuffer.input().textChanged.connect(self.on_text_edited) def set_isearch_direction(self, direction): self.isearch_direction = direction self._update_label() def set_page_scroll_pos(self, page_scroll_pos): self.page.set_scroll_pos(*page_scroll_pos) def find_text(self): if self.isearch_direction: self.page.findText(self.minibuffer.input().text(), self.isearch_direction) else: self.page.findText(self.minibuffer.input().text()) def on_text_edited(self, text): self.find_text() if not self.minibuffer.input().text(): self.set_page_scroll_pos(self.page_scroll_pos) def _update_label(self): direction = "forward" if self.isearch_direction == 0 else "backward" self.minibuffer.label.setText("ISearch (%s):" % direction) def close(self): self.minibuffer.input().textChanged.disconnect(self.on_text_edited) Prompt.close(self) if self._caret_browsing: caret_browsing_commands.init(self.ctx) @define_command("i-search-forward") def i_search_forward(ctx): """ Begin an incremental search (forward). """ ctx.minibuffer.do_prompt(ISearchPrompt(ctx)) class ISearchPromptBackward(ISearchPrompt): isearch_direction = QWebEnginePage.FindFlag.FindBackward @define_command("i-search-backward") def i_search_backward(ctx): """ Begin an incremental search (backward). """ ctx.minibuffer.do_prompt(ISearchPromptBackward(ctx)) ================================================ FILE: webmacs/commands/minibuffer.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import define_command WORD_SEPS = "#/.-_:" def move_next_word(edit, forward, mark): action = edit.cursorWordForward if forward else edit.cursorWordBackward cursor = edit.cursorPosition() txt = edit.text() while True: action(mark) nc = edit.cursorPosition() if nc == cursor: break cursor = nc if forward: nc -= 1 if txt[nc] not in WORD_SEPS: break @define_command("minibuffer-select-complete") def complete(ctx): """ Complete completion. """ input = ctx.minibuffer.input() if not input.popup().isVisible(): input.show_completions() else: input.select_next_completion() @define_command("minibuffer-select-next") def next_completion(ctx): """ Select next completion entry. """ ctx.minibuffer.input().select_next_completion() @define_command("minibuffer-select-prev") def previous_completion(ctx): """ Select previous completion entry. """ ctx.minibuffer.input().select_next_completion(False) @define_command("minibuffer-select-first") def first_complqetion(ctx): """ Select first completion entry. """ ctx.minibuffer.input().select_first_completion() @define_command("minibuffer-select-last") def last_completion(ctx): """ Select last completion entry. """ ctx.minibuffer.input().select_last_completion() @define_command("minibuffer-select-next-page") def next_page_completion(ctx): """ Move one page down in completion entry list. """ ctx.minibuffer.input().select_next_page_completion() @define_command("minibuffer-select-prev-page") def previous_page_completion(ctx): """ Move one page up in completion entry list. """ ctx.minibuffer.input().select_next_page_completion(False) def _prompt_history(ctx, func): minibuff = ctx.minibuffer history = minibuff.prompt().history if history: if history.in_user_value(): history.set_user_value(minibuff.input().text()) text = func(history) if text is not None: minibuff.input().setText(text) @define_command("minibuffer-history-next") def prompt_history_next(ctx): """ Insert next history value. """ _prompt_history(ctx, lambda h: h.get_next()) @define_command("minibuffer-history-prev") def prompt_history_previous(ctx): """ Insert previous history value. """ _prompt_history(ctx, lambda h: h.get_previous()) @define_command("minibuffer-validate") def edition_finished(ctx): """ Validate input in minibuffer. """ minibuffer_input = ctx.minibuffer.input() minibuffer_input.complete() minibuffer_input.popup().hide() minibuffer_input.returnPressed.emit() @define_command("minibuffer-abort") def cancel(ctx): """ Abort edition of the minibuffer. """ minibuffer = ctx.minibuffer input = minibuffer.input() if input.popup().isVisible(): input.popup().hide() minibuffer.close_prompt() @define_command("minibuffer-delete-backward-word") def clean_aindent_bsunindent(ctx): """ Delete the word backward. """ edit = ctx.minibuffer.input() if edit.hasSelectedText(): edit.deselect() move_next_word(edit, False, True) if edit.hasSelectedText(): edit.del_() @define_command("minibuffer-mark") def set_mark(ctx): """ Set or unset the edit mark. """ minibuffer_input = ctx.minibuffer.input() if not minibuffer_input.set_mark(): minibuffer_input.deselect() @define_command("minibuffer-select-all") def select_all(ctx): """ Select all text in the minibuffer. """ edit = ctx.minibuffer.input() edit.selectAll() @define_command("minibuffer-forward-char") def forward_char(ctx): """ Move one character forward. """ edit = ctx.minibuffer.input() edit.cursorForward(edit.mark(), 1) @define_command("minibuffer-backward-char") def backward_char(ctx): """ Move one character backward. """ edit = ctx.minibuffer.input() edit.cursorBackward(edit.mark(), 1) @define_command("minibuffer-forward-word") def forward_word(ctx): """ Move one word forward. """ edit = ctx.minibuffer.input() move_next_word(edit, True, edit.mark()) @define_command("minibuffer-backward-word") def backward_word(ctx): """ Move one word backward. """ edit = ctx.minibuffer.input() move_next_word(edit, False, edit.mark()) @define_command("minibuffer-copy") def copy(ctx): """ Copy selected text in the minibuffer. """ edit = ctx.minibuffer.input() edit.copy() edit.deselect() @define_command("minibuffer-cut") def cut(ctx): """ Cut selected text in the minibuffer. """ ctx.minibuffer.input().cut() @define_command("minibuffer-paste") def paste(ctx): """ Paste text in the minibuffer. """ ctx.minibuffer.input().paste() @define_command("minibuffer-delete-forward-char") def delete_char(ctx): """ Delete forward character. """ ctx.minibuffer.input().del_() @define_command("minibuffer-delete-forward-word") def delete_word(ctx): """ Delete forward word. """ edit = ctx.minibuffer.input() if edit.hasSelectedText(): edit.deselect() move_next_word(edit, True, True) if edit.hasSelectedText(): edit.del_() @define_command("minibuffer-beginning-of-line") def beginning_of_line(ctx): """ Move cursor to the beginning of the line. """ edit = ctx.minibuffer.input() edit.home(edit.mark()) @define_command("minibuffer-end-of-line") def end_of_line(ctx): """ Move cursor to the end of the line. """ edit = ctx.minibuffer.input() edit.end(edit.mark()) @define_command("minibuffer-undo") def undo(ctx): """ Undo in the minibuffer. """ ctx.minibuffer.input().undo() @define_command("minibuffer-redo") def redo(ctx): """ Redo in the minibuffer. """ ctx.minibuffer.input().redo() ================================================ FILE: webmacs/commands/webbuffer.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import itertools from PyQt6.QtCore import QAbstractTableModel, QModelIndex, Qt from PyQt6.QtGui import QColor from PyQt6.QtPrintSupport import QPrinter, QPrintDialog from PyQt6.QtWebEngineCore import QWebEngineScript from ..application import app from ..commands import define_command from ..minibuffer import Prompt from ..webbuffer import WebBuffer, close_buffer, create_buffer from ..killed_buffers import KilledBuffer from ..keyboardhandler import send_key_event from .. import BUFFERS, version, current_buffer, recent_buffers from .. import variables, clipboard, GLOBAL_OBJECTS from ..keymaps import KeyPress, BUFFERLIST_KEYMAP from ..password_manager import PasswordManagerNotReady switch_buffer_current_color = variables.define_variable( "switch-buffer-current-color", "The color to use for the current buffer in the switch-buffer list." " Set to an empty string if you don't want a special color.", "#c0d5f7", type=variables.String(), ) class BufferTableModel(QAbstractTableModel): def __init__(self, buffers): QAbstractTableModel.__init__(self) self._buffers = list(buffers) def rowCount(self, index=QModelIndex()): return len(self._buffers) def columnCount(self, index=QModelIndex()): return 2 def data(self, index, role=Qt.ItemDataRole.DisplayRole): buff = index.internalPointer() if not buff: return col = index.column() if role == Qt.ItemDataRole.DisplayRole: if col == 0: return buff.url().toString() else: return "[{}] {}".format(BUFFERS.index(buff) + 1, buff.title()) elif role == Qt.ItemDataRole.DecorationRole and col == 0: return buff.icon() elif role == Qt.ItemDataRole.BackgroundRole: if buff == current_buffer(): if switch_buffer_current_color.value: return QColor(switch_buffer_current_color.value) def index(self, row, col, parent=QModelIndex()): try: return self.createIndex(row, col, self._buffers[row]) except IndexError: return QModelIndex() def close_buffer_at(self, index): try: if not close_buffer(self._buffers[index.row()]): return except ValueError: return self.beginRemoveRows(QModelIndex(), index.row(), index.row()) self._buffers.pop(index.row()) self.endRemoveRows() @define_command("buffer-list-delete-highlighted") def close_buffer_in_prompt_selection(ctx): """ Close currently highlighted buffer. """ pinput = ctx.minibuffer.input() selection = pinput.popup().selectionModel().currentIndex() if not selection.isValid(): return selection = selection.model().mapToSource(selection) pinput.completer_model().close_buffer_at(selection) class BufferListPrompt(Prompt): label = "select buffer:" complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } keymap = BUFFERLIST_KEYMAP value_return_index_data = True def completer_model(self): return BufferTableModel(self.ordered_buffers()) def ordered_buffers(self): """ How to display buffers. """ return BUFFERS def enable(self, minibuffer): Prompt.enable(self, minibuffer) # select the next buffer buffers = self.ordered_buffers() index = buffers.index(current_buffer()) + 1 if index >= len(buffers): index = 0 minibuffer.input().popup().selectRow(index) class RecentBufferListPrompt(BufferListPrompt): def ordered_buffers(self): return recent_buffers() class BufferSwitchListPrompt(BufferListPrompt): label = "switch to buffer:" class RecentBufferSwitchListPrompt(RecentBufferListPrompt): label = "switch to buffer:" class BufferKillListPrompt(BufferListPrompt): label = "kill all buffers except:" def enable(self, minibuffer): Prompt.enable(self, minibuffer) # auto-select the currently visible buffer buffers = self.ordered_buffers() minibuffer.input().popup().selectRow(buffers.index(current_buffer())) def show_buffer(buffer, view): """ Display the given buffer in the given view. """ if view.buffer() == buffer: # switch to the same buffer, nothing to do return if buffer.view(): # swap buffers if the buffer is already displayed otherbuffer = view.buffer() view.setBuffer(None) otherview = buffer.view() otherview.setBuffer(otherbuffer) view.setBuffer(buffer) @define_command("switch-buffer") def switch_buffer(ctx): """ Prompt to select a buffer to display in the current view. """ buffer = ctx.minibuffer.do_prompt(BufferSwitchListPrompt(ctx)) if buffer: show_buffer(buffer, ctx.view) @define_command("switch-recent-buffer") def switch_recent_buffer(ctx): """ Prompt to select a buffer to display in the current view. """ buffer = ctx.minibuffer.do_prompt(RecentBufferSwitchListPrompt(ctx)) if buffer: show_buffer(buffer, ctx.view) def _next_buffer(ctx, reverse=False): if len(BUFFERS) <= 1: return buffers = itertools.cycle(reversed(BUFFERS) if reverse else BUFFERS) next_b = next(buffers) while next_b != ctx.buffer: next_b = next(buffers) show_buffer(next(buffers), ctx.view) @define_command("next-buffer") def next_buffer(ctx): """ Cycle to the next buffer in the current view. """ _next_buffer(ctx) @define_command("previous-buffer") def previous_buffer(ctx): """ Cycle to the previous buffer in the current view. """ _next_buffer(ctx, reverse=True) class OpenDevToolsPrompt(BufferListPrompt): label = "open dev tools for buffer:" keymap = None def enable(self, minibuffer): Prompt.enable(self, minibuffer) # auto-select the currently visible buffer minibuffer.input().popup().selectRow(0) @define_command("open-dev-tools") def open_dev_tools(ctx): """ Opens a dev tool page for a buffer. """ buffer = ctx.minibuffer.do_prompt(OpenDevToolsPrompt(ctx)) if buffer: dev_tools = create_buffer() buffer.setDevToolsPage(dev_tools) @define_command("go-forward") def go_forward(ctx): """ Navigate forward in history for the current buffer. """ if not ctx.buffer.history().canGoForward(): ctx.minibuffer.show_info("Can't go forward in history.") else: ctx.buffer.runJavaScript("history.go(1)", QWebEngineScript.ScriptWorldId.ApplicationWorld) @define_command("go-backward") def go_backward(ctx): """ Navigate backward in history for the current buffer. """ if not ctx.buffer.history().canGoBack(): ctx.minibuffer.show_info("Can't go back in history.") else: ctx.buffer.runJavaScript("history.go(-1)", QWebEngineScript.ScriptWorldId.ApplicationWorld) @define_command("scroll-down") def scroll_down(ctx): """ Scroll the current buffer down a bit. """ ctx.buffer.scroll_by(y=20) @define_command("scroll-up") def scroll_up(ctx): """ Scroll the current buffer up a bit. """ ctx.buffer.scroll_by(y=-20) @define_command("scroll-page-down") def scroll_page_down(ctx): """ Scroll the current buffer one page down. """ send_key_event(KeyPress.from_str("PageDown")) @define_command("scroll-page-up") def scroll_page_up(ctx): """ Scroll the current buffer one page up. """ send_key_event(KeyPress.from_str("PageUp")) @define_command("scroll-top") def scroll_top(ctx): """ Scroll the current buffer to the top. """ send_key_event(KeyPress.from_str("Home")) @define_command("scroll-bottom") def scroll_bottom(ctx): """ Scroll the current buffer to the bottom. """ send_key_event(KeyPress.from_str("End")) @define_command("webcontent-copy") def webcontent_copy(ctx): """ Copy the selection in the current buffer. """ ctx.buffer.triggerAction(WebBuffer.WebAction.Copy) @define_command("webcontent-cut") def webcontent_cut(ctx): """ Cut the selection in the current buffer. """ ctx.buffer.triggerAction(WebBuffer.WebAction.Cut) @define_command("webcontent-paste") def webcontent_paste(ctx): """ Paste the selection in the current buffer. """ ctx.buffer.triggerAction(WebBuffer.WebAction.Paste) @define_command("reload-buffer") def reload_buffer(ctx): """ Reload the current buffer. """ ctx.buffer.triggerAction(WebBuffer.WebAction.Reload) @define_command("reload-buffer-no-cache") def reload_buffer_no_cache(ctx): """ Reload the current buffer bypassing any cache. """ ctx.buffer.triggerAction(WebBuffer.WebAction.ReloadAndBypassCache) @define_command("close-buffer") def buffer_close(ctx): """ Close the current buffer. """ close_buffer(ctx.buffer) @define_command("close-other-buffers") def close_other_buffers(ctx): """ Close all but one buffer. """ # Select a buffer buffer = ctx.minibuffer.do_prompt(BufferKillListPrompt(ctx)) if buffer: # Get all other buffers and kill them for wb in [b for b in BUFFERS if b != buffer]: close_buffer(wb) @define_command("select-buffer-content") def buffer_select_content(ctx): """ Select all content in the buffer. """ ctx.buffer.triggerAction(WebBuffer.WebAction.SelectAll) @define_command("zoom-in") def zoom_in(ctx): """ Zoom-in in the buffer. """ ctx.buffer.zoom_in() @define_command("zoom-out") def zoom_out(ctx): """ Zoom-out in the buffer. """ ctx.buffer.zoom_out() @define_command("zoom-normal") def zoom_normal(ctx): """ Zoom-normal in the buffer. """ ctx.buffer.zoom_normal() def _show_info_text_zoom(ctx): def _wrapper(ratio): ctx.minibuffer.show_info("Text zoom level: %02d%%" % (ratio * 100)) return _wrapper @define_command("text-zoom-in") def text_zoom_in(ctx): """ Zom in (text only) in the buffer. """ ctx.buffer.runJavaScript("textzoom.changeFont(0.1);", QWebEngineScript.ScriptWorldId.ApplicationWorld, _show_info_text_zoom(ctx)) @define_command("text-zoom-out") def text_zoom_out(ctx): """ Zom out (text only) in the buffer. """ ctx.buffer.runJavaScript("textzoom.changeFont(-0.1);", QWebEngineScript.ScriptWorldId.ApplicationWorld, _show_info_text_zoom(ctx)) @define_command("text-zoom-reset") def text_zoom_reset(ctx): """ Reset the zoom (text only) in the buffer. """ ctx.buffer.runJavaScript("textzoom.resetChangeFont();", QWebEngineScript.ScriptWorldId.ApplicationWorld, _show_info_text_zoom(ctx)) @define_command("buffer-unselect") def buffer_unselect(ctx): """ Unselect selection in the current web buffer. """ ctx.buffer.triggerAction(WebBuffer.WebAction.Unselect) @define_command("buffer-escape") def buffer_escape(ctx): """ Clear selection or menus in the current buffer. The implementation clears the selection in the buffer if there is any, else it sends the Escape key which usually closes whatever takes the focus. """ if ctx.buffer.hasSelection(): buffer_unselect(ctx) else: send_key_event(KeyPress.from_str("Esc")) class KilledBufferTableModel(QAbstractTableModel): def __init__(self): QAbstractTableModel.__init__(self) self._buffers = list(KilledBuffer.all) def rowCount(self, index=QModelIndex()): return len(self._buffers) def columnCount(self, index=QModelIndex()): return 2 def data(self, index, role=Qt.ItemDataRole.DisplayRole): killed_buff = index.internalPointer() if not killed_buff: return col = index.column() if role == Qt.ItemDataRole.DisplayRole: if col == 0: return killed_buff.url.toString() else: return killed_buff.title elif role == Qt.ItemDataRole.DecorationRole and col == 0: return killed_buff.icon def index(self, row, col, parent=QModelIndex()): try: return self.createIndex(row, col, self._buffers[row]) except IndexError: return QModelIndex() class KilledBufferListPrompt(Prompt): label = "buffer to revive:" complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } value_return_index_data = True def completer_model(self): return KilledBufferTableModel() def enable(self, minibuffer): Prompt.enable(self, minibuffer) if KilledBuffer.all: minibuffer.input().popup().selectRow(0) @define_command("revive-buffer") def revive_buffer(ctx): """ Revive a previously killed buffer in the current view. """ killed_buffer = ctx.minibuffer.do_prompt(KilledBufferListPrompt(ctx)) if killed_buffer: buff = killed_buffer.revive() ctx.window.current_webview().setBuffer(buff) @define_command("copy-current-link") def copy_current_link(ctx): """ Copy the current link to the clipboard. """ # note the implementation does not rely on the CopyLinkToClipboard action # as it does not work fully (e.g, in case a link is set current using an # incremental search). buffer = ctx.buffer minibuff = ctx.minibuffer def copy_to_clipboard(url): buffer.content_handler.foundCurrentLinkUrl \ .disconnect(copy_to_clipboard) if url: clipboard.set_text(url) else: minibuff.show_info("No current link url to copy.") buffer.content_handler.foundCurrentLinkUrl.connect(copy_to_clipboard) buffer.runJavaScript("currentLinkUrl();", QWebEngineScript.ScriptWorldId.ApplicationWorld) @define_command("copy-current-buffer-url") def copy_buffer_url(ctx): """ Copy the URL of the current buffer to the clipboard. """ url = str(ctx.buffer.url().toEncoded(), "utf-8") clipboard.set_text(url) @define_command("copy-current-buffer-title") def copy_buffer_title(ctx): """ Copy the title of the current buffer to the clipboard. """ clipboard.set_text(ctx.buffer.title()) @define_command("print-buffer") def print_buffer(ctx): """ Opens a dialog to select the printer and prints the current buffer. """ from ..application import WithoutAppEventFilter def notif(ok): GLOBAL_OBJECTS.unref(printer) ctx.minibuffer.show_info("print successful" if ok else "failed to print") printer = QPrinter() dlg = QPrintDialog(printer) with WithoutAppEventFilter(): ok = dlg.exec() == dlg.Accepted if ok: # printer must be kept around to avoid a crash. # it must be released in the notif callback GLOBAL_OBJECTS.ref(printer) ctx.minibuffer.show_info("printing...") ctx.buffer.print(printer, notif) @define_command("password-manager-fill-buffer") def password_manager_fill_buffer(ctx): """ Fill the current buffer inputs using password manager data. """ password_mgr = app().profile.password_manager url = ctx.buffer.url().toString() try: cred = password_mgr.credential_for_url(url) except PasswordManagerNotReady as exc: ctx.minibuffer.show_info(str(exc)) return if cred: password_mgr.complete_buffer(ctx.buffer, cred) ================================================ FILE: webmacs/commands/webjump.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import re import logging from collections import namedtuple from PyQt6.QtCore import QUrl, pyqtSlot as Slot, \ pyqtSignal as Signal, QStringListModel, QObject, QEvent, Qt from PyQt6.QtNetwork import QNetworkRequest from ..commands import define_command from ..minibuffer.prompt import Prompt, PromptTableModel, PromptHistory from .. keymaps import WEBJUMP_KEYMAP from ..commands import register_prompt_opener_commands from .. import current_buffer from ..application import app from .. import variables from .. import version WebJump = namedtuple( "WebJump", ("name", "url", "doc", "allow_args", "complete_fn", "protocol")) WEBJUMPS = {} webjump_default = variables.define_variable( "webjump-default", "The default webjump", "", type=variables.String(choices=WEBJUMPS), ) def define_webjump(name, url, doc="", complete_fn=None, protocol=False): """ Define a webjump. A webjump is a quick way to access a URL, optionally with a variable section (for example a URL for a Google search). A function may be given to provide auto-completion. :param name: the name of the webjump. :param url: the url of the webjump. If the url contains "%s", it is assumed that it has a variable part. :param doc: associated documentation for the webjump. :param complete_fn: a function that should create a suitable :class:`WebJumpCompleter` to provide auto-completion, or None if there is no completion support for this webjump. :param protocol: True if the webjump should be treated as the protocol part of a URI (eg: file://) """ allow_args = "%s" in url WEBJUMPS[name.strip()] = WebJump( name.strip(), url, doc, allow_args, complete_fn or empty_completer, protocol ) def define_webjump_alias(alias_name, webjump_name): """ Define an alias for an existing webjump. The alias can then be used as a shortcut. :param alias_name: the name of the alias to be created. :param webjump_name: the name of the existing webjump. """ w = WEBJUMPS[webjump_name] define_webjump(alias_name.strip(), url=w.url, doc=w.doc, complete_fn=w.complete_fn, protocol=w.protocol) def define_protocol(name, doc="", complete_fn=None): define_webjump(name, name + "://%s", doc, complete_fn, True) def set_default(name): """ Set the default webjump. Deprecated: use the *webjump-default* variable instead. :param name: the name of the webjump. """ webjump_default.set_value(name) class WebJumpCompleter(object if version.building_doc else QObject): """ Provides auto-completion in webjumps. An instance is created automatically when required, and lives while the webjump is active. When a key is entered in the minibuffer input, the method :meth:`complete` is called with the current text, asking for completion. The signal `completed` must then be emitted with the list of possible completions. Note that there is no underlying thread in the completion framework. """ completed = Signal(list) def complete(self, text): """Must be implemented by subclasses.""" raise NotImplementedError def abort(self): """ Called when the completion request should be aborted. Subclasses should implement this if possible. """ pass class SyncWebJumpCompleter(WebJumpCompleter): """ A simple completer that provides completion given a function. This completer will block the UI: use it with care. :param complete_fn: a function that takes the current string, and must return the possible completions as a list of strings. """ def __init__(self, complete_fn): WebJumpCompleter.__init__(self) self.complete_fn = complete_fn def complete(self, text): self.completed.emit(self.complete_fn(text)) def empty_completer(): return SyncWebJumpCompleter(lambda _: []) class WebJumpRequestCompleter(WebJumpCompleter): """ A completer that executes a Web request to provide completion. This completer will not block the UI. :param url_fn: a function that takes the text to complete, and returns a URL that will provide completion. The returned value can be none if no URL is suitable for the given text. :param extract_completions_fn: a function that takes the bytes of the request reply, and must convert them to the completions (a string list). """ def __init__(self, url_fn, extract_completions_fn): WebJumpCompleter.__init__(self) self.url_fn = url_fn self.extract_completions_fn = extract_completions_fn self.reply = None def complete(self, text): url = self.url_fn(text) if not url: self.completed.emit([]) return elif not isinstance(url, QUrl): url = QUrl(url) req = QNetworkRequest(QUrl(url)) self.reply = app().network_manager.get(req) self.reply.finished.connect(self._on_reply_finished) def abort(self): if self.reply: self.reply.abort() def _on_reply_finished(self): if self.reply.error() == self.reply.NetworkError.NoError: try: completions = self.extract_completions_fn(self.reply.readAll()) except Exception: logging.exception( "Error when trying to extract completions from %s" % self.reply.url().toString() ) completions = [] self.completed.emit(completions) self.reply.deleteLater() self.reply = None @define_command("webjump-complete") def wb_complete(ctx): """ Complete webjump name in the minibuffer. """ input = ctx.minibuffer.input() if not input.popup().isVisible(): input.show_completions() else: input.complete() ctx.minibuffer.prompt()._text_edited(input.text()) class WebJumpPrompt(Prompt): label = "url/webjump:" complete_options = { "match": Prompt.SimpleMatch } history = PromptHistory() keymap = WEBJUMP_KEYMAP default_input = "alternate" def completer_model(self): data = [] for name, w in WEBJUMPS.items(): data.append((name, w.doc)) for url, name in self.bookmarks: data.append((name, url)) return PromptTableModel(data) def enable(self, minibuffer): self.bookmarks = app().bookmarks().list() Prompt.enable(self, minibuffer) minibuffer.input().textEdited.connect(self._text_edited) minibuffer.input().installEventFilter(self) self._wc_model = QStringListModel() self._wb_model = minibuffer.input().completer_model() self._active_webjump = None self._completer = None self._popup_sel_model = None input = minibuffer.input() if self.default_input in ("current_url", "alternate"): url = current_buffer().url().toString() input.setText(url) input.setSelection(0, len(url)) if self.default_input == "alternate": input.deselect() elif self.default_input == "default_webjump": wj = WEBJUMPS.get(webjump_default.value) if wj: input.setText( wj.name + ("://" if wj.protocol else " ") ) def eventFilter(self, obj, event): # call _text_edited on backspace release, as this is not reported by # the textEdited slot. if event.type() == QEvent.Type.KeyRelease: if event.key() == Qt.Key.Key_Backspace: self._text_edited(self.minibuffer.input().text()) return Prompt.eventFilter(self, obj, event) def _set_active_webjump(self, wj): if self._active_webjump == wj: return if self._active_webjump: if self._completer: self._completer.completed.disconnect(self._got_completions) self._completer.abort() self._completer.deleteLater() self._completer = None m_input = self.minibuffer.input() if wj: self._completer = wj.complete_fn() self._completer.completed.connect(self._got_completions) # set matching strategy m_input.set_match(None) model = self._wc_model else: m_input.set_match(Prompt.SimpleMatch) model = self._wb_model self._active_webjump = wj if m_input.completer_model() != model: m_input.popup().hide() m_input.set_completer_model(model) if self._popup_sel_model: self._popup_sel_model.selectionChanged.disconnect( self._popup_selection_changed ) self._popup_sel_model = None if wj: m_input.popup().selectionModel()\ .selectionChanged.connect( self._popup_selection_changed ) def _popup_selection_changed(self, _sel, _desel): # try to abort any completion if the user select something in # the popup if self._completer: self._completer.abort() def _text_edited(self, text): # search for a matching webjump first_word = text.split(" ")[0].split("://")[0] if first_word in [w for w in WEBJUMPS if len(w) < len(text)]: self._set_active_webjump(WEBJUMPS[first_word]) self.start_completion(self._active_webjump) else: # didn't find a webjump, go back to matching # webjump/bookmark/history self._set_active_webjump(None) def start_completion(self, webjump): text = self.minibuffer.input().text() prefix = webjump.name + ("://" if webjump.protocol else " ") self._completer.abort() self._completer.complete(text[len(prefix):]) @Slot(list) def _got_completions(self, data): if self._active_webjump: self._wc_model.setStringList(data) text = self.minibuffer.input().text() prefix = self._active_webjump.name + \ ("://" if self._active_webjump.protocol else " ") self.minibuffer.input().show_completions(text[len(prefix):]) def close(self): Prompt.close(self) self.minibuffer.input().removeEventFilter(self) # not sure if those are required; self._wb_model.deleteLater() self._wc_model.deleteLater() def _on_completion_activated(self, index): super()._on_completion_activated(index) chosen_text = self.minibuffer.input().text() # if there is already an active webjump, if self._active_webjump: # add the selected completion after it if self._active_webjump.protocol: self.minibuffer.input().setText( self._active_webjump.name + "://" + chosen_text) else: self.minibuffer.input().setText( self._active_webjump.name + " " + chosen_text) # if we just chose a webjump # and not WEBJUMPS[chosen_text].protocol: elif chosen_text in WEBJUMPS: # add a space after the selection self.minibuffer.input().setText( chosen_text + (" " if not WEBJUMPS[chosen_text].protocol else "://")) def value(self): value = super().value() if value is None: return # split webjumps and protocols between command and argument if re.match(r"^\S+://.*", value): args = value.split("://", 1) else: args = value.split(" ", 1) command = args[0] # Look for webjumps webjump = None if command in WEBJUMPS: webjump = WEBJUMPS[command] else: # Look for a incomplete webjump, accepting a candidate # if there is a single option candidates = [wj for wj in WEBJUMPS if wj.startswith(command)] if len(candidates) == 1: webjump = WEBJUMPS[candidates[0]] if webjump: if not webjump.allow_args: # send the url as is return webjump.url elif len(args) < 2: # send the url without a search string return webjump.url.replace("%s", "") else: # format the url as entered if webjump.protocol: return value else: return webjump.url.replace( "%s", str(QUrl.toPercentEncoding(args[1]), "utf-8") ) # Look for a bookmark bookmarks = {name: url for url, name in self.bookmarks} if value in bookmarks: return bookmarks[value] # Look for a incomplete bookmarks, accepting a candidate # if there is a single option candidates = [bm for bm in bookmarks if bm.startswith(command)] if len(candidates) == 1: return bookmarks[candidates[0]] # No webjump, no bookmark, look for a url if "://" not in value: url = QUrl.fromUserInput(value) if url.isValid(): # default scheme is https for us if url.scheme() == "http": url.setScheme("https") return url return value def wj_prompt(default_input): def prompt_ctor(ctx): p = WebJumpPrompt(ctx) p.default_input = default_input return p return prompt_ctor register_prompt_opener_commands( "go-to", wj_prompt("current_url"), "Prompt to open a URL or a webjump", ) register_prompt_opener_commands( "go-to-alternate-url", wj_prompt("alternate"), "Prompt to open an alternative URL from the current one", ) register_prompt_opener_commands( "search-default", wj_prompt("default_webjump"), "Prompt to open a URL with the default webjump", ) ================================================ FILE: webmacs/content_handler.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import json from PyQt6.QtCore import QObject, pyqtSlot as Slot, pyqtSignal as Signal from PyQt6.QtWebEngineCore import QWebEngineScript from .keyboardhandler import LOCAL_KEYMAP_SETTER from .external_editor import open_external_editor from . import clipboard class WebContentHandler(QObject): """ Interface to communicate with the javascript side in the web pages. """ browserObjectActivated = Signal(dict) foundCurrentLinkUrl = Signal(str) # for testing, when the hints are ready browserObjectsInited = Signal() def __init__(self, buff): QObject.__init__(self) self.buffer = buff @Slot(bool) def onTextFocus(self, enabled): LOCAL_KEYMAP_SETTER.web_content_edit_focus_changed(self.buffer, enabled) @Slot(str) def currentLinkUrl(self, url): self.foundCurrentLinkUrl.emit(url) @Slot(bool) def onCaretBrowsing(self, enabled): LOCAL_KEYMAP_SETTER.caret_browsing_changed(self.buffer, enabled) @Slot(str) def _browserObjectActivated(self, obj): # It is hard to pass dict objects from javascript, so a string is used # and decoded here. obj = json.loads(obj) if obj is not None: self.browserObjectActivated.emit(obj) else: self.browserObjectsInited.emit() @Slot(str) def copyToClipboard(self, text): clipboard.set_text(text) @Slot(str, str) def openExternalEditor(self, request_id, content): new_content = open_external_editor(content.encode("utf-8")) if new_content is None: new_content = 'false' else: new_content = repr(new_content) self.buffer.runJavaScript( "textedit.external_editor_finish({}, {});".format( repr(request_id), new_content ), QWebEngineScript.ScriptWorldId.ApplicationWorld ) ================================================ FILE: webmacs/default_webjumps.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import json from PyQt6.QtCore import QUrl from .commands.webjump import define_webjump, define_protocol, \ webjump_default, WebJumpRequestCompleter, SyncWebJumpCompleter from .minibuffer.prompt import FSModel from .scheme_handlers.webmacs import PAGES as webmacs_pages # ----------- doc example def complete_google(): def url_fn(text): if not text: return None return ( "https://www.google.com/complete/search?client=firefox&q=" + str(QUrl.toPercentEncoding(text), "utf-8")) return WebJumpRequestCompleter( url_fn, lambda response: json.loads(str(response, "utf-8"))[1] ) define_webjump("google", "https://www.google.com/search?q=%s&ie=utf-8&oe=utf-8", "Google Search", complete_fn=complete_google) # ----------- end of doc example def complete_fs(): model = FSModel() def _complete(text): model.text_changed(text) dircontent = [model.data(model.index(i, 0)) for i in range(model.rowCount())] return [c for c in dircontent if c.startswith(text)] return SyncWebJumpCompleter(_complete) define_protocol("file", "Local uris", complete_fn=complete_fs) def complete_pages(): return SyncWebJumpCompleter( lambda text: [p for p in webmacs_pages if text in p] ) define_protocol("webmacs", "webmacs internal pages", complete_fn=complete_pages) def complete_protocol(protocol): def complete(): completer = complete_google() extract_fn = completer.extract_completions_fn url_fn = completer.url_fn completer.extract_completions_fn \ = lambda data: [r[len(protocol):] for r in extract_fn(data) if r.startswith(protocol)] completer.url_fn \ = lambda text: url_fn(protocol + text) return completer return complete define_protocol("http", "web sites", complete_fn=complete_protocol("http://")) define_protocol("https", "secure web sites", complete_fn=complete_protocol("https://")) def complete_duckduckgo(): def url_fn(text): if not text: return None # took from https://github.com/jarun/ddgr/blob/master/ddgr return ( 'https://duckduckgo.com/ac/?q=%s&kl=wt-wt' % str(QUrl.toPercentEncoding(text), "utf-8") ) return WebJumpRequestCompleter( url_fn, lambda response: [e["phrase"] for e in json.loads(str(response, "utf-8"))] ) define_webjump("duckduckgo", "https://www.duckduckgo.com/?q=%s", "Duckduckgo Search", complete_fn=complete_duckduckgo) webjump_default.set_value("google") ================================================ FILE: webmacs/download_manager/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import re import json import shlex import logging import itertools import tempfile from PyQt6.QtCore import QObject, pyqtSlot as Slot, pyqtSignal as Signal, \ QProcess from PyQt6.QtWebEngineCore import QWebEngineDownloadRequest from .prompts import (DlChooseActionPrompt, DlOpenActionPrompt, DlPrompt, OverwriteFilePrompt) from .. import current_minibuffer, hooks, variables default_download_dir = variables.define_variable( "default-download-dir", "Change the default download dir.", "", type=variables.String(), ) TEMPORARY_DOWNLOAD_DIR = None keep_temporary_download_dir = variables.define_variable( "keep-temporary-download-dir", "If set to True, the download dir proposed will be the last used one.", False, type=variables.Bool(), ) def dl_path(dl): return os.path.join(dl.downloadDirectory(), dl.suggestedFileName()) def get_user_download_dir(): """ Returns the directory the user wants to put its download in. Return None if there is no specific directory. """ if keep_temporary_download_dir.value and TEMPORARY_DOWNLOAD_DIR: return TEMPORARY_DOWNLOAD_DIR return default_download_dir.value or None def find_unique_suggested_path(dirname, filename): """ Do the same logic as chromium does to create a unique path suggestion. """ fnames = set(os.listdir(dirname)) if filename not in fnames: return os.path.join(dirname, filename) parts = filename.split(".", 1) if len(parts) == 1: name, ext = parts[0], "" else: name, ext = parts[0], "." + parts[1] counter = itertools.count(1) while True: newfname = "{}({}){}".format(name, next(counter), ext) if newfname not in fnames: return os.path.join(dirname, newfname) STATE_STR = { QWebEngineDownloadRequest.DownloadState.DownloadRequested: "Requested", QWebEngineDownloadRequest.DownloadState.DownloadInProgress: "In progress", QWebEngineDownloadRequest.DownloadState.DownloadCompleted: "Completed", QWebEngineDownloadRequest.DownloadState.DownloadCancelled: "Cancelled", QWebEngineDownloadRequest.DownloadState.DownloadInterrupted: "Interrupted", } def state_str(state): return STATE_STR.get(state, "Unknown state") def download_to_json(dlitem): try: progress = (round(dlitem.receivedBytes() / float(dlitem.totalBytes()) * 100, 2)) except ZeroDivisionError: progress = -1 return json.dumps({ "path": dl_path(dlitem), "state": state_str(dlitem.state()), "id": dlitem.id(), "isFinished": dlitem.isFinished(), "progress": progress, }) class DownloadManager(QObject): download_started = Signal(object) def __init__(self, parent=None): QObject.__init__(self, parent) self.downloads = [] self._buffers = [] # list of web buffers currently showing downloads self._running_procs = {} def on_buffer_load_finished(buff): url = buff.url() if url.scheme() == "webmacs" and url.authority() == "downloads": self.attach_buffer(buff) else: self.detach_buffer(buff) hooks.webbuffer_load_finished.add(on_buffer_load_finished) hooks.webbuffer_closed.add(self.detach_buffer) def attach_buffer(self, buffer): self._buffers.append(buffer) for dl in self.downloads: buffer.runJavaScript("add_download(%s);" % download_to_json(dl)) def detach_buffer(self, buffer): try: self._buffers.remove(buffer) except ValueError: pass def _start_download(self, dlitem): dlitem.accept() self.downloads.append(dlitem) dlitem.destroyed.connect(lambda: self.downloads.remove(dlitem)) self.download_started.emit(dlitem) dl = download_to_json(dlitem) for buffer in self._buffers: buffer.runJavaScript("add_download(%s);" % dl) dlitem.receivedBytesChanged.connect(self._download_state_changed) dlitem.totalBytesChanged.connect(self._download_state_changed) dlitem.stateChanged.connect(self._download_state_changed) dlitem.isFinishedChanged.connect(self._download_state_changed) dlitem.isFinishedChanged.connect(dlitem.deleteLater) @Slot() def _download_state_changed(self): dlitem = self.sender() dl = download_to_json(dlitem) for buffer in self._buffers: buffer.runJavaScript("update_download(%s);" % dl) @Slot("QWebEngineDownloadRequest*") def download_requested(self, dl): minibuff = current_minibuffer() prompt = DlChooseActionPrompt(os.path.join(dl.downloadDirectory(), dl.suggestedFileName()), dl.mimeType()) action = minibuff.do_prompt(prompt) if action == "open": prompt = DlOpenActionPrompt() executable = minibuff.do_prompt(prompt) if executable is None: return dl.setDownloadDirectory(tempfile.gettempdir()) logging.info(f"Downloading {dl_path(dl)}...") def finished(): if dl.state() == \ QWebEngineDownloadRequest.DownloadState.DownloadCompleted: logging.info( f"Opening external file {dl_path(dl)} with {executable}") self._run_program(executable, dl_path(dl)) dl.isFinishedChanged.connect(finished) self._start_download(dl) elif action == "download": dl_dir = get_user_download_dir() or dl.downloadDirectory() name = dl.suggestedFileName() path = os.path.join(dl_dir, name) if os.path.exists(path): try: path = find_unique_suggested_path(dl_dir, name) except OSError as exc: logging.warning( "Can't use user_dir %s: %s", user_dir, str(exc) ) prompt = DlPrompt(path, dl.mimeType()) path = minibuff.do_prompt(prompt) if path is None: return if os.path.isdir(path): path = find_unique_suggested_path(path, name) if os.path.isfile(path): if not minibuff.do_prompt(OverwriteFilePrompt(path)): return dl.setDownloadDirectory(os.path.dirname(path)) dl.setDownloadFileName(os.path.basename(path)) if keep_temporary_download_dir.value: global TEMPORARY_DOWNLOAD_DIR TEMPORARY_DOWNLOAD_DIR = os.path.dirname(path) logging.info("Downloading %s...", path) def finished(): state = state_str(dl.state()) logging.info("Finished download [%s] of %s", state, dl_path(dl)) minibuff.show_info("[{}] download: {}".format(state, dl_path(dl))) dl.isFinishedChanged.connect(finished) self._start_download(dl) def _run_program(self, executable, path): shell_arg = "{} {}".format(executable, shlex.quote(path)) args = ["-c", shell_arg] shell = get_shell() proc = QProcess() self._running_procs[proc] = path logging.debug("Executing command: %s %s", shell, " ".join(args)) proc.finished.connect(self._program_finished) proc.start(shell, args, QProcess.OpenModeFlag.ReadOnly) @Slot(int, QProcess.ExitStatus) def _program_finished(self, code, status): proc = self.sender() path = self._running_procs.pop(proc) logging.debug("Removing downloaded file %s", path) try: os.unlink(path) except Exception: pass def get_shell(): return os.environ.get("SHELL", "/bin/sh") ================================================ FILE: webmacs/download_manager/prompts.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os from ..minibuffer.prompt import Prompt, FSModel, PromptTableModel, YesNoPrompt def OverwriteFilePrompt(path): return YesNoPrompt("File {} already exists. Overwrite it?".format(path)) class DlChooseActionPrompt(Prompt): complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } value_return_index_data = True def __init__(self, path, mimetype): Prompt.__init__(self, None) self.__actions = [ ("download", "download file on disk"), ("open", "open file with external command"), ] name = os.path.basename(path) if len(name) > 33: name = name[:30] + "..." self.label = "File {} [{}]: ".format(name, mimetype) def completer_model(self): return PromptTableModel(self.__actions) def enable(self, minibuffer): super().enable(minibuffer) minibuffer.input().popup().selectRow(0) class DlOpenActionPrompt(Prompt): complete_options = { "match": Prompt.FuzzyMatch, "complete-empty": True, } label = "Open file with:" value_return_index_data = True def __init__(self): Prompt.__init__(self, None) def completer_model(self): return PromptTableModel([[e] for e in list_executables()]) class DlPrompt(Prompt): complete_options = { "autocomplete": True } def __init__(self, path, mimetype): Prompt.__init__(self, None) self.label = "Download file [{}]:".format(mimetype) self._dlpath = path def completer_model(self): # todo, not working model = FSModel(self) return model def enable(self, minibuffer): super().enable(minibuffer) minibuffer.input().setText(self._dlpath) def list_executables(): try: paths = os.environ["PATH"].split(os.pathsep) except KeyError: return [] executables = [] for path in paths: try: for file_ in os.listdir(path): if os.access(os.path.join(path, file_), os.X_OK): executables.append(file_) except Exception: pass return executables ================================================ FILE: webmacs/egrid.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from PyQt6.QtWidgets import QLayout from PyQt6.QtCore import QRect, QSize from . import call_later, BUFFERS from .webview import WebView class LayoutEntry(object): def __init__(self, parent=None, item=None): self.parent = parent self.item = item self.split = None self.children = [] def do_split(self, item, direction): assert self.item if self.parent and self.parent.split == direction: index = self.parent.children.index(self) self.parent.children.insert( index+1, LayoutEntry(parent=self.parent, item=item)) else: self.split = direction self.children.append(LayoutEntry(parent=self, item=self.item)) self.children.append(LayoutEntry(parent=self, item=item)) self.item = None def pop(self): assert self.parent parent = self.parent parent.children.remove(self) # if there is only one sibling left, replace the parent by # this sibling. if len(parent.children) == 1: other = parent.children[0] parent.item = other.item parent.split = other.split parent.children = other.children def set_geometry(self, rect): if self.item: self.item.setGeometry(rect) elif self.split == ViewGridLayout.VERTICAL: x = rect.x() width = round(rect.width() / len(self.children)) for child in self.children: cr = QRect(x, rect.y(), width, int(rect.height())) child.set_geometry(cr) x += width elif self.split == ViewGridLayout.HORIZONTAL: y = rect.y() height = round(rect.height() / len(self.children)) for child in self.children: cr = QRect(rect.x(), y, rect.width(), height) child.set_geometry(cr) y += height def __iter__(self): entries = [self] while entries: entry = entries.pop(0) yield entry entries.extend(entry.children) def entry_for_item(self, item): for entry in self: if entry.item == item: return entry return None class ViewGridLayout(QLayout): VERTICAL = 1 HORIZONTAL = 2 def __init__(self, window=None): QLayout.__init__(self) self._window = window main_view = WebView(window) # keep an ordered list of the widgets self._views = [] self._current_view = main_view # to avoid asking reordering many times self.__view_sort_asked = False self._item_added = None self._root = LayoutEntry() self.add_view(main_view, self._root) def current_view(self): return self._current_view def set_current_view(self, widget): assert widget in self._views self._current_view = widget def views(self): return self._views def __sort_views_by_position(self): def top_top_bottom(w): return w.geometry().center().y() def left_to_right(w): return w.geometry().center().x() self._views = sorted(self._views, key=top_top_bottom) self._views = sorted(self._views, key=left_to_right) self.__view_sort_asked = False def _sort_views_by_position(self): # compress requests for reordering widgets if self.__view_sort_asked: return self.__view_sort_asked = True call_later(self.__sort_views_by_position) def add_view(self, widget, parent_entry, direction=None): self.addWidget(widget) self._views.append(widget) self._sort_views_by_position() item = self._item_added self._item_added = None if direction is not None: parent_entry.do_split(item, direction) else: parent_entry.item = item def entries(self): return [e for e in self._root if e.item] def addItem(self, item): self._item_added = item def count(self): return len(self.entries()) def itemAt(self, index): try: return self.entries()[index].item except IndexError: return None def takeAt(self, index): entry = self.entries()[index] if entry.item: self._views.remove(entry.item.widget()) self._sort_views_by_position() return entry.pop() def sizeHint(self): size = QSize(0, 0) for entry in self.entries(): size = size.expandedTo(entry.item.sizeHint()) return size + self.count() * QSize(self.spacing(), self.spacing()) def setGeometry(self, rect): self._root.set_geometry(rect) def split_view(self, direction, reference=None): widget = WebView(self._window) refindex = self.indexOf(reference or self._current_view) refitem = self.itemAt(refindex) for entry in self._root: if entry.item == refitem: self.add_view(widget, entry, direction) self.invalidate() break return widget def dump_state(self): def item_dump_state(entry): if entry.item is None: return { "split": ("horizontal" if entry.split == self.HORIZONTAL else "vertical"), "views": [item_dump_state(c) for c in entry.children] } else: view = entry.item.widget() buffer_index = BUFFERS.index(view.buffer()) if view == self._current_view: return {"buffer": buffer_index, "current": True} else: return {"buffer": buffer_index} return item_dump_state(self._root) def restore_state(self, grid_data): main_view = self._current_view def restore(data, view): split = data.get("split") if split is None: # attach the buffer to the view. view.setBuffer(BUFFERS[data["buffer"]], update_last_use=False) if data.get("current"): self._current_view = view else: # we have splits to do. split = (self.HORIZONTAL if split == "horizontal" else self.VERTICAL) # first split everything, to create the views rest = [(data["views"][0], view)] for wdata in data["views"][1:]: view = self.split_view(split, view) rest.append((wdata, view)) # and now let's recurse to set nested buffers for wdata, view in rest: restore(wdata, view) restore(grid_data, main_view) # and update the focus of the views for w in self._views: w.show_focused(w == self._current_view) # required to have the right keyboard focus main_view.main_window.set_current_webview(main_view) ================================================ FILE: webmacs/external_editor.py ================================================ import os import tempfile import subprocess from . import variables import shlex editor_cmd = variables.define_variable( "external-editor-command", "command to open an external editor. You must use the {file}" " placeholder in the command, as it will be used to open the" " temporary file.", "emacsclient -c -a '' {file}", type=variables.String(), ) def open_external_editor(content): with tempfile.NamedTemporaryFile(delete=False) as tf: tf.write(content) cmd = editor_cmd.value.format(file=shlex.quote(tf.name)) if subprocess.call(cmd, shell=True) != 0: return with open(tf.name) as f: return f.read() os.unlink(tf.name) ================================================ FILE: webmacs/features.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import sqlite3 import logging from PyQt6.QtWebEngineCore import QWebEnginePage class Features(object): def __init__(self, db_path): self._conn = sqlite3.connect(db_path) self._conn.execute(""" CREATE TABLE IF NOT EXISTS features (url TEXT, feature NUMBER, permission NUMBER, PRIMARY KEY(url, feature)); """) def set_permission(self, url, feature, permission): logging.info(f"[{url}]: Saving {feature} to {permission}") self._conn.execute(""" INSERT OR REPLACE INTO features (url, feature, permission) VALUES (?, ?, ?) """, (url, feature.value, permission.value)) self._conn.commit() def get_permission(self, url, feature): permission_value = self._conn.execute( "SELECT permission FROM features WHERE url = ? AND feature = ?", (url, feature.value)).fetchone() permission = QWebEnginePage.PermissionPolicy.PermissionUnknown if permission_value: for p in QWebEnginePage.PermissionPolicy: if p.value == permission_value[0]: permission = p logging.info(f"[{url}]: Found permission {permission} for {feature}") break else: logging.info(f"[{url}] No permission found for {feature}") return permission ================================================ FILE: webmacs/filter_webengine_output.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import re import sys import logging import traceback from PyQt6.QtNetwork import QLocalSocket from . import version class NoFilter(object): def __init__(self, *a, **kw): pass def enable(self): pass class OutputFilter(object): """ Filter what is send on stderr file descriptor, on unix systems only. """ def __init__(self, regexes): # duplicate stderr on a new file descriptor that we assign to # sys.stderr (so from python there is no difference when writing to the # standard error) and redirect stderr to a pipe that we will read, # filter and log manually. sys.stderr.flush() r, w = os.pipe() newerr = os.dup(sys.stderr.fileno()) os.dup2(w, sys.stderr.fileno()) os.close(w) sys.stderr = os.fdopen(newerr, "w") def _excepthook(e, v, tb): # use our custom stderr traceback.print_exception(e, v, tb, None, sys.stderr) sys.exit(1) sys.excepthook = _excepthook self._notifier = QLocalSocket() self._fd = r self._regexes = regexes def enable(self): self._notifier.setSocketDescriptor(self._fd) self._notifier.readyRead.connect(self._redirect) def _redirect(self): while self._notifier.canReadLine(): line = self._notifier.readLine().data().rstrip().decode("utf-8") logging.log(self._regexes.get_level_for_line(line), line) class FilterRegexes(object): def __init__(self): self._data = [] def get_level_for_line(self, line): for regex, level in self._data: if regex.match(line): return level return logging.CRITICAL def filter(self, regexstr, level=logging.DEBUG): self._data.append((re.compile(regexstr), level)) def make_filter(): # when there is a qtwebengine crash, the OutputFilter will prevent the # stack trace from being printed. Need to find a way to have those stack # traces somewhere before filtering. return NoFilter() regexes = FilterRegexes() regexes.filter(r"^libpng warning: iCCP: known incorrect sRGB profile$") regexes.filter(r".*gles2_cmd_decoder_autogen.h.*") if version.qt_version >= (5, 12): regexes.filter( r"QNetworkReplyHttpImplPrivate::_q_startOperation was called more" r" than once.*" ) else: # see https://bugreports.qt.io/browse/QTBUG-68547 regexes.filter(r".*stack_trace_posix\.cc.* Failed to open file: .*") regexes.filter(r"^ Error: No such file or directory$") regexes.filter(r".*nss_ocsp.cc.*No URLRequestContext for NSS HTTP" r" handler..*") cls = OutputFilter if version.is_posix else NoFilter return cls(regexes) ================================================ FILE: webmacs/hooks.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . class Hook(list): def call(self, *arg, **kwargs): for callback in self: callback(*arg, **kwargs) __call__ = call add = list.append def remove_if_exists(self, cb): try: self.remove(cb) except ValueError: pass webbuffer_created = Hook() webbuffer_closed = Hook() webbuffer_load_finished = Hook() webbuffer_current_changed = Hook() local_mode_changed = Hook() window_activated = Hook() window_closed = Hook() ================================================ FILE: webmacs/ignore_certificates.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import sqlite3 class IgnoredCertificates(object): def __init__(self, dbbath): self._conn = sqlite3.connect(dbbath) self._conn.execute(""" CREATE TABLE IF NOT EXISTS ignorecerts (url TEXT PRIMARY KEY); """) def is_ignored(self, url): return self._conn.execute(""" SELECT url from ignorecerts WHERE url = ? """, (url,)).fetchone() is not None def ignore(self, url): self._conn.execute(""" INSERT OR REPLACE INTO ignorecerts (url) VALUES (?) """, (url,)) self._conn.commit() def remove(self, url): self._conn.execute(""" DELETE from ignorecerts WHERE url = ? """, (url,)) ================================================ FILE: webmacs/ipc.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import json import struct import logging from PyQt6.QtCore import QObject, pyqtSlot as Slot, pyqtSignal as Signal, Qt, \ QDir from PyQt6.QtNetwork import QLocalServer, QLocalSocket from . import version HEADER_FMT = "!I" HEADER_SIZE = struct.calcsize(HEADER_FMT) class IPcReader(QObject): message_received = Signal(object) def __init__(self, sock): QObject.__init__(self) self.sock = sock self._msg_size = None self._data = b"" @Slot() def on_ready_read(self): if self._msg_size is None: if self.sock.bytesAvailable() < HEADER_SIZE: # not enough data yet return self._msg_size = struct.unpack(HEADER_FMT, self.sock.read(HEADER_SIZE))[0] remaining = self._msg_size - len(self._data) if remaining <= 0: return self._data += self.sock.read(min(remaining, self.sock.bytesAvailable())) if len(self._data) == self._msg_size: msg = json.loads(self._data.decode("utf-8")) self.message_received.emit(msg) return msg @Slot(object) def send_data(self, data): data = json.dumps(data).encode("utf-8") len_data = len(data) self.sock.write(struct.pack(HEADER_FMT, len_data)) self.sock.write(data) def get_data(self): while self.sock.waitForReadyRead(): r = self.on_ready_read() if r is not None: return r def clear(self): self.sock.deleteLater() self.sock = None class IpcServer(QObject): @classmethod def get_sock_name(cls, instance): run_path = f"/run/user/{os.getuid()}" prefix = run_path if os.access(run_path, os.W_OK) else "" if instance == "default": return os.path.join(prefix, "webmacs.ipc") return os.path.join(prefix, f"webmacs.{instance}.ipc") @classmethod def list_all_instances(cls, check=True): if version.is_windows: logging.error( "list all instances is not supported on windows" ) return [] # from qt sources, named pipes are created in QDir.tempPath() instances = [ n[8:-4] or "default" for n in os.listdir(QDir.tempPath()) if n.startswith("webmacs.") and n.endswith(".ipc") ] if check: new_instances = [] for instance in instances: local = cls.check_server_connection(instance) if local is not None: local.clear() new_instances.append(instance) instances = new_instances return instances @classmethod def instance_send(cls, instance, data, cb=None): """ Send some data to a webmacs instance asynchronously. """ conn = cls.check_server_connection(instance) if conn is None: return def callback(result): conn.clear() if cb is not None: cb(result) conn.message_received.connect(callback) conn.send_data(data) @classmethod def check_server_connection(cls, instance=None): sock = QLocalSocket() sock.connectToServer(cls.get_sock_name(instance)) if sock.waitForConnected(1000): return IPcReader(sock) return None def __init__(self, instance=None): QObject.__init__(self) sock_name = self.get_sock_name(instance) QLocalServer.removeServer(sock_name) self._server = QLocalServer() self._server.setSocketOptions( QLocalServer.SocketOption.UserAccessOption) self._server.newConnection.connect(self._on_new_connection) if not self._server.listen(sock_name): logging.error("Can not start ipc: %s" % self._server.errorString()) self._readers = {} def cleanup(self): try: os.unlink(self._server.fullServerName()) except OSError: pass @Slot() def _on_new_connection(self): conn = self._server.nextPendingConnection() reader = IPcReader(conn) reader.message_received.connect(self.handle_data) conn.readyRead.connect(reader.on_ready_read) conn.disconnected.connect(self.reader_disconnected) self._readers[conn] = reader @Slot(object) def handle_data(self, data): reader = self.sender() try: res = ipc_dispatch(data) except Exception as exc: res = str(exc) if res in (True, None): reader.send_data({"result": True}) else: reader.send_data({"result": False, "message": res}) def reader_disconnected(self): conn = self.sender() reader = self._readers.pop(conn) reader.clear() reader.deleteLater() def ipc_dispatch(data): from . import current_window from .webbuffer import create_buffer win = current_window() url = data.get("url") if url: view = win.current_webview() view.setBuffer(create_buffer(url)) # this is quite hard to raise a window. The following works fine # for me with gnome 3. flags = win.windowFlags() win.setWindowFlags(flags | Qt.WindowType.Popup) win.raise_() win.activateWindow() win.setWindowFlags(flags) win.show() ================================================ FILE: webmacs/keyboardhandler.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import logging from PyQt6.QtCore import QObject, QEvent from PyQt6.QtGui import QWindow from .keymaps import KeyPress, GLOBAL_KEYMAP, CHAR2KEY from . import hooks from . import COMMANDS, minibuffer_show_info, current_minibuffer, \ current_window from .mode import Mode class CommandContext(object): def __init__(self): self.window = current_window() self.view = self.window.current_webview() if self.window else None self.buffer = self.view.buffer() if self.view else None self.current_prefix_arg = KEY_EATER._prefix_arg self.prompt = None @property def minibuffer(self): win = self.window if win: return win.minibuffer() class LocalKeymapSetter(QObject): def __init__(self): QObject.__init__(self) self.enabled_minibuffer = False def eventFilter(self, obj, evt): # event filter on the global app is required to avoid click on webviews t = evt.type() if t == QEvent.Type.KeyPress and isinstance(obj, QWindow): return KEY_EATER.event_filter(obj, evt) elif t == QEvent.Type.ShortcutOverride: # disable automatic shortcuts in browser, like C-a return True elif self.enabled_minibuffer and t in ( QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonDblClick, QEvent.Type.MouseButtonRelease, QEvent.Type.MouseMove): minibuff = current_minibuffer() if minibuff: # allow clicks in minibuffer inputs and popup only # note: QWidget.underMouse does not works here. input = minibuff.input() if input.rect().contains( input.mapFromGlobal(evt.globalPosition().toPoint())): return False else: popup = input.popup() if popup.isVisible() and popup.rect().contains( popup.mapFromGlobal( evt.globalPosition().toPoint())): return False # else flash the minibuffer on click. if evt.type() in (QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonDblClick) \ and minibuff.prompt(): # noqa: 125 minibuff.prompt().flash() return True return False def minibuffer_input_focus_changed(self, mbi, enabled): self.enabled_minibuffer = enabled if enabled: set_local_keymap(mbi.keymap()) else: if not mbi.isVisible(): # when the minibuffer input is hidden, enable its view's # buffer buff = mbi.parent().parent().current_webview().buffer() set_local_keymap(buff.active_keymap()) def view_focus_changed(self, view, enabled): if enabled and not self.enabled_minibuffer: # fixes issue were a raw qwebenginepage comes here. To # reproduce, have two opened buffers, then C-x 2, C-x 3. if hasattr(view.buffer(), "active_keymap"): set_local_keymap(view.buffer().active_keymap()) if view.main_window.current_webview() == view: hooks.webbuffer_current_changed(view.buffer()) def web_content_edit_focus_changed(self, buff, enabled): if enabled: buff.set_keymap_mode(Mode.KEYMAP_CONTENT_EDIT) if not self.enabled_minibuffer: set_local_keymap(buff.active_keymap()) else: buff.set_keymap_mode(Mode.KEYMAP_NORMAL) if not self.enabled_minibuffer: set_local_keymap(buff.active_keymap()) def caret_browsing_changed(self, buff, enabled): if enabled: buff.set_keymap_mode(Mode.KEYMAP_CARET_BROWSING) if not self.enabled_minibuffer: set_local_keymap(buff.active_keymap()) else: buff.set_keymap_mode(Mode.KEYMAP_NORMAL) if not self.enabled_minibuffer: set_local_keymap(buff.active_keymap()) def buffer_mode_changed(self, buffer, old_mode): # check that the previous keymap was the one corresponding to the mode old_km = old_mode.keymap_for_mode(buffer.keymap_mode) if old_km == local_keymap(): set_local_keymap(buffer.active_keymap()) def buffer_opened_in_view(self, buffer): if not self.enabled_minibuffer: set_local_keymap(buffer.active_keymap()) LOCAL_KEYMAP_SETTER = LocalKeymapSetter() class KeyEater(object): """ Handle Qt keypresses events. """ def __init__(self): self.set_call_handler(CallHandler()) self._keypresses = [] self._local_key_map = None self._use_global_keymap = True self.universal_key = KeyPress.from_str("C-u") self._prefix_arg = None self._prefix_arg_keys = [] self._allowed_universal_keys = {} for i in "1234567890": self._allowed_universal_keys[CHAR2KEY[i]] \ = lambda: self._num_update_prefix_arg(i) def set_call_handler(self, call_handler): self.call_handler = call_handler def set_local_key_map(self, keymap): if keymap != self._local_key_map: self._local_key_map = keymap hooks.local_mode_changed(keymap) logging.debug("local keymap activated: %s", keymap) def local_key_map(self): return self._local_key_map def set_global_keymap_enabled(self, enable): self._use_global_keymap = enable def event_filter(self, obj, event): key = KeyPress.from_qevent(event) if key is None: return False if self._handle_keypress(obj, key): return True return False def active_keymaps(self): if self._local_key_map: yield self._local_key_map if self._use_global_keymap: yield GLOBAL_KEYMAP def _add_keypress(self, keypress): self._keypresses.append(keypress) logging.debug("keychord: %s" % self._keypresses) def _num_update_prefix_arg(self, numstr): if not isinstance(self._prefix_arg, int): self._prefix_arg = int(numstr) else: self._prefix_arg = int(str(self._prefix_arg) + numstr) def _show_info_kbd(self, extra=""): all_presses = self._prefix_arg_keys + self._keypresses minibuffer_show_info( " ".join((str(k) for k in all_presses)) + extra ) def _handle_keypress(self, sender, keypress): if keypress == self.universal_key and not self._keypresses: if isinstance(self._prefix_arg, tuple): self._prefix_arg = (self._prefix_arg[0] * 4,) else: self._prefix_arg = (4,) self._prefix_arg_keys.append(keypress) self._show_info_kbd() return True if self._prefix_arg is not None: try: func = self._allowed_universal_keys[keypress.key] except KeyError: pass else: if not keypress.has_any_modifier(): func() self._prefix_arg_keys.append(keypress) self._show_info_kbd() return True result = None self._add_keypress(keypress) for keymap in self.active_keymaps(): result = keymap.lookup(self._keypresses) if result: break if not result: if len(self._keypresses) > 1: self._show_info_kbd(" is undefined.") else: minibuffer_show_info("") self._keypresses = [] self.call_handler.no_call(sender, keymap, keypress) self._prefix_arg = None self._prefix_arg_keys = [] return False if result.complete: self._show_info_kbd() self._keypresses = [] ctx = CommandContext() self._prefix_arg = None self._prefix_arg_keys = [] try: self.call_handler.call(ctx, keymap, keypress, result.command) except Exception: logging.exception("Error calling command:") else: self._show_info_kbd(" -") self.call_handler.partial_call(sender, keymap, keypress) return result is not None class CallHandler(object): def __init__(self): self._commands = COMMANDS def call(self, ctx, keymap, keypress, command): if isinstance(command, str): try: command = self._commands[command] except KeyError: raise KeyError("No such command: %s" % command) command(ctx) def no_call(self, sender, keymap, keypress): pass def partial_call(self, sender, keymap, keypress): pass KEY_EATER = KeyEater() def send_key_event(keypress): from .application import app as _app app = _app() w = app.focusWindow() app.postEvent(w, keypress.to_qevent(QEvent.Type.KeyPress)) app.postEvent(w, keypress.to_qevent(QEvent.Type.KeyRelease)) def local_keymap(): return KEY_EATER.local_key_map() def set_local_keymap(keymap): KEY_EATER.set_local_key_map(keymap) def set_global_keymap_enabled(enable): KEY_EATER.set_global_keymap_enabled(enable) ================================================ FILE: webmacs/keymaps/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import warnings from collections import namedtuple from PyQt6.QtCore import Qt from PyQt6.QtGui import QKeyEvent from .. import COMMANDS KEY2CHAR = {} CHAR2KEY = {} KEYMAPS = {} def _set_key(key, char, *chars): KEY2CHAR[key] = char CHAR2KEY[char] = key for ch in chars: CHAR2KEY[ch] = key # see http://doc.qt.io/qt-5/qt.html#Key-enum, # https://www.blunix.org/using-german-umlauts-on-us-layout-keyboards/ _set_key(Qt.Key.Key_Escape, "Esc") _set_key(Qt.Key.Key_Tab, "Tab") _set_key(Qt.Key.Key_Backtab, "Backtab") _set_key(Qt.Key.Key_Backspace, "Backspace") _set_key(Qt.Key.Key_Return, "Return") _set_key(Qt.Key.Key_Enter, "Enter") _set_key(Qt.Key.Key_Insert, "Insert") _set_key(Qt.Key.Key_Delete, "Delete") _set_key(Qt.Key.Key_Pause, "Pause") # pause/break key, not media pause _set_key(Qt.Key.Key_Print, "Print") _set_key(Qt.Key.Key_SysReq, "SysReq") _set_key(Qt.Key.Key_Clear, "Clear") _set_key(Qt.Key.Key_Home, "Home") _set_key(Qt.Key.Key_End, "End") _set_key(Qt.Key.Key_Left, "Left") _set_key(Qt.Key.Key_Up, "Up") _set_key(Qt.Key.Key_Right, "Right") _set_key(Qt.Key.Key_Down, "Down") _set_key(Qt.Key.Key_PageUp, "PageUp") _set_key(Qt.Key.Key_PageDown, "PageDown") _set_key(Qt.Key.Key_F1, "F1") _set_key(Qt.Key.Key_F2, "F2") _set_key(Qt.Key.Key_F3, "F3") _set_key(Qt.Key.Key_F4, "F4") _set_key(Qt.Key.Key_F5, "F5") _set_key(Qt.Key.Key_F6, "F6") _set_key(Qt.Key.Key_F7, "F7") _set_key(Qt.Key.Key_F8, "F8") _set_key(Qt.Key.Key_F9, "F9") _set_key(Qt.Key.Key_F10, "F10") _set_key(Qt.Key.Key_F11, "F11") _set_key(Qt.Key.Key_F12, "F12") _set_key(Qt.Key.Key_Space, "Space") _set_key(Qt.Key.Key_Exclam, "!") _set_key(Qt.Key.Key_QuoteDbl, '"') _set_key(Qt.Key.Key_Dollar, '$') _set_key(Qt.Key.Key_Percent, "%") _set_key(Qt.Key.Key_Ampersand, "&") _set_key(Qt.Key.Key_Apostrophe, "'") _set_key(Qt.Key.Key_ParenLeft, "(") _set_key(Qt.Key.Key_ParenRight, ")") _set_key(Qt.Key.Key_Asterisk, "*") _set_key(Qt.Key.Key_Plus, "+") _set_key(Qt.Key.Key_Comma, ",") _set_key(Qt.Key.Key_Minus, "-") _set_key(Qt.Key.Key_Period, ".") _set_key(Qt.Key.Key_Slash, "/") _set_key(Qt.Key.Key_0, "0") _set_key(Qt.Key.Key_1, "1") _set_key(Qt.Key.Key_2, "2") _set_key(Qt.Key.Key_3, "3") _set_key(Qt.Key.Key_4, "4") _set_key(Qt.Key.Key_5, "5") _set_key(Qt.Key.Key_6, "6") _set_key(Qt.Key.Key_7, "7") _set_key(Qt.Key.Key_8, "8") _set_key(Qt.Key.Key_9, "9") _set_key(Qt.Key.Key_Colon, ":") _set_key(Qt.Key.Key_Semicolon, ";") _set_key(Qt.Key.Key_Less, "<") _set_key(Qt.Key.Key_Equal, "=") _set_key(Qt.Key.Key_Greater, ">") _set_key(Qt.Key.Key_Question, "?") _set_key(Qt.Key.Key_At, "@") _set_key(Qt.Key.Key_A, "a", "A") _set_key(Qt.Key.Key_B, "b", "B") _set_key(Qt.Key.Key_C, "c", "C") _set_key(Qt.Key.Key_D, "d", "D") _set_key(Qt.Key.Key_E, "e", "E") _set_key(Qt.Key.Key_F, "f", "F") _set_key(Qt.Key.Key_G, "g", "G") _set_key(Qt.Key.Key_H, "h", "H") _set_key(Qt.Key.Key_I, "i", "I") _set_key(Qt.Key.Key_J, "j", "J") _set_key(Qt.Key.Key_K, "k", "K") _set_key(Qt.Key.Key_L, "l", "L") _set_key(Qt.Key.Key_M, "m", "M") _set_key(Qt.Key.Key_N, "n", "N") _set_key(Qt.Key.Key_O, "o", "O") _set_key(Qt.Key.Key_P, "p", "P") _set_key(Qt.Key.Key_Q, "q", "Q") _set_key(Qt.Key.Key_R, "r", "R") _set_key(Qt.Key.Key_S, "s", "S") _set_key(Qt.Key.Key_T, "t", "T") _set_key(Qt.Key.Key_U, "u", "U") _set_key(Qt.Key.Key_V, "v", "V") _set_key(Qt.Key.Key_W, "w", "W") _set_key(Qt.Key.Key_X, "x", "X") _set_key(Qt.Key.Key_Y, "y", "Y") _set_key(Qt.Key.Key_Z, "z", "Z") _set_key(Qt.Key.Key_BracketLeft, "[") _set_key(Qt.Key.Key_Backslash, "\\") _set_key(Qt.Key.Key_BracketRight, "]") _set_key(Qt.Key.Key_AsciiCircum, "^") _set_key(Qt.Key.Key_Underscore, "_") _set_key(Qt.Key.Key_Underscore, "_") # _set_key(Qt.Key.Key_QuoteLeft, "") _set_key(Qt.Key.Key_BraceLeft, "{") _set_key(Qt.Key.Key_Bar, "|") _set_key(Qt.Key.Key_BraceRight, "}") _set_key(Qt.Key.Key_AsciiTilde, "~") _set_key(Qt.Key.Key_nobreakspace, " ") _set_key(Qt.Key.Key_nobreakspace, " ") _set_key(Qt.Key.Key_exclamdown, "¡") _set_key(Qt.Key.Key_cent, "¢") _set_key(Qt.Key.Key_sterling, "£") _set_key(Qt.Key.Key_currency, "¤") _set_key(Qt.Key.Key_yen, "¥") _set_key(Qt.Key.Key_brokenbar, "¦") _set_key(Qt.Key.Key_section, "§") _set_key(Qt.Key.Key_diaeresis, "¨") _set_key(Qt.Key.Key_copyright, "©") _set_key(Qt.Key.Key_ordfeminine, "ª") _set_key(Qt.Key.Key_guillemotleft, "«") _set_key(Qt.Key.Key_notsign, "¬") # _set_key(Qt.Key.Key_hyphen, "") _set_key(Qt.Key.Key_registered, "®") _set_key(Qt.Key.Key_macron, "¯") _set_key(Qt.Key.Key_degree, "°") _set_key(Qt.Key.Key_plusminus, "±") _set_key(Qt.Key.Key_twosuperior, "²") _set_key(Qt.Key.Key_threesuperior, "³") _set_key(Qt.Key.Key_acute, "´") _set_key(Qt.Key.Key_mu, "µ") _set_key(Qt.Key.Key_paragraph, "¶") _set_key(Qt.Key.Key_periodcentered, "·") _set_key(Qt.Key.Key_cedilla, "¸") _set_key(Qt.Key.Key_onesuperior, "¹") _set_key(Qt.Key.Key_masculine, "º") _set_key(Qt.Key.Key_guillemotright, "»") _set_key(Qt.Key.Key_onequarter, "¼") _set_key(Qt.Key.Key_onehalf, "½") _set_key(Qt.Key.Key_threequarters, "¾") _set_key(Qt.Key.Key_questiondown, "¿") _set_key(Qt.Key.Key_Agrave, "à", "À") _set_key(Qt.Key.Key_Aacute, "á", "Á") _set_key(Qt.Key.Key_Acircumflex, "â", "Â") _set_key(Qt.Key.Key_Atilde, "ã", "Ã") _set_key(Qt.Key.Key_Adiaeresis, "ä", "Ä") _set_key(Qt.Key.Key_Aring, "å", "Å") _set_key(Qt.Key.Key_AE, "æ", "Æ") _set_key(Qt.Key.Key_Ccedilla, "ç", "Ç") _set_key(Qt.Key.Key_Egrave, "è", "È") _set_key(Qt.Key.Key_Eacute, "é", "É") _set_key(Qt.Key.Key_Ecircumflex, "ê", "Ê") _set_key(Qt.Key.Key_Ediaeresis, "Ë", "ë") _set_key(Qt.Key.Key_Igrave, "ì", "Ì") _set_key(Qt.Key.Key_Iacute, "í", "Í") _set_key(Qt.Key.Key_Icircumflex, "î", "Î") _set_key(Qt.Key.Key_Idiaeresis, "ï", "Ï") _set_key(Qt.Key.Key_ETH, "Ð") _set_key(Qt.Key.Key_Ntilde, "ñ", "Ñ") _set_key(Qt.Key.Key_Ograve, "ò", "Ò") _set_key(Qt.Key.Key_Oacute, "ó", "Ó") _set_key(Qt.Key.Key_Ocircumflex, "ô", "Ô") _set_key(Qt.Key.Key_Odiaeresis, "ö", "Ö") _set_key(Qt.Key.Key_multiply, "×") _set_key(Qt.Key.Key_Ooblique, "Ø", "ø") _set_key(Qt.Key.Key_Ugrave, "ù", "Ù") _set_key(Qt.Key.Key_Uacute, "ú", "Ú") _set_key(Qt.Key.Key_Ucircumflex, "û", "Û") _set_key(Qt.Key.Key_Udiaeresis, "ü", "Ü") _set_key(Qt.Key.Key_Yacute, "ý", "Ý") _set_key(Qt.Key.Key_THORN, "þ", "Þ") def is_one_letter_upcase(char): return len(char) == 1 and char.isalpha() and char.isupper() _KeyPress = namedtuple("_KeyPress", ("key", "control_modifier", "alt_modifier", "super_modifier", "is_upper_case")) class KeyPress(_KeyPress): @classmethod def from_qevent(cls, event): text = event.text() # Try to get the key value depending on the text. Despite what the qt # doc says, it seems more reliable to get the good value this way. For # example, to match C-? on my french keyboard (using the bépo layout) # this is required (Ctrl-Shift-'), else event.key() is equal to the key # DOWN. key = CHAR2KEY.get(text) if key is None: key = event.key() if key not in KEY2CHAR: return None modifiers = event.modifiers() return cls( key, bool(modifiers & Qt.KeyboardModifier.ControlModifier), bool(modifiers & Qt.KeyboardModifier.AltModifier), bool(modifiers & Qt.KeyboardModifier.MetaModifier), is_one_letter_upcase(text) ) @classmethod def from_str(cls, string): ctrl, alt, super = False, False, False left, _, text = string.rpartition("-") if text == "": text = "-" parts = left.split("-") for p in parts: if p == "": break elif p == "C": ctrl = True elif p == "M": alt = True elif p == "S": super = True else: raise Exception( "Unknown key modifier: %s in key definition %s" % (p, string) ) try: key = CHAR2KEY[text] except KeyError: raise Exception("Unknown key %s" % text) return cls( key, ctrl, alt, super, is_one_letter_upcase(text) ) def to_qevent(self, type): modifiers = Qt.KeyboardModifier.NoModifier key = self.key if self.control_modifier: modifiers |= Qt.KeyboardModifier.ControlModifier if self.alt_modifier: modifiers |= Qt.KeyboardModifier.AltModifier if self.super_modifier: modifiers |= Qt.KeyboardModifier.MetaModifier if self.is_upper_case: return QKeyEvent(type, key, modifiers, KEY2CHAR[key].upper()) else: return QKeyEvent(type, key, modifiers) def has_any_modifier(self): return (self.control_modifier or self.alt_modifier or self.super_modifier) def char(self): char = KEY2CHAR[self.key] if self.is_upper_case: return char.upper() return char def __str__(self): keyrepr = [] if self.control_modifier: keyrepr.append("C") if self.alt_modifier: keyrepr.append("M") if self.super_modifier: keyrepr.append("S") keyrepr.append(self.char()) return "-".join(keyrepr) def __repr__(self): return "<%s (%s)>" % (self.__class__.__name__, str(self)) KeymapLookupResult = namedtuple("KeymapLookupResult", ("complete", "command", "keymap")) class InternalKeymap(object): __slots__ = ("bindings", "parent") def __init__(self, parent=None): self.bindings = {} self.parent = parent def _traverse_commands(self, prefix, acc_fn, parent=None): for keypress, cmd in self.bindings.items(): new_prefix = prefix + [keypress] if isinstance(cmd, InternalKeymap): cmd._traverse_commands(new_prefix, acc_fn, parent) else: acc_fn(new_prefix, cmd, parent) if self.parent: for keypress, cmd in self.parent.bindings.items(): if keypress not in self.bindings: new_prefix = prefix + [keypress] if isinstance(cmd, InternalKeymap): cmd._traverse_commands(new_prefix, acc_fn, self.parent) else: acc_fn(new_prefix, cmd, self.parent) def traverse_commands(self, acc_fn): self._traverse_commands([], acc_fn) def all_bindings(self, raw_fn=False, with_parent=True): """ Returns the list of bindings as (keychord, command-name) tuples. """ acc = [] def add(prefix, cmd, parent): if not with_parent and parent is not None: return if isinstance(cmd, str): acc.append((" ".join(str(k) for k in prefix), cmd)) elif raw_fn: acc.append((" ".join(str(k) for k in prefix), cmd.__name__)) self.traverse_commands(add) return acc def _define_key(self, key, binding): keys = [KeyPress.from_str(k) for k in key.split()] assert keys, "key should not be empty" assert callable(binding) or isinstance(binding, str), \ "binding should be callable or a command name" kmap = self for keypress in keys[:-1]: if keypress in kmap.bindings: othermap = kmap.bindings[keypress] if not isinstance(othermap, InternalKeymap): othermap = InternalKeymap() else: othermap = InternalKeymap() kmap.bindings[keypress] = othermap kmap = othermap kmap.bindings[keys[-1]] = binding def define_key(self, key, binding=None): """ Define a binding (callable or command name) for a key chord. :param key: a string representing the key chord, such as "C-c x". :param binding: A command name (a string), a callable, or None. If None, it must be used as a function decorator. """ if binding is None: def wrapper(func): self._define_key(key, func) return func return wrapper else: if isinstance(binding, str): if binding not in COMMANDS: raise KeyError("No such command: %s" % binding) self._define_key(key, binding) def undefine_key(self, key): """ Undefine the binding under a key chord. :param key: a string representing the key chord, such as "C-c x". """ keys = [KeyPress.from_str(k) for k in key.split()] if not keys: return None res = self.lookup(keys) if res is not None and res.complete: del res.keymap.bindings[keys[-1]] return res.keymap return None def _look_up(self, keypress): keymap = self while keymap: try: return keymap.bindings[keypress] except KeyError: keymap = keymap.parent def lookup(self, keypresses): partial_match = False keymap = self for keypress in keypresses: while keymap: entry = keymap.bindings.get(keypress) if entry is not None: if isinstance(entry, InternalKeymap): keymap = entry partial_match = True break else: return KeymapLookupResult(True, entry, keymap) keymap = keymap.parent if keymap is None: return None elif partial_match: return KeymapLookupResult(False, None, keymap) else: return None class Keymap(InternalKeymap): __slots__ = InternalKeymap.__slots__ + ("name", "doc") def __init__(self, name, parent=None, doc=None): InternalKeymap.__init__(self, parent=parent) self.name = name self.doc = doc if self.name in KEYMAPS: raise ValueError("A keymap named %s already exists." % self.name) KEYMAPS[self.name] = self def __str__(self): return self.name @property def brief_doc(self): if self.doc: return self.doc.split("\n", 1)[0] EMPTY_KEYMAP = Keymap("empty") GLOBAL_KEYMAP = Keymap("global", doc="""\ The global keymap is always active. It act as a fallback to other keymaps, which are considered local. Only one local keymap can be active at a time. A binding is first searched in the currently active local keymap, and if not found the global keymap is used. Only bindings with modifiers should be bound to it, else it will be impossible to edit text inside the browser.""") BUFFER_KEYMAP = Keymap("webbuffer", doc="""\ Local keymap activated when a web buffer is focused.\ A web buffer is focused when there is no text editing, no caret browsing, or when the minibuffer input is not shown... It is enabled when no other local keymap is enabled.""") CONTENT_EDIT_KEYMAP = Keymap("webcontent-edit", doc="""\ Local keymap activated when a webcontent field (input, textarea, ...) is \ focused.""") CARET_BROWSING_KEYMAP = Keymap("caret-browsing", doc="""\ Local keymap activated when you are navigating the webbuffer with a caret.\ """) FULLSCREEN_KEYMAP = Keymap("video-fullscreen", doc="""\ Local Keymap activated when a video is played full screen. """) MINIBUFFER_KEYMAP = Keymap("minibuffer", doc="""\ Local keymap activated when input is in the minibuffer line edit. """) VISITEDLINKS_KEYMAP = Keymap("visited-links-list", parent=MINIBUFFER_KEYMAP, doc="""\ Local keymap activated while looking into visited links. """) BOOKMARKS_KEYMAP = Keymap("bookmarks-list", parent=MINIBUFFER_KEYMAP, doc="""\ Local keymap activated while looking into bookmarks. """) BUFFERLIST_KEYMAP = Keymap("buffer-list", parent=MINIBUFFER_KEYMAP, doc="""\ Local keymap activated while looking into buffers. """) WEBJUMP_KEYMAP = Keymap("webjump", parent=MINIBUFFER_KEYMAP, doc="""\ Local keymap activated while using webjumps. """) HINT_KEYMAP = Keymap("hint", parent=MINIBUFFER_KEYMAP, doc="""\ Local keymap used when hinting. """) ISEARCH_KEYMAP = Keymap("i-search", parent=MINIBUFFER_KEYMAP, doc="""\ Local keymap used in incremental search. """) def global_keymap(): """ Returns the global :class:`Keymap`. It is almost always active, and act as a fallback if there is an active keymap. """ warnings.warn( "global_keymap() is deprecated, use keymap('global') instead", DeprecationWarning ) return GLOBAL_KEYMAP def webbuffer_keymap(): """ Returns the :class:`Keymap` associated to web buffers. This keymap is active when there is no focus for an editable element in web contents. """ warnings.warn( "webbuffer_keymap() is deprecated, use keymap('webbuffer') instead", DeprecationWarning ) return BUFFER_KEYMAP def content_edit_keymap(): """ Returns the :class:`Keymap` associated to content editing. Local keymap activated when a webcontent field (input, textarea, ...) is focused """ warnings.warn( "content_edit_keymap() is deprecated, use keymap('webcontent-edit')" " instead", DeprecationWarning ) return CONTENT_EDIT_KEYMAP def keymap(name): """Get a keymap given its name.""" return KEYMAPS[name] ================================================ FILE: webmacs/keymaps/caret_browsing.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import CARET_BROWSING_KEYMAP as KEYMAP KEYMAP.define_key("C-n", "caret-browsing-down") KEYMAP.define_key("n", "caret-browsing-down") KEYMAP.define_key("Down", "caret-browsing-down") KEYMAP.define_key("C-p", "caret-browsing-up") KEYMAP.define_key("p", "caret-browsing-up") KEYMAP.define_key("Up", "caret-browsing-up") KEYMAP.define_key("C-g", "caret-browsing-shutdown") KEYMAP.define_key("Esc", "caret-browsing-shutdown") KEYMAP.define_key("C-f", "caret-browsing-forward-char") KEYMAP.define_key("f", "caret-browsing-forward-char") KEYMAP.define_key("Right", "caret-browsing-forward-char") KEYMAP.define_key("C-b", "caret-browsing-backward-char") KEYMAP.define_key("b", "caret-browsing-backward-char") KEYMAP.define_key("Left", "caret-browsing-backward-char") KEYMAP.define_key("M-f", "caret-browsing-forward-word") KEYMAP.define_key("C-Right", "caret-browsing-forward-word") KEYMAP.define_key("M-b", "caret-browsing-backward-word") KEYMAP.define_key("C-Left", "caret-browsing-backward-word") KEYMAP.define_key("C-Space", "caret-browsing-toggle-mark") KEYMAP.define_key("M-w", "caret-browsing-cut") KEYMAP.define_key("C-a", "caret-browsing-beginning-of-line") KEYMAP.define_key("C-e", "caret-browsing-end-of-line") KEYMAP.define_key("M-<", "caret-browsing-beginning-of-document") KEYMAP.define_key("M->", "caret-browsing-end-of-document") KEYMAP.define_key("M-{", "caret-browsing-backward-paragraph") KEYMAP.define_key("M-}", "caret-browsing-forward-paragraph") KEYMAP.define_key("C-s", "i-search-forward") KEYMAP.define_key("C-r", "i-search-backward") ================================================ FILE: webmacs/keymaps/content_edit.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import CONTENT_EDIT_KEYMAP as KEYMAP KEYMAP.define_key("C-g", "content-edit-cancel") KEYMAP.define_key("C-n", "send-key-down") KEYMAP.define_key("C-p", "send-key-up") KEYMAP.define_key("C-Space", "content-edit-set-mark") KEYMAP.define_key("C-f", "content-edit-forward-char") KEYMAP.define_key("C-b", "content-edit-backward-char") KEYMAP.define_key("M-f", "content-edit-forward-word") KEYMAP.define_key("M-b", "content-edit-backward-word") KEYMAP.define_key("C-a", "content-edit-beginning-of-line") KEYMAP.define_key("C-e", "content-edit-end-of-line") KEYMAP.define_key("C-d", "content-edit-delete-forward-char") KEYMAP.define_key("M-d", "content-edit-delete-forward-word") KEYMAP.define_key("M-Backspace", "content-edit-delete-backward-word") KEYMAP.define_key("M-w", "content-edit-copy") KEYMAP.define_key("C-w", "content-edit-cut") KEYMAP.define_key("C-y", "webcontent-paste") KEYMAP.define_key("C-k", "content-edit-kill") KEYMAP.define_key("M-u", "content-edit-upcase-forward-word") KEYMAP.define_key("M-l", "content-edit-downcase-forward-word") KEYMAP.define_key("M-c", "content-edit-capitalize-forward-word") KEYMAP.define_key("C-x e", "content-edit-open-external-editor") KEYMAP.define_key("C-x C-e", "content-edit-open-external-editor") KEYMAP.define_key("C-/", "content-edit-undo") KEYMAP.define_key("C-?", "content-edit-redo") KEYMAP.define_key("C-x h", "content-edit-select-all") ================================================ FILE: webmacs/keymaps/fullscreen.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import FULLSCREEN_KEYMAP from .. import current_window from PyQt6.QtWebEngineCore import QWebEnginePage @FULLSCREEN_KEYMAP.define_key("q") @FULLSCREEN_KEYMAP.define_key("C-g") @FULLSCREEN_KEYMAP.define_key("Esc") def exit_full_screen(ctx): fw = current_window().fullscreen_window if fw: fw.internal_view.triggerPageAction( QWebEnginePage.WebAction.ExitFullScreen) ================================================ FILE: webmacs/keymaps/global.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from ..keymaps import GLOBAL_KEYMAP KEYMAP = GLOBAL_KEYMAP KEYMAP.define_key("C-x C-c", "quit") KEYMAP.define_key("M-x", "M-x") KEYMAP.define_key("C-x C-f", "go-to-new-buffer") KEYMAP.define_key("C-x b", "switch-recent-buffer") KEYMAP.define_key("C-x C-b", "switch-recent-buffer") KEYMAP.define_key("C-x o", "other-view") KEYMAP.define_key("C-x 3", "split-view-right") KEYMAP.define_key("C-x 2", "split-view-bottom") KEYMAP.define_key("C-x 0", "close-view") KEYMAP.define_key("C-x 1", "maximise-view") KEYMAP.define_key("C-x k", "close-buffer") KEYMAP.define_key("C-x r", "revive-buffer") KEYMAP.define_key("C-h v", "describe-variable") KEYMAP.define_key("C-h f", "describe-command") KEYMAP.define_key("C-h c", "describe-key-briefly") KEYMAP.define_key("C-h k", "describe-key") KEYMAP.define_key("C-h w", "where-is") KEYMAP.define_key("M-n", "next-buffer") KEYMAP.define_key("M-p", "previous-buffer") ================================================ FILE: webmacs/keymaps/hints.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import HINT_KEYMAP as KEYMAP KEYMAP.define_key("C-g", "hint-abort") KEYMAP.define_key("Esc", "hint-abort") KEYMAP.define_key("C-n", "hint-next") KEYMAP.define_key("Down", "hint-next") KEYMAP.define_key("C-p", "hint-prev") KEYMAP.define_key("Up", "hint-prev") ================================================ FILE: webmacs/keymaps/isearch.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import ISEARCH_KEYMAP as KEYMAP KEYMAP.define_key("C-n", "i-search-next") KEYMAP.define_key("C-s", "i-search-next") KEYMAP.define_key("C-p", "i-search-prev") KEYMAP.define_key("C-r", "i-search-prev") KEYMAP.define_key("Return", "i-search-validate") KEYMAP.define_key("C-g", "i-search-abort") KEYMAP.define_key("Esc", "i-search-abort") ================================================ FILE: webmacs/keymaps/minibuffer.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import MINIBUFFER_KEYMAP as KEYMAP, VISITEDLINKS_KEYMAP, \ BOOKMARKS_KEYMAP, BUFFERLIST_KEYMAP, WEBJUMP_KEYMAP KEYMAP.define_key("Tab", "minibuffer-select-complete") KEYMAP.define_key("C-n", "minibuffer-select-next") KEYMAP.define_key("Down", "minibuffer-select-next") KEYMAP.define_key("C-p", "minibuffer-select-prev") KEYMAP.define_key("Up", "minibuffer-select-prev") KEYMAP.define_key("M-<", "minibuffer-select-first") KEYMAP.define_key("M->", "minibuffer-select-last") KEYMAP.define_key("C-v", "minibuffer-select-next-page") KEYMAP.define_key("M-v", "minibuffer-select-prev-page") KEYMAP.define_key("M-n", "minibuffer-history-next") KEYMAP.define_key("M-p", "minibuffer-history-prev") KEYMAP.define_key("Return", "minibuffer-validate") KEYMAP.define_key("C-g", "minibuffer-abort") KEYMAP.define_key("Esc", "minibuffer-abort") KEYMAP.define_key("M-Backspace", "minibuffer-delete-backward-word") KEYMAP.define_key("C-Space", "minibuffer-mark") KEYMAP.define_key("C-x h", "minibuffer-select-all") KEYMAP.define_key("C-f", "minibuffer-forward-char") KEYMAP.define_key("Right", "minibuffer-forward-char") KEYMAP.define_key("C-b", "minibuffer-backward-char") KEYMAP.define_key("Left", "minibuffer-backward-char") KEYMAP.define_key("M-f", "minibuffer-forward-word") KEYMAP.define_key("M-Right", "minibuffer-forward-word") KEYMAP.define_key("M-b", "minibuffer-backward-word") KEYMAP.define_key("M-Left", "minibuffer-backward-word") KEYMAP.define_key("M-w", "minibuffer-copy") KEYMAP.define_key("C-w", "minibuffer-cut") KEYMAP.define_key("C-y", "minibuffer-paste") KEYMAP.define_key("C-d", "minibuffer-delete-forward-char") KEYMAP.define_key("M-d", "minibuffer-delete-forward-word") KEYMAP.define_key("C-a", "minibuffer-beginning-of-line") KEYMAP.define_key("C-e", "minibuffer-end-of-line") KEYMAP.define_key("C-/", "minibuffer-undo") KEYMAP.define_key("C-?", "minibuffer-redo") VISITEDLINKS_KEYMAP.define_key("C-k", "visited-links-delete-highlighted") BOOKMARKS_KEYMAP.define_key("C-k", "bookmarks-delete-highlighted") BUFFERLIST_KEYMAP.define_key("C-k", "buffer-list-delete-highlighted") WEBJUMP_KEYMAP.define_key("Tab", "webjump-complete") ================================================ FILE: webmacs/keymaps/webbuffer.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import BUFFER_KEYMAP as KEYMAP KEYMAP.define_key("g", "go-to") KEYMAP.define_key("s", "search-default") KEYMAP.define_key("G", "go-to-alternate-url") KEYMAP.define_key("b", "buffer-history") KEYMAP.define_key("F", "go-forward") KEYMAP.define_key("B", "go-backward") KEYMAP.define_key("C-s", "i-search-forward") KEYMAP.define_key("C-r", "i-search-backward") KEYMAP.define_key("C-v", "scroll-page-down") KEYMAP.define_key("M-v", "scroll-page-up") KEYMAP.define_key("M->", "scroll-bottom") KEYMAP.define_key("M-<", "scroll-top") KEYMAP.define_key("f", "follow") KEYMAP.define_key("c l", "copy-link") KEYMAP.define_key("c c", "copy-current-link") KEYMAP.define_key("c t", "copy-current-buffer-title") KEYMAP.define_key("c u", "copy-current-buffer-url") KEYMAP.define_key("M-w", "webcontent-copy") KEYMAP.define_key("r", "reload-buffer") KEYMAP.define_key("R", "reload-buffer-no-cache") KEYMAP.define_key("h", "visited-links-history") KEYMAP.define_key("q", "close-buffer") KEYMAP.define_key("C-x h", "select-buffer-content") KEYMAP.define_key("C", "caret-browsing-init") KEYMAP.define_key("m", "bookmark-open") KEYMAP.define_key("M", "bookmark-add") KEYMAP.define_key("C-+", "text-zoom-in") KEYMAP.define_key("C--", "text-zoom-out") KEYMAP.define_key("C-=", "text-zoom-reset") KEYMAP.define_key("+", "zoom-in") KEYMAP.define_key("-", "zoom-out") KEYMAP.define_key("=", "zoom-normal") KEYMAP.define_key("C-n", "send-key-down") KEYMAP.define_key("n", "send-key-down") KEYMAP.define_key("C-p", "send-key-up") KEYMAP.define_key("p", "send-key-up") KEYMAP.define_key("P", "password-manager-fill-buffer") KEYMAP.define_key("C-f", "send-key-right") KEYMAP.define_key("C-b", "send-key-left") KEYMAP.define_key("C-g", "buffer-escape") KEYMAP.define_key("C-x p", "print-buffer") ================================================ FILE: webmacs/killed_buffers.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import collections from PyQt6.QtCore import QDataStream, QByteArray, QIODevice from .webbuffer import create_buffer from . import variables, hooks max_size = variables.define_variable( "revive-buffers-limit", "The maximum number of killed buffers that can be revived." " If set to a -1, there is no limit. Default to 10.", 10, type=variables.Int(min=-1), callbacks=( lambda v: KilledBuffer.update_max_size(v.value) ), ) class KilledBuffer(object): all = collections.deque(maxlen=max_size.value) @classmethod def update_max_size(cls, nb): new_all = collections.deque(maxlen=nb if nb >= 0 else None) for item in reversed(cls.all): new_all.appendleft(item) cls.all = new_all def __init__(self, url, title, icon, history_data, delayed): self.url = url self.title = title self.icon = icon self.history_data = history_data self.delayed = delayed self.all.appendleft(self) @classmethod def from_buffer(cls, buff): data = QByteArray() stream = QDataStream(data, QIODevice.OpenModeFlag.WriteOnly) stream << buff.history() return cls( buff.url(), buff.title(), buff.icon(), data, buff.delayed_loading_url() ) def revive(self): buff = create_buffer() stream = QDataStream(self.history_data, QIODevice.OpenModeFlag.ReadOnly) stream >> buff.history() self.all.remove(self) if self.delayed: buff.load(self.delayed.url) return buff hooks.webbuffer_closed.add(KilledBuffer.from_buffer) ================================================ FILE: webmacs/main.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import argparse import signal import socket import logging import sys import atexit import os import warnings from PyQt6.QtNetwork import QAbstractSocket from .ipc import IpcServer from . import variables, filter_webengine_output log_to_disk = variables.define_variable( "log-to-disk-max-files", "Maximum number of log files to keep. Log files are stored in" " ~/.webmacs/logs. Setting this to 0 will deactivate file logging" " completely.", 0, type=variables.Int(min=0), ) def signal_wakeup(app): """ Allow to be notified in Python for signals when in long-running calls from the C or c++ side, like QApplication.exec(). See https://stackoverflow.com/a/37229299. """ sock = QAbstractSocket(QAbstractSocket.SocketType.UdpSocket, app) # Create a socket pair sock.wsock, sock.rsock = socket.socketpair(type=socket.SOCK_DGRAM) # Let Qt listen on the one end sock.setSocketDescriptor(sock.rsock.fileno()) # And let Python write on the other end sock.wsock.setblocking(False) signal.set_wakeup_fd(sock.wsock.fileno()) # add a dummy callback just to be on the python side as soon as possible. sock.readyRead.connect(lambda: None) def setup_logging(level, webcontent_level): root = logging.getLogger() webcontent = logging.getLogger("webcontent") for logger, format, lvl in ( (root, "%(levelname)s: %(message)s", level), (webcontent, "%(levelname)s %(name)s: [%(url)s] %(message)s", webcontent_level)): logger.setLevel(logging.DEBUG) handler = logging.StreamHandler() fmt = logging.Formatter(format) handler.setFormatter(fmt) handler.setLevel(lvl) logger.addHandler(handler) webcontent.propagate = False warnings.filterwarnings('always', r"^.*$", DeprecationWarning, r"^webmacs.*$") def setup_logging_on_disk(log_dir, backup_count=5): from logging.handlers import RotatingFileHandler root = logging.getLogger() webcontent = logging.getLogger("webcontent") class Formatter(logging.Formatter): def formatMessage(self, record): fmt = ("%(levelname)s %(name)s: [%(url)s] %(message)s" if record.name == "webcontent" else "%(levelname)s: %(message)s") return fmt % record.__dict__ if not os.path.isdir(log_dir): os.makedirs(log_dir) handler = RotatingFileHandler(os.path.join(log_dir, "log"), backupCount=backup_count, delay=True) handler.setFormatter(Formatter()) handler.doRollover() handler.setLevel(logging.DEBUG) for logger in (root, webcontent): logger.addHandler(handler) def parse_args(argv=None): parser = argparse.ArgumentParser() parser.add_argument("-l", "--log-level", help="Set the log level, defaults to %(default)s.", default="warning", choices=("debug", "info", "warning", "error", "critical")) # There is no such JavaScript error level, critical - still since there # are some logs that are printed anyway and that it is easier to implement. # Let's keep the critical level. parser.add_argument("-w", "--webcontent-log-level", help="Set the log level for the web contents," " defaults to %(default)s.", default="critical", choices=("info", "warning", "error", "critical")) parser.add_argument("-i", "--instance", default="default", help="Create or reuse a named webmacs instance." " If the given instance name is the empty string, an" " automatically generated name will be used.") parser.add_argument("-p", "--profile", default="default", help="Use the named profile directory." " Each profile will contain distinct navigation data" " (history, cookies, ...).") parser.add_argument("--list-instances", action="store_true", help="List running instances and exit.") parser.add_argument("--off-the-record", action="store_true", help="Private browsing mode.") parser.add_argument("url", nargs="?", help="url to open") opts = parser.parse_args(argv) # handle local file path if opts.url and os.path.exists(opts.url) \ and not os.path.isabs(opts.url): opts.url = os.path.realpath(opts.url) return opts def init(opts): """ Default initialization of webmacs. If a URL is given on the command line, this method opens it. Else, it tries to load the buffers that were opened the last time webmacs has exited. If none of that works, the default is to open a buffer with an url to the duckduck go search engine. Also open the view maximized. :param opts: the result of the parsed command line. """ from .application import app from .session import session_load, session_save from .window import Window from .webbuffer import create_buffer a = app() a.aboutToQuit.connect(lambda: session_save(a.profile.session_file)) def create_window(url): w = Window() buff = create_buffer(url) w.current_webview().setBuffer(buff) w.showMaximized() if opts.url: create_window(opts.url) return home_page = variables.get("home-page") session_file = a.profile.session_file if home_page: create_window(home_page) return if session_file and os.path.exists(session_file): try: session_load(session_file) return except Exception: logging.exception("Unable to load session from '%s'", session_file) create_window("about:blank") def _handle_user_init_error(conf_path, msg): import traceback stack_size = 0 tbs = traceback.extract_tb(sys.exc_info()[2]) for i, t in enumerate(tbs): if t[0].startswith(conf_path): stack_size = -len(tbs[i:]) break logging.critical(("%s\n\n" % msg) + traceback.format_exc(stack_size)) sys.exit(1) if sys.version_info >= (3, 5): import importlib.machinery import importlib.util def load_user_module(conf_path): spec = importlib.machinery.PathFinder.find_spec("init", [conf_path]) if spec is None: return None user_init = importlib.util.module_from_spec(spec) sys.modules["init"] = user_init spec.loader.exec_module(user_init) return user_init else: import imp def load_user_module(conf_path): try: spec = imp.find_module("init", [conf_path]) except ImportError: return None return imp.load_module("_webmacs_userconfig", *spec) def main(): opts = parse_args() if opts.list_instances: for instance in IpcServer.list_all_instances(): print(instance) sys.exit(0) elif not opts.instance: # pick a random instance name. uniq = [int(n) for n in IpcServer.list_all_instances(check=False) if n.isdigit()] opts.instance = str(max(uniq) + 1) if uniq else "1" conf_path = os.path.join(os.path.expanduser("~"), ".webmacs") if not os.path.isdir(conf_path): os.makedirs(conf_path) out_filter = filter_webengine_output.make_filter() setup_logging(getattr(logging, opts.log_level.upper()), getattr(logging, opts.webcontent_log_level.upper())) conn = IpcServer.check_server_connection(opts.instance) if conn: conn.send_data(opts.__dict__) data = conn.get_data() conn.sock.close() msg = data.get("message") if msg: print(msg) return # Delay loading after command line parsing and ipc checking. # Loading qwebengine stuff takes a couple of seconds... from .application import Application, _app_requires _app_requires() # load a user init module if any try: user_init = load_user_module(conf_path) except Exception: _handle_user_init_error( conf_path, "Error reading the user configuration." ) os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = os.path.join(conf_path, "spell_checking") app = Application(conf_path, [ # The first argument passed to the QApplication args defines # the x11 property WM_CLASS. "webmacs" if opts.instance == "default" else "webmacs-%s" % opts.instance ], instance_name=opts.instance, profile_name=opts.profile, off_the_record=opts.off_the_record) server = IpcServer(opts.instance) atexit.register(server.cleanup) out_filter.enable() # execute the user init function if there is one if user_init is None or not hasattr(user_init, "init"): init(opts) else: try: user_init.init(opts) except Exception: _handle_user_init_error( conf_path, "Error executing user init function in %s." % user_init.__file__ ) if log_to_disk.value > 0 and not opts.off_the_record: setup_logging_on_disk(os.path.join(conf_path, "logs"), backup_count=log_to_disk.value) app.post_init() signal_wakeup(app) signal.signal(signal.SIGINT, lambda s, h: app.quit()) sys.exit(app.exec()) if __name__ == '__main__': main() ================================================ FILE: webmacs/minibuffer/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from PyQt6.QtWidgets import QWidget, QLineEdit, QHBoxLayout, QLabel, \ QTableView, QHeaderView, QApplication, QSizePolicy, QFrame from PyQt6.QtGui import QPainter from PyQt6.QtCore import pyqtSignal as Signal, \ QEvent, QSortFilterProxyModel, QRegularExpression, Qt, QModelIndex, \ pyqtProperty from ..keymaps import MINIBUFFER_KEYMAP as KEYMAP from .prompt import Prompt from .. import variables from .. import windows from ..keyboardhandler import LOCAL_KEYMAP_SETTER class Popup(QTableView): def __init__(self, window, buffer_input): QTableView.__init__(self, window) # do not diplay more than one line in a cell, and elide text on middle # (best for urls) self.setWordWrap(False) self.setTextElideMode(Qt.TextElideMode.ElideMiddle) self.setVisible(False) self.setFrameStyle(QFrame.Shape.Box) self._window = window self._buffer_input = buffer_input window.installEventFilter(self) self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.horizontalHeader().hide() self.verticalHeader().hide() self.verticalHeader().setSectionResizeMode( QHeaderView.ResizeMode.Fixed) self.verticalHeader().setDefaultSectionSize(24) self.setHorizontalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setEditTriggers(QTableView.EditTrigger.NoEditTriggers) self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) self.setSelectionMode(QTableView.SelectionMode.SingleSelection) self.setShowGrid(False) self._max_visible_items = 10 def _resize(self, size): # size is calculated given the window and the minibuffer input # geometries h = (24) * min(self._max_visible_items, self.model().rowCount()) + ( 2 * self.lineWidth()) w = size.width() y = size.height() - h - self._buffer_input.height() self.setGeometry(0, y, w, h) # Split the columns width, nicer when we have at least two of them. cols = self.model().columnCount() if cols > 0: col_width = round(w / cols) for i in range(cols): self.setColumnWidth(i, col_width) def popup(self): self._resize(self._window.size()) if not self.isVisible(): self.show() def eventFilter(self, obj, event): # resize the popup when the window is resized if obj == self._window and event.type() == QEvent.Type.Resize: self._resize(event.size()) return False class MinibufferInput(QLineEdit): completion_activated = Signal(QModelIndex) FuzzyMatch = Prompt.FuzzyMatch SimpleMatch = Prompt.SimpleMatch def __init__(self, parent, window): QLineEdit.__init__(self, parent) self._completer_model = None self._popup = Popup(window, self) self.textEdited.connect(self._show_completions) self._popup.installEventFilter(self) self.installEventFilter(self) self._eat_focusout = False self._proxy_model = QSortFilterProxyModel(self) self._proxy_model.setFilterKeyColumn(-1) self._popup.setModel(self._proxy_model) self._popup.activated.connect(self._on_completion_activated) self._popup.selectionModel().currentRowChanged.connect( self._on_row_changed) self._right_italic_text = "" self._mark = False self.configure_completer({}) def configure_completer(self, opts): self._popup._max_visible_items = opts.get("max-visible-items", 10) self._match = opts.get("match", self.SimpleMatch) self._autocomplete_single = opts.get("autocomplete-single", True) self._autocomplete = opts.get("autocomplete", False) if self._autocomplete: self._autocomplete_single = False self._complete_empty = opts.get("complete-empty", False) def keymap(self): prompt = self.parent()._prompt if prompt and prompt.keymap: return prompt.keymap return KEYMAP def eventFilter(self, obj, event): etype = event.type() if etype == QEvent.Type.FocusOut and obj == self \ and self._eat_focusout and self._popup.isVisible(): # keep the focus on the line edit return True elif etype == QEvent.Type.MouseButtonPress: # if we've clicked in the widget (or its descendant), let it handle # the click pos = obj.mapToGlobal(event.pos()) target = QApplication.widgetAt(pos) if target and (self.isAncestorOf(target) or target == self): if not self._popup.underMouse(): self._popup.hide() target.event(event) return True if not self._popup.underMouse(): self._popup.hide() return True elif etype in (QEvent.Type.KeyPress, QEvent.Type.KeyRelease): # send event to the line edit self._eat_focusout = True self.event(event) self._eat_focusout = False return True return QLineEdit.eventFilter(self, obj, event) def event(self, evt): t = evt.type() if t == QEvent.Type.Show: LOCAL_KEYMAP_SETTER.minibuffer_input_focus_changed(self, True) elif t == QEvent.Type.Hide: LOCAL_KEYMAP_SETTER.minibuffer_input_focus_changed(self, False) return QLineEdit.event(self, evt) def set_completer_model(self, completer_model): self._proxy_model.setSourceModel(completer_model) def completer_model(self): return self._proxy_model.sourceModel() def set_match(self, type): self._match = type if self._popup.isVisible(): self._show_completions(self.text()) def _on_row_changed(self, current, old): if self._autocomplete: self.complete(hide_popup=False) def _show_completions(self, txt, force=False): force = force or self._complete_empty if self._match is not None: if self._match == self.SimpleMatch: pattern = "^" + QRegularExpression.escape(txt) elif self._match == self.FuzzyMatch: pattern = ".*".join(QRegularExpression.escape(t) for t in txt.split()) self._proxy_model.setFilterRegularExpression(QRegularExpression( pattern, QRegularExpression.PatternOption.CaseInsensitiveOption )) else: self._proxy_model.setFilterRegularExpression(None) if self._proxy_model.rowCount() == 0: self._popup.hide() elif not txt and not force: self._popup.hide() else: self._popup.popup() def show_completions(self, filter_text=None): self._show_completions( filter_text if filter_text is not None else self.text(), True) def _on_completion_activated(self, index, hide_popup=True): if hide_popup: self._popup.hide() model = index.model() if index.column() != 0: index = model.index(index.row(), 0) self.setText(model.data(index)) self.completion_activated.emit(model.mapToSource(index)) def popup(self): return self._popup def complete(self, hide_popup=True): if not self._popup.isVisible(): return index = self._popup.selectionModel().currentIndex() if index.isValid(): self._on_completion_activated(index, hide_popup=hide_popup) elif self._autocomplete_single and self._proxy_model.rowCount() == 1: self._on_completion_activated(self._proxy_model.index(0, 0), hide_popup=hide_popup) def select_next_completion(self, forward=True, steps=1): model = self._proxy_model entries = model.rowCount() if entries == 0: return selection = self._popup.selectionModel().currentIndex() if not selection.isValid(): row = 0 if forward else (entries - 1) else: row = selection.row() if forward: row = row + steps if row >= entries: row = 0 else: row = row - steps if row < 0: row = (entries - 1) self._popup.selectRow(row) def select_first_completion(self): self._popup.selectRow(0) def select_last_completion(self): entries = self._proxy_model.rowCount() self._popup.selectRow(entries - 1) def select_next_page_completion(self, forward=True): self.select_next_completion(forward=forward, steps=self._popup._max_visible_items - 1) def mark(self): return self._mark def set_mark(self, value=None): if value is None: value = not self._mark self._mark = value return self._mark def reinit(self): self.setText("") self.setEchoMode(self.EchoMode.Normal) self.setValidator(None) self._right_italic_text = "" def set_right_italic_text(self, text): self._right_italic_text = text self.update() def paintEvent(self, event): QLineEdit.paintEvent(self, event) if not self._right_italic_text: return painter = QPainter(self) font = painter.font() font.setItalic(True) painter.setFont(font) painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) painter.drawText(self.rect().adjusted(0, 0, -10, 0), Qt.AlignmentFlag.AlignRight, self._right_italic_text) @pyqtProperty("QColor") def background_color(self): return self.palette().color(self.backgroundRole()) @background_color.setter def background_color(self, color): palette = self.palette() palette.setColor(self.backgroundRole(), color) self.setPalette(palette) def _update_minibuffer_height(var): for window in windows(): window.minibuffer().set_height(var.value) MINIBUFFER_HEIGHT = variables.define_variable( "minibuffer-height", "The height in pixel of the minibuffer.", 25, type=variables.Int(min=1), callbacks=(_update_minibuffer_height,) ) class Minibuffer(QWidget): def __init__(self, window): QWidget.__init__(self, window) layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.label = QLabel(self) self.rlabel = QLabel(self) self.__default_label_policy = self.label.sizePolicy() # when input line edit is hidden, this size policy allow to not resize # the parent widget if the text in the label is too long. self.label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed) layout.addWidget(self.label) self._input = MinibufferInput(self, window) layout.addWidget(self._input) self.rlabel.setAlignment( Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) layout.addWidget(self.rlabel) self.set_height(MINIBUFFER_HEIGHT.value) self._input.installEventFilter(self) self._input.hide() self._prompt = None def set_height(self, height): self.label.setMinimumHeight(height) def eventFilter(self, obj, event): if obj == self._input: if event.type() == QEvent.Type.Hide: self.label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed) elif event.type() == QEvent.Type.Show: self.label.setSizePolicy(self.__default_label_policy) obj.setMaximumHeight(self.label.height()) return False def show_info(self, text): if self._input.isHidden(): self.label.setText(text) def input(self): return self._input def prompt(self): return self._prompt def do_prompt(self, prompt, **kwargs): self.close_prompt() self._prompt = prompt if prompt: prompt.closed.connect(self._prompt_closed) prompt.closed.connect(prompt.deleteLater) return prompt.exec(self, **kwargs) def close_prompt(self): if self._prompt: self._prompt.close() self._prompt = None def _prompt_closed(self): self._prompt = None ================================================ FILE: webmacs/minibuffer/prompt.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import itertools import collections from PyQt6.QtCore import QObject, QAbstractTableModel, QModelIndex, Qt, \ pyqtSlot as Slot, pyqtSignal as Signal, QEventLoop, QPropertyAnimation, \ QEvent, QRegularExpression from PyQt6.QtGui import QColor, QRegularExpressionValidator from ..keyboardhandler import set_global_keymap_enabled from ..keymaps import Keymap from .. import variables FLASH_DURATION = variables.define_variable( "minibuffer-flash-duration", "Total duration in seconds of the minibuffer flash animation.", 0.3, type=variables.Float(min=0.0), ) FLASH_COLOR = variables.define_variable( "minibuffer-flash-color", "Color for the minibuffer flash animation. Should be given as" " an hexadecimal string.", "#ff0000", type=variables.String(), ) FLASH_COUNT = variables.define_variable( "minibuffer-flash-count", "How many flashes should be displayed during the minibuffer" " flash animation.", 2, type=variables.Int(min=0), ) class FSModel(QAbstractTableModel): """ A custom filesystemmodel that does work with the custom completer; May not be as efficient as the qt version, but works without much pain. """ def __init__(self, parent=None): QAbstractTableModel.__init__(self, parent) self._root_dir = "" self._files = [] def rowCount(self, index=QModelIndex()): return len(self._files) def columnCount(self, index=QModelIndex()): return 1 def data(self, index, role=Qt.ItemDataRole.DisplayRole): if role != Qt.ItemDataRole.DisplayRole: return None try: return os.path.join(self._root_dir, self._files[index.row()]) except IndexError: return None @Slot(str) def text_changed(self, text): if text.endswith("/"): root_dir = text else: root_dir = os.path.dirname(text) if root_dir != self._root_dir: try: files = os.listdir(root_dir) except OSError: return self.beginResetModel() self._files = files self._root_dir = root_dir self.endResetModel() class PromptTableModel(QAbstractTableModel): def __init__(self, data, parent=None): QAbstractTableModel.__init__(self, parent) self._data = data def rowCount(self, index=QModelIndex()): return len(self._data) def columnCount(self, index=QModelIndex()): if self._data: return len(self._data[0]) return 0 def data(self, index, role=Qt.ItemDataRole.DisplayRole): if role != Qt.ItemDataRole.DisplayRole: return None return index.internalPointer() def index(self, row, col, parent=QModelIndex()): try: return self.createIndex(row, col, self._data[row][col]) except IndexError: return QModelIndex() def _prompt_exec(prompt, loop): # mocked in tests to not block. loop.exec() class Prompt(QObject): label = "" complete_options = {} keymap = None history = None value_return_index_data = False SimpleMatch = 0 FuzzyMatch = 1 finished = Signal() closed = Signal() def __init__(self, ctx): QObject.__init__(self) self.ctx = ctx self.__finished = False def completer_model(self): return None def enable(self, minibuffer): self.__flash = None self.__index = QModelIndex() self.minibuffer = minibuffer minibuffer.label.setText(self.label) buffer_input = minibuffer.input() buffer_input.reinit() buffer_input.show() buffer_input.setFocus() # keeping valid references of qobjects is really hard sometimes # without this completer reference on self, visited_links_history # will (quite) randomly generate segfault... self.__completer_model = completer_model = self.completer_model() if hasattr(completer_model, "text_changed"): buffer_input.textEdited.connect(completer_model.text_changed) buffer_input.set_completer_model(completer_model) buffer_input.returnPressed.connect(self._on_edition_finished) buffer_input.completion_activated.connect( self._on_completion_activated) buffer_input.configure_completer(self.complete_options) if self.complete_options.get("complete-empty"): buffer_input.show_completions() def close(self): minibuffer = self.minibuffer minibuffer.label.setText("") buffer_input = minibuffer.input() buffer_input.returnPressed.disconnect(self._on_edition_finished) buffer_input.completion_activated.disconnect( self._on_completion_activated) view = minibuffer.parent().current_webview() # calling setFocus() on the view is required, else the view is scrolled # to the top automatically. But we don't even get a focus in event; view.internal_view().setFocus() # and to not lose the keyboard focus view.show_focused(True) buffer_input.hide() buffer_input.set_mark(False) c_model = buffer_input.completer_model() buffer_input.set_completer_model(None) if c_model: c_model.deleteLater() if self.history: self.history.reset() if self.__flash: self.__flash.stop() self.__flash.deleteLater() self.closed.emit() def flash(self): if self.__flash is None: self.__flash = self._create_flash_animation() if self.__flash: self.__flash.start() elif self.__flash.state() == self.__flash.State.Stopped: self.__flash.start() def _create_flash_animation(self): if FLASH_COUNT.value <= 0 or \ FLASH_DURATION.value <= 0: return None minibuff_input = self.minibuffer.input() anim = QPropertyAnimation(minibuff_input, b"background_color") base = minibuff_input.property(b"background_color") flash_color = QColor(FLASH_COLOR.value) anim.setDuration(int(FLASH_DURATION.value * 1000)) step = 1./(FLASH_COUNT.value * 2) pos = step colors = itertools.cycle((flash_color, base)) anim.setStartValue(base) while pos < 1: anim.setKeyValueAt(pos, next(colors)) pos += step anim.setEndValue(base) return anim def _on_completion_activated(self, index): self.__index = index def value(self): if not self.__finished: return None if self.value_return_index_data: index = self.index() if index: return index.internalPointer() else: return self.minibuffer.input().text() def index(self): return self.__index @Slot() def _on_edition_finished(self): history = self.history if history: history.push(self.minibuffer.input().text()) self.close() self.__finished = True self.finished.emit() def exec(self, minibuffer, flash=False, sync=True): self.enable(minibuffer) if flash: self.flash() if sync: loop = QEventLoop() self.closed.connect(loop.quit) _prompt_exec(self, loop) return self.value() class PromptHistory(object): """ In memory history for prompts. """ def __init__(self, maxsize=50): self._history = collections.deque((), maxlen=maxsize) self.reset() def reset(self): self._in_user_value = True self._user_value = "" self._cursor = 0 def push(self, text): # avoid following duplicates if self._history and self._history[0] == text: return self._history.append(text) def in_user_value(self): """ indicate if we are in the state where the user see its custom value """ return self._in_user_value def set_user_value(self, text): self._user_value = text def __get(self, delta): # delta must be 1 or -1 size = len(self._history) if size == 0: return self._user_value if self._in_user_value: self._in_user_value = False self._cursor = 0 if delta > 0 else size - 1 else: cursor = self._cursor + delta if cursor >= size or cursor < 0: self._in_user_value = True return self._user_value self._cursor = cursor return self._history[self._cursor] def get_next(self): return self.__get(1) def get_previous(self): return self.__get(-1) class YesNoPrompt(Prompt): NO = 0 YES = 1 ALWAYS = 2 NEVER = 3 keymap = Keymap("yes-no") # an empty keymap def __init__(self, label, parent=None, always=False, never=False): Prompt.__init__(self, parent) self.never = never self.always = always self.label = label + self.build_label() self.valid_keys = self.build_valid_keys() self._value = 0 def build_label(self): optional = "" if self.always: optional += "/Always" if self.never: optional += "/Never" return "[yes/no{}]".format(optional) def build_valid_keys(self): optional = "" if self.always: optional += "A" if self.never: optional += "N" return "yYn{}".format(optional) def enable(self, minibuffer): set_global_keymap_enabled(False) # disable any global keychord Prompt.enable(self, minibuffer) buffer_input = minibuffer.input() validator = QRegularExpressionValidator(QRegularExpression( "[" + self.valid_keys + "]"), buffer_input) buffer_input.setValidator(validator) minibuffer.input().installEventFilter(self) buffer_input.textEdited.connect(self._on_text_edited) def _on_text_edited(self, text): if text in ("y", "Y"): self._value = self.YES elif text == "N": self._value = self.NEVER elif text == "A": self._value = self.ALWAYS else: self._value = self.NO self.close() set_global_keymap_enabled(True) def value(self): return self._value def eventFilter(self, obj, evt): if evt.type() in (QEvent.Type.KeyPress, QEvent.Type.KeyRelease, QEvent.Type.ShortcutOverride): if evt.text() in self.valid_keys: return False evt.accept() self.flash() return True return False def close(self): self.minibuffer.input().removeEventFilter(self) set_global_keymap_enabled(True) Prompt.close(self) class AskPasswordPrompt(Prompt): def __init__(self, buffer): Prompt.__init__(self, None) self.buffer = buffer self.username, self.password = "", "" self.label = "username: " set_global_keymap_enabled(False) def _on_edition_finished(self): input = self.minibuffer.input() if not self.username: self.username = input.text() input.clear() input.setEchoMode(input.EchoMode.Password) self.minibuffer.label.setText("password: ") else: self.password = input.text() Prompt._on_edition_finished(self) set_global_keymap_enabled(True) ================================================ FILE: webmacs/minibuffer/right_label.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from .. import hooks, variables, keyboardhandler from .. import windows, BUFFERS, call_later MINIBUFFER_RIGHTLABEL = variables.define_variable( "minibuffer-right-label", "Format for displaying some information in right label of minibuffer.", "{loading}{mode}: {local_keymap} [{buffer_current}/{buffer_count}]", type=variables.String(), ) REQUEST_WINDOW_UPDATE = {} def update_minibuffer_right_label(window): # only really update the minibuffer once in a qt loop cycle. if window in REQUEST_WINDOW_UPDATE: return call_later(lambda: _update_minibuffer_right_label(window)) REQUEST_WINDOW_UPDATE[window] = True def _update_minibuffer_right_label(window): from ..application import app try: # KeyError if the window was closed, in such case no update is # required. del REQUEST_WINDOW_UPDATE[window] except KeyError: return buff = window.current_webview().buffer() try: loading_p = BUFF_PROGRESS[buff] except KeyError: loading = "" else: loading = " loading: %d%% " % loading_p label = window.minibuffer().rlabel if not label.text(): # first time we update the label if app().profile.is_off_the_record(): label.setStyleSheet("QLabel { background: orangered }") label.setText( MINIBUFFER_RIGHTLABEL.value.format( buffer_current=BUFFERS.index(buff) + 1, buffer_count=len(BUFFERS), local_keymap=keyboardhandler.local_keymap(), mode=getattr(buff, "mode", "unknown"), loading=loading, ) ) def update_minibuffer_right_labels(): for window in windows(): update_minibuffer_right_label(window) BUFF_PROGRESS = {} def update_label_for_buffer(buff): update_minibuffer_right_labels() def init_minibuffer_right_labels(): def register_buffer_load_progress(buff): # first update all labels because the buffer count changed update_minibuffer_right_labels() def load_changed(p): if p is None: del BUFF_PROGRESS[buff] else: BUFF_PROGRESS[buff] = p view = buff.view() if view and view.main_window.current_webview() == view: update_minibuffer_right_label(view.main_window) # then connect this buffer to keep track of its load percent buff.loadStarted.connect(lambda: load_changed(0)) buff.loadProgress.connect(load_changed) buff.loadFinished.connect(lambda: load_changed(None)) for buff in BUFFERS: register_buffer_load_progress(buff) hooks.webbuffer_created.add(register_buffer_load_progress) hooks.webbuffer_current_changed.add(update_label_for_buffer) def on_buffer_closed(buff): # first update all labels because the buffer count changed update_minibuffer_right_labels() BUFF_PROGRESS.pop(buff, None) hooks.local_mode_changed.add( lambda a: update_minibuffer_right_labels() ) hooks.webbuffer_closed.add(on_buffer_closed) # following hook should not be useful, but this ensure we don't keep # window references hooks.window_closed.add( lambda w: REQUEST_WINDOW_UPDATE.pop(w, None) ) update_minibuffer_right_labels() ================================================ FILE: webmacs/mode.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import re from .keymaps import BUFFER_KEYMAP, CONTENT_EDIT_KEYMAP, \ CARET_BROWSING_KEYMAP, EMPTY_KEYMAP, FULLSCREEN_KEYMAP from . import variables MODES = {} class Mode(object): KEYMAP_NORMAL = 1 KEYMAP_CONTENT_EDIT = 2 KEYMAP_CARET_BROWSING = 3 KEYMAP_FULLSCREEN = 4 def __init__(self, name, description): self.name = name self.description = description self._mode_to_km = { self.KEYMAP_NORMAL: self.keymap, self.KEYMAP_CONTENT_EDIT: self.content_edit_keymap, self.KEYMAP_CARET_BROWSING: self.caret_browsing_keymap, self.KEYMAP_FULLSCREEN: self.fullscreen_keymap, } def keymap(self): return BUFFER_KEYMAP def content_edit_keymap(self): return CONTENT_EDIT_KEYMAP def caret_browsing_keymap(self): return CARET_BROWSING_KEYMAP def keymap_for_mode(self, mode): return self._mode_to_km[mode]() def fullscreen_keymap(self): return FULLSCREEN_KEYMAP def __str__(self): return self.name def get_mode(name): return MODES[name] def define_mode(mode): assert mode.name not in MODES MODES[mode.name] = mode define_mode(Mode("standard-mode", "standard navigation mode")) class EmptyMode(Mode): def keymap(self): return EMPTY_KEYMAP content_edit_keymap = keymap caret_browsing_keymap = keymap fullscreen_keymap = keymap define_mode(EmptyMode("no-keybindings", "no-keybindings navigation mode")) AUTO_MODES = [] def get_auto_modename_for_url(url, default="standard-mode"): for reg, mode in AUTO_MODES: if reg.match(url): return mode return default def _set_auto_buffer_modes(modes): global AUTO_MODES AUTO_MODES = [(( re.compile(reg) if isinstance(reg, str) else reg), mode ) for reg, mode in modes.value] auto_buffer_modes = variables.define_variable( "auto-buffer-modes", "List of tuple of regexes and mode name to automatically associate" " web pages to some mode. If nothing matches the url, standard-mode is" " used.", (), type=variables.List( variables.Tuple(variables.String(), variables.String(choices=MODES)) ), callbacks=( _set_auto_buffer_modes, ), ) ================================================ FILE: webmacs/password_manager/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from .. import require, variables import json from PyQt6.QtCore import QObject from PyQt6.QtWebEngineCore import QWebEngineScript from collections import namedtuple password_managers = { "none": lambda: BasePaswordManager(), "passwordstore": lambda: require("webmacs.password_manager.password_store").Pass() } password_manager = variables.define_variable( "password-manager", """Which password manager to use. - none: no password manager - passwordstore: the standard unix password manager. """, "none", type=variables.String(choices=password_managers.keys()), ) def make_password_manager(): return password_managers[password_manager.value]() Credentials = namedtuple( "Credentials", ("username", "password", "fields") ) class PasswordManagerNotReady(Exception): pass class BasePaswordManager(QObject): def __init__(self, parent=None): QObject.__init__(self, parent) def credential_for_url(self, url): "Get credentials for the given url." raise PasswordManagerNotReady("No password manager set.") def complete_buffer(self, buffer, credential): """Fill buffer with the given credentials""" dct = json.dumps(credential._asdict()) buffer.runJavaScript( f"password_manager.complete_form_data({dct})", QWebEngineScript.ScriptWorldId.ApplicationWorld) ================================================ FILE: webmacs/password_manager/password_store.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from . import BasePaswordManager, Credentials, PasswordManagerNotReady from ..task import Task from .. import variables from PyQt6.QtCore import QProcess import os import logging import glob pass_store_path = variables.define_variable( "password-manager-pass-store-path", "The path to store the pass passwords. Defaults to ~/.password-store.", os.path.expanduser("~/.password-store"), type=variables.String() ) class PassCredentials: """ Object to store the credentials read from the passwordstore utility. """ def __init__(self): self.__cred_by_name = {} self.__cred_by_url = {} self.__search_needs_compil = True self.__search_url_list = None def add_credential(self, name, url, credential): self.__cred_by_name[name] = credential if url: self.__cred_by_url[url] = credential self.__search_needs_compil = True def compile(self): if self.__search_needs_compil: def by_len_name(tup): return len(tup[0]) shortened_names = [(k.rsplit("/", 1)[-1], v) for k, v in self.__cred_by_name.items()] # order by reverse len of url to find best suitable match first self.__search_url_list = \ sorted(self.__cred_by_url.items(), key=by_len_name, reverse=True) + \ sorted(shortened_names, key=by_len_name, reverse=True) self.__search_needs_compil = False def for_url(self, url): """ Try to find the appropriate Credential for the given url, or None. """ self.compile() for url_part, credential in self.__search_url_list: if url.find(url_part) >= 0: return credential def names(self): """ Return the list of known passwordstore names (file names, without .gpg extension) """ return list(self.__cred_by_name.keys()) def for_name(self, name): """ Return the Credential associated to the given name. """ return self.__cred_by_name[name] class ReadCredentialsTask(Task): def __init__(self): Task.__init__(self) self.__names = None # one proc at a time, otherwise authentication might fail;; self.__proc = None self.__output = b"" self.__credentials = PassCredentials() def start(self): self.__names = [os.path.splitext(fname)[0] for fname in glob.glob("**/*.gpg", recursive=True, root_dir=pass_store_path.value)] self.__process_next() def __process_next(self): self.__output = b"" if not self.__names: self.finished.emit() return self.__name = name = self.__names.pop(0) self.__proc = QProcess() self.__proc.finished.connect(self.__process_finished) self.__proc.readyReadStandardOutput.connect(self.__process_read) logging.info(f"Running external command: pass show {name}") self.__proc.start("pass", ["show", name]) def __process_finished(self, code, status): if status == QProcess.ExitStatus.CrashExit: self.set_error_message("The pass process crashed.") self.finished.emit() elif code != 0: self.set_error_message(f"The pass process exited with {code}.") self.finished.emit() else: lines = self.__output.decode("utf-8").splitlines() passwd = lines[0].rstrip() fields = {} for line in lines[1:]: try: k, v = line.split(":", 1) except ValueError: pass else: fields[k.rstrip()] = v.strip() username = fields.pop("login", None) self.__credentials.add_credential(self.__name, fields.pop("url", None), Credentials(username, passwd, fields)) self.__process_next() def __process_read(self): self.__output += bytes(self.__proc.readAllStandardOutput()) def credentials(self): return self.__credentials def abort(self): if self.__proc: self.__proc.finished.disconnect(self.__process_finished) self.__proc.kill() self.__proc.waitForFinished(300) class Pass(BasePaswordManager): """ This is the public object that the rest of the application can use. By using reload(), it reads the passwordstore credentials (this takes time, a few seconds!) and then keep the data in memory. """ def __init__(self): BasePaswordManager.__init__(self) self.__creds = None self.__reloading = False self.reload() def __on_reloaded(self): self.__reloading = False task = self.sender() if not task.error(): self.__creds = task.credentials() def reload(self): from ..application import app as _app if not self.__reloading: self.__reloading = True app = _app() task = ReadCredentialsTask() task.finished.connect(self.__on_reloaded) app.task_runner.run(task) def credential_for_url(self, url): if self.__reloading: raise PasswordManagerNotReady( "passwordstore not ready - still reading configuration.") return self.__creds.for_url(url) ================================================ FILE: webmacs/profile.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEngineScript, \ QWebEngineSettings from PyQt6.QtCore import QFile, QTextStream from .scheme_handlers import all_schemes from .visited_links import VisitedLinks from .ignore_certificates import IgnoredCertificates from .bookmarks import Bookmarks from .features import Features from . import variables, version, require from .password_manager import make_password_manager from .variables import define_variable, Bool THIS_DIR = os.path.dirname(os.path.realpath(__file__)) enable_javascript = define_variable( "enable-javascript", "Enable the running of javascript programs. Default to True.", True, type=Bool(), ) enable_pdfviewer = define_variable( "enable-pdfviewer", "Specifies that PDF documents will be opened in the internal PDF viewer." " Default to False", False, type=Bool(), ) def make_dir(*parts): path = os.path.join(*parts) os.makedirs(path, exist_ok=True) return path class Profile(object): def __init__(self, name, off_the_record=False): self.name = name if off_the_record: self.q_profile = QWebEngineProfile() else: self.q_profile = QWebEngineProfile(name) self._scheme_handlers = {} # keep a python reference app = require(".application").app() self.q_profile.setUrlRequestInterceptor(app.url_interceptor()) for handler in all_schemes(): h = handler(app) self._scheme_handlers[handler.scheme] = h self.q_profile.installUrlSchemeHandler(handler.scheme, h) self.path = None self.session_file = None visited_links, ignored_certs, bookmarks, features = \ ":memory:", ":memory:", ":memory:", ":memory:" if not off_the_record: self.path = path = make_dir(app.profiles_path(), self.name) persistent_path = make_dir(path, "persistent") cache_path = make_dir(path, "cache") self.q_profile.setPersistentStoragePath(persistent_path) self.q_profile.setPersistentCookiesPolicy( QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies) self.q_profile.setHttpCacheType(QWebEngineProfile.HttpCacheType.DiskHttpCache) self.q_profile.setCachePath(cache_path) if app.instance_name == "default": session_fname = "session.json" else: session_fname = "session-{}.json".format(app.instance_name) self.session_file = os.path.join(path, session_fname) visited_links = os.path.join(path, "visitedlinks.db") ignored_certs = os.path.join(path, "ignoredcerts.db") bookmarks = os.path.join(path, "bookmarks.db") features = os.path.join(path, "features.db") self.visitedlinks = VisitedLinks(visited_links) self.ignored_certs = IgnoredCertificates(ignored_certs) self.bookmarks = Bookmarks(bookmarks) self.features = Features(features) self.q_profile.downloadRequested.connect( app.download_manager().download_requested ) self.password_manager = make_password_manager() self.update_spell_checking() def inject_js(filepath, ipoint=QWebEngineScript.InjectionPoint.DocumentCreation, iid=QWebEngineScript.ScriptWorldId.ApplicationWorld, sub_frames=False, script_transform=None): f = QFile(filepath) assert f.open( QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text) src = QTextStream(f).readAll() if script_transform: src = script_transform(src) script = QWebEngineScript() script.setInjectionPoint(ipoint) script.setSourceCode(src) script.setWorldId(iid) script.setRunsOnSubFrames(sub_frames) self.q_profile.scripts().insert(script) inject_js(":/qtwebchannel/qwebchannel.js") contentjs = ["WEBMACS_SECURE_ID = {};".format(str(id(self)))] with open(os.path.join(THIS_DIR, "scripts", "setup.js")) as f: contentjs.append(f.read()) with open(os.path.join(THIS_DIR, "scripts", "textedit.js")) as f: contentjs.append(f.read()) with open(os.path.join(THIS_DIR, "scripts", "hint.js")) as f: contentjs.append(f.read()) with open(os.path.join(THIS_DIR, "scripts", "textzoom.js")) as f: contentjs.append(f.read()) with open(os.path.join(THIS_DIR, "scripts", "caret_browsing.js")) as f: contentjs.append(f.read()) script = QWebEngineScript() script.setInjectionPoint( QWebEngineScript.InjectionPoint.DocumentCreation) script.setSourceCode("\n".join(contentjs)) script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld) script.setRunsOnSubFrames(True) self.q_profile.scripts().insert(script) inject_js(os.path.join(THIS_DIR, "scripts", "password_manager.js")) settings = self.q_profile.settings() settings.setAttribute( QWebEngineSettings.WebAttribute.LinksIncludedInFocusChain, False, ) settings.setAttribute( QWebEngineSettings.WebAttribute.PluginsEnabled, True, ) settings.setAttribute( QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True, ) settings.setAttribute( QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True, ) settings.setAttribute( QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, False, ) settings.setAttribute( QWebEngineSettings.WebAttribute.JavascriptEnabled, enable_javascript.value, ) settings.setAttribute( QWebEngineSettings.WebAttribute.PdfViewerEnabled, enable_pdfviewer.value ) def update_spell_checking(self): dicts = variables.get("spell-checking-dictionaries") self.q_profile.setSpellCheckEnabled(bool(dicts)) self.q_profile.setSpellCheckLanguages(dicts) def is_off_the_record(self): return self.q_profile.isOffTheRecord() named_profile = Profile ================================================ FILE: webmacs/scheme_handlers/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . from .webmacs import WebmacsSchemeHandler from PyQt6.QtWebEngineCore import QWebEngineUrlScheme def all_schemes(): return (WebmacsSchemeHandler,) def register_schemes(): for scheme in all_schemes(): qscheme = QWebEngineUrlScheme(scheme.scheme) QWebEngineUrlScheme.registerScheme(qscheme) ================================================ FILE: webmacs/scheme_handlers/webmacs/__init__.py ================================================ # This file is part of webmacs. # # webmacs 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. # # webmacs 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 webmacs. If not, see . import os import sys import re import importlib import inspect from itertools import groupby from PyQt6.QtCore import QBuffer, QFile, QUrlQuery from PyQt6.QtWebEngineCore import QWebEngineUrlSchemeHandler from jinja2 import Environment, PackageLoader from ... import version, COMMANDS from ...variables import VARIABLES from ...keymaps import KEYMAPS PAGES = [] _REQUESTS_HANDLER = [] THIS_DIR = os.path.dirname(os.path.realpath(__file__)) def register_page(match_url=None, visible=True): def wrapper(meth): if visible: PAGES.append(meth.__name__) if match_url is None: match = re.compile(r"^(%s)$" % re.escape(meth.__name__)) elif isinstance(match_url, str): match = re.compile(match_url) else: match = match_url _REQUESTS_HANDLER.append((match, meth)) return meth return wrapper class WebmacsSchemeHandler(QWebEngineUrlSchemeHandler): scheme = b"webmacs" def __init__(self, parent=None): QWebEngineUrlSchemeHandler.__init__(self, parent) self.env = Environment( autoescape=True, loader=PackageLoader(__name__), ) def reply_template(self, job, tpl_name, data): template = self.env.get_template(tpl_name + ".html") buffer = QBuffer(self) buffer.setData(template.render(**data).encode("utf-8")) job.reply(b"text/html", buffer) def requestStarted(self, job): url = job.requestUrl() url_s = url.toString()[10:] # strip webmacs:// for re_match, meth in _REQUESTS_HANDLER: res = re_match.match(url_s) if res: meth(self, job, url, *res.groups()) return @register_page(match_url=r"^/js/(.+\.js)$", visible=False) def to_js(self, job, _, path): js_path = os.path.join(THIS_DIR, "js", path) if os.path.isfile(js_path): f = QFile(js_path, self) job.reply(b"application/javascript", f) @register_page() def version(self, job, _, name): rev = version.webmacs_revision() or "" if rev: rev = " (%s)" % rev self.reply_template(job, name, { "versions": ( ("Webmacs version", version.WEBMACS_VERSION_STR + rev), ("Operating system", sys.platform), ("Python version", sys.version), ("Qt version", version.QT_VERSION_STR), ("PyQt version", version.PYQT_VERSION_STR), ("Chromium version", version.chromium_version()), ) }) @register_page() def downloads(self, job, _, name): self.reply_template(job, name, {}) @register_page() def commands(self, job, _, name): self.reply_template(job, name, {"commands": COMMANDS}) @register_page(match_url=r"^command/(\S+)$", visible=False) def command(self, job, _, command): used_in_keymaps = [] for name, km in KEYMAPS.items(): def add(prefix, cmd, parent): if cmd == command: used_in_keymaps.append(( " ".join(str(k) for k in prefix), name, )) km.traverse_commands(add) cmd = COMMANDS[command] src_url = get_src_url(cmd.binding) self.reply_template(job, "command", { "command_name": command, "command": cmd, "command_src_url": src_url, "used_in_keymaps": used_in_keymaps, }) @register_page() def variables(self, job, _, name): self.reply_template(job, name, {"variables": VARIABLES}) @register_page(match_url=r"^variable/(\S+)$", visible=False) def variable(self, job, _, name): self.reply_template(job, "variable", {"variable": VARIABLES[name]}) @register_page(match_url=r"^keymap/(\S+)$", visible=False) def keymap(self, job, _, keymap): km = KEYMAPS[keymap] acc = [] def add(prefix, cmd, parent): if isinstance(cmd, str): acc.append((" ".join(str(k) for k in prefix), cmd, parent)) km.traverse_commands(add) def by_parent(v): return v[2].name if v[2] else "" acc = sorted(acc, key=lambda v: v[0]) acc = sorted(acc, key=by_parent) self.reply_template(job, "keymap", { "name": keymap, "keymap": km, "bindings": groupby(acc, by_parent), }) @register_page() def bindings(self, job, _, name): self.reply_template(job, name, {"keymaps": KEYMAPS}) @register_page(match_url=r"^pydoc/.+$", visible=False) def pydoc(self, job, url): from pygments.formatters import HtmlFormatter from pygments.lexers.python import Python3Lexer from pygments import highlight modname = url.path().lstrip("/") query = QUrlQuery(url) extras = {} if query.hasQueryItem("hl_lines"): start, end = query.queryItemValue("hl_lines").split("-") extras["hl_lines"] = list(range(int(start), int(end) + 1)) mod = importlib.import_module(modname) filepath = inspect.getsourcefile(mod) formatter = HtmlFormatter( title="Module %s" % modname, full=True, lineanchors="line", **extras ) with open(filepath) as f: code = highlight(f.read(), Python3Lexer(), formatter) buffer = QBuffer(self) buffer.setData(code.encode("utf-8")) job.reply(b"text/html", buffer) @register_page(match_url=r"^key/.+$", visible=False) def key(self, job, url): key = url.path().lstrip("/") query = QUrlQuery(url) command = query.queryItemValue("command") keymap = query.queryItemValue("keymap") if ":" in command: modname, fname = command.split(":", 1) fn = getattr(importlib.import_module(modname), fname) command_name = fn.__name__ named_command = False else: command_name = command cmd = COMMANDS[command] fn = cmd.binding named_command = True modname = fn.__module__ command_doc = fn.__doc__ src_url = get_src_url(fn) def _get_all_keys(km): acc = [] cmd = command_name if named_command else fn def add(prefix, cmd_, parent_): if cmd == cmd_: acc.append(" ".join(str(k) for k in prefix)) km.traverse_commands(add) return acc try: all_keys = _get_all_keys(KEYMAPS[keymap]) except KeyError: all_keys = (key,) self.reply_template(job, "key", { "command_name": command_name, "keymap": keymap, "key": key, "command_doc": command_doc, "named_command": named_command, "command_src_url": src_url, "modname": modname, "all_keys": all_keys, }) def get_src_url(obj): lines, loc = inspect.getsourcelines(obj) return "webmacs://pydoc/{}?hl_lines={}-{}#line-{}".format( obj.__module__, loc, loc + len(lines), loc ) ================================================ FILE: webmacs/scheme_handlers/webmacs/js/vue.js ================================================ /*! * Vue.js v2.4.4 * (c) 2014-2017 Evan You * Released under the MIT License. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.Vue = factory()); }(this, (function () { 'use strict'; /* */ // these helpers produces better vm code in JS engines due to their // explicitness and function inlining function isUndef (v) { return v === undefined || v === null } function isDef (v) { return v !== undefined && v !== null } function isTrue (v) { return v === true } function isFalse (v) { return v === false } /** * Check if value is primitive */ function isPrimitive (value) { return ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ) } /** * Quick object check - this is primarily used to tell * Objects from primitive values when we know the value * is a JSON-compliant type. */ function isObject (obj) { return obj !== null && typeof obj === 'object' } var _toString = Object.prototype.toString; /** * Strict object type check. Only returns true * for plain JavaScript objects. */ function isPlainObject (obj) { return _toString.call(obj) === '[object Object]' } function isRegExp (v) { return _toString.call(v) === '[object RegExp]' } /** * Check if val is a valid array index. */ function isValidArrayIndex (val) { var n = parseFloat(val); return n >= 0 && Math.floor(n) === n && isFinite(val) } /** * Convert a value to a string that is actually rendered. */ function toString (val) { return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val) } /** * Convert a input value to a number for persistence. * If the conversion fails, return original string. */ function toNumber (val) { var n = parseFloat(val); return isNaN(n) ? val : n } /** * Make a map and return a function for checking if a key * is in that map. */ function makeMap ( str, expectsLowerCase ) { var map = Object.create(null); var list = str.split(','); for (var i = 0; i < list.length; i++) { map[list[i]] = true; } return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : function (val) { return map[val]; } } /** * Check if a tag is a built-in tag. */ var isBuiltInTag = makeMap('slot,component', true); /** * Check if a attribute is a reserved attribute. */ var isReservedAttribute = makeMap('key,ref,slot,is'); /** * Remove an item from an array */ function remove (arr, item) { if (arr.length) { var index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1) } } } /** * Check whether the object has the property. */ var hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn (obj, key) { return hasOwnProperty.call(obj, key) } /** * Create a cached version of a pure function. */ function cached (fn) { var cache = Object.create(null); return (function cachedFn (str) { var hit = cache[str]; return hit || (cache[str] = fn(str)) }) } /** * Camelize a hyphen-delimited string. */ var camelizeRE = /-(\w)/g; var camelize = cached(function (str) { return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) }); /** * Capitalize a string. */ var capitalize = cached(function (str) { return str.charAt(0).toUpperCase() + str.slice(1) }); /** * Hyphenate a camelCase string. */ var hyphenateRE = /\B([A-Z])/g; var hyphenate = cached(function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase() }); /** * Simple bind, faster than native */ function bind (fn, ctx) { function boundFn (a) { var l = arguments.length; return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx) } // record original fn length boundFn._length = fn.length; return boundFn } /** * Convert an Array-like object to a real Array. */ function toArray (list, start) { start = start || 0; var i = list.length - start; var ret = new Array(i); while (i--) { ret[i] = list[i + start]; } return ret } /** * Mix properties into target object. */ function extend (to, _from) { for (var key in _from) { to[key] = _from[key]; } return to } /** * Merge an Array of Objects into a single Object. */ function toObject (arr) { var res = {}; for (var i = 0; i < arr.length; i++) { if (arr[i]) { extend(res, arr[i]); } } return res } /** * Perform no operation. * Stubbing args to make Flow happy without leaving useless transpiled code * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) */ function noop (a, b, c) {} /** * Always return false. */ var no = function (a, b, c) { return false; }; /** * Return same value */ var identity = function (_) { return _; }; /** * Generate a static keys string from compiler modules. */ function genStaticKeys (modules) { return modules.reduce(function (keys, m) { return keys.concat(m.staticKeys || []) }, []).join(',') } /** * Check if two values are loosely equal - that is, * if they are plain objects, do they have the same shape? */ function looseEqual (a, b) { if (a === b) { return true } var isObjectA = isObject(a); var isObjectB = isObject(b); if (isObjectA && isObjectB) { try { var isArrayA = Array.isArray(a); var isArrayB = Array.isArray(b); if (isArrayA && isArrayB) { return a.length === b.length && a.every(function (e, i) { return looseEqual(e, b[i]) }) } else if (!isArrayA && !isArrayB) { var keysA = Object.keys(a); var keysB = Object.keys(b); return keysA.length === keysB.length && keysA.every(function (key) { return looseEqual(a[key], b[key]) }) } else { /* istanbul ignore next */ return false } } catch (e) { /* istanbul ignore next */ return false } } else if (!isObjectA && !isObjectB) { return String(a) === String(b) } else { return false } } function looseIndexOf (arr, val) { for (var i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) { return i } } return -1 } /** * Ensure a function is called only once. */ function once (fn) { var called = false; return function () { if (!called) { called = true; fn.apply(this, arguments); } } } var SSR_ATTR = 'data-server-rendered'; var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; var LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated' ]; /* */ var config = ({ /** * Option merge strategies (used in core/util/options) */ optionMergeStrategies: Object.create(null), /** * Whether to suppress warnings. */ silent: false, /** * Show production mode tip message on boot? */ productionTip: "development" !== 'production', /** * Whether to enable devtools */ devtools: "development" !== 'production', /** * Whether to record perf */ performance: false, /** * Error handler for watcher errors */ errorHandler: null, /** * Warn handler for watcher warns */ warnHandler: null, /** * Ignore certain custom elements */ ignoredElements: [], /** * Custom user key aliases for v-on */ keyCodes: Object.create(null), /** * Check if a tag is reserved so that it cannot be registered as a * component. This is platform-dependent and may be overwritten. */ isReservedTag: no, /** * Check if an attribute is reserved so that it cannot be used as a component * prop. This is platform-dependent and may be overwritten. */ isReservedAttr: no, /** * Check if a tag is an unknown element. * Platform-dependent. */ isUnknownElement: no, /** * Get the namespace of an element */ getTagNamespace: noop, /** * Parse the real tag name for the specific platform. */ parsePlatformTagName: identity, /** * Check if an attribute must be bound using property, e.g. value * Platform-dependent. */ mustUseProp: no, /** * Exposed for legacy reasons */ _lifecycleHooks: LIFECYCLE_HOOKS }); /* */ var emptyObject = Object.freeze({}); /** * Check if a string starts with $ or _ */ function isReserved (str) { var c = (str + '').charCodeAt(0); return c === 0x24 || c === 0x5F } /** * Define a property. */ function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); } /** * Parse simple path. */ var bailRE = /[^\w.$]/; function parsePath (path) { if (bailRE.test(path)) { return } var segments = path.split('.'); return function (obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj } } /* */ var warn = noop; var tip = noop; var formatComponentName = (null); // work around flow check { var hasConsole = typeof console !== 'undefined'; var classifyRE = /(?:^|[-_])(\w)/g; var classify = function (str) { return str .replace(classifyRE, function (c) { return c.toUpperCase(); }) .replace(/[-_]/g, ''); }; warn = function (msg, vm) { var trace = vm ? generateComponentTrace(vm) : ''; if (config.warnHandler) { config.warnHandler.call(null, msg, vm, trace); } else if (hasConsole && (!config.silent)) { console.error(("[Vue warn]: " + msg + trace)); } }; tip = function (msg, vm) { if (hasConsole && (!config.silent)) { console.warn("[Vue tip]: " + msg + ( vm ? generateComponentTrace(vm) : '' )); } }; formatComponentName = function (vm, includeFile) { if (vm.$root === vm) { return '' } var name = typeof vm === 'string' ? vm : typeof vm === 'function' && vm.options ? vm.options.name : vm._isVue ? vm.$options.name || vm.$options._componentTag : vm.name; var file = vm._isVue && vm.$options.__file; if (!name && file) { var match = file.match(/([^/\\]+)\.vue$/); name = match && match[1]; } return ( (name ? ("<" + (classify(name)) + ">") : "") + (file && includeFile !== false ? (" at " + file) : '') ) }; var repeat = function (str, n) { var res = ''; while (n) { if (n % 2 === 1) { res += str; } if (n > 1) { str += str; } n >>= 1; } return res }; var generateComponentTrace = function (vm) { if (vm._isVue && vm.$parent) { var tree = []; var currentRecursiveSequence = 0; while (vm) { if (tree.length > 0) { var last = tree[tree.length - 1]; if (last.constructor === vm.constructor) { currentRecursiveSequence++; vm = vm.$parent; continue } else if (currentRecursiveSequence > 0) { tree[tree.length - 1] = [last, currentRecursiveSequence]; currentRecursiveSequence = 0; } } tree.push(vm); vm = vm.$parent; } return '\n\nfound in\n\n' + tree .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") : formatComponentName(vm))); }) .join('\n') } else { return ("\n\n(found in " + (formatComponentName(vm)) + ")") } }; } /* */ function handleError (err, vm, info) { if (config.errorHandler) { config.errorHandler.call(null, err, vm, info); } else { { warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); } /* istanbul ignore else */ if (inBrowser && typeof console !== 'undefined') { console.error(err); } else { throw err } } } /* */ /* globals MutationObserver */ // can we use __proto__? var hasProto = '__proto__' in {}; // Browser environment sniffing var inBrowser = typeof window !== 'undefined'; var UA = inBrowser && window.navigator.userAgent.toLowerCase(); var isIE = UA && /msie|trident/.test(UA); var isIE9 = UA && UA.indexOf('msie 9.0') > 0; var isEdge = UA && UA.indexOf('edge/') > 0; var isAndroid = UA && UA.indexOf('android') > 0; var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA); var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; // Firefox has a "watch" function on Object.prototype... var nativeWatch = ({}).watch; var supportsPassive = false; if (inBrowser) { try { var opts = {}; Object.defineProperty(opts, 'passive', ({ get: function get () { /* istanbul ignore next */ supportsPassive = true; } })); // https://github.com/facebook/flow/issues/285 window.addEventListener('test-passive', null, opts); } catch (e) {} } // this needs to be lazy-evaled because vue may be required before // vue-server-renderer can set VUE_ENV var _isServer; var isServerRendering = function () { if (_isServer === undefined) { /* istanbul ignore if */ if (!inBrowser && typeof global !== 'undefined') { // detect presence of vue-server-renderer and avoid // Webpack shimming the process _isServer = global['process'].env.VUE_ENV === 'server'; } else { _isServer = false; } } return _isServer }; // detect devtools var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; /* istanbul ignore next */ function isNative (Ctor) { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) } var hasSymbol = typeof Symbol !== 'undefined' && isNative(Symbol) && typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); /** * Defer a task to execute it asynchronously. */ var nextTick = (function () { var callbacks = []; var pending = false; var timerFunc; function nextTickHandler () { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } } // the nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore if */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = function (err) { console.error(err); }; timerFunc = function () { p.then(nextTickHandler).catch(logError); // in problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) { setTimeout(noop); } }; } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 var counter = 1; var observer = new MutationObserver(nextTickHandler); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // fallback to setTimeout /* istanbul ignore next */ timerFunc = function () { setTimeout(nextTickHandler, 0); }; } return function queueNextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })(); var _Set; /* istanbul ignore if */ if (typeof Set !== 'undefined' && isNative(Set)) { // use native Set when available. _Set = Set; } else { // a non-standard Set polyfill that only works with primitive keys. _Set = (function () { function Set () { this.set = Object.create(null); } Set.prototype.has = function has (key) { return this.set[key] === true }; Set.prototype.add = function add (key) { this.set[key] = true; }; Set.prototype.clear = function clear () { this.set = Object.create(null); }; return Set; }()); } /* */ var uid = 0; /** * A dep is an observable that can have multiple * directives subscribing to it. */ var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub); }; Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; // the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null; var targetStack = []; function pushTarget (_target) { if (Dep.target) { targetStack.push(Dep.target); } Dep.target = _target; } function popTarget () { Dep.target = targetStack.pop(); } /* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto);[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); }); /* */ var arrayKeys = Object.getOwnPropertyNames(arrayMethods); /** * By default, when a reactive property is set, the new value is * also converted to become reactive. However when passing down props, * we don't want to force conversion because the value may be a nested value * under a frozen data structure. Converting it would defeat the optimization. */ var observerState = { shouldConvert: true }; /** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object's property keys into getter/setters that * collect dependencies and dispatches updates. */ var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } }; /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i], obj[keys[i]]); } }; /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } }; // helpers /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ function protoAugment (target, src, keys) { /* eslint-disable no-proto */ target.__proto__ = src; /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; def(target, key, src[key]); } } /** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ function observe (value, asRootData) { if (!isObject(value)) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob } /** * Define a reactive property on an Object. */ function defineReactive$$1 ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if ("development" !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); } /** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ function set (target, key, val) { if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val } if (hasOwn(target, key)) { target[key] = val; return val } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { "development" !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ); return val } if (!ob) { target[key] = val; return val } defineReactive$$1(ob.value, key, val); ob.dep.notify(); return val } /** * Delete a property and trigger change if necessary. */ function del (target, key) { if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1); return } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { "development" !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ); return } if (!hasOwn(target, key)) { return } delete target[key]; if (!ob) { return } ob.dep.notify(); } /** * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. */ function dependArray (value) { for (var e = (void 0), i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); } } } /* */ /** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. */ var strats = config.optionMergeStrategies; /** * Options with restrictions */ { strats.el = strats.propsData = function (parent, child, vm, key) { if (!vm) { warn( "option \"" + key + "\" can only be used during instance " + 'creation with the `new` keyword.' ); } return defaultStrat(parent, child) }; } /** * Helper that recursively merges two data objects together. */ function mergeData (to, from) { if (!from) { return to } var key, toVal, fromVal; var keys = Object.keys(from); for (var i = 0; i < keys.length; i++) { key = keys[i]; toVal = to[key]; fromVal = from[key]; if (!hasOwn(to, key)) { set(to, key, fromVal); } else if (isPlainObject(toVal) && isPlainObject(fromVal)) { mergeData(toVal, fromVal); } } return to } /** * Data */ function mergeDataOrFn ( parentVal, childVal, vm ) { if (!vm) { // in a Vue.extend merge, both should be functions if (!childVal) { return parentVal } if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this) : childVal, typeof parentVal === 'function' ? parentVal.call(this) : parentVal ) } } else if (parentVal || childVal) { return function mergedInstanceDataFn () { // instance merge var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal; var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : parentVal; if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } } } strats.data = function ( parentVal, childVal, vm ) { if (!vm) { if (childVal && typeof childVal !== 'function') { "development" !== 'production' && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ); return parentVal } return mergeDataOrFn.call(this, parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) }; /** * Hooks and props are merged as arrays. */ function mergeHook ( parentVal, childVal ) { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } LIFECYCLE_HOOKS.forEach(function (hook) { strats[hook] = mergeHook; }); /** * Assets * * When a vm is present (instance creation), we need to do * a three-way merge between constructor options, instance * options and parent options. */ function mergeAssets (parentVal, childVal) { var res = Object.create(parentVal || null); return childVal ? extend(res, childVal) : res } ASSET_TYPES.forEach(function (type) { strats[type + 's'] = mergeAssets; }); /** * Watchers. * * Watchers hashes should not overwrite one * another, so we merge them as arrays. */ strats.watch = function (parentVal, childVal) { // work around Firefox's Object.prototype.watch... if (parentVal === nativeWatch) { parentVal = undefined; } if (childVal === nativeWatch) { childVal = undefined; } /* istanbul ignore if */ if (!childVal) { return Object.create(parentVal || null) } if (!parentVal) { return childVal } var ret = {}; extend(ret, parentVal); for (var key in childVal) { var parent = ret[key]; var child = childVal[key]; if (parent && !Array.isArray(parent)) { parent = [parent]; } ret[key] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child]; } return ret }; /** * Other object hashes. */ strats.props = strats.methods = strats.inject = strats.computed = function (parentVal, childVal) { if (!parentVal) { return childVal } var ret = Object.create(null); extend(ret, parentVal); if (childVal) { extend(ret, childVal); } return ret }; strats.provide = mergeDataOrFn; /** * Default strategy. */ var defaultStrat = function (parentVal, childVal) { return childVal === undefined ? parentVal : childVal }; /** * Validate component names */ function checkComponents (options) { for (var key in options.components) { var lower = key.toLowerCase(); if (isBuiltInTag(lower) || config.isReservedTag(lower)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + key ); } } } /** * Ensure all props option syntax are normalized into the * Object-based format. */ function normalizeProps (options) { var props = options.props; if (!props) { return } var res = {}; var i, val, name; if (Array.isArray(props)) { i = props.length; while (i--) { val = props[i]; if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else { warn('props must be strings when using array syntax.'); } } } else if (isPlainObject(props)) { for (var key in props) { val = props[key]; name = camelize(key); res[name] = isPlainObject(val) ? val : { type: val }; } } options.props = res; } /** * Normalize all injections into Object-based format */ function normalizeInject (options) { var inject = options.inject; if (Array.isArray(inject)) { var normalized = options.inject = {}; for (var i = 0; i < inject.length; i++) { normalized[inject[i]] = inject[i]; } } } /** * Normalize raw function directives into object format. */ function normalizeDirectives (options) { var dirs = options.directives; if (dirs) { for (var key in dirs) { var def = dirs[key]; if (typeof def === 'function') { dirs[key] = { bind: def, update: def }; } } } } /** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ function mergeOptions ( parent, child, vm ) { { checkComponents(child); } if (typeof child === 'function') { child = child.options; } normalizeProps(child); normalizeInject(child); normalizeDirectives(child); var extendsFrom = child.extends; if (extendsFrom) { parent = mergeOptions(parent, extendsFrom, vm); } if (child.mixins) { for (var i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); } } var options = {}; var key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } function mergeField (key) { var strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } return options } /** * Resolve an asset. * This function is used because child instances need access * to assets defined in its ancestor chain. */ function resolveAsset ( options, type, id, warnMissing ) { /* istanbul ignore if */ if (typeof id !== 'string') { return } var assets = options[type]; // check local registration variations first if (hasOwn(assets, id)) { return assets[id] } var camelizedId = camelize(id); if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } var PascalCaseId = capitalize(camelizedId); if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } // fallback to prototype chain var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; if ("development" !== 'production' && warnMissing && !res) { warn( 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, options ); } return res } /* */ function validateProp ( key, propOptions, propsData, vm ) { var prop = propOptions[key]; var absent = !hasOwn(propsData, key); var value = propsData[key]; // handle boolean props if (isType(Boolean, prop.type)) { if (absent && !hasOwn(prop, 'default')) { value = false; } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) { value = true; } } // check default value if (value === undefined) { value = getPropDefaultValue(vm, prop, key); // since the default value is a fresh copy, // make sure to observe it. var prevShouldConvert = observerState.shouldConvert; observerState.shouldConvert = true; observe(value); observerState.shouldConvert = prevShouldConvert; } { assertProp(prop, key, value, vm, absent); } return value } /** * Get the default value of a prop. */ function getPropDefaultValue (vm, prop, key) { // no default, return undefined if (!hasOwn(prop, 'default')) { return undefined } var def = prop.default; // warn against non-factory defaults for Object & Array if ("development" !== 'production' && isObject(def)) { warn( 'Invalid default value for prop "' + key + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', vm ); } // the raw prop value was also undefined from previous render, // return previous default value to avoid unnecessary watcher trigger if (vm && vm.$options.propsData && vm.$options.propsData[key] === undefined && vm._props[key] !== undefined ) { return vm._props[key] } // call factory function for non-Function types // a value is Function if its prototype is function even across different execution context return typeof def === 'function' && getType(prop.type) !== 'Function' ? def.call(vm) : def } /** * Assert whether a prop is valid. */ function assertProp ( prop, name, value, vm, absent ) { if (prop.required && absent) { warn( 'Missing required prop: "' + name + '"', vm ); return } if (value == null && !prop.required) { return } var type = prop.type; var valid = !type || type === true; var expectedTypes = []; if (type) { if (!Array.isArray(type)) { type = [type]; } for (var i = 0; i < type.length && !valid; i++) { var assertedType = assertType(value, type[i]); expectedTypes.push(assertedType.expectedType || ''); valid = assertedType.valid; } } if (!valid) { warn( 'Invalid prop: type check failed for prop "' + name + '".' + ' Expected ' + expectedTypes.map(capitalize).join(', ') + ', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.', vm ); return } var validator = prop.validator; if (validator) { if (!validator(value)) { warn( 'Invalid prop: custom validator check failed for prop "' + name + '".', vm ); } } } var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; function assertType (value, type) { var valid; var expectedType = getType(type); if (simpleCheckRE.test(expectedType)) { var t = typeof value; valid = t === expectedType.toLowerCase(); // for primitive wrapper objects if (!valid && t === 'object') { valid = value instanceof type; } } else if (expectedType === 'Object') { valid = isPlainObject(value); } else if (expectedType === 'Array') { valid = Array.isArray(value); } else { valid = value instanceof type; } return { valid: valid, expectedType: expectedType } } /** * Use function string name to check built-in types, * because a simple equality check will fail when running * across different vms / iframes. */ function getType (fn) { var match = fn && fn.toString().match(/^\s*function (\w+)/); return match ? match[1] : '' } function isType (type, fn) { if (!Array.isArray(fn)) { return getType(fn) === getType(type) } for (var i = 0, len = fn.length; i < len; i++) { if (getType(fn[i]) === getType(type)) { return true } } /* istanbul ignore next */ return false } /* */ var mark; var measure; { var perf = inBrowser && window.performance; /* istanbul ignore if */ if ( perf && perf.mark && perf.measure && perf.clearMarks && perf.clearMeasures ) { mark = function (tag) { return perf.mark(tag); }; measure = function (name, startTag, endTag) { perf.measure(name, startTag, endTag); perf.clearMarks(startTag); perf.clearMarks(endTag); perf.clearMeasures(name); }; } } /* not type checking this file because flow doesn't play well with Proxy */ var initProxy; { var allowedGlobals = makeMap( 'Infinity,undefined,NaN,isFinite,isNaN,' + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + 'require' // for Webpack/Browserify ); var warnNonPresent = function (target, key) { warn( "Property or method \"" + key + "\" is not defined on the instance but " + "referenced during render. Make sure to declare reactive data " + "properties in the data option.", target ); }; var hasProxy = typeof Proxy !== 'undefined' && Proxy.toString().match(/native code/); if (hasProxy) { var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta'); config.keyCodes = new Proxy(config.keyCodes, { set: function set (target, key, value) { if (isBuiltInModifier(key)) { warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); return false } else { target[key] = value; return true } } }); } var hasHandler = { has: function has (target, key) { var has = key in target; var isAllowed = allowedGlobals(key) || key.charAt(0) === '_'; if (!has && !isAllowed) { warnNonPresent(target, key); } return has || !isAllowed } }; var getHandler = { get: function get (target, key) { if (typeof key === 'string' && !(key in target)) { warnNonPresent(target, key); } return target[key] } }; initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use var options = vm.$options; var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; vm._renderProxy = new Proxy(vm, handlers); } else { vm._renderProxy = vm; } }; } /* */ var VNode = function VNode ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) { this.tag = tag; this.data = data; this.children = children; this.text = text; this.elm = elm; this.ns = undefined; this.context = context; this.functionalContext = undefined; this.key = data && data.key; this.componentOptions = componentOptions; this.componentInstance = undefined; this.parent = undefined; this.raw = false; this.isStatic = false; this.isRootInsert = true; this.isComment = false; this.isCloned = false; this.isOnce = false; this.asyncFactory = asyncFactory; this.asyncMeta = undefined; this.isAsyncPlaceholder = false; }; var prototypeAccessors = { child: {} }; // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ prototypeAccessors.child.get = function () { return this.componentInstance }; Object.defineProperties( VNode.prototype, prototypeAccessors ); var createEmptyVNode = function (text) { if ( text === void 0 ) text = ''; var node = new VNode(); node.text = text; node.isComment = true; return node }; function createTextVNode (val) { return new VNode(undefined, undefined, undefined, String(val)) } // optimized shallow clone // used for static nodes and slot nodes because they may be reused across // multiple renders, cloning them avoids errors when DOM manipulations rely // on their elm reference. function cloneVNode (vnode, deep) { var cloned = new VNode( vnode.tag, vnode.data, vnode.children, vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ); cloned.ns = vnode.ns; cloned.isStatic = vnode.isStatic; cloned.key = vnode.key; cloned.isComment = vnode.isComment; cloned.isCloned = true; if (deep && vnode.children) { cloned.children = cloneVNodes(vnode.children); } return cloned } function cloneVNodes (vnodes, deep) { var len = vnodes.length; var res = new Array(len); for (var i = 0; i < len; i++) { res[i] = cloneVNode(vnodes[i], deep); } return res } /* */ var normalizeEvent = cached(function (name) { var passive = name.charAt(0) === '&'; name = passive ? name.slice(1) : name; var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first name = once$$1 ? name.slice(1) : name; var capture = name.charAt(0) === '!'; name = capture ? name.slice(1) : name; var plain = !(passive || once$$1 || capture); return { name: name, plain: plain, once: once$$1, capture: capture, passive: passive } }); function createFnInvoker (fns) { function invoker () { var arguments$1 = arguments; var fns = invoker.fns; if (Array.isArray(fns)) { var cloned = fns.slice(); for (var i = 0; i < cloned.length; i++) { cloned[i].apply(null, arguments$1); } } else { // return handler return value for single handlers return fns.apply(null, arguments) } } invoker.fns = fns; return invoker } // #6552 function prioritizePlainEvents (a, b) { return a.plain ? -1 : b.plain ? 1 : 0 } function updateListeners ( on, oldOn, add, remove$$1, vm ) { var name, cur, old, event; var toAdd = []; var hasModifier = false; for (name in on) { cur = on[name]; old = oldOn[name]; event = normalizeEvent(name); if (!event.plain) { hasModifier = true; } if (isUndef(cur)) { "development" !== 'production' && warn( "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), vm ); } else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur); } event.handler = cur; toAdd.push(event); } else if (cur !== old) { old.fns = cur; on[name] = old; } } if (toAdd.length) { if (hasModifier) { toAdd.sort(prioritizePlainEvents); } for (var i = 0; i < toAdd.length; i++) { var event$1 = toAdd[i]; add(event$1.name, event$1.handler, event$1.once, event$1.capture, event$1.passive); } } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name); remove$$1(event.name, oldOn[name], event.capture); } } } /* */ function mergeVNodeHook (def, hookKey, hook) { var invoker; var oldHook = def[hookKey]; function wrappedHook () { hook.apply(this, arguments); // important: remove merged hook to ensure it's called only once // and prevent memory leak remove(invoker.fns, wrappedHook); } if (isUndef(oldHook)) { // no existing hook invoker = createFnInvoker([wrappedHook]); } else { /* istanbul ignore if */ if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { // already a merged invoker invoker = oldHook; invoker.fns.push(wrappedHook); } else { // existing plain hook invoker = createFnInvoker([oldHook, wrappedHook]); } } invoker.merged = true; def[hookKey] = invoker; } /* */ function extractPropsFromVNodeData ( data, Ctor, tag ) { // we are only extracting raw values here. // validation and default values are handled in the child // component itself. var propOptions = Ctor.options.props; if (isUndef(propOptions)) { return } var res = {}; var attrs = data.attrs; var props = data.props; if (isDef(attrs) || isDef(props)) { for (var key in propOptions) { var altKey = hyphenate(key); { var keyInLowerCase = key.toLowerCase(); if ( key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase) ) { tip( "Prop \"" + keyInLowerCase + "\" is passed to component " + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + " \"" + key + "\". " + "Note that HTML attributes are case-insensitive and camelCased " + "props need to use their kebab-case equivalents when using in-DOM " + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." ); } } checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false); } } return res } function checkProp ( res, hash, key, altKey, preserve ) { if (isDef(hash)) { if (hasOwn(hash, key)) { res[key] = hash[key]; if (!preserve) { delete hash[key]; } return true } else if (hasOwn(hash, altKey)) { res[key] = hash[altKey]; if (!preserve) { delete hash[altKey]; } return true } } return false } /* */ // The template compiler attempts to minimize the need for normalization by // statically analyzing the template at compile time. // // For plain HTML markup, normalization can be completely skipped because the // generated render function is guaranteed to return Array. There are // two cases where extra normalization is needed: // 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. function simpleNormalizeChildren (children) { for (var i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { return Array.prototype.concat.apply([], children) } } return children } // 2. When the children contains constructs that always generated nested Arrays, // e.g.