summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLembrun <amadeusk7@free.fr>2021-03-07 18:00:36 +0100
committerLembrun <amadeusk7@free.fr>2021-03-07 18:00:36 +0100
commitff83241c81a6574106c70642b7de4824ccc1e9be (patch)
tree22cf073a1a60d3d9f15c3a6c68203e43d1317867
parenta015e2603b91cdc796c3729e612112dacc0a62fc (diff)
parent423e7e25bdd7de78c7a75d70bfc92171fcf8ccaf (diff)
downloadqutebrowser-ff83241c81a6574106c70642b7de4824ccc1e9be.tar.gz
qutebrowser-ff83241c81a6574106c70642b7de4824ccc1e9be.zip
Merge branch 'master' into Add-utils/resources.py
-rw-r--r--README.asciidoc4
-rw-r--r--doc/changelog.asciidoc6
-rw-r--r--doc/help/settings.asciidoc14
-rw-r--r--doc/img/cheatsheet-big.pngbin779344 -> 781120 bytes
-rw-r--r--doc/img/cheatsheet-small.pngbin30208 -> 30252 bytes
-rw-r--r--misc/cheatsheet.svg14
-rw-r--r--misc/requirements/requirements-tests.txt3
-rw-r--r--misc/requirements/requirements-tests.txt-raw4
-rw-r--r--misc/userscripts/README.md2
-rwxr-xr-xmisc/userscripts/qute-keepassxc361
-rw-r--r--qutebrowser/browser/browsertab.py14
-rw-r--r--qutebrowser/browser/commands.py4
-rw-r--r--qutebrowser/browser/inspector.py25
-rw-r--r--qutebrowser/browser/shared.py67
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py9
-rw-r--r--qutebrowser/browser/webkit/webkittab.py3
-rw-r--r--qutebrowser/config/configdata.yml14
-rw-r--r--qutebrowser/misc/guiprocess.py5
-rwxr-xr-xscripts/dev/run_vulture.py1
-rw-r--r--tests/end2end/features/editor.feature49
-rw-r--r--tests/end2end/features/sessions.feature5
-rw-r--r--tests/end2end/features/tabs.feature5
-rw-r--r--tests/end2end/features/test_editor_bdd.py37
-rw-r--r--tests/unit/completion/test_models.py11
-rw-r--r--tests/unit/misc/test_guiprocess.py2
25 files changed, 549 insertions, 110 deletions
diff --git a/README.asciidoc b/README.asciidoc
index 704058bd7..43e6a19e4 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -92,7 +92,6 @@ websites and using it for transmission of sensitive data._
* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.12.0 or newer
for Python 3
* https://palletsprojects.com/p/jinja/[jinja2]
-* https://pygments.org/[pygments]
* https://github.com/yaml/pyyaml[PyYAML]
On older Python versions (3.6/3.7/3.8), the following backports are also required:
@@ -103,6 +102,9 @@ On older Python versions (3.6/3.7/3.8), the following backports are also require
The following libraries are optional:
* https://pypi.org/project/adblock/[adblock] (for improved adblocking using ABP syntax)
+* https://pygments.org/[pygments] for syntax highlighting with `:view-source`
+ on QtWebKit, or when using `:view-source --pygments` with the (default)
+ QtWebEngine backend.
* On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log
output.
* https://importlib-metadata.readthedocs.io/[importlib_resources] on Python 3.7
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index c5636204f..3840f369d 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -27,6 +27,7 @@ Added
- New optional dependency on the `importlib_metadata` project on Python 3.7 and
below. This is only relevant when PyQtWebEngine is installed via pip - thus,
this dependency usually isn't relevant for packagers.
+- New `qute-keepassxc` userscript integrating with the KeePassXC browser API.
Changed
~~~~~~~
@@ -54,6 +55,11 @@ Changed
long line.
- If a command stats with space (e.g. `: open ...`, it's now not saved to
command history anymore (similar to how some shells work).
+- When a tab is pinned, running `:open` will now open a new tab instead of
+ displaying an error.
+- The `fileselect.*.command` settings now support file selectors writing the
+ selected paths to stdout, which is used if no `{}` placeholder is contained in
+ the configured command.
Fixed
~~~~~
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 7d0b3469c..392f60c49 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -207,8 +207,8 @@
|<<editor.command,editor.command>>|Editor (and arguments) to use for the `edit-*` commands.
|<<editor.encoding,editor.encoding>>|Encoding to use for the editor.
|<<fileselect.handler,fileselect.handler>>|Handler for selecting file(s) in forms. If `external`, then the commands specified by `fileselect.single_file.command` and `fileselect.multiple_files.command` are used to select one or multiple files respectively.
-|<<fileselect.multiple_files.command,fileselect.multiple_files.command>>|Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file, separated by newlines.
-|<<fileselect.single_file.command,fileselect.single_file.command>>|Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file.
+|<<fileselect.multiple_files.command,fileselect.multiple_files.command>>|Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file or to stdout, separated by newlines.
+|<<fileselect.single_file.command,fileselect.single_file.command>>|Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file or stdout.
|<<fonts.completion.category,fonts.completion.category>>|Font used in the completion categories.
|<<fonts.completion.entry,fonts.completion.entry>>|Font used in the completion widget.
|<<fonts.contextmenu,fonts.contextmenu>>|Font used for the context menu.
@@ -2825,9 +2825,10 @@ Default: +pass:[default]+
[[fileselect.multiple_files.command]]
=== fileselect.multiple_files.command
-Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file, separated by newlines.
+Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file or to stdout, separated by newlines.
The following placeholders are defined:
-* `{}`: Filename of the file to be written to.
+* `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
Type: <<types,ShellCommand>>
@@ -2840,9 +2841,10 @@ Default:
[[fileselect.single_file.command]]
=== fileselect.single_file.command
-Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file.
+Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file or stdout.
The following placeholders are defined:
-* `{}`: Filename of the file to be written to.
+* `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
Type: <<types,ShellCommand>>
diff --git a/doc/img/cheatsheet-big.png b/doc/img/cheatsheet-big.png
index ecd52c14e..75e2abb89 100644
--- a/doc/img/cheatsheet-big.png
+++ b/doc/img/cheatsheet-big.png
Binary files differ
diff --git a/doc/img/cheatsheet-small.png b/doc/img/cheatsheet-small.png
index 0dc01e8b4..e97d63367 100644
--- a/doc/img/cheatsheet-small.png
+++ b/doc/img/cheatsheet-small.png
Binary files differ
diff --git a/misc/cheatsheet.svg b/misc/cheatsheet.svg
index 7e8a7b381..e908f9496 100644
--- a/misc/cheatsheet.svg
+++ b/misc/cheatsheet.svg
@@ -11,7 +11,7 @@
height="682.66669"
id="svg2"
sodipodi:version="0.32"
- inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
version="1.0"
sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
@@ -30,16 +30,16 @@
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="1.7536248"
- inkscape:cx="466.08451"
- inkscape:cy="268.64059"
+ inkscape:zoom="2.48"
+ inkscape:cx="834.18001"
+ inkscape:cy="692.30401"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="1024px"
height="640px"
showgrid="false"
- inkscape:window-width="3822"
- inkscape:window-height="2128"
+ inkscape:window-width="1914"
+ inkscape:window-height="1048"
inkscape:window-x="0"
inkscape:window-y="16"
showguides="true"
@@ -3113,8 +3113,6 @@
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3925">ss - set setting (sl: temp)</flowPara><flowPara
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
- id="flowPara3927" /><flowPara
- style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3929">sk - bind key</flowPara><flowPara
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3931">Ss - show settings</flowPara><flowPara
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index c93b5896d..bf214be0d 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -8,7 +8,7 @@ chardet==4.0.0
cheroot==8.5.2
click==7.1.2
# colorama==0.4.4
-coverage==5.5 ; python_version!="3.10"
+coverage==5.5
EasyProcess==0.3
execnet==1.8.0
filelock==3.0.12
@@ -59,4 +59,3 @@ toml==0.10.2
urllib3==1.26.3
vulture==2.3
Werkzeug==1.0.1
-coverage==5.4; python_version=="3.10"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index 196a80753..ab580ac4b 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -34,7 +34,3 @@ pytest-icdiff
tldextract
#@ ignore: Jinja2, MarkupSafe, colorama
-
-# WORKAROUND for https://github.com/nedbat/coveragepy/issues/1129
-#@ markers: coverage python_version!="3.10"
-#@ add: coverage==5.4; python_version=="3.10"
diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md
index 938dd776d..395797805 100644
--- a/misc/userscripts/README.md
+++ b/misc/userscripts/README.md
@@ -17,6 +17,8 @@ The following userscripts are included in the current directory.
current website.
- [qute-keepass](./qute-keepass): Insertion of usernames and passwords from keepass
databases using pykeepass.
+- [qute-keepassxc](./qute-keepassxc): Insert credentials from open KeepassXC database
+ using keepassxc-browser protocol.
- [qute-pass](./qute-pass): Insert login information using pass and a
dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).
- [qute-lastpass](./qute-lastpass): Similar to qute-pass, for Lastpass.
diff --git a/misc/userscripts/qute-keepassxc b/misc/userscripts/qute-keepassxc
new file mode 100755
index 000000000..f0127590b
--- /dev/null
+++ b/misc/userscripts/qute-keepassxc
@@ -0,0 +1,361 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.com>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser 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.
+#
+# qutebrowser 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 qutebrowser. If not, see <https://www.gnu.org/licenses/>.
+
+"""
+# Introduction
+
+This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database.
+
+
+# Installation
+
+First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config.
+
+
+Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3].
+
+
+Third, install the python module `pynacl`.
+
+
+Finally, adapt your qutebrowser config.
+You can e.g. add the following lines to your `~/.config/qutebrowser/config.py`
+Remember to replace `ABC1234` with your actual GPG key.
+
+```python
+config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert')
+config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
+```
+
+
+# Usage
+
+If you are on a webpage with a login form, simply activate one of the configured key-bindings.
+
+The first time you run this script, KeepassXC will ask you for authentication like with any other browser extension.
+Just provide a name of your choice and accept the request if nothing looks fishy.
+
+
+# How it works
+
+This script will talk to KeepassXC using the native [KeepassXC-Browser protocol][4].
+
+
+This script needs to store the key used to associate with your KeepassXC instance somewhere.
+Unlike most browser extensions which only use plain local storage, this one attempts to do so in a safe way
+by storing the key in encrypted form using GPG.
+Therefore you need to have a public-key-pair readily set up.
+
+GPG might then ask for your private-key passwort whenever you query the database for login credentials.
+
+
+[1]: https://keepassxc.org/
+[2]: https://qutebrowser.org/
+[3]: https://gnupg.org/
+[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
+[5]: https://github.com/qutebrowser/qutebrowser/blob/master/doc/userscripts.asciidoc
+[6]: https://keepassxc.org/docs/keepassxc-browser-migration/
+"""
+
+import sys
+import os
+import socket
+import json
+import base64
+import subprocess
+import argparse
+
+import nacl.utils
+import nacl.public
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(description="Full passwords from KeepassXC")
+ parser.add_argument('url', nargs='?', default=os.environ.get('QUTE_URL'))
+ parser.add_argument('--socket', '-s', default='/run/user/{}/org.keepassxc.KeePassXC.BrowserServer'.format(os.getuid()),
+ help='Path to KeepassXC browser socket')
+ parser.add_argument('--key', '-k', default='alice@example.com',
+ help='GPG key to encrypt KeepassXC auth key with')
+ parser.add_argument('--insecure', action='store_true',
+ help="Do not encrypt auth key")
+ return parser.parse_args()
+
+
+class KeepassError(Exception):
+ def __init__(self, code, desc):
+ self.code = code
+ self.description = desc
+
+ def __str__(self):
+ return f"KeepassXC Error [{self.code}]: {self.description}"
+
+
+class KeepassXC:
+ """ Wrapper around the KeepassXC socket API """
+ def __init__(self, id=None, *, key, socket_path):
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.id = id
+ self.socket_path = socket_path
+ self.client_key = nacl.public.PrivateKey.generate()
+ self.id_key = nacl.public.PrivateKey.from_seed(key)
+ self.cryptobox = None
+
+ def connect(self):
+ if not os.path.exists(self.socket_path):
+ raise KeepassError(-1, "KeepassXC Browser socket does not exists")
+ self.client_id = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8')
+ self.sock.connect(self.socket_path)
+
+ self.send_raw_msg(dict(
+ action = 'change-public-keys',
+ publicKey = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+ nonce = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8'),
+ clientID = self.client_id
+ ))
+
+ resp = self.recv_raw_msg()
+ assert resp['action'] == 'change-public-keys'
+ assert resp['success'] == 'true'
+ assert resp['nonce']
+ self.cryptobox = nacl.public.Box(
+ self.client_key,
+ nacl.public.PublicKey(base64.b64decode(resp['publicKey']))
+ )
+
+ def get_databasehash(self):
+ self.send_msg(dict(action='get-databasehash'))
+ return self.recv_msg()['hash']
+
+ def lock_database(self):
+ self.send_msg(dict(action='lock-database'))
+ try:
+ self.recv_msg()
+ except KeepassError as e:
+ if e.code == 1:
+ return True
+ raise
+ return False
+
+
+ def test_associate(self):
+ if not self.id:
+ return False
+ self.send_msg(dict(
+ action = 'test-associate',
+ id = self.id,
+ key = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+ ))
+ return self.recv_msg()['success'] == 'true'
+
+ def associate(self):
+ self.send_msg(dict(
+ action = 'associate',
+ key = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+ idKey = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+ ))
+ resp = self.recv_msg()
+ self.id = resp['id']
+
+ def get_logins(self, url):
+ self.send_msg(dict(
+ action = 'get-logins',
+ url = url,
+ keys = [{ 'id': self.id, 'key': base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') }]
+ ))
+ return self.recv_msg()['entries']
+
+ def send_raw_msg(self, msg):
+ self.sock.send( json.dumps(msg).encode('utf-8') )
+
+ def recv_raw_msg(self):
+ return json.loads( self.sock.recv(4096).decode('utf-8') )
+
+ def send_msg(self, msg, **extra):
+ nonce = nacl.utils.random(nacl.public.Box.NONCE_SIZE)
+ self.send_raw_msg(dict(
+ action = msg['action'],
+ message = base64.b64encode(self.cryptobox.encrypt(json.dumps(msg).encode('utf-8'), nonce).ciphertext).decode('utf-8'),
+ nonce = base64.b64encode(nonce).decode('utf-8'),
+ clientID = self.client_id,
+ **extra
+ ))
+
+ def recv_msg(self):
+ resp = self.recv_raw_msg()
+ if 'error' in resp:
+ raise KeepassError(resp['errorCode'], resp['error'])
+ assert resp['action']
+ return json.loads(self.cryptobox.decrypt(base64.b64decode(resp['message']), base64.b64decode(resp['nonce'])).decode('utf-8'))
+
+
+
+class SecretKeyStore:
+ def __init__(self, gpgkey):
+ self.gpgkey = gpgkey
+ if gpgkey is None:
+ self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key')
+ else:
+ self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key.gpg')
+
+ def load(self):
+ "Load existing association key from file"
+ if self.gpgkey is None:
+ jsondata = open(self.path, 'r').read()
+ else:
+ jsondata = subprocess.check_output(['gpg', '--decrypt', self.path]).decode('utf-8')
+ data = json.loads(jsondata)
+ self.id = data['id']
+ self.key = base64.b64decode(data['key'])
+
+ def create(self):
+ "Create new association key"
+ self.key = nacl.utils.random(32)
+ self.id = None
+
+ def store(self, id):
+ "Store newly created association key in file"
+ self.id = id
+ jsondata = json.dumps({'id':self.id, 'key':base64.b64encode(self.key).decode('utf-8')})
+ if self.gpgkey is None:
+ open(self.path, "w").write(jsondata)
+ else:
+ subprocess.run(['gpg', '--encrypt', '-o', self.path, '-r', self.gpgkey], input=jsondata.encode('utf-8'), check=True)
+
+
+def qute(cmd):
+ with open(os.environ['QUTE_FIFO'], 'w') as fifo:
+ fifo.write(cmd)
+ fifo.write('\n')
+ fifo.flush()
+
+def error(msg):
+ print(msg, file=sys.stderr)
+ qute('message-error "{}"'.format(msg))
+
+
+def connect_to_keepassxc(args):
+ assert args.key or args.insecure, "Missing GPG key to use for auth key encryption"
+ keystore = SecretKeyStore(args.key)
+ if os.path.isfile(keystore.path):
+ keystore.load()
+ kp = KeepassXC(keystore.id, key=keystore.key, socket_path=args.socket)
+ kp.connect()
+ if not kp.test_associate():
+ error('No KeepassXC association')
+ return None
+ else:
+ keystore.create()
+ kp = KeepassXC(key=keystore.key, socket_path=args.socket)
+ kp.connect()
+ kp.associate()
+ if not kp.test_associate():
+ error('No KeepassXC association')
+ return None
+ keystore.store(kp.id)
+ return kp
+
+
+def make_js_code(username, password):
+ return ' '.join("""
+ function isVisible(elem) {
+ var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
+
+ if (style.getPropertyValue("visibility") !== "visible" ||
+ style.getPropertyValue("display") === "none" ||
+ style.getPropertyValue("opacity") === "0") {
+ return false;
+ }
+
+ return elem.offsetWidth > 0 && elem.offsetHeight > 0;
+ };
+
+ function hasPasswordField(form) {
+ var inputs = form.getElementsByTagName("input");
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (input.type === "password") {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ function loadData2Form (form) {
+ var inputs = form.getElementsByTagName("input");
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (isVisible(input) && (input.type === "text" || input.type === "email")) {
+ input.focus();
+ input.value = %s;
+ input.dispatchEvent(new Event('input', { 'bubbles': true }));
+ input.dispatchEvent(new Event('change', { 'bubbles': true }));
+ input.blur();
+ }
+ if (input.type === "password") {
+ input.focus();
+ input.value = %s;
+ input.dispatchEvent(new Event('input', { 'bubbles': true }));
+ input.dispatchEvent(new Event('change', { 'bubbles': true }));
+ input.blur();
+ }
+ }
+ };
+
+ function fillFirstForm() {
+ var forms = document.getElementsByTagName("form");
+ for (i = 0; i < forms.length; i++) {
+ if (hasPasswordField(forms[i])) {
+ loadData2Form(forms[i]);
+ return;
+ }
+ }
+ alert("No Credentials Form found");
+ };
+
+ fillFirstForm()
+ """.splitlines()) % (json.dumps(username), json.dumps(password))
+
+
+def main():
+ if 'QUTE_FIFO' not in os.environ:
+ print(f"No QUTE_FIFO found - {sys.argv[0]} must be run as a qutebrowser userscript")
+ sys.exit(-1)
+
+ try:
+ args = parse_args()
+ assert args.url, "Missing URL"
+ kp = connect_to_keepassxc(args)
+ if not kp:
+ error('Could not connect to KeepassXC')
+ return
+ creds = kp.get_logins(args.url)
+ if not creds:
+ error('No credentials found')
+ return
+ # TODO: handle multiple matches
+ name, pw = creds[0]['login'], creds[0]['password']
+ if name and pw:
+ qute('jseval -q ' + make_js_code(name, pw))
+ except Exception as e:
+ error(str(e))
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 4c26da69d..581d33507 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -870,7 +870,7 @@ class AbstractTabPrivate:
tabdata = self._tab.data
if tabdata.inspector is None:
assert tabdata.splitter is not None
- tabdata.inspector = inspector.create(
+ tabdata.inspector = self._init_inspector(
splitter=tabdata.splitter,
win_id=self._tab.win_id)
self._tab.shutting_down.connect(tabdata.inspector.shutdown)
@@ -878,6 +878,18 @@ class AbstractTabPrivate:
tabdata.inspector.inspect(self._widget.page())
tabdata.inspector.set_position(position)
+ def _init_inspector(self, splitter: 'miscwidgets.InspectorSplitter',
+ win_id: int,
+ parent: QWidget = None) -> 'AbstractWebInspector':
+ """Get a WebKitInspector/WebEngineInspector.
+
+ Args:
+ splitter: InspectorSplitter where the inspector can be placed.
+ win_id: The window ID this inspector is associated with.
+ parent: The Qt parent to set.
+ """
+ raise NotImplementedError
+
class AbstractTab(QWidget):
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index dc0664238..f2dd282df 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -329,7 +329,9 @@ class CommandDispatcher:
# Explicit count with a tab that doesn't exist.
return
elif curtab.navigation_blocked():
- message.info("Tab is pinned!")
+ message.info("Tab is pinned! Opening in new tab.")
+ self._tabbed_browser.tabopen(cur_url)
+
else:
curtab.load_url(cur_url)
diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py
index 9ed5b52ea..2b40e97e4 100644
--- a/qutebrowser/browser/inspector.py
+++ b/qutebrowser/browser/inspector.py
@@ -30,30 +30,9 @@ from PyQt5.QtGui import QCloseEvent
from qutebrowser.browser import eventfilter
from qutebrowser.config import configfiles
-from qutebrowser.utils import log, usertypes, utils
+from qutebrowser.utils import log, usertypes
from qutebrowser.keyinput import modeman
-from qutebrowser.misc import miscwidgets, objects
-
-
-def create(*, splitter: 'miscwidgets.InspectorSplitter',
- win_id: int,
- parent: QWidget = None) -> 'AbstractWebInspector':
- """Get a WebKitInspector/WebEngineInspector.
-
- Args:
- splitter: InspectorSplitter where the inspector can be placed.
- win_id: The window ID this inspector is associated with.
- parent: The Qt parent to set.
- """
- # Importing modules here so we don't depend on QtWebEngine without the
- # argument and to avoid circular imports.
- if objects.backend == usertypes.Backend.QtWebEngine:
- from qutebrowser.browser.webengine import webengineinspector
- return webengineinspector.WebEngineInspector(splitter, win_id, parent)
- elif objects.backend == usertypes.Backend.QtWebKit:
- from qutebrowser.browser.webkit import webkitinspector
- return webkitinspector.WebKitInspector(splitter, win_id, parent)
- raise utils.Unreachable(objects.backend)
+from qutebrowser.misc import miscwidgets
class Position(enum.Enum):
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 78b475835..94332ffcb 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -23,7 +23,7 @@ import os
import sys
import html
import netrc
-from typing import Callable, Mapping, List
+from typing import Callable, Mapping, List, Optional
import tempfile
from PyQt5.QtCore import QUrl
@@ -360,23 +360,60 @@ def choose_file(multiple: bool) -> List[str]:
A list of selected file paths, or empty list if no file is selected.
If multiple is False, the return value will have at most 1 item.
"""
- handle = tempfile.NamedTemporaryFile(prefix='qutebrowser-fileselect-', delete=False)
- handle.close()
- tmpfilename = handle.name
- with utils.cleanup_file(tmpfilename):
- if multiple:
- command = config.val.fileselect.multiple_files.command
- else:
- command = config.val.fileselect.single_file.command
+ if multiple:
+ command = config.val.fileselect.multiple_files.command
+ else:
+ command = config.val.fileselect.single_file.command
+ use_tmp_file = any('{}' in arg for arg in command[1:])
+ if use_tmp_file:
+ handle = tempfile.NamedTemporaryFile(
+ prefix='qutebrowser-fileselect-',
+ delete=False,
+ )
+ handle.close()
+ tmpfilename = handle.name
+ with utils.cleanup_file(tmpfilename):
+ command = (
+ command[:1] +
+ [arg.replace('{}', tmpfilename) for arg in command[1:]]
+ )
+ return _execute_fileselect_command(
+ command=command,
+ multiple=multiple,
+ tmpfilename=tmpfilename,
+ )
+ else:
+ return _execute_fileselect_command(
+ command=command,
+ multiple=multiple,
+ )
- proc = guiprocess.GUIProcess(what='choose-file')
- proc.start(command[0],
- [arg.replace('{}', tmpfilename) for arg in command[1:]])
- loop = qtutils.EventLoop()
- proc.finished.connect(lambda _code, _status: loop.exit())
- loop.exec()
+def _execute_fileselect_command(
+ command: List[str],
+ multiple: bool,
+ tmpfilename: Optional[str] = None
+) -> List[str]:
+ """Execute external command to choose file.
+ Args:
+ multiple: Should selecting multiple files be allowed.
+ tmpfilename: Path to the temporary file if used, otherwise None.
+
+ Return:
+ A list of selected file paths, or empty list if no file is selected.
+ If multiple is False, the return value will have at most 1 item.
+ """
+ proc = guiprocess.GUIProcess(what='choose-file')
+ proc.start(command[0], command[1:])
+
+ loop = qtutils.EventLoop()
+ proc.finished.connect(lambda _code, _status: loop.exit())
+ loop.exec()
+
+ if tmpfilename is None:
+ selected_files = proc.final_stdout.splitlines()
+ else:
try:
with open(tmpfilename, mode='r', encoding=sys.getfilesystemencoding()) as f:
selected_files = f.read().splitlines()
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index a2f6682de..4a80dd623 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -34,11 +34,13 @@ from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngin
from qutebrowser.config import config
from qutebrowser.browser import browsertab, eventfilter, shared, webelem, greasemonkey
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
- webenginesettings, certificateerror)
-from qutebrowser.misc import miscwidgets, objects
+ webenginesettings, certificateerror,
+ webengineinspector)
+
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
resources, message, jinja, debug, version)
from qutebrowser.qt import sip
+from qutebrowser.misc import objects, miscwidgets
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
@@ -1207,6 +1209,9 @@ class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
def run_js_sync(self, code):
raise browsertab.UnsupportedOperationError
+ def _init_inspector(self, splitter, win_id, parent=None):
+ return webengineinspector.WebEngineInspector(splitter, win_id, parent)
+
class WebEngineTab(browsertab.AbstractTab):
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 9f15e9fb4..a1d3e5574 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -808,6 +808,9 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate):
result = document_element.evaluateJavaScript(code)
return result
+ def _init_inspector(self, splitter, win_id, parent=None):
+ return webkitinspector.WebKitInspector(splitter, win_id, parent)
+
class WebKitTab(browsertab.AbstractTab):
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index b4805665b..34d8bec96 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -1259,7 +1259,7 @@ fileselect.handler:
fileselect.single_file.command:
type:
name: ShellCommand
- placeholder: true
+ placeholder: false
completions:
- ['["xterm", "-e", "ranger", "--choosefile={}"]', "Ranger in xterm"]
- ['["xterm", "-e", "vifm", "--choose-files", "{}"]', "vifm in xterm"]
@@ -1267,16 +1267,17 @@ fileselect.single_file.command:
default: ['xterm', '-e', 'ranger', '--choosefile={}']
desc: >-
Command (and arguments) to use for selecting a single file in forms.
- The command should write the selected file path to the specified file.
+ The command should write the selected file path to the specified file or stdout.
The following placeholders are defined:
- * `{}`: Filename of the file to be written to.
+ * `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
fileselect.multiple_files.command:
type:
name: ShellCommand
- placeholder: true
+ placeholder: false
completions:
- ['["xterm", "-e", "ranger", "--choosefiles={}"]', "Ranger in xterm"]
- ['["xterm", "-e", "vifm", "--choose-files", "{}"]', "vifm in xterm"]
@@ -1284,12 +1285,13 @@ fileselect.multiple_files.command:
default: ['xterm', '-e', 'ranger', '--choosefiles={}']
desc: >-
Command (and arguments) to use for selecting multiple files in forms.
- The command should write the selected file paths to the specified file,
+ The command should write the selected file paths to the specified file or to stdout,
separated by newlines.
The following placeholders are defined:
- * `{}`: Filename of the file to be written to.
+ * `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
## hints
diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py
index 1273b227e..79c84c346 100644
--- a/qutebrowser/misc/guiprocess.py
+++ b/qutebrowser/misc/guiprocess.py
@@ -61,6 +61,9 @@ class GUIProcess(QObject):
self.cmd = None
self.args = None
+ self.final_stdout: str = ""
+ self.final_stderr: str = ""
+
self._proc = QProcess(self)
self._proc.errorOccurred.connect(self._on_error)
self._proc.errorOccurred.connect(self.error)
@@ -125,6 +128,8 @@ class GUIProcess(QObject):
log.procs.error("Process stderr:\n" + stderr.strip())
qutescheme.spawn_output = self._spawn_format(exitinfo, stdout, stderr)
+ self.final_stdout = stdout
+ self.final_stderr = stderr
def _spawn_format(self, exitinfo, stdout, stderr):
"""Produce a formatted string for spawn output."""
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index 8895da55f..612b88637 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -61,6 +61,7 @@ def whitelist_generator(): # noqa: C901
yield 'scripts.utils.bg_colors'
yield 'qutebrowser.misc.sql.SqliteErrorCode.CONSTRAINT'
yield 'qutebrowser.misc.throttle.Throttle.set_delay'
+ yield 'qutebrowser.misc.guiprocess.GUIProcess.final_stderr'
# Qt attributes
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature
index 52756422c..47cb1230a 100644
--- a/tests/end2end/features/editor.feature
+++ b/tests/end2end/features/editor.feature
@@ -6,14 +6,14 @@ Feature: Opening external editors
Scenario: Editing a URL
When I open data/numbers/1.txt
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url
Then data/numbers/2.txt should be loaded
Scenario: Editing a URL with -t
When I run :tab-only
And I open data/numbers/1.txt
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -t
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
@@ -24,7 +24,7 @@ Feature: Opening external editors
When I set tabs.new_position.related to prev
And I open data/numbers/1.txt
And I run :tab-only
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -rt
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
@@ -34,7 +34,7 @@ Feature: Opening external editors
Scenario: Editing a URL with -b
When I run :tab-only
And I open data/numbers/1.txt
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -b
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
@@ -45,7 +45,7 @@ Feature: Opening external editors
When I run :window-only
And I open data/numbers/1.txt in a new tab
And I run :tab-only
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -w
Then data/numbers/2.txt should be loaded
And the session should look like:
@@ -65,7 +65,7 @@ Feature: Opening external editors
When I open data/numbers/1.txt in a new tab
And I run :tab-only
And I run :window-only
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -p
Then data/numbers/2.txt should be loaded
And the session should look like:
@@ -90,13 +90,13 @@ Feature: Opening external editors
Scenario: Editing a URL with invalid URL
When I set url.auto_search to never
And I open data/hello.txt
- And I set up a fake editor replacing "http://localhost:(port)/data/hello.txt" by "foo!"
+ And I setup a fake editor replacing "http://localhost:(port)/data/hello.txt" by "foo!"
And I run :edit-url
Then the error "Invalid URL" should be shown
Scenario: Spawning an editor successfully
Given I have a fresh instance
- When I set up a fake editor returning "foobar"
+ When I setup a fake editor returning "foobar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -105,7 +105,7 @@ Feature: Opening external editors
Then the javascript message "text: foobar" should be logged
Scenario: Spawning an editor in normal mode
- When I set up a fake editor returning "foobar"
+ When I setup a fake editor returning "foobar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -119,7 +119,7 @@ Feature: Opening external editors
# There's no guarantee that the tab gets deleted...
@posix
Scenario: Spawning an editor and closing the tab
- When I set up a fake editor that writes "foobar" on save
+ When I setup a fake editor that writes "foobar" on save
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -134,7 +134,7 @@ Feature: Opening external editors
# Could not get signals working on Windows
@posix
Scenario: Spawning an editor and saving
- When I set up a fake editor that writes "foobar" on save
+ When I setup a fake editor that writes "foobar" on save
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -145,7 +145,7 @@ Feature: Opening external editors
Then the javascript message "text: foobar" should be logged
Scenario: Spawning an editor in caret mode
- When I set up a fake editor returning "foobar"
+ When I setup a fake editor returning "foobar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -159,7 +159,7 @@ Feature: Opening external editors
Then the javascript message "text: foobar" should be logged
Scenario: Spawning an editor with existing text
- When I set up a fake editor replacing "foo" by "bar"
+ When I setup a fake editor replacing "foo" by "bar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -173,20 +173,20 @@ Feature: Opening external editors
Scenario: Edit a command and run it
When I run :set-cmd-text :message-info foo
- And I set up a fake editor replacing "foo" by "bar"
+ And I setup a fake editor replacing "foo" by "bar"
And I run :edit-command --run
Then the message "bar" should be shown
And "Leaving mode KeyMode.command (reason: cmd accept)" should be logged
Scenario: Edit a command and omit the start char
- When I set up a fake editor returning "message-info foo"
+ When I setup a fake editor returning "message-info foo"
And I run :edit-command
Then the error "command must start with one of :/?" should be shown
And "Leaving mode KeyMode.command *" should not be logged
Scenario: Edit a command to be empty
When I run :set-cmd-text :
- When I set up a fake editor returning empty text
+ When I setup a fake editor returning empty text
And I run :edit-command
Then the error "command must start with one of :/?" should be shown
And "Leaving mode KeyMode.command *" should not be logged
@@ -194,13 +194,20 @@ Feature: Opening external editors
## select single file
Scenario: Select one file with single command
- When I set up a fake "single_file" fileselector selecting "tests/end2end/data/numbers/1.txt"
+ When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to a temporary file
+ And I open data/fileselect.html
+ And I run :click-element id single_file
+ Then the javascript message "Files: 1.txt" should be logged
+
+ Scenario: Select one file with single command that writes to stdout
+ When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to stdout
And I open data/fileselect.html
And I run :click-element id single_file
Then the javascript message "Files: 1.txt" should be logged
Scenario: Select two files with single command
- When I set up a fake "single_file" fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt"
+ When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt" and writes to a temporary file
+
And I open data/fileselect.html
And I run :click-element id single_file
Then the javascript message "Files: 1.txt" should be logged
@@ -209,13 +216,15 @@ Feature: Opening external editors
## select multiple files
Scenario: Select one file with multiple command
- When I set up a fake "multiple_files" fileselector selecting "tests/end2end/data/numbers/1.txt"
+ When I setup a fake multiple_files fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to a temporary file
+
And I open data/fileselect.html
And I run :click-element id multiple_files
Then the javascript message "Files: 1.txt" should be logged
Scenario: Select two files with multiple command
- When I set up a fake "multiple_files" fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt"
+ When I setup a fake multiple_files fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt" and writes to a temporary file
+
And I open data/fileselect.html
And I run :click-element id multiple_files
Then the javascript message "Files: 1.txt, 2.txt" should be logged
diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature
index 0f0c015e0..e48947cbd 100644
--- a/tests/end2end/features/sessions.feature
+++ b/tests/end2end/features/sessions.feature
@@ -395,9 +395,10 @@ Feature: Saving and loading sessions
And I run :session-load -c pin_session
And I wait until data/numbers/3.txt is loaded
And I run :tab-focus 2
- And I run :open hello world
- Then the message "Tab is pinned!" should be shown
+ And I open data/numbers/4.txt
+ Then the message "Tab is pinned! Opening in new tab." should be shown
And the following tabs should be open:
- data/numbers/1.txt
- data/numbers/2.txt (active) (pinned)
+ - data/numbers/4.txt
- data/numbers/3.txt
diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature
index ca0efefc4..7db054573 100644
--- a/tests/end2end/features/tabs.feature
+++ b/tests/end2end/features/tabs.feature
@@ -1534,10 +1534,11 @@ Feature: Tab management
Scenario: :tab-pin open url
When I open data/numbers/1.txt
And I run :tab-pin
- And I open data/numbers/2.txt without waiting
- Then the message "Tab is pinned!" should be shown
+ And I open data/numbers/2.txt
+ Then the message "Tab is pinned! Opening in new tab." should be shown
And the following tabs should be open:
- data/numbers/1.txt (active) (pinned)
+ - data/numbers/2.txt
Scenario: :tab-pin open url with tabs.pinned.frozen = false
When I set tabs.pinned.frozen to false
diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py
index 445691bee..40f77a0f7 100644
--- a/tests/end2end/features/test_editor_bdd.py
+++ b/tests/end2end/features/test_editor_bdd.py
@@ -32,7 +32,7 @@ bdd.scenarios('editor.feature')
from qutebrowser.utils import utils
-@bdd.when(bdd.parsers.parse('I set up a fake editor replacing "{text}" by '
+@bdd.when(bdd.parsers.parse('I setup a fake editor replacing "{text}" by '
'"{replacement}"'))
def set_up_editor_replacement(quteproc, server, tmpdir, text, replacement):
"""Set up editor.command to a small python script doing a replacement."""
@@ -53,7 +53,7 @@ def set_up_editor_replacement(quteproc, server, tmpdir, text, replacement):
quteproc.set_setting('editor.command', editor)
-@bdd.when(bdd.parsers.parse('I set up a fake editor returning "{text}"'))
+@bdd.when(bdd.parsers.parse('I setup a fake editor returning "{text}"'))
def set_up_editor(quteproc, tmpdir, text):
"""Set up editor.command to a small python script inserting a text."""
script = tmpdir / 'script.py'
@@ -67,7 +67,7 @@ def set_up_editor(quteproc, tmpdir, text):
quteproc.set_setting('editor.command', editor)
-@bdd.when(bdd.parsers.parse('I set up a fake editor returning empty text'))
+@bdd.when(bdd.parsers.parse('I setup a fake editor returning empty text'))
def set_up_editor_empty(quteproc, tmpdir):
"""Set up editor.command to a small python script inserting empty text."""
set_up_editor(quteproc, tmpdir, "")
@@ -107,7 +107,7 @@ def editor_pid_watcher(tmpdir):
return EditorPidWatcher(tmpdir)
-@bdd.when(bdd.parsers.parse('I set up a fake editor that writes "{text}" on '
+@bdd.when(bdd.parsers.parse('I setup a fake editor that writes "{text}" on '
'save'))
def set_up_editor_wait(quteproc, tmpdir, text, editor_pid_watcher):
"""Set up editor.command to a small python script inserting a text."""
@@ -180,18 +180,31 @@ def save_editor_wait(tmpdir):
os.kill(pid, signal.SIGUSR2)
-@bdd.when(bdd.parsers.parse('I set up a fake "{kind}" fileselector '
- 'selecting "{files}"'))
-def set_up_fileselector(quteproc, py_proc, kind, files):
+@bdd.when(bdd.parsers.parse('I setup a fake {kind} fileselector '
+ 'selecting "{files}" and writes to {output_type}'))
+def set_up_fileselector(quteproc, py_proc, kind, files, output_type):
"""Set up fileselect.xxx.command to select the file(s)."""
cmd, args = py_proc(r"""
import os
import sys
- tmp_file = sys.argv[1]
- with open(tmp_file, 'w') as f:
- for selected_file in sys.argv[2:]:
- f.write(os.path.abspath(selected_file) + "\n")
+ tmp_file = None
+ for i, arg in enumerate(sys.argv):
+ if arg.startswith('--file='):
+ tmp_file = arg[len('--file='):]
+ sys.argv.pop(i)
+ break
+ selected_files = sys.argv[1:]
+ if tmp_file is None:
+ for selected_file in selected_files:
+ print(os.path.abspath(selected_file))
+ else:
+ with open(tmp_file, 'w') as f:
+ for selected_file in selected_files:
+ f.write(os.path.abspath(selected_file) + '\n')
""")
- fileselect_cmd = json.dumps([cmd, *args, '{}', *files.split(' ')])
+ args += files.split(' ')
+ if output_type == "a temporary file":
+ args += ['--file={}']
+ fileselect_cmd = json.dumps([cmd, *args])
quteproc.set_setting('fileselect.handler', 'external')
quteproc.set_setting(f'fileselect.{kind}.command', fileselect_cmd)
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index 8a6b24557..22e9c6490 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -28,7 +28,7 @@ from datetime import datetime
from unittest import mock
import hypothesis
-import hypothesis.strategies
+import hypothesis.strategies as hst
import pytest
from PyQt5.QtCore import QUrl, QDateTime
try:
@@ -459,9 +459,10 @@ def test_filesystem_completion_model_interface(info, local_files_path):
@hypothesis.given(
- as_uri=hypothesis.strategies.booleans(),
- add_sep=hypothesis.strategies.booleans(),
- text=hypothesis.strategies.text(),
+ as_uri=hst.booleans(),
+ add_sep=hst.booleans(),
+ text=hst.text(alphabet=hst.characters(
+ blacklist_categories=['Cc'], blacklist_characters='\x00')),
)
def test_filesystem_completion_hypothesis(info, as_uri, add_sep, text):
if as_uri:
@@ -1445,7 +1446,7 @@ def undo_completion_retains_sort_order(tabbed_browser_stubs, info):
_check_completions(model, {"Closed tabs": expected})
-@hypothesis.given(text=hypothesis.strategies.text())
+@hypothesis.given(text=hst.text())
def test_listcategory_hypothesis(text):
"""Make sure we can't produce invalid patterns."""
cat = listcategory.ListCategory("test", [])
diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py
index a2acad1ac..9e1b3916c 100644
--- a/tests/unit/misc/test_guiprocess.py
+++ b/tests/unit/misc/test_guiprocess.py
@@ -127,9 +127,11 @@ def test_start_output_message(proc, qtbot, caplog, message_mock, py_proc,
if stdout_msg is not None:
assert stdout_msg.level == usertypes.MessageLevel.info
assert stdout_msg.text == 'stdout text'
+ assert proc.final_stdout.strip() == "stdout text", proc.final_stdout
if stderr_msg is not None:
assert stderr_msg.level == usertypes.MessageLevel.error
assert stderr_msg.text == 'stderr text'
+ assert proc.final_stderr.strip() == "stderr text", proc.final_stderr
def test_start_env(monkeypatch, qtbot, py_proc):