summaryrefslogtreecommitdiff
path: root/.config/qutebrowser/scripts
diff options
context:
space:
mode:
Diffstat (limited to '.config/qutebrowser/scripts')
-rwxr-xr-x.config/qutebrowser/scripts/__init__.py3
-rwxr-xr-x.config/qutebrowser/scripts/asciidoc2html.py303
-rw-r--r--.config/qutebrowser/scripts/cycle-inputs.js46
-rw-r--r--.config/qutebrowser/scripts/dev/Makefile-dmg71
-rw-r--r--.config/qutebrowser/scripts/dev/__init__.py3
-rwxr-xr-x.config/qutebrowser/scripts/dev/build_release.py419
-rw-r--r--.config/qutebrowser/scripts/dev/check_coverage.py348
-rwxr-xr-x.config/qutebrowser/scripts/dev/check_doc_changes.py48
-rw-r--r--.config/qutebrowser/scripts/dev/ci/travis_backtrace.sh18
-rw-r--r--.config/qutebrowser/scripts/dev/ci/travis_install.sh108
-rw-r--r--.config/qutebrowser/scripts/dev/ci/travis_run.sh32
-rwxr-xr-x.config/qutebrowser/scripts/dev/cleanup.py69
-rw-r--r--.config/qutebrowser/scripts/dev/download_release.sh34
-rw-r--r--.config/qutebrowser/scripts/dev/gen_resources.py26
-rw-r--r--.config/qutebrowser/scripts/dev/gen_versioninfo.py85
-rw-r--r--.config/qutebrowser/scripts/dev/get_coredumpctl_traces.py176
-rw-r--r--.config/qutebrowser/scripts/dev/misc_checks.py163
-rw-r--r--.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/__init__.py1
-rw-r--r--.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/config.py84
-rw-r--r--.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/modeline.py63
-rw-r--r--.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/openencoding.py83
-rw-r--r--.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/settrace.py49
-rw-r--r--.config/qutebrowser/scripts/dev/pylint_checkers/setup.py25
-rwxr-xr-x.config/qutebrowser/scripts/dev/quit_segfault_test.sh14
-rw-r--r--.config/qutebrowser/scripts/dev/recompile_requirements.py136
-rwxr-xr-x.config/qutebrowser/scripts/dev/run_profile.py93
-rw-r--r--.config/qutebrowser/scripts/dev/run_pylint_on_tests.py79
-rwxr-xr-x.config/qutebrowser/scripts/dev/run_vulture.py196
-rwxr-xr-x.config/qutebrowser/scripts/dev/segfault_test.py120
-rwxr-xr-x.config/qutebrowser/scripts/dev/src2asciidoc.py561
-rw-r--r--.config/qutebrowser/scripts/dev/standardpaths_tester.py69
-rw-r--r--.config/qutebrowser/scripts/dev/strip_whitespace.sh10
-rwxr-xr-x.config/qutebrowser/scripts/dev/ua_fetch.py122
-rwxr-xr-x.config/qutebrowser/scripts/dev/update_3rdparty.py166
-rwxr-xr-x.config/qutebrowser/scripts/dictcli.py283
-rwxr-xr-x.config/qutebrowser/scripts/hist_importer.py174
-rwxr-xr-x.config/qutebrowser/scripts/hostblock_blame.py55
-rwxr-xr-x.config/qutebrowser/scripts/importer.py349
-rwxr-xr-x.config/qutebrowser/scripts/keytester.py56
-rwxr-xr-x.config/qutebrowser/scripts/link_pyqt.py233
-rwxr-xr-x.config/qutebrowser/scripts/open_url_in_instance.sh15
-rwxr-xr-x.config/qutebrowser/scripts/setupcommon.py74
-rw-r--r--.config/qutebrowser/scripts/testbrowser/cpp/webengine/main.cpp13
-rw-r--r--.config/qutebrowser/scripts/testbrowser/cpp/webengine/testbrowser.pro6
-rw-r--r--.config/qutebrowser/scripts/testbrowser/cpp/webkit/main.cpp13
-rw-r--r--.config/qutebrowser/scripts/testbrowser/cpp/webkit/testbrowser.pro6
-rwxr-xr-x.config/qutebrowser/scripts/testbrowser/testbrowser_webengine.py50
-rwxr-xr-x.config/qutebrowser/scripts/testbrowser/testbrowser_webkit.py56
-rwxr-xr-x.config/qutebrowser/scripts/utils.py103
49 files changed, 5309 insertions, 0 deletions
diff --git a/.config/qutebrowser/scripts/__init__.py b/.config/qutebrowser/scripts/__init__.py
new file mode 100755
index 0000000..90be1e0
--- /dev/null
+++ b/.config/qutebrowser/scripts/__init__.py
@@ -0,0 +1,3 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+"""Various utility scripts."""
diff --git a/.config/qutebrowser/scripts/asciidoc2html.py b/.config/qutebrowser/scripts/asciidoc2html.py
new file mode 100755
index 0000000..c4af174
--- /dev/null
+++ b/.config/qutebrowser/scripts/asciidoc2html.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Generate the html documentation based on the asciidoc files."""
+
+import re
+import os
+import os.path
+import sys
+import subprocess
+import glob
+import shutil
+import tempfile
+import argparse
+import io
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
+
+from scripts import utils
+
+
+class AsciiDoc:
+
+ """Abstraction of an asciidoc subprocess."""
+
+ FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts']
+
+ def __init__(self, args):
+ self._cmd = None
+ self._args = args
+ self._homedir = None
+ self._themedir = None
+ self._tempdir = None
+ self._failed = False
+
+ def prepare(self):
+ """Get the asciidoc command and create the homedir to use."""
+ self._cmd = self._get_asciidoc_cmd()
+ self._homedir = tempfile.mkdtemp()
+ self._themedir = os.path.join(
+ self._homedir, '.asciidoc', 'themes', 'qute')
+ self._tempdir = os.path.join(self._homedir, 'tmp')
+ os.makedirs(self._tempdir)
+ os.makedirs(self._themedir)
+
+ def cleanup(self):
+ """Clean up the temporary home directory for asciidoc."""
+ if self._homedir is not None and not self._failed:
+ shutil.rmtree(self._homedir)
+
+ def build(self):
+ """Build either the website or the docs."""
+ if self._args.website:
+ self._build_website()
+ else:
+ self._build_docs()
+ self._copy_images()
+
+ def _build_docs(self):
+ """Render .asciidoc files to .html sites."""
+ files = [('doc/{}.asciidoc'.format(f),
+ 'qutebrowser/html/doc/{}.html'.format(f))
+ for f in self.FILES]
+ for src in glob.glob('doc/help/*.asciidoc'):
+ name, _ext = os.path.splitext(os.path.basename(src))
+ dst = 'qutebrowser/html/doc/{}.html'.format(name)
+ files.append((src, dst))
+
+ # patch image links to use local copy
+ replacements = [
+ ("https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png",
+ "qute://help/img/cheatsheet-big.png"),
+ ("https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png",
+ "qute://help/img/cheatsheet-small.png")
+ ]
+ asciidoc_args = ['-a', 'source-highlighter=pygments']
+
+ for src, dst in files:
+ src_basename = os.path.basename(src)
+ modified_src = os.path.join(self._tempdir, src_basename)
+ with open(modified_src, 'w', encoding='utf-8') as modified_f, \
+ open(src, 'r', encoding='utf-8') as f:
+ for line in f:
+ for orig, repl in replacements:
+ line = line.replace(orig, repl)
+ modified_f.write(line)
+ self.call(modified_src, dst, *asciidoc_args)
+
+ def _copy_images(self):
+ """Copy image files to qutebrowser/html/doc."""
+ print("Copying files...")
+ dst_path = os.path.join('qutebrowser', 'html', 'doc', 'img')
+ try:
+ os.mkdir(dst_path)
+ except FileExistsError:
+ pass
+ for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']:
+ src = os.path.join('doc', 'img', filename)
+ dst = os.path.join(dst_path, filename)
+ shutil.copy(src, dst)
+
+ def _build_website_file(self, root, filename):
+ """Build a single website file."""
+ src = os.path.join(root, filename)
+ src_basename = os.path.basename(src)
+ parts = [self._args.website[0]]
+ dirname = os.path.dirname(src)
+ if dirname:
+ parts.append(os.path.relpath(os.path.dirname(src)))
+ parts.append(
+ os.extsep.join((os.path.splitext(src_basename)[0],
+ 'html')))
+ dst = os.path.join(*parts)
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
+
+ modified_src = os.path.join(self._tempdir, src_basename)
+ shutil.copy('www/header.asciidoc', modified_src)
+
+ outfp = io.StringIO()
+
+ with open(modified_src, 'r', encoding='utf-8') as header_file:
+ header = header_file.read()
+ header += "\n\n"
+
+ with open(src, 'r', encoding='utf-8') as infp:
+ outfp.write("\n\n")
+ hidden = False
+ found_title = False
+ title = ""
+ last_line = ""
+
+ for line in infp:
+ line = line.rstrip()
+ if line == '// QUTE_WEB_HIDE':
+ assert not hidden
+ hidden = True
+ elif line == '// QUTE_WEB_HIDE_END':
+ assert hidden
+ hidden = False
+ elif line == "The Compiler <mail@qutebrowser.org>":
+ continue
+ elif re.fullmatch(r':\w+:.*', line):
+ # asciidoc field
+ continue
+
+ if not found_title:
+ if re.fullmatch(r'=+', line):
+ line = line.replace('=', '-')
+ found_title = True
+ title = last_line + " | qutebrowser\n"
+ title += "=" * (len(title) - 1)
+ elif re.fullmatch(r'= .+', line):
+ line = '==' + line[1:]
+ found_title = True
+ title = last_line + " | qutebrowser\n"
+ title += "=" * (len(title) - 1)
+
+ if not hidden:
+ outfp.write(line.replace(".asciidoc[", ".html[") + '\n')
+ last_line = line
+
+ current_lines = outfp.getvalue()
+ outfp.close()
+
+ with open(modified_src, 'w+', encoding='utf-8') as final_version:
+ final_version.write(title + "\n\n" + header + current_lines)
+
+ asciidoc_args = ['--theme=qute', '-a toc', '-a toc-placement=manual',
+ '-a', 'source-highlighter=pygments']
+ self.call(modified_src, dst, *asciidoc_args)
+
+ def _build_website(self):
+ """Prepare and build the website."""
+ theme_file = os.path.abspath(os.path.join('www', 'qute.css'))
+ shutil.copy(theme_file, self._themedir)
+
+ outdir = self._args.website[0]
+
+ for root, _dirs, files in os.walk(os.getcwd()):
+ for filename in files:
+ basename, ext = os.path.splitext(filename)
+ if (ext != '.asciidoc' or
+ basename in ['header', 'OpenSans-License']):
+ continue
+ self._build_website_file(root, filename)
+
+ copy = {'icons': 'icons', 'doc/img': 'doc/img', 'www/media': 'media/'}
+
+ for src, dest in copy.items():
+ full_dest = os.path.join(outdir, dest)
+ try:
+ shutil.rmtree(full_dest)
+ except FileNotFoundError:
+ pass
+ shutil.copytree(src, full_dest)
+
+ for dst, link_name in [
+ ('README.html', 'index.html'),
+ (os.path.join('doc', 'quickstart.html'), 'quickstart.html')]:
+ try:
+ os.symlink(dst, os.path.join(outdir, link_name))
+ except FileExistsError:
+ pass
+
+ def _get_asciidoc_cmd(self):
+ """Try to find out what commandline to use to invoke asciidoc."""
+ if self._args.asciidoc is not None:
+ return self._args.asciidoc
+
+ try:
+ subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ except OSError:
+ pass
+ else:
+ return ['asciidoc']
+
+ try:
+ subprocess.run(['asciidoc.py'], stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ except OSError:
+ pass
+ else:
+ return ['asciidoc.py']
+
+ raise FileNotFoundError
+
+ def call(self, src, dst, *args):
+ """Call asciidoc for the given files.
+
+ Args:
+ src: The source .asciidoc file.
+ dst: The destination .html file, or None to auto-guess.
+ *args: Additional arguments passed to asciidoc.
+ """
+ print("Calling asciidoc for {}...".format(os.path.basename(src)))
+ cmdline = self._cmd[:]
+ if dst is not None:
+ cmdline += ['--out-file', dst]
+ cmdline += args
+ cmdline.append(src)
+ try:
+ env = os.environ.copy()
+ env['HOME'] = self._homedir
+ subprocess.run(cmdline, check=True, env=env)
+ except (subprocess.CalledProcessError, OSError) as e:
+ self._failed = True
+ utils.print_col(str(e), 'red')
+ print("Keeping modified sources in {}.".format(self._homedir))
+ sys.exit(1)
+
+
+def main(colors=False):
+ """Generate html files for the online documentation."""
+ utils.change_cwd()
+ utils.use_color = colors
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--website', help="Build website into a given "
+ "directory.", nargs=1)
+ parser.add_argument('--asciidoc', help="Full path to python and "
+ "asciidoc.py. If not given, it's searched in PATH.",
+ nargs=2, required=False,
+ metavar=('PYTHON', 'ASCIIDOC'))
+ args = parser.parse_args()
+ try:
+ os.mkdir('qutebrowser/html/doc')
+ except FileExistsError:
+ pass
+
+ asciidoc = AsciiDoc(args)
+ try:
+ asciidoc.prepare()
+ except FileNotFoundError:
+ utils.print_col("Could not find asciidoc! Please install it, or use "
+ "the --asciidoc argument to point this script to the "
+ "correct python/asciidoc.py location!", 'red')
+ sys.exit(1)
+
+ try:
+ asciidoc.build()
+ finally:
+ asciidoc.cleanup()
+
+
+if __name__ == '__main__':
+ main(colors=True)
diff --git a/.config/qutebrowser/scripts/cycle-inputs.js b/.config/qutebrowser/scripts/cycle-inputs.js
new file mode 100644
index 0000000..bb667bd
--- /dev/null
+++ b/.config/qutebrowser/scripts/cycle-inputs.js
@@ -0,0 +1,46 @@
+/* Cycle <input> text boxes.
+ * works with the types defined in 'types'.
+ * Note: Does not work for <textarea>.
+ *
+ * Example keybind:
+ * CYCLE_INPUTS = "jseval -q -f ~/.config/qutebrowser/cycle-inputs.js"
+ * config.bind('gi', CYCLE_INPUTS)
+ *
+ * By dive on freenode <dave@dawoodfall.net>
+ */
+
+(function() {
+ "use strict";
+ const inputs = document.getElementsByTagName("input");
+ const types = /text|password|date|email|month|number|range|search|tel|time|url|week/;
+ const hidden = /hidden/;
+ let found = false;
+
+ function ishidden(el) {
+ return hidden.test(el.attributes.value) || el.offsetParent === null;
+ }
+
+ for (let i = 0; i < inputs.length; i++) {
+ if (inputs[i] === document.activeElement) {
+ for (let k = i + 1; k < inputs.length; k++) {
+ if (!ishidden(inputs[k]) && types.test(inputs[k].type)) {
+ inputs[k].focus();
+ found = true;
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ if (!found) {
+ for (let i = 0; i < inputs.length; i++) {
+ if (!ishidden(inputs[i]) && types.test(inputs[i].type)) {
+ inputs[i].focus();
+ break;
+ }
+ }
+ }
+})();
+
+// vim: tw=0 expandtab tabstop=4 softtabstop=4 shiftwidth=4
diff --git a/.config/qutebrowser/scripts/dev/Makefile-dmg b/.config/qutebrowser/scripts/dev/Makefile-dmg
new file mode 100644
index 0000000..1cf4cfb
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/Makefile-dmg
@@ -0,0 +1,71 @@
+#
+# Build file for creating DMG files.
+#
+# The DMG packager looks for a template.dmg.bz2 for using as its
+# DMG template. If it doesn't find one, it generates a clean one.
+#
+# If you create a DMG template, you should make one containing all
+# the files listed in $(SOURCE_FILES) below, and arrange everything to suit
+# your style. The contents of the files themselves does not matter, so
+# they can be empty (they will be overwritten later).
+#
+# Remko Tronçon
+# https://el-tramo.be
+# Licensed under the MIT License. See COPYING for details.
+
+
+################################################################################
+# Customizable variables
+################################################################################
+
+NAME ?= qutebrowser
+
+SOURCE_DIR ?= .
+SOURCE_FILES ?= dist/qutebrowser.app LICENSE
+
+TEMPLATE_DMG ?= template.dmg
+TEMPLATE_SIZE ?= 300m
+
+################################################################################
+# DMG building. No editing should be needed beyond this point.
+################################################################################
+
+MASTER_DMG=$(NAME).dmg
+WC_DMG=wc.dmg
+WC_DIR=wc
+
+.PHONY: all
+all: $(MASTER_DMG)
+
+$(TEMPLATE_DMG):
+ @echo
+ @echo --------------------- Generating empty template --------------------
+ mkdir template
+ hdiutil create -fs HFSX -layout SPUD -size $(TEMPLATE_SIZE) "$(TEMPLATE_DMG)" -srcfolder template -format UDRW -volname "$(NAME)" -quiet
+ rmdir template
+
+$(WC_DMG): $(TEMPLATE_DMG)
+ cp $< $@
+
+$(MASTER_DMG): $(WC_DMG) $(addprefix $(SOURCE_DIR)/,$(SOURCE_FILES))
+ @echo
+ @echo --------------------- Creating Disk Image --------------------
+ mkdir -p $(WC_DIR)
+ hdiutil attach "$(WC_DMG)" -noautoopen -quiet -mountpoint "$(WC_DIR)"
+ for i in $(SOURCE_FILES); do \
+ rm -rf "$(WC_DIR)/$$i"; \
+ ditto -rsrc "$(SOURCE_DIR)/$$i" "$(WC_DIR)/$${i##*/}"; \
+ done
+ ln -s /Applications $(WC_DIR)
+ #rm -f "$@"
+ #hdiutil create -srcfolder "$(WC_DIR)" -format UDZO -imagekey zlib-level=9 "$@" -volname "$(NAME) $(VERSION)" -scrub -quiet
+ WC_DEV=`hdiutil info | grep "$(WC_DIR)" | grep "Apple_HFS" | awk '{print $$1}'` && \
+ hdiutil detach $$WC_DEV -quiet -force
+ rm -f "$(MASTER_DMG)"
+ hdiutil convert "$(WC_DMG)" -quiet -format UDZO -imagekey zlib-level=9 -o "$@"
+ rm -rf $(WC_DIR)
+ @echo
+
+.PHONY: clean
+clean:
+ -rm -rf $(TEMPLATE_DMG) $(MASTER_DMG) $(WC_DMG)
diff --git a/.config/qutebrowser/scripts/dev/__init__.py b/.config/qutebrowser/scripts/dev/__init__.py
new file mode 100644
index 0000000..7dc0433
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/__init__.py
@@ -0,0 +1,3 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+"""Various scripts used for developing qutebrowser."""
diff --git a/.config/qutebrowser/scripts/dev/build_release.py b/.config/qutebrowser/scripts/dev/build_release.py
new file mode 100755
index 0000000..254132b
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/build_release.py
@@ -0,0 +1,419 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Build a new release."""
+
+
+import os
+import os.path
+import sys
+import time
+import glob
+import shutil
+import plistlib
+import subprocess
+import argparse
+import tarfile
+import tempfile
+import collections
+
+try:
+ import winreg
+except ImportError:
+ pass
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+import qutebrowser
+from scripts import utils
+# from scripts.dev import update_3rdparty
+
+
+def call_script(name, *args, python=sys.executable):
+ """Call a given shell script.
+
+ Args:
+ name: The script to call.
+ *args: The arguments to pass.
+ python: The python interpreter to use.
+ """
+ path = os.path.join(os.path.dirname(__file__), os.pardir, name)
+ subprocess.run([python, path] + list(args), check=True)
+
+
+def call_tox(toxenv, *args, python=sys.executable):
+ """Call tox.
+
+ Args:
+ toxenv: Which tox environment to use
+ *args: The arguments to pass.
+ python: The python interpreter to use.
+ """
+ env = os.environ.copy()
+ env['PYTHON'] = python
+ env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python)
+ subprocess.run(
+ [sys.executable, '-m', 'tox', '-vv', '-e', toxenv] + list(args),
+ env=env, check=True)
+
+
+def run_asciidoc2html(args):
+ """Common buildsteps used for all OS'."""
+ utils.print_title("Running asciidoc2html.py")
+ if args.asciidoc is not None:
+ a2h_args = ['--asciidoc'] + args.asciidoc
+ else:
+ a2h_args = []
+ call_script('asciidoc2html.py', *a2h_args)
+
+
+def _maybe_remove(path):
+ """Remove a path if it exists."""
+ try:
+ shutil.rmtree(path)
+ except FileNotFoundError:
+ pass
+
+
+def smoke_test(executable):
+ """Try starting the given qutebrowser executable."""
+ subprocess.run([executable, '--no-err-windows', '--nowindow',
+ '--temp-basedir', 'about:blank', ':later 500 quit'],
+ check=True)
+
+
+def patch_mac_app():
+ """Patch .app to copy missing data and link some libs.
+
+ See https://github.com/pyinstaller/pyinstaller/issues/2276
+ """
+ app_path = os.path.join('dist', 'qutebrowser.app')
+ qtwe_core_dir = os.path.join('.tox', 'pyinstaller', 'lib', 'python3.6',
+ 'site-packages', 'PyQt5', 'Qt', 'lib',
+ 'QtWebEngineCore.framework')
+ # Copy QtWebEngineProcess.app
+ proc_app = 'QtWebEngineProcess.app'
+ shutil.copytree(os.path.join(qtwe_core_dir, 'Helpers', proc_app),
+ os.path.join(app_path, 'Contents', 'MacOS', proc_app))
+ # Copy resources
+ for f in glob.glob(os.path.join(qtwe_core_dir, 'Resources', '*')):
+ dest = os.path.join(app_path, 'Contents', 'Resources')
+ if os.path.isdir(f):
+ dir_dest = os.path.join(dest, os.path.basename(f))
+ print("Copying directory {} to {}".format(f, dir_dest))
+ shutil.copytree(f, dir_dest)
+ else:
+ print("Copying {} to {}".format(f, dest))
+ shutil.copy(f, dest)
+ # Link dependencies
+ for lib in ['QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtNetwork',
+ 'QtGui', 'QtWebChannel', 'QtPositioning']:
+ dest = os.path.join(app_path, lib + '.framework', 'Versions', '5')
+ os.makedirs(dest)
+ os.symlink(os.path.join(os.pardir, os.pardir, os.pardir, 'Contents',
+ 'MacOS', lib),
+ os.path.join(dest, lib))
+ # Patch Info.plist - pyinstaller's options are too limiting
+ plist_path = os.path.join(app_path, 'Contents', 'Info.plist')
+ with open(plist_path, "rb") as f:
+ plist_data = plistlib.load(f)
+ plist_data.update(INFO_PLIST_UPDATES)
+ with open(plist_path, "wb") as f:
+ plistlib.dump(plist_data, f)
+
+
+INFO_PLIST_UPDATES = {
+ 'CFBundleVersion': qutebrowser.__version__,
+ 'CFBundleShortVersionString': qutebrowser.__version__,
+ 'NSSupportsAutomaticGraphicsSwitching': True,
+ 'NSHighResolutionCapable': True,
+ 'CFBundleURLTypes': [{
+ "CFBundleURLName": "http(s) URL",
+ "CFBundleURLSchemes": ["http", "https"]
+ }, {
+ "CFBundleURLName": "local file URL",
+ "CFBundleURLSchemes": ["file"]
+ }],
+ 'CFBundleDocumentTypes': [{
+ "CFBundleTypeExtensions": ["html", "htm"],
+ "CFBundleTypeMIMETypes": ["text/html"],
+ "CFBundleTypeName": "HTML document",
+ "CFBundleTypeOSTypes": ["HTML"],
+ "CFBundleTypeRole": "Viewer",
+ }, {
+ "CFBundleTypeExtensions": ["xhtml"],
+ "CFBundleTypeMIMETypes": ["text/xhtml"],
+ "CFBundleTypeName": "XHTML document",
+ "CFBundleTypeRole": "Viewer",
+ }]
+}
+
+
+def build_mac():
+ """Build macOS .dmg/.app."""
+ utils.print_title("Cleaning up...")
+ for f in ['wc.dmg', 'template.dmg']:
+ try:
+ os.remove(f)
+ except FileNotFoundError:
+ pass
+ for d in ['dist', 'build']:
+ shutil.rmtree(d, ignore_errors=True)
+ utils.print_title("Updating 3rdparty content")
+ # Currently disabled because QtWebEngine has no pdfjs support
+ # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
+ utils.print_title("Building .app via pyinstaller")
+ call_tox('pyinstaller', '-r')
+ utils.print_title("Patching .app")
+ patch_mac_app()
+ utils.print_title("Building .dmg")
+ subprocess.run(['make', '-f', 'scripts/dev/Makefile-dmg'], check=True)
+
+ dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__)
+ os.rename('qutebrowser.dmg', dmg_name)
+
+ utils.print_title("Running smoke test")
+
+ try:
+ with tempfile.TemporaryDirectory() as tmpdir:
+ subprocess.run(['hdiutil', 'attach', dmg_name,
+ '-mountpoint', tmpdir], check=True)
+ try:
+ binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents',
+ 'MacOS', 'qutebrowser')
+ smoke_test(binary)
+ finally:
+ time.sleep(5)
+ subprocess.run(['hdiutil', 'detach', tmpdir])
+ except PermissionError as e:
+ print("Failed to remove tempdir: {}".format(e))
+
+ return [(dmg_name, 'application/x-apple-diskimage', 'macOS .dmg')]
+
+
+def patch_windows(out_dir):
+ """Copy missing DLLs for windows into the given output."""
+ dll_dir = os.path.join('.tox', 'pyinstaller', 'lib', 'site-packages',
+ 'PyQt5', 'Qt', 'bin')
+ dlls = ['libEGL.dll', 'libGLESv2.dll', 'libeay32.dll', 'ssleay32.dll']
+ for dll in dlls:
+ shutil.copy(os.path.join(dll_dir, dll), out_dir)
+
+
+def build_windows():
+ """Build windows executables/setups."""
+ utils.print_title("Updating 3rdparty content")
+ # Currently disabled because QtWebEngine has no pdfjs support
+ # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
+
+ utils.print_title("Building Windows binaries")
+ parts = str(sys.version_info.major), str(sys.version_info.minor)
+ ver = ''.join(parts)
+ dot_ver = '.'.join(parts)
+
+ # Get python path from registry if possible
+ try:
+ reg64_key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,
+ r'SOFTWARE\Python\PythonCore'
+ r'\{}\InstallPath'.format(dot_ver))
+ python_x64 = winreg.QueryValueEx(reg64_key, 'ExecutablePath')[0]
+ except FileNotFoundError:
+ python_x64 = r'C:\Python{}\python.exe'.format(ver)
+
+ out_pyinstaller = os.path.join('dist', 'qutebrowser')
+ out_64 = os.path.join('dist',
+ 'qutebrowser-{}-x64'.format(qutebrowser.__version__))
+
+ artifacts = []
+
+ from scripts.dev import gen_versioninfo
+ utils.print_title("Updating VersionInfo file")
+ gen_versioninfo.main()
+
+ utils.print_title("Running pyinstaller 64bit")
+ _maybe_remove(out_64)
+ call_tox('pyinstaller', '-r', python=python_x64)
+ shutil.move(out_pyinstaller, out_64)
+ patch_windows(out_64)
+
+ utils.print_title("Building installers")
+ subprocess.run(['makensis.exe',
+ '/DX64',
+ '/DVERSION={}'.format(qutebrowser.__version__),
+ 'misc/qutebrowser.nsi'], check=True)
+
+ name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
+
+ artifacts += [
+ (os.path.join('dist', name_64),
+ 'application/vnd.microsoft.portable-executable',
+ 'Windows 64bit installer'),
+ ]
+
+ utils.print_title("Running 64bit smoke test")
+ smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
+
+ utils.print_title("Zipping 64bit standalone...")
+ name = 'qutebrowser-{}-windows-standalone-amd64'.format(
+ qutebrowser.__version__)
+ shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64))
+ artifacts.append(('{}.zip'.format(name),
+ 'application/zip',
+ 'Windows 64bit standalone'))
+
+ return artifacts
+
+
+def build_sdist():
+ """Build an sdist and list the contents."""
+ utils.print_title("Building sdist")
+
+ _maybe_remove('dist')
+
+ subprocess.run([sys.executable, 'setup.py', 'sdist'], check=True)
+ dist_files = os.listdir(os.path.abspath('dist'))
+ assert len(dist_files) == 1
+
+ dist_file = os.path.join('dist', dist_files[0])
+ subprocess.run(['gpg', '--detach-sign', '-a', dist_file], check=True)
+
+ tar = tarfile.open(dist_file)
+ by_ext = collections.defaultdict(list)
+
+ for tarinfo in tar.getmembers():
+ if not tarinfo.isfile():
+ continue
+ name = os.sep.join(tarinfo.name.split(os.sep)[1:])
+ _base, ext = os.path.splitext(name)
+ by_ext[ext].append(name)
+
+ assert '.pyc' not in by_ext
+
+ utils.print_title("sdist contents")
+
+ for ext, files in sorted(by_ext.items()):
+ utils.print_subtitle(ext)
+ print('\n'.join(files))
+
+ filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__)
+ artifacts = [
+ (os.path.join('dist', filename), 'application/gzip', 'Source release'),
+ (os.path.join('dist', filename + '.asc'), 'application/pgp-signature',
+ 'Source release - PGP signature'),
+ ]
+
+ return artifacts
+
+
+def test_makefile():
+ """Make sure the Makefile works correctly."""
+ utils.print_title("Testing makefile")
+ with tempfile.TemporaryDirectory() as tmpdir:
+ subprocess.run(['make', '-f', 'misc/Makefile',
+ 'DESTDIR={}'.format(tmpdir), 'install'], check=True)
+
+
+def read_github_token():
+ """Read the GitHub API token from disk."""
+ token_file = os.path.join(os.path.expanduser('~'), '.gh_token')
+ with open(token_file, encoding='ascii') as f:
+ token = f.read().strip()
+ return token
+
+
+def github_upload(artifacts, tag):
+ """Upload the given artifacts to GitHub.
+
+ Args:
+ artifacts: A list of (filename, mimetype, description) tuples
+ tag: The name of the release tag
+ """
+ import github3
+ utils.print_title("Uploading to github...")
+
+ token = read_github_token()
+ gh = github3.login(token=token)
+ repo = gh.repository('qutebrowser', 'qutebrowser')
+
+ release = None # to satisfy pylint
+ for release in repo.releases():
+ if release.tag_name == tag:
+ break
+ else:
+ raise Exception("No release found for {!r}!".format(tag))
+
+ for filename, mimetype, description in artifacts:
+ with open(filename, 'rb') as f:
+ basename = os.path.basename(filename)
+ asset = release.upload_asset(mimetype, basename, f)
+ asset.edit(basename, description)
+
+
+def pypi_upload(artifacts):
+ """Upload the given artifacts to PyPI using twine."""
+ filenames = [a[0] for a in artifacts]
+ subprocess.run(['twine', 'upload'] + filenames, check=True)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--asciidoc', help="Full path to python and "
+ "asciidoc.py. If not given, it's searched in PATH.",
+ nargs=2, required=False,
+ metavar=('PYTHON', 'ASCIIDOC'))
+ parser.add_argument('--upload', help="Tag to upload the release for",
+ nargs=1, required=False, metavar='TAG')
+ args = parser.parse_args()
+ utils.change_cwd()
+
+ upload_to_pypi = False
+
+ if args.upload is not None:
+ # Fail early when trying to upload without github3 installed
+ # or without API token
+ import github3 # pylint: disable=unused-variable
+ read_github_token()
+
+ run_asciidoc2html(args)
+ if os.name == 'nt':
+ artifacts = build_windows()
+ elif sys.platform == 'darwin':
+ artifacts = build_mac()
+ else:
+ test_makefile()
+ artifacts = build_sdist()
+ upload_to_pypi = True
+
+ if args.upload is not None:
+ utils.print_title("Press enter to release...")
+ input()
+ github_upload(artifacts, args.upload[0])
+ if upload_to_pypi:
+ pypi_upload(artifacts)
+ else:
+ print()
+ utils.print_title("Artifacts")
+ for artifact in artifacts:
+ print(artifact)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/check_coverage.py b/.config/qutebrowser/scripts/dev/check_coverage.py
new file mode 100644
index 0000000..32c5afc
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/check_coverage.py
@@ -0,0 +1,348 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2015-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Enforce perfect coverage on some files."""
+
+import os
+import os.path
+import sys
+import enum
+import subprocess
+from xml.etree import ElementTree
+
+import attr
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+from scripts import utils as scriptutils
+from qutebrowser.utils import utils
+
+
+@attr.s
+class Message:
+
+ """A message shown by coverage.py."""
+
+ typ = attr.ib()
+ filename = attr.ib()
+ text = attr.ib()
+
+
+MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')
+
+
+# A list of (test_file, tested_file) tuples. test_file can be None.
+PERFECT_FILES = [
+ (None,
+ 'commands/cmdexc.py'),
+ ('tests/unit/commands/test_cmdutils.py',
+ 'commands/cmdutils.py'),
+ ('tests/unit/commands/test_argparser.py',
+ 'commands/argparser.py'),
+
+ ('tests/unit/browser/webkit/test_cache.py',
+ 'browser/webkit/cache.py'),
+ ('tests/unit/browser/webkit/test_cookies.py',
+ 'browser/webkit/cookies.py'),
+ ('tests/unit/browser/test_history.py',
+ 'browser/history.py'),
+ ('tests/unit/browser/webkit/http/test_http.py',
+ 'browser/webkit/http.py'),
+ ('tests/unit/browser/webkit/http/test_content_disposition.py',
+ 'browser/webkit/rfc6266.py'),
+ # ('tests/unit/browser/webkit/test_webkitelem.py',
+ # 'browser/webkit/webkitelem.py'),
+ # ('tests/unit/browser/webkit/test_webkitelem.py',
+ # 'browser/webelem.py'),
+ ('tests/unit/browser/webkit/network/test_filescheme.py',
+ 'browser/webkit/network/filescheme.py'),
+ ('tests/unit/browser/webkit/network/test_networkreply.py',
+ 'browser/webkit/network/networkreply.py'),
+
+ ('tests/unit/browser/test_signalfilter.py',
+ 'browser/signalfilter.py'),
+ (None,
+ 'browser/webengine/certificateerror.py'),
+ # ('tests/unit/browser/test_tab.py',
+ # 'browser/tab.py'),
+
+ ('tests/unit/keyinput/test_basekeyparser.py',
+ 'keyinput/basekeyparser.py'),
+ ('tests/unit/keyinput/test_keyutils.py',
+ 'keyinput/keyutils.py'),
+
+ ('tests/unit/misc/test_autoupdate.py',
+ 'misc/autoupdate.py'),
+ ('tests/unit/misc/test_readline.py',
+ 'misc/readline.py'),
+ ('tests/unit/misc/test_split.py',
+ 'misc/split.py'),
+ ('tests/unit/misc/test_msgbox.py',
+ 'misc/msgbox.py'),
+ ('tests/unit/misc/test_checkpyver.py',
+ 'misc/checkpyver.py'),
+ ('tests/unit/misc/test_guiprocess.py',
+ 'misc/guiprocess.py'),
+ ('tests/unit/misc/test_editor.py',
+ 'misc/editor.py'),
+ ('tests/unit/misc/test_cmdhistory.py',
+ 'misc/cmdhistory.py'),
+ ('tests/unit/misc/test_ipc.py',
+ 'misc/ipc.py'),
+ ('tests/unit/misc/test_keyhints.py',
+ 'misc/keyhintwidget.py'),
+ ('tests/unit/misc/test_pastebin.py',
+ 'misc/pastebin.py'),
+ (None,
+ 'misc/objects.py'),
+
+ (None,
+ 'mainwindow/statusbar/keystring.py'),
+ ('tests/unit/mainwindow/statusbar/test_percentage.py',
+ 'mainwindow/statusbar/percentage.py'),
+ ('tests/unit/mainwindow/statusbar/test_progress.py',
+ 'mainwindow/statusbar/progress.py'),
+ ('tests/unit/mainwindow/statusbar/test_tabindex.py',
+ 'mainwindow/statusbar/tabindex.py'),
+ ('tests/unit/mainwindow/statusbar/test_textbase.py',
+ 'mainwindow/statusbar/textbase.py'),
+ ('tests/unit/mainwindow/statusbar/test_url.py',
+ 'mainwindow/statusbar/url.py'),
+ ('tests/unit/mainwindow/statusbar/test_backforward.py',
+ 'mainwindow/statusbar/backforward.py'),
+ ('tests/unit/mainwindow/test_messageview.py',
+ 'mainwindow/messageview.py'),
+
+ ('tests/unit/config/test_config.py',
+ 'config/config.py'),
+ ('tests/unit/config/test_configdata.py',
+ 'config/configdata.py'),
+ ('tests/unit/config/test_configexc.py',
+ 'config/configexc.py'),
+ ('tests/unit/config/test_configfiles.py',
+ 'config/configfiles.py'),
+ ('tests/unit/config/test_configtypes.py',
+ 'config/configtypes.py'),
+ ('tests/unit/config/test_configinit.py',
+ 'config/configinit.py'),
+ ('tests/unit/config/test_configcommands.py',
+ 'config/configcommands.py'),
+ ('tests/unit/config/test_configutils.py',
+ 'config/configutils.py'),
+
+ ('tests/unit/utils/test_qtutils.py',
+ 'utils/qtutils.py'),
+ ('tests/unit/utils/test_standarddir.py',
+ 'utils/standarddir.py'),
+ ('tests/unit/utils/test_urlutils.py',
+ 'utils/urlutils.py'),
+ ('tests/unit/utils/usertypes',
+ 'utils/usertypes.py'),
+ ('tests/unit/utils/test_utils.py',
+ 'utils/utils.py'),
+ ('tests/unit/utils/test_version.py',
+ 'utils/version.py'),
+ ('tests/unit/utils/test_debug.py',
+ 'utils/debug.py'),
+ ('tests/unit/utils/test_jinja.py',
+ 'utils/jinja.py'),
+ ('tests/unit/utils/test_error.py',
+ 'utils/error.py'),
+ ('tests/unit/utils/test_javascript.py',
+ 'utils/javascript.py'),
+ ('tests/unit/utils/test_urlmatch.py',
+ 'utils/urlmatch.py'),
+
+ (None,
+ 'completion/models/util.py'),
+ ('tests/unit/completion/test_models.py',
+ 'completion/models/urlmodel.py'),
+ ('tests/unit/completion/test_models.py',
+ 'completion/models/configmodel.py'),
+ ('tests/unit/completion/test_histcategory.py',
+ 'completion/models/histcategory.py'),
+ ('tests/unit/completion/test_listcategory.py',
+ 'completion/models/listcategory.py'),
+
+ ('tests/unit/browser/webengine/test_spell.py',
+ 'browser/webengine/spell.py'),
+
+]
+
+
+# 100% coverage because of end2end tests, but no perfect unit tests yet.
+WHITELISTED_FILES = [
+ 'browser/webkit/webkitinspector.py',
+ 'keyinput/macros.py',
+ 'browser/webkit/webkitelem.py',
+]
+
+
+class Skipped(Exception):
+
+ """Exception raised when skipping coverage checks."""
+
+ def __init__(self, reason):
+ self.reason = reason
+ super().__init__("Skipping coverage checks " + reason)
+
+
+def _get_filename(filename):
+ """Transform the absolute test filenames to relative ones."""
+ if os.path.isabs(filename):
+ basedir = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '..', '..'))
+ common_path = os.path.commonprefix([basedir, filename])
+ if common_path:
+ filename = filename[len(common_path):].lstrip('/')
+ if filename.startswith('qutebrowser/'):
+ filename = filename.split('/', maxsplit=1)[1]
+
+ return filename
+
+
+def check(fileobj, perfect_files):
+ """Main entry point which parses/checks coverage.xml if applicable."""
+ if not utils.is_linux:
+ raise Skipped("on non-Linux system.")
+ elif '-k' in sys.argv[1:]:
+ raise Skipped("because -k is given.")
+ elif '-m' in sys.argv[1:]:
+ raise Skipped("because -m is given.")
+ elif '--lf' in sys.argv[1:]:
+ raise Skipped("because --lf is given.")
+
+ perfect_src_files = [e[1] for e in perfect_files]
+
+ filename_args = [arg for arg in sys.argv[1:]
+ if arg.startswith('tests' + os.sep)]
+ filtered_files = [tpl[1] for tpl in perfect_files if tpl[0] in
+ filename_args]
+
+ if filename_args and not filtered_files:
+ raise Skipped("because there is nothing to check.")
+
+ tree = ElementTree.parse(fileobj)
+ classes = tree.getroot().findall('./packages/package/classes/class')
+
+ messages = []
+
+ for klass in classes:
+ filename = _get_filename(klass.attrib['filename'])
+
+ line_cov = float(klass.attrib['line-rate']) * 100
+ branch_cov = float(klass.attrib['branch-rate']) * 100
+
+ if filtered_files and filename not in filtered_files:
+ continue
+
+ assert 0 <= line_cov <= 100, line_cov
+ assert 0 <= branch_cov <= 100, branch_cov
+ assert '\\' not in filename, filename
+
+ is_bad = line_cov < 100 or branch_cov < 100
+
+ if filename in perfect_src_files and is_bad:
+ text = "{} has {:.2f}% line and {:.2f}% branch coverage!".format(
+ filename, line_cov, branch_cov)
+ messages.append(Message(MsgType.insufficent_coverage, filename,
+ text))
+ elif (filename not in perfect_src_files and not is_bad and
+ filename not in WHITELISTED_FILES):
+ text = ("{} has 100% coverage but is not in "
+ "perfect_files!".format(filename))
+ messages.append(Message(MsgType.perfect_file, filename, text))
+
+ return messages
+
+
+def main_check():
+ """Check coverage after a test run."""
+ try:
+ with open('coverage.xml', encoding='utf-8') as f:
+ messages = check(f, PERFECT_FILES)
+ except Skipped as e:
+ print(e)
+ messages = []
+
+ if messages:
+ print()
+ print()
+ scriptutils.print_title("Coverage check failed")
+ for msg in messages:
+ print(msg.text)
+ print()
+ filters = ','.join('qutebrowser/' + msg.filename for msg in messages)
+ subprocess.run([sys.executable, '-m', 'coverage', 'report',
+ '--show-missing', '--include', filters], check=True)
+ print()
+ print("To debug this, run 'tox -e py36-pyqt59-cov' "
+ "(or py35-pyqt59-cov) locally and check htmlcov/index.html")
+ print("or check https://codecov.io/github/qutebrowser/qutebrowser")
+ print()
+
+ if 'CI' in os.environ:
+ print("Keeping coverage.xml on CI.")
+ else:
+ os.remove('coverage.xml')
+ return 1 if messages else 0
+
+
+def main_check_all():
+ """Check the coverage for all files individually.
+
+ This makes sure the files have 100% coverage without running unrelated
+ tests.
+
+ This runs pytest with the used executable, so check_coverage.py should be
+ called with something like ./.tox/py36/bin/python.
+ """
+ for test_file, src_file in PERFECT_FILES:
+ if test_file is None:
+ continue
+ subprocess.run(
+ [sys.executable, '-m', 'pytest', '--cov', 'qutebrowser',
+ '--cov-report', 'xml', test_file], check=True)
+ with open('coverage.xml', encoding='utf-8') as f:
+ messages = check(f, [(test_file, src_file)])
+ os.remove('coverage.xml')
+
+ messages = [msg for msg in messages
+ if msg.typ == MsgType.insufficent_coverage]
+ if messages:
+ for msg in messages:
+ print(msg.text)
+ return 1
+ else:
+ print("Check ok!")
+ return 0
+
+
+def main():
+ scriptutils.change_cwd()
+ if '--check-all' in sys.argv:
+ return main_check_all()
+ else:
+ return main_check()
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/.config/qutebrowser/scripts/dev/check_doc_changes.py b/.config/qutebrowser/scripts/dev/check_doc_changes.py
new file mode 100755
index 0000000..3d90bea
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/check_doc_changes.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2016-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Check if docs changed and output an error if so."""
+
+import sys
+import subprocess
+import os
+
+code = subprocess.run(['git', '--no-pager', 'diff',
+ '--exit-code', '--stat']).returncode
+
+if os.environ.get('TRAVIS_PULL_REQUEST', 'false') != 'false':
+ if code != 0:
+ print("Docs changed but ignoring change as we're building a PR")
+ sys.exit(0)
+
+if code != 0:
+ print()
+ print('The autogenerated docs changed, please run this to update them:')
+ print(' tox -e docs')
+ print(' git commit -am "Update docs"')
+ print()
+ print('(Or you have uncommitted changes, in which case you can ignore '
+ 'this.)')
+ if 'TRAVIS' in os.environ:
+ print()
+ print("travis_fold:start:gitdiff")
+ subprocess.run(['git', '--no-pager', 'diff'])
+ print("travis_fold:end:gitdiff")
+sys.exit(code)
diff --git a/.config/qutebrowser/scripts/dev/ci/travis_backtrace.sh b/.config/qutebrowser/scripts/dev/ci/travis_backtrace.sh
new file mode 100644
index 0000000..227dde8
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/ci/travis_backtrace.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+#
+# Find all possible core files under current directory. Attempt
+# to determine exe using file(1) and dump stack trace with gdb.
+#
+
+case $TESTENV in
+ py3*-pyqt*)
+ exe=$(readlink -f ".tox/$TESTENV/bin/python")
+ full=
+ ;;
+ *)
+ echo "Skipping coredump analysis in testenv $TESTENV!"
+ exit 0
+ ;;
+esac
+
+find . \( -name "*.core" -o -name core \) -exec gdb --batch --quiet -ex "thread apply all bt $full" "$exe" {} \;
diff --git a/.config/qutebrowser/scripts/dev/ci/travis_install.sh b/.config/qutebrowser/scripts/dev/ci/travis_install.sh
new file mode 100644
index 0000000..18f5aa9
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/ci/travis_install.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+# Stolen from https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh
+# and adjusted to use ((...))
+travis_retry() {
+ local ANSI_RED='\033[31;1m'
+ local ANSI_RESET='\033[0m'
+ local result=0
+ local count=1
+ while (( count < 3 )); do
+ if (( result != 0 )); then
+ echo -e "\\n${ANSI_RED}The command \"$*\" failed. Retrying, $count of 3.${ANSI_RESET}\\n" >&2
+ fi
+ "$@"
+ result=$?
+ (( result == 0 )) && break
+ count=$(( count + 1 ))
+ sleep 1
+ done
+
+ if (( count > 3 )); then
+ echo -e "\\n${ANSI_RED}The command \"$*\" failed 3 times.${ANSI_RESET}\\n" >&2
+ fi
+
+ return $result
+}
+
+pip_install() {
+ travis_retry python3 -m pip install "$@"
+}
+
+npm_install() {
+ # Make sure npm is up-to-date first
+ travis_retry npm install -g npm
+ travis_retry npm install -g "$@"
+}
+
+check_pyqt() {
+ python3 <<EOF
+import sys
+from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
+try:
+ from PyQt.sip import SIP_VERSION_STR
+except ModuleNotFoundError:
+ from sip import SIP_VERSION_STR
+
+print("Python {}".format(sys.version))
+print("PyQt5 {}".format(PYQT_VERSION_STR))
+print("Qt5 {} (runtime {})".format(QT_VERSION_STR, qVersion()))
+print("sip {}".format(SIP_VERSION_STR))
+EOF
+}
+
+set -e
+
+if [[ $DOCKER ]]; then
+ exit 0
+elif [[ $TRAVIS_OS_NAME == osx ]]; then
+ # Disable App Nap
+ defaults write NSGlobalDomain NSAppSleepDisabled -bool YES
+
+ curl -LO https://bootstrap.pypa.io/get-pip.py
+ sudo -H python get-pip.py
+
+ brew --version
+ brew update
+ brew upgrade python libyaml
+ brew install qt5 pyqt5
+
+ pip_install -r misc/requirements/requirements-tox.txt
+ python3 -m pip --version
+ tox --version
+ check_pyqt
+ exit 0
+fi
+
+case $TESTENV in
+ eslint)
+ npm_install eslint
+ ;;
+ shellcheck)
+ ;;
+ *)
+ pip_install pip
+ pip_install -r misc/requirements/requirements-tox.txt
+ if [[ $TESTENV == *-cov ]]; then
+ pip_install -r misc/requirements/requirements-codecov.txt
+ fi
+ ;;
+esac
diff --git a/.config/qutebrowser/scripts/dev/ci/travis_run.sh b/.config/qutebrowser/scripts/dev/ci/travis_run.sh
new file mode 100644
index 0000000..55ca7c1
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/ci/travis_run.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+if [[ $DOCKER ]]; then
+ docker run \
+ --privileged \
+ -v "$PWD:/outside" \
+ -e "QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE" \
+ -e "DOCKER=$DOCKER" \
+ -e "CI=$CI" \
+ -e "TRAVIS=$TRAVIS" \
+ "qutebrowser/travis:$DOCKER"
+elif [[ $TESTENV == eslint ]]; then
+ # Can't run this via tox as we can't easily install tox in the javascript
+ # travis env
+ cd qutebrowser/javascript || exit 1
+ eslint --color --report-unused-disable-directives .
+elif [[ $TESTENV == shellcheck ]]; then
+ SCRIPTS=$( mktemp )
+ find scripts/dev/ -name '*.sh' >"$SCRIPTS"
+ find misc/userscripts/ -type f -exec grep -lE '[/ ][bd]ash$|[/ ]sh$|[/ ]ksh$' {} + >>"$SCRIPTS"
+ mapfile -t scripts <"$SCRIPTS"
+ rm -f "$SCRIPTS"
+ docker run \
+ -v "$PWD:/outside" \
+ -w /outside \
+ koalaman/shellcheck:latest "${scripts[@]}"
+else
+ args=()
+ [[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb' 'tests/unit')
+
+ tox -e "$TESTENV" -- "${args[@]}"
+fi
diff --git a/.config/qutebrowser/scripts/dev/cleanup.py b/.config/qutebrowser/scripts/dev/cleanup.py
new file mode 100755
index 0000000..d1bb84a
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/cleanup.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Script to clean up the mess made by Python/setuptools/PyInstaller."""
+
+import os
+import os.path
+import sys
+import glob
+import shutil
+import fnmatch
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+from scripts import utils
+
+
+recursive_lint = ('__pycache__', '*.pyc')
+lint = ('build', 'dist', 'pkg/pkg', 'pkg/qutebrowser-*.pkg.tar.xz', 'pkg/src',
+ 'pkg/qutebrowser', 'qutebrowser.egg-info', 'setuptools-*.egg',
+ 'setuptools-*.zip', 'doc/qutebrowser.asciidoc', 'doc/*.html',
+ 'doc/qutebrowser.1', 'README.html', 'qutebrowser/html/doc')
+
+
+def remove(path):
+ """Remove either a file or directory unless --dry-run is given."""
+ if os.path.isdir(path):
+ print("rm -r '{}'".format(path))
+ if '--dry-run' not in sys.argv:
+ shutil.rmtree(path)
+ else:
+ print("rm '{}'".format(path))
+ if '--dry-run' not in sys.argv:
+ os.remove(path)
+
+
+def main():
+ """Clean up lint in the current dir."""
+ utils.change_cwd()
+ for elem in lint:
+ for f in glob.glob(elem):
+ remove(f)
+
+ for root, _dirs, _files in os.walk(os.getcwd()):
+ path = os.path.basename(root)
+ if any(fnmatch.fnmatch(path, e) for e in recursive_lint):
+ remove(root)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/download_release.sh b/.config/qutebrowser/scripts/dev/download_release.sh
new file mode 100644
index 0000000..207da21
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/download_release.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+# This script downloads the given release from GitHub so we can mirror it on
+# qutebrowser.org.
+
+tmpdir=$(mktemp -d)
+oldpwd=$PWD
+
+if [[ $# != 1 ]]; then
+ echo "Usage: $0 <version>" >&2
+ exit 1
+fi
+
+cd "$tmpdir"
+mkdir windows
+
+base="https://github.com/qutebrowser/qutebrowser/releases/download/v$1"
+
+wget "$base/qutebrowser-$1.tar.gz"
+wget "$base/qutebrowser-$1.tar.gz.asc"
+wget "$base/qutebrowser-$1.dmg"
+wget "$base/qutebrowser_${1}-1_all.deb"
+
+cd windows
+wget "$base/qutebrowser-${1}-amd64.msi"
+wget "$base/qutebrowser-${1}-win32.msi"
+wget "$base/qutebrowser-${1}-windows-standalone-amd64.zip"
+wget "$base/qutebrowser-${1}-windows-standalone-win32.zip"
+
+dest="/srv/http/qutebrowser/releases/v$1"
+cd "$oldpwd"
+sudo mv "$tmpdir" "$dest"
+sudo chown -R http:http "$dest"
diff --git a/.config/qutebrowser/scripts/dev/gen_resources.py b/.config/qutebrowser/scripts/dev/gen_resources.py
new file mode 100644
index 0000000..cbfc69b
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/gen_resources.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# copyright 2014 florian bruhin (the compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Generate Qt resources based on source files."""
+
+import subprocess
+
+with open('qutebrowser/resources.py', 'w', encoding='utf-8') as f:
+ subprocess.run(['pyrcc5', 'qutebrowser.rcc'], stdout=f, check=True)
diff --git a/.config/qutebrowser/scripts/dev/gen_versioninfo.py b/.config/qutebrowser/scripts/dev/gen_versioninfo.py
new file mode 100644
index 0000000..1aa4b64
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/gen_versioninfo.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Generate file_version_info.txt for Pyinstaller use with Windows builds."""
+
+import os.path
+import sys
+
+# pylint: disable=import-error,no-member,useless-suppression
+from PyInstaller.utils.win32 import versioninfo as vs
+# pylint: enable=import-error,no-member,useless-suppression
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+import qutebrowser
+from scripts import utils
+
+
+def main():
+ utils.change_cwd()
+ out_filename = 'misc/file_version_info.txt'
+
+ filevers = qutebrowser.__version_info__ + (0,)
+ prodvers = qutebrowser.__version_info__ + (0,)
+ str_filevers = qutebrowser.__version__
+ str_prodvers = qutebrowser.__version__
+
+ comment_text = qutebrowser.__doc__
+ copyright_text = qutebrowser.__copyright__
+ trademark_text = ("qutebrowser is free software under the GNU General "
+ "Public License")
+
+ # https://www.science.co.il/language/Locale-codes.php#definitions
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756.aspx
+ en_us = 1033 # 0x0409
+ utf_16 = 1200 # 0x04B0
+
+ ffi = vs.FixedFileInfo(filevers, prodvers)
+
+ kids = [
+ vs.StringFileInfo([
+ # 0x0409: MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)
+ # 0x04B0: codepage 1200 (UTF-16LE)
+ vs.StringTable('040904B0', [
+ vs.StringStruct('Comments', comment_text),
+ vs.StringStruct('CompanyName', "qutebrowser.org"),
+ vs.StringStruct('FileDescription', "qutebrowser"),
+ vs.StringStruct('FileVersion', str_filevers),
+ vs.StringStruct('InternalName', "qutebrowser"),
+ vs.StringStruct('LegalCopyright', copyright_text),
+ vs.StringStruct('LegalTrademarks', trademark_text),
+ vs.StringStruct('OriginalFilename', "qutebrowser.exe"),
+ vs.StringStruct('ProductName', "qutebrowser"),
+ vs.StringStruct('ProductVersion', str_prodvers)
+ ]),
+ ]),
+ vs.VarFileInfo([vs.VarStruct('Translation', [en_us, utf_16])]),
+ ]
+
+ file_version_info = vs.VSVersionInfo(ffi, kids)
+
+ with open(out_filename, 'w', encoding='utf-8') as f:
+ f.write(str(file_version_info))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/get_coredumpctl_traces.py b/.config/qutebrowser/scripts/dev/get_coredumpctl_traces.py
new file mode 100644
index 0000000..d286d38
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/get_coredumpctl_traces.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2015-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Get qutebrowser crash information and stacktraces from coredumpctl."""
+
+import os
+import os.path
+import sys
+import argparse
+import subprocess
+import tempfile
+
+import attr
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+from scripts import utils
+
+
+@attr.s
+class Line:
+
+ """A line in "coredumpctl list"."""
+
+ time = attr.ib()
+ pid = attr.ib()
+ uid = attr.ib()
+ gid = attr.ib()
+ sig = attr.ib()
+ present = attr.ib()
+ exe = attr.ib()
+
+
+def _convert_present(data):
+ """Convert " "/"*" to True/False for parse_coredumpctl_line."""
+ if data == '*':
+ return True
+ elif data == ' ':
+ return False
+ else:
+ raise ValueError(data)
+
+
+def parse_coredumpctl_line(line):
+ """Parse a given string coming from coredumpctl and return a Line object.
+
+ Example input:
+ Mon 2015-09-28 23:22:24 CEST 10606 1000 1000 11 /usr/bin/python3.4
+ """
+ fields = {
+ 'time': (0, 28, str),
+ 'pid': (29, 35, int),
+ 'uid': (36, 41, int),
+ 'gid': (42, 47, int),
+ 'sig': (48, 51, int),
+ 'present': (52, 53, _convert_present),
+ 'exe': (54, None, str),
+ }
+
+ data = {}
+ for name, (start, end, converter) in fields.items():
+ data[name] = converter(line[start:end])
+ return Line(**data)
+
+
+def get_info(pid):
+ """Get and parse "coredumpctl info" output for the given PID."""
+ data = {}
+ output = subprocess.run(['coredumpctl', 'info', str(pid)], check=True,
+ stdout=subprocess.PIPE).stdout
+ output = output.decode('utf-8')
+ for line in output.split('\n'):
+ if not line.strip():
+ continue
+ try:
+ key, value = line.split(':', maxsplit=1)
+ except ValueError:
+ # systemd stack output
+ continue
+ data[key.strip()] = value.strip()
+ return data
+
+
+def is_qutebrowser_dump(parsed):
+ """Check if the given Line is a qutebrowser dump."""
+ basename = os.path.basename(parsed.exe)
+ if basename == 'python' or basename.startswith('python3'):
+ info = get_info(parsed.pid)
+ try:
+ cmdline = info['Command Line']
+ except KeyError:
+ return True
+ else:
+ return '-m qutebrowser' in cmdline
+ else:
+ return basename == 'qutebrowser'
+
+
+def dump_infos_gdb(parsed):
+ """Dump all needed infos for the given crash using gdb."""
+ with tempfile.TemporaryDirectory() as tempdir:
+ coredump = os.path.join(tempdir, 'dump')
+ subprocess.run(['coredumpctl', 'dump', '-o', coredump,
+ str(parsed.pid)], check=True)
+ subprocess.run(['gdb', parsed.exe, coredump,
+ '-ex', 'info threads',
+ '-ex', 'thread apply all bt full',
+ '-ex', 'quit'], check=True)
+
+
+def dump_infos(parsed):
+ """Dump all possible infos for the given crash."""
+ if not parsed.present:
+ info = get_info(parsed.pid)
+ print("{}: Signal {} with no coredump: {}".format(
+ parsed.time, info.get('Signal', None),
+ info.get('Command Line', None)))
+ else:
+ print('\n\n\n')
+ utils.print_title('{} - {}'.format(parsed.time, parsed.pid))
+ sys.stdout.flush()
+ dump_infos_gdb(parsed)
+
+
+def check_prerequisites():
+ """Check if coredumpctl/gdb are installed."""
+ for binary in ['coredumpctl', 'gdb']:
+ try:
+ subprocess.run([binary, '--version'], check=True)
+ except FileNotFoundError:
+ print("{} is needed to run this script!".format(binary),
+ file=sys.stderr)
+ sys.exit(1)
+
+
+def main():
+ check_prerequisites()
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--all', help="Also list crashes without coredumps.",
+ action='store_true')
+ args = parser.parse_args()
+
+ coredumps = subprocess.run(['coredumpctl', 'list'], check=True,
+ stdout=subprocess.PIPE).stdout
+ lines = coredumps.decode('utf-8').split('\n')
+ for line in lines[1:]:
+ if not line.strip():
+ continue
+ parsed = parse_coredumpctl_line(line)
+ if not parsed.present and not args.all:
+ continue
+ if is_qutebrowser_dump(parsed):
+ dump_infos(parsed)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/misc_checks.py b/.config/qutebrowser/scripts/dev/misc_checks.py
new file mode 100644
index 0000000..2992464
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/misc_checks.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Various small code checkers."""
+
+import os
+import os.path
+import re
+import sys
+import argparse
+import subprocess
+import tokenize
+import traceback
+import collections
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+from scripts import utils
+
+
+def _get_files(only_py=False):
+ """Iterate over all python files and yield filenames."""
+ for (dirpath, _dirnames, filenames) in os.walk('.'):
+ parts = dirpath.split(os.sep)
+ if len(parts) >= 2:
+ rootdir = parts[1]
+ if rootdir.startswith('.') or rootdir == 'htmlcov':
+ # ignore hidden dirs and htmlcov
+ continue
+
+ if only_py:
+ endings = {'.py'}
+ else:
+ endings = {'.py', '.asciidoc', '.js', '.feature'}
+ files = (e for e in filenames if os.path.splitext(e)[1] in endings)
+ for name in files:
+ yield os.path.join(dirpath, name)
+
+
+def check_git():
+ """Check for uncommitted git files.."""
+ if not os.path.isdir(".git"):
+ print("No .git dir, ignoring")
+ print()
+ return False
+ untracked = []
+ gitst = subprocess.run(['git', 'status', '--porcelain'], check=True,
+ stdout=subprocess.PIPE).stdout
+ gitst = gitst.decode('UTF-8').strip()
+ for line in gitst.splitlines():
+ s, name = line.split(maxsplit=1)
+ if s == '??' and name != '.venv/':
+ untracked.append(name)
+ status = True
+ if untracked:
+ status = False
+ utils.print_col("Untracked files:", 'red')
+ print('\n'.join(untracked))
+ print()
+ return status
+
+
+def check_spelling():
+ """Check commonly misspelled words."""
+ # Words which I often misspell
+ words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
+ '[Oo]ccur[^rs .]', '[Ss]eperator', '[Ee]xplicitely',
+ '[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
+ '[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
+ '[Rr]eproducable', '[Aa]n [Uu]ser', '[Cc]onvienience',
+ '[Ww]ether', '[Pp]rogramatically', '[Ss]plitted', '[Ee]xitted',
+ '[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily',
+ '[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting',
+ 'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations'}
+
+ # Words which look better when splitted, but might need some fine tuning.
+ words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence',
+ '[Nn]ormalmode', '[Ee]ventloops', '[Ss]izehint',
+ '[Ss]tatemachine', '[Mm]etaobject', '[Ll]ogrecord',
+ '[Ff]iletype'}
+
+ # Files which should be ignored, e.g. because they come from another
+ # package
+ ignored = [
+ os.path.join('.', 'scripts', 'dev', 'misc_checks.py'),
+ os.path.join('.', 'qutebrowser', '3rdparty', 'pdfjs'),
+ os.path.join('.', 'tests', 'end2end', 'data', 'hints', 'ace',
+ 'ace.js'),
+ ]
+
+ seen = collections.defaultdict(list)
+ try:
+ ok = True
+ for fn in _get_files():
+ with tokenize.open(fn) as f:
+ if any(fn.startswith(i) for i in ignored):
+ continue
+ for line in f:
+ for w in words:
+ if (re.search(w, line) and
+ fn not in seen[w] and
+ '# pragma: no spellcheck' not in line):
+ print('Found "{}" in {}!'.format(w, fn))
+ seen[w].append(fn)
+ ok = False
+ print()
+ return ok
+ except Exception:
+ traceback.print_exc()
+ return None
+
+
+def check_vcs_conflict():
+ """Check VCS conflict markers."""
+ try:
+ ok = True
+ for fn in _get_files(only_py=True):
+ with tokenize.open(fn) as f:
+ for line in f:
+ if any(line.startswith(c * 7) for c in '<>=|'):
+ print("Found conflict marker in {}".format(fn))
+ ok = False
+ print()
+ return ok
+ except Exception:
+ traceback.print_exc()
+ return None
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('checker', choices=('git', 'vcs', 'spelling'),
+ help="Which checker to run.")
+ args = parser.parse_args()
+ if args.checker == 'git':
+ ok = check_git()
+ elif args.checker == 'vcs':
+ ok = check_vcs_conflict()
+ elif args.checker == 'spelling':
+ ok = check_spelling()
+ return 0 if ok else 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/__init__.py b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/__init__.py
new file mode 100644
index 0000000..1341a93
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/__init__.py
@@ -0,0 +1 @@
+"""Custom pylint checkers."""
diff --git a/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/config.py b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/config.py
new file mode 100644
index 0000000..5aa5250
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/config.py
@@ -0,0 +1,84 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Custom astroid checker for config calls."""
+
+import sys
+import pathlib
+
+import yaml
+import astroid
+from pylint import interfaces, checkers
+from pylint.checkers import utils
+
+
+OPTIONS = None
+FAILED_LOAD = False
+
+
+class ConfigChecker(checkers.BaseChecker):
+
+ """Custom astroid checker for config calls."""
+
+ __implements__ = interfaces.IAstroidChecker
+ name = 'config'
+ msgs = {
+ 'E9998': ('%s is no valid config option.', # flake8: disable=S001
+ 'bad-config-option',
+ None),
+ }
+ priority = -1
+ printed_warning = False
+
+ @utils.check_messages('bad-config-option')
+ def visit_attribute(self, node):
+ """Visit a getattr node."""
+ # At the end of a config.val.foo.bar chain
+ if not isinstance(node.parent, astroid.Attribute):
+ # FIXME:conf do some proper check for this...
+ node_str = node.as_string()
+ prefix = 'config.val.'
+ if node_str.startswith(prefix):
+ self._check_config(node, node_str[len(prefix):])
+
+ def _check_config(self, node, name):
+ """Check that we're accessing proper config options."""
+ if FAILED_LOAD:
+ if not ConfigChecker.printed_warning:
+ print("[WARN] Could not find configdata.yml. Please run "
+ "pylint from qutebrowser root.", file=sys.stderr)
+ print("Skipping some checks...", file=sys.stderr)
+ ConfigChecker.printed_warning = True
+ return
+ if name not in OPTIONS:
+ self.add_message('bad-config-option', node=node, args=name)
+
+
+def register(linter):
+ """Register this checker."""
+ linter.register_checker(ConfigChecker(linter))
+ global OPTIONS
+ global FAILED_LOAD
+ yaml_file = pathlib.Path('qutebrowser') / 'config' / 'configdata.yml'
+ if not yaml_file.exists():
+ OPTIONS = None
+ FAILED_LOAD = True
+ return
+ with yaml_file.open(mode='r', encoding='utf-8') as f:
+ OPTIONS = list(yaml.load(f))
diff --git a/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/modeline.py b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/modeline.py
new file mode 100644
index 0000000..429974c
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/modeline.py
@@ -0,0 +1,63 @@
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Checker for vim modelines in files."""
+
+import os.path
+import contextlib
+
+from pylint import interfaces, checkers
+
+
+class ModelineChecker(checkers.BaseChecker):
+
+ """Check for vim modelines in files."""
+
+ __implements__ = interfaces.IRawChecker
+
+ name = 'modeline'
+ msgs = {'W9002': ('Does not have vim modeline', 'modeline-missing', None),
+ 'W9003': ('Modeline is invalid', 'invalid-modeline', None),
+ 'W9004': ('Modeline position is wrong', 'modeline-position', None)}
+ options = ()
+ priority = -1
+
+ def process_module(self, node):
+ """Process the module."""
+ if os.path.basename(os.path.splitext(node.file)[0]) == '__init__':
+ return
+ max_lineno = 1
+ with contextlib.closing(node.stream()) as stream:
+ for (lineno, line) in enumerate(stream):
+ if lineno == 1 and line.startswith(b'#!'):
+ max_lineno += 1
+ continue
+ elif line.startswith(b'# vim:'):
+ if lineno > max_lineno:
+ self.add_message('modeline-position', line=lineno)
+ if (line.rstrip() != b'# vim: ft=python '
+ b'fileencoding=utf-8 sts=4 sw=4 et:'):
+ self.add_message('invalid-modeline', line=lineno)
+ break
+ else:
+ self.add_message('modeline-missing', line=1)
+
+
+def register(linter):
+ """Register the checker."""
+ linter.register_checker(ModelineChecker(linter))
diff --git a/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/openencoding.py b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/openencoding.py
new file mode 100644
index 0000000..f577011
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/openencoding.py
@@ -0,0 +1,83 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Make sure open() has an encoding set."""
+
+import astroid
+from pylint import interfaces, checkers
+from pylint.checkers import utils
+
+
+class OpenEncodingChecker(checkers.BaseChecker):
+
+ """Checker to check open() has an encoding set."""
+
+ __implements__ = interfaces.IAstroidChecker
+ name = 'open-encoding'
+
+ msgs = {
+ 'W9400': ('open() called without encoding', 'open-without-encoding',
+ None),
+ }
+
+ @utils.check_messages('open-without-encoding')
+ def visit_call(self, node):
+ """Visit a Call node."""
+ if hasattr(node, 'func'):
+ infer = utils.safe_infer(node.func)
+ if infer and infer.root().name == '_io':
+ if getattr(node.func, 'name', None) in ['open', 'file']:
+ self._check_open_encoding(node)
+
+ def _check_open_encoding(self, node):
+ """Check that an open() call always has an encoding set."""
+ try:
+ mode_arg = utils.get_argument_from_call(node, position=1,
+ keyword='mode')
+ except utils.NoSuchArgumentError:
+ mode_arg = None
+ _encoding = None
+ try:
+ _encoding = utils.get_argument_from_call(node, position=2)
+ except utils.NoSuchArgumentError:
+ try:
+ _encoding = utils.get_argument_from_call(node,
+ keyword='encoding')
+ except utils.NoSuchArgumentError:
+ pass
+ if _encoding is None:
+ if mode_arg is None:
+ mode = None
+ else:
+ mode = utils.safe_infer(mode_arg)
+ if mode is not None and not isinstance(mode, astroid.Const):
+ # We can't say what mode is exactly.
+ return
+ if mode is None:
+ self.add_message('open-without-encoding', node=node)
+ elif 'b' in getattr(mode, 'value', ''):
+ # Files opened as binary don't need an encoding.
+ return
+ else:
+ self.add_message('open-without-encoding', node=node)
+
+
+def register(linter):
+ """Register this checker."""
+ linter.register_checker(OpenEncodingChecker(linter))
diff --git a/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/settrace.py b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/settrace.py
new file mode 100644
index 0000000..c82d646
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/pylint_checkers/qute_pylint/settrace.py
@@ -0,0 +1,49 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Custom astroid checker for set_trace calls."""
+
+from pylint.interfaces import IAstroidChecker
+from pylint.checkers import BaseChecker, utils
+
+
+class SetTraceChecker(BaseChecker):
+
+ """Custom astroid checker for set_trace calls."""
+
+ __implements__ = IAstroidChecker
+ name = 'settrace'
+ msgs = {
+ 'E9101': ('set_trace call found', 'set-trace', None),
+ }
+ priority = -1
+
+ @utils.check_messages('set-trace')
+ def visit_call(self, node):
+ """Visit a Call node."""
+ if hasattr(node, 'func'):
+ infer = utils.safe_infer(node.func)
+ if infer:
+ if getattr(node.func, 'name', None) == 'set_trace':
+ self.add_message('set-trace', node=node)
+
+
+def register(linter):
+ """Register this checker."""
+ linter.register_checker(SetTraceChecker(linter))
diff --git a/.config/qutebrowser/scripts/dev/pylint_checkers/setup.py b/.config/qutebrowser/scripts/dev/pylint_checkers/setup.py
new file mode 100644
index 0000000..7833c7d
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/pylint_checkers/setup.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2016-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""This is only here so we can install those plugins in tox.ini easily."""
+
+from setuptools import setup
+setup(name='qute_pylint', packages=['qute_pylint'])
diff --git a/.config/qutebrowser/scripts/dev/quit_segfault_test.sh b/.config/qutebrowser/scripts/dev/quit_segfault_test.sh
new file mode 100755
index 0000000..389f125
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/quit_segfault_test.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+[[ $PWD == */scripts ]] && cd ..
+
+echo > crash.log
+while :; do
+ exit=0
+ while (( exit == 0 )); do
+ duration=$(( RANDOM % 10000 ))
+ python3 -m qutebrowser --debug ":later $duration quit" http://www.heise.de/
+ exit=$?
+ done
+ echo "$(date) $exit $duration" >> crash.log
+done
diff --git a/.config/qutebrowser/scripts/dev/recompile_requirements.py b/.config/qutebrowser/scripts/dev/recompile_requirements.py
new file mode 100644
index 0000000..6e26145
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/recompile_requirements.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2016-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Script to regenerate requirements files in misc/requirements."""
+
+import re
+import sys
+import os.path
+import glob
+import subprocess
+import tempfile
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+from scripts import utils
+
+REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ '..', '..') # /scripts/dev -> /scripts -> /
+REQ_DIR = os.path.join(REPO_DIR, 'misc', 'requirements')
+
+
+def convert_line(line, comments):
+ """Convert the given requirement line to place into the output."""
+ for pattern, repl in comments['replace'].items():
+ line = re.sub(pattern, repl, line)
+
+ pkgname = line.split('=')[0]
+
+ if pkgname in comments['ignore']:
+ line = '# ' + line
+
+ try:
+ line += ' # ' + comments['comment'][pkgname]
+ except KeyError:
+ pass
+
+ try:
+ line += ' # rq.filter: {}'.format(comments['filter'][pkgname])
+ except KeyError:
+ pass
+
+ return line
+
+
+def read_comments(fobj):
+ """Find special comments in the config.
+
+ Args:
+ fobj: A file object for the config.
+
+ Return:
+ A dict with the parsed comment data.
+ """
+ comments = {
+ 'filter': {},
+ 'comment': {},
+ 'ignore': [],
+ 'replace': {},
+ }
+ for line in fobj:
+ if line.startswith('#@'):
+ command, args = line[2:].split(':', maxsplit=1)
+ command = command.strip()
+ args = args.strip()
+ if command == 'filter':
+ pkg, filt = args.split(' ', maxsplit=1)
+ comments['filter'][pkg] = filt
+ elif command == 'comment':
+ pkg, comment = args.split(' ', maxsplit=1)
+ comments['comment'][pkg] = comment
+ elif command == 'ignore':
+ comments['ignore'] += args.split(', ')
+ elif command == 'replace':
+ pattern, replacement = args.split(' ', maxsplit=1)
+ comments['replace'][pattern] = replacement
+ return comments
+
+
+def get_all_names():
+ """Get all requirement names based on filenames."""
+ for filename in glob.glob(os.path.join(REQ_DIR, 'requirements-*.txt-raw')):
+ basename = os.path.basename(filename)
+ yield basename[len('requirements-'):-len('.txt-raw')]
+
+
+def main():
+ """Re-compile the given (or all) requirement files."""
+ names = sys.argv[1:] if len(sys.argv) > 1 else sorted(get_all_names())
+
+ for name in names:
+ utils.print_title(name)
+ filename = os.path.join(REQ_DIR,
+ 'requirements-{}.txt-raw'.format(name))
+ if name == 'qutebrowser':
+ outfile = os.path.join(REPO_DIR, 'requirements.txt')
+ else:
+ outfile = os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name))
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ pip_bin = os.path.join(tmpdir, 'bin', 'pip')
+ subprocess.run(['virtualenv', tmpdir], check=True)
+ subprocess.run([pip_bin, 'install', '-r', filename], check=True)
+ proc = subprocess.run([pip_bin, 'freeze'], check=True,
+ stdout=subprocess.PIPE)
+ reqs = proc.stdout.decode('utf-8')
+
+ with open(filename, 'r', encoding='utf-8') as f:
+ comments = read_comments(f)
+
+ with open(outfile, 'w', encoding='utf-8') as f:
+ f.write("# This file is automatically generated by "
+ "scripts/dev/recompile_requirements.py\n\n")
+ for line in reqs.splitlines():
+ f.write(convert_line(line, comments) + '\n')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/run_profile.py b/.config/qutebrowser/scripts/dev/run_profile.py
new file mode 100755
index 0000000..93e0b61
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/run_profile.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Profile qutebrowser."""
+
+import sys
+import cProfile
+import os.path
+import os
+import tempfile
+import subprocess
+import shutil
+import argparse
+import shlex
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+import qutebrowser.qutebrowser
+
+
+def parse_args():
+ """Parse commandline arguments.
+
+ Return:
+ A (namespace, remaining_args) tuple from argparse.
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--profile-tool', metavar='TOOL',
+ action='store', choices=['kcachegrind', 'snakeviz',
+ 'gprof2dot', 'none'],
+ default='snakeviz',
+ help="The tool to use to view the profiling data")
+ parser.add_argument('--profile-file', metavar='FILE', action='store',
+ help="The filename to use with --profile-tool=none")
+ return parser.parse_known_args()
+
+
+def main():
+ args, remaining = parse_args()
+ tempdir = tempfile.mkdtemp()
+
+ if args.profile_tool == 'none':
+ profilefile = os.path.join(os.getcwd(), args.profile_file)
+ else:
+ profilefile = os.path.join(tempdir, 'profile')
+
+ sys.argv = [sys.argv[0]] + remaining
+
+ profiler = cProfile.Profile()
+ profiler.runcall(qutebrowser.qutebrowser.main)
+
+ # If we have an exception after here, we don't want the qutebrowser
+ # exception hook to take over.
+ sys.excepthook = sys.__excepthook__
+ profiler.dump_stats(profilefile)
+
+ if args.profile_tool == 'none':
+ pass
+ elif args.profile_tool == 'gprof2dot':
+ # yep, shell=True. I know what I'm doing.
+ subprocess.run(
+ 'gprof2dot -f pstats {} | dot -Tpng | feh -F -'.format(
+ shlex.quote(profilefile)), shell=True)
+ elif args.profile_tool == 'kcachegrind':
+ callgraphfile = os.path.join(tempdir, 'callgraph')
+ subprocess.run(['pyprof2calltree', '-k', '-i', profilefile,
+ '-o', callgraphfile])
+ elif args.profile_tool == 'snakeviz':
+ subprocess.run(['snakeviz', profilefile])
+
+ shutil.rmtree(tempdir)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/run_pylint_on_tests.py b/.config/qutebrowser/scripts/dev/run_pylint_on_tests.py
new file mode 100644
index 0000000..7adf45f
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/run_pylint_on_tests.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Run pylint on tests.
+
+This is needed because pylint can't check a folder which isn't a package:
+https://bitbucket.org/logilab/pylint/issue/512/
+"""
+
+import os
+import os.path
+import sys
+import subprocess
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+from scripts import utils
+
+
+def main():
+ """Main entry point.
+
+ Return:
+ The pylint exit status.
+ """
+ utils.change_cwd()
+ files = []
+ for dirpath, _dirnames, filenames in os.walk('tests'):
+ for fn in filenames:
+ if os.path.splitext(fn)[1] == '.py':
+ files.append(os.path.join(dirpath, fn))
+
+ disabled = [
+ # pytest fixtures
+ 'redefined-outer-name',
+ 'unused-argument',
+ # things which are okay in tests
+ 'missing-docstring',
+ 'protected-access',
+ 'len-as-condition',
+ # directories without __init__.py...
+ 'import-error',
+ ]
+
+ toxinidir = sys.argv[1]
+ pythonpath = os.environ.get('PYTHONPATH', '').split(os.pathsep) + [
+ toxinidir,
+ ]
+
+ args = (['--disable={}'.format(','.join(disabled)),
+ '--ignored-modules=helpers,pytest,PyQt5'] +
+ sys.argv[2:] + files)
+ env = os.environ.copy()
+ env['PYTHONPATH'] = os.pathsep.join(pythonpath)
+
+ ret = subprocess.run(['pylint'] + args, env=env).returncode
+ return ret
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/.config/qutebrowser/scripts/dev/run_vulture.py b/.config/qutebrowser/scripts/dev/run_vulture.py
new file mode 100755
index 0000000..cb19d62
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/run_vulture.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2015-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Run vulture on the source files and filter out false-positives."""
+
+import sys
+import os
+import re
+import tempfile
+import inspect
+import argparse
+
+import vulture
+
+import qutebrowser.app # pylint: disable=unused-import
+from qutebrowser.commands import cmdutils
+from qutebrowser.utils import utils
+from qutebrowser.browser.webkit import rfc6266
+# To run the decorators from there
+# pylint: disable=unused-import
+from qutebrowser.browser.webkit.network import webkitqutescheme
+# pylint: enable=unused-import
+from qutebrowser.browser import qutescheme
+from qutebrowser.config import configtypes
+
+
+def whitelist_generator(): # noqa
+ """Generator which yields lines to add to a vulture whitelist."""
+ # qutebrowser commands
+ for cmd in cmdutils.cmd_dict.values():
+ yield utils.qualname(cmd.handler)
+
+ # pyPEG2 classes
+ for name, member in inspect.getmembers(rfc6266, inspect.isclass):
+ for attr in ['grammar', 'regex']:
+ if hasattr(member, attr):
+ yield 'qutebrowser.browser.webkit.rfc6266.{}.{}'.format(name,
+ attr)
+
+ # PyQt properties
+ yield 'qutebrowser.mainwindow.statusbar.bar.StatusBar.color_flags'
+ yield 'qutebrowser.mainwindow.statusbar.url.UrlText.urltype'
+
+ # Not used yet, but soon (or when debugging)
+ yield 'qutebrowser.utils.debug.log_events'
+ yield 'qutebrowser.utils.debug.log_signals'
+ yield 'qutebrowser.utils.debug.qflags_key'
+ yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
+ yield 'scripts.utils.bg_colors'
+
+ # Qt attributes
+ yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
+ yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().content'
+ yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().encoding'
+ yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames'
+ yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor'
+
+ ## qute://... handlers
+ for name in qutescheme._HANDLERS: # pylint: disable=protected-access
+ name = name.replace('-', '_')
+ yield 'qutebrowser.browser.qutescheme.qute_' + name
+
+ # Other false-positives
+ yield 'qutebrowser.completion.models.listcategory.ListCategory().lessThan'
+ yield 'qutebrowser.utils.jinja.Loader.get_source'
+ yield 'qutebrowser.utils.log.QtWarningFilter.filter'
+ yield 'qutebrowser.browser.pdfjs.is_available'
+ yield 'qutebrowser.misc.guiprocess.spawn_output'
+ yield 'QEvent.posted'
+ yield 'log_stack' # from message.py
+ yield 'propagate' # logging.getLogger('...).propagate = False
+ # vulture doesn't notice the hasattr() and thus thinks netrc_used is unused
+ # in NetworkManager.on_authentication_required
+ yield 'PyQt5.QtNetwork.QNetworkReply.netrc_used'
+ yield 'qutebrowser.browser.downloads.last_used_directory'
+ yield 'PaintContext.clip' # from completiondelegate.py
+ yield 'logging.LogRecord.log_color' # from logging.py
+ yield 'scripts.utils.use_color' # from asciidoc2html.py
+ for attr in ['pyeval_output', 'log_clipboard', 'fake_clipboard']:
+ yield 'qutebrowser.misc.utilcmds.' + attr
+
+ for attr in ['fileno', 'truncate', 'closed', 'readable']:
+ yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr
+
+ for attr in ['msgs', 'priority', 'visit_attribute']:
+ yield 'scripts.dev.pylint_checkers.config.' + attr
+ for attr in ['visit_call', 'process_module']:
+ yield 'scripts.dev.pylint_checkers.modeline.' + attr
+
+ for name, _member in inspect.getmembers(configtypes, inspect.isclass):
+ yield 'qutebrowser.config.configtypes.' + name
+ yield 'qutebrowser.config.configexc.ConfigErrorDesc.traceback'
+ yield 'qutebrowser.config.configfiles.ConfigAPI.load_autoconfig'
+ yield 'types.ModuleType.c' # configfiles:read_config_py
+ for name in ['configdir', 'datadir']:
+ yield 'qutebrowser.config.configfiles.ConfigAPI.' + name
+
+ yield 'include_aliases'
+
+ for attr in ['_get_default_metavar_for_optional',
+ '_get_default_metavar_for_positional', '_metavar_formatter']:
+ yield 'scripts.dev.src2asciidoc.UsageFormatter.' + attr
+
+ # attrs
+ yield 'qutebrowser.browser.webkit.network.networkmanager.ProxyId.hostname'
+ yield 'qutebrowser.command.command.ArgInfo._validate_exclusive'
+ yield 'scripts.get_coredumpctl_traces.Line.uid'
+ yield 'scripts.get_coredumpctl_traces.Line.gid'
+ yield 'scripts.importer.import_moz_places.places.row_factory'
+
+
+def filter_func(item):
+ """Check if a missing function should be filtered or not.
+
+ Return:
+ True if the missing function should be filtered/ignored, False
+ otherwise.
+ """
+ return bool(re.fullmatch(r'[a-z]+[A-Z][a-zA-Z]+', item.name))
+
+
+def report(items):
+ """Generate a report based on the given vulture.Item's.
+
+ Based on vulture.Vulture.report, but we can't use that as we can't set the
+ properties which get used for the items.
+ """
+ output = []
+ for item in sorted(items,
+ key=lambda e: (e.filename.lower(), e.first_lineno)):
+ output.append(item.get_report())
+ return output
+
+
+def run(files):
+ """Run vulture over the given files."""
+ with tempfile.NamedTemporaryFile(mode='w', delete=False) as whitelist_file:
+ for line in whitelist_generator():
+ whitelist_file.write(line + '\n')
+
+ whitelist_file.close()
+
+ vult = vulture.Vulture(verbose=False)
+ vult.scavenge(files + [whitelist_file.name])
+
+ os.remove(whitelist_file.name)
+
+ filters = {
+ 'unused_funcs': filter_func,
+ 'unused_props': lambda item: False,
+ 'unused_vars': lambda item: False,
+ 'unused_attrs': lambda item: False,
+ }
+
+ items = []
+
+ for attr, func in filters.items():
+ sub_items = getattr(vult, attr)
+ for item in sub_items:
+ filtered = func(item)
+ if not filtered:
+ items.append(item)
+
+ return report(items)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('files', nargs='*', default=['qutebrowser', 'scripts',
+ 'setup.py'])
+ args = parser.parse_args()
+ out = run(args.files)
+ for line in out:
+ print(line)
+ sys.exit(bool(out))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/segfault_test.py b/.config/qutebrowser/scripts/dev/segfault_test.py
new file mode 100755
index 0000000..aaf495f
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/segfault_test.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Tester for Qt segfaults with different harfbuzz engines."""
+
+import os
+import os.path
+import signal
+import sys
+import subprocess
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+from scripts import utils
+
+
+SCRIPT = """
+import sys
+
+from PyQt5.QtCore import QUrl
+from PyQt5.QtWidgets import QApplication
+from PyQt5.QtWebKitWidgets import QWebView
+
+def on_load_finished(ok):
+ if ok:
+ app.exit(0)
+ else:
+ app.exit(1)
+
+app = QApplication([])
+wv = QWebView()
+wv.loadFinished.connect(on_load_finished)
+wv.load(QUrl(sys.argv[1]))
+#wv.show()
+app.exec_()
+"""
+
+
+def print_ret(ret):
+ """Print information about an exit status."""
+ if ret == 0:
+ utils.print_col("success", 'green')
+ elif ret == -signal.SIGSEGV:
+ utils.print_col("segfault", 'red')
+ else:
+ utils.print_col("error {}".format(ret), 'yellow')
+ print()
+
+
+def main():
+ retvals = []
+ if len(sys.argv) < 2:
+ # pages which previously caused problems
+ pages = [
+ # ANGLE, https://bugreports.qt.io/browse/QTBUG-39723
+ ('http://www.binpress.com/', False),
+ ('http://david.li/flow/', False),
+ ('https://imzdl.com/', False),
+ # not reproducible
+ # https://bugreports.qt.io/browse/QTBUG-39847
+ ('http://www.20min.ch/', True),
+ # HarfBuzz, https://bugreports.qt.io/browse/QTBUG-39278
+ ('http://www.the-compiler.org/', True),
+ ('http://phoronix.com', True),
+ ('http://twitter.com', True),
+ # HarfBuzz #2, https://bugreports.qt.io/browse/QTBUG-36099
+ ('http://lenta.ru/', True),
+ # Unknown, https://bugreports.qt.io/browse/QTBUG-41360
+ ('http://salt.readthedocs.org/en/latest/topics/pillar/', True),
+ ]
+ else:
+ pages = [(e, True) for e in sys.argv[1:]]
+ for page, test_harfbuzz in pages:
+ utils.print_bold("==== {} ====".format(page))
+ if test_harfbuzz:
+ print("With system harfbuzz:")
+ ret = subprocess.run([sys.executable, '-c', SCRIPT, page]).returncode
+ print_ret(ret)
+ retvals.append(ret)
+ if test_harfbuzz:
+ print("With QT_HARFBUZZ=old:")
+ env = dict(os.environ)
+ env['QT_HARFBUZZ'] = 'old'
+ ret = subprocess.run([sys.executable, '-c', SCRIPT, page],
+ env=env).returncode
+ print_ret(ret)
+ retvals.append(ret)
+ print("With QT_HARFBUZZ=new:")
+ env = dict(os.environ)
+ env['QT_HARFBUZZ'] = 'new'
+ ret = subprocess.run([sys.executable, '-c', SCRIPT, page],
+ env=env).returncode
+ print_ret(ret)
+ retvals.append(ret)
+ if all(r == 0 for r in retvals):
+ sys.exit(0)
+ else:
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/src2asciidoc.py b/.config/qutebrowser/scripts/dev/src2asciidoc.py
new file mode 100755
index 0000000..cc00c37
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/src2asciidoc.py
@@ -0,0 +1,561 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Generate asciidoc source for qutebrowser based on docstrings."""
+
+import os
+import os.path
+import sys
+import shutil
+import inspect
+import subprocess
+import tempfile
+import argparse
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+
+# We import qutebrowser.app so all @cmdutils-register decorators are run.
+import qutebrowser.app
+from qutebrowser import qutebrowser, commands
+from qutebrowser.commands import cmdutils, argparser
+from qutebrowser.config import configdata, configtypes
+from qutebrowser.utils import docutils, usertypes
+from scripts import asciidoc2html, utils
+
+FILE_HEADER = """
+// DO NOT EDIT THIS FILE DIRECTLY!
+// It is autogenerated by running:
+// $ python3 scripts/dev/src2asciidoc.py
+// vim: readonly:
+
+""".lstrip()
+
+
+class UsageFormatter(argparse.HelpFormatter):
+
+ """Patched HelpFormatter to include some asciidoc markup in the usage.
+
+ This does some horrible things, but the alternative would be to reimplement
+ argparse.HelpFormatter while copying 99% of the code :-/
+ """
+
+ def _format_usage(self, usage, actions, groups, _prefix):
+ """Override _format_usage to not add the 'usage:' prefix."""
+ return super()._format_usage(usage, actions, groups, '')
+
+ def _get_default_metavar_for_optional(self, action):
+ """Do name transforming when getting metavar."""
+ return argparser.arg_name(action.dest.upper())
+
+ def _get_default_metavar_for_positional(self, action):
+ """Do name transforming when getting metavar."""
+ return argparser.arg_name(action.dest)
+
+ def _metavar_formatter(self, action, default_metavar):
+ """Override _metavar_formatter to add asciidoc markup to metavars.
+
+ Most code here is copied from Python 3.4's argparse.py.
+ """
+ if action.metavar is not None:
+ result = "'{}'".format(action.metavar)
+ elif action.choices is not None:
+ choice_strs = [str(choice) for choice in action.choices]
+ result = ('{' + ','.join('*{}*'.format(e) for e in choice_strs) +
+ '}')
+ else:
+ result = "'{}'".format(default_metavar)
+
+ def fmt(tuple_size):
+ """Format the result according to the tuple size."""
+ if isinstance(result, tuple):
+ return result
+ else:
+ return (result, ) * tuple_size
+ return fmt
+
+ def _format_actions_usage(self, actions, groups):
+ """Override _format_actions_usage to add asciidoc markup to flags.
+
+ Because argparse.py's _format_actions_usage is very complex, we first
+ monkey-patch the option strings to include the asciidoc markup, then
+ run the original method, then undo the patching.
+ """
+ old_option_strings = {}
+ for action in actions:
+ old_option_strings[action] = action.option_strings[:]
+ action.option_strings = ['*{}*'.format(s)
+ for s in action.option_strings]
+ ret = super()._format_actions_usage(actions, groups)
+ for action in actions:
+ action.option_strings = old_option_strings[action]
+ return ret
+
+
+def _open_file(name, mode='w'):
+ """Open a file with a preset newline/encoding mode."""
+ return open(name, mode, newline='\n', encoding='utf-8')
+
+
+def _get_cmd_syntax(_name, cmd):
+ """Get the command syntax for a command.
+
+ We monkey-patch the parser's formatter_class here to use our UsageFormatter
+ which adds some asciidoc markup.
+ """
+ old_fmt_class = cmd.parser.formatter_class
+ cmd.parser.formatter_class = UsageFormatter
+ usage = cmd.parser.format_usage().rstrip()
+ cmd.parser.formatter_class = old_fmt_class
+ return usage
+
+
+def _get_command_quickref(cmds):
+ """Generate the command quick reference."""
+ out = []
+ out.append('[options="header",width="75%",cols="25%,75%"]')
+ out.append('|==============')
+ out.append('|Command|Description')
+ for name, cmd in cmds:
+ desc = inspect.getdoc(cmd.handler).splitlines()[0]
+ out.append('|<<{},{}>>|{}'.format(name, name, desc))
+ out.append('|==============')
+ return '\n'.join(out)
+
+
+def _get_setting_quickref():
+ """Generate the settings quick reference."""
+ out = []
+ out.append('')
+ out.append('[options="header",width="75%",cols="25%,75%"]')
+ out.append('|==============')
+ out.append('|Setting|Description')
+ for opt in sorted(configdata.DATA.values()):
+ desc = opt.description.splitlines()[0]
+ out.append('|<<{},{}>>|{}'.format(opt.name, opt.name, desc))
+ out.append('|==============')
+ return '\n'.join(out)
+
+
+def _get_configtypes():
+ """Get configtypes classes to document."""
+ predicate = lambda e: (
+ inspect.isclass(e) and
+ # pylint: disable=protected-access
+ e not in [configtypes.BaseType, configtypes.MappingType,
+ configtypes._Numeric] and
+ # pylint: enable=protected-access
+ issubclass(e, configtypes.BaseType))
+ yield from inspect.getmembers(configtypes, predicate)
+
+
+def _get_setting_types_quickref():
+ """Generate the setting types quick reference."""
+ out = []
+ out.append('[[types]]')
+ out.append('[options="header",width="75%",cols="25%,75%"]')
+ out.append('|==============')
+ out.append('|Type|Description')
+
+ for name, typ in _get_configtypes():
+ parser = docutils.DocstringParser(typ)
+ desc = parser.short_desc
+ if parser.long_desc:
+ desc += '\n\n' + parser.long_desc
+ out.append('|{}|{}'.format(name, desc))
+
+ out.append('|==============')
+ return '\n'.join(out)
+
+
+def _get_command_doc(name, cmd):
+ """Generate the documentation for a command."""
+ output = ['[[{}]]'.format(name)]
+ output += ['=== {}'.format(name)]
+ syntax = _get_cmd_syntax(name, cmd)
+ if syntax != name:
+ output.append('Syntax: +:{}+'.format(syntax))
+ output.append("")
+ parser = docutils.DocstringParser(cmd.handler)
+ output.append(parser.short_desc)
+ if parser.long_desc:
+ output.append("")
+ output.append(parser.long_desc)
+
+ output += list(_get_command_doc_args(cmd, parser))
+ output += list(_get_command_doc_count(cmd, parser))
+ output += list(_get_command_doc_notes(cmd))
+
+ output.append("")
+ output.append("")
+ return '\n'.join(output)
+
+
+def _get_command_doc_args(cmd, parser):
+ """Get docs for the arguments of a command.
+
+ Args:
+ cmd: The Command to get the docs for.
+ parser: The DocstringParser to use.
+
+ Yield:
+ Strings which should be added to the docs.
+ """
+ if cmd.pos_args:
+ yield ""
+ yield "==== positional arguments"
+ for arg, name in cmd.pos_args:
+ try:
+ yield "* +'{}'+: {}".format(name, parser.arg_descs[arg])
+ except KeyError as e:
+ raise KeyError("No description for arg {} of command "
+ "'{}'!".format(e, cmd.name)) from e
+
+ if cmd.opt_args:
+ yield ""
+ yield "==== optional arguments"
+ for arg, (long_flag, short_flag) in cmd.opt_args.items():
+ try:
+ yield '* +*{}*+, +*{}*+: {}'.format(short_flag, long_flag,
+ parser.arg_descs[arg])
+ except KeyError as e:
+ raise KeyError("No description for arg {} of command "
+ "'{}'!".format(e, cmd.name)) from e
+
+
+def _get_command_doc_count(cmd, parser):
+ """Get docs for the count of a command.
+
+ Args:
+ cmd: The Command to get the docs for.
+ parser: The DocstringParser to use.
+
+ Yield:
+ Strings which should be added to the docs.
+ """
+ for param in inspect.signature(cmd.handler).parameters.values():
+ if cmd.get_arg_info(param).count:
+ yield ""
+ yield "==== count"
+ try:
+ yield parser.arg_descs[param.name]
+ except KeyError as e:
+ raise KeyError("No description for count arg {!r} of command "
+ "{!r}!".format(param.name, cmd.name)) from e
+
+
+def _get_command_doc_notes(cmd):
+ """Get docs for the notes of a command.
+
+ Args:
+ cmd: The Command to get the docs for.
+ parser: The DocstringParser to use.
+
+ Yield:
+ Strings which should be added to the docs.
+ """
+ if (cmd.maxsplit is not None or cmd.no_cmd_split or
+ cmd.no_replace_variables and cmd.name != "spawn"):
+ yield ""
+ yield "==== note"
+ if cmd.maxsplit is not None:
+ yield ("* This command does not split arguments after the last "
+ "argument and handles quotes literally.")
+ if cmd.no_cmd_split:
+ yield ("* With this command, +;;+ is interpreted literally "
+ "instead of splitting off a second command.")
+ if cmd.no_replace_variables and cmd.name != "spawn":
+ yield r"* This command does not replace variables like +\{url\}+."
+
+
+def _get_action_metavar(action, nargs=1):
+ """Get the metavar to display for an argparse action.
+
+ Args:
+ action: The argparse action to get the metavar for.
+ nargs: The nargs setting for the related argument.
+ """
+ if action.metavar is not None:
+ if isinstance(action.metavar, str):
+ elems = [action.metavar] * nargs
+ else:
+ elems = action.metavar
+ return ' '.join("'{}'".format(e) for e in elems)
+ elif action.choices is not None:
+ choices = ','.join(str(e) for e in action.choices)
+ return "'{{{}}}'".format(choices)
+ else:
+ return "'{}'".format(action.dest.upper())
+
+
+def _format_action_args(action):
+ """Get an argument string based on an argparse action."""
+ if action.nargs is None:
+ return _get_action_metavar(action)
+ elif action.nargs == '?':
+ return '[{}]'.format(_get_action_metavar(action))
+ elif action.nargs == '*':
+ return '[{mv} [{mv} ...]]'.format(mv=_get_action_metavar(action))
+ elif action.nargs == '+':
+ return '{mv} [{mv} ...]'.format(mv=_get_action_metavar(action))
+ elif action.nargs == '...':
+ return '...'
+ else:
+ return _get_action_metavar(action, nargs=action.nargs)
+
+
+def _format_action(action):
+ """Get an invocation string/help from an argparse action."""
+ if action.help == argparse.SUPPRESS:
+ return None
+ if not action.option_strings:
+ invocation = '*{}*::'.format(_get_action_metavar(action))
+ else:
+ parts = []
+ if action.nargs == 0:
+ # Doesn't take a value, so the syntax is -s, --long
+ parts += ['*{}*'.format(s) for s in action.option_strings]
+ else:
+ # Takes a value, so the syntax is -s ARGS or --long ARGS.
+ args_string = _format_action_args(action)
+ for opt in action.option_strings:
+ parts.append('*{}* {}'.format(opt, args_string))
+ invocation = ', '.join(parts) + '::'
+ return '{}\n {}\n'.format(invocation, action.help)
+
+
+def generate_commands(filename):
+ """Generate the complete commands section."""
+ with _open_file(filename) as f:
+ f.write(FILE_HEADER)
+ f.write("= Commands\n\n")
+ f.write(commands.__doc__)
+ normal_cmds = []
+ other_cmds = []
+ debug_cmds = []
+ for name, cmd in cmdutils.cmd_dict.items():
+ if cmd.deprecated:
+ continue
+ if usertypes.KeyMode.normal not in cmd.modes:
+ other_cmds.append((name, cmd))
+ elif cmd.debug:
+ debug_cmds.append((name, cmd))
+ else:
+ normal_cmds.append((name, cmd))
+ normal_cmds.sort()
+ other_cmds.sort()
+ debug_cmds.sort()
+ f.write("\n")
+ f.write("== Normal commands\n")
+ f.write(".Quick reference\n")
+ f.write(_get_command_quickref(normal_cmds) + '\n')
+ for name, cmd in normal_cmds:
+ f.write(_get_command_doc(name, cmd))
+ f.write("\n")
+ f.write("== Commands not usable in normal mode\n")
+ f.write(".Quick reference\n")
+ f.write(_get_command_quickref(other_cmds) + '\n')
+ for name, cmd in other_cmds:
+ f.write(_get_command_doc(name, cmd))
+ f.write("\n")
+ f.write("== Debugging commands\n")
+ f.write("These commands are mainly intended for debugging. They are "
+ "hidden if qutebrowser was started without the "
+ "`--debug`-flag.\n")
+ f.write("\n")
+ f.write(".Quick reference\n")
+ f.write(_get_command_quickref(debug_cmds) + '\n')
+ for name, cmd in debug_cmds:
+ f.write(_get_command_doc(name, cmd))
+
+
+def _generate_setting_backend_info(f, opt):
+ """Generate backend information for the given option."""
+ all_backends = [usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine]
+ if opt.raw_backends is not None:
+ for name, conditional in sorted(opt.raw_backends.items()):
+ if conditional is True:
+ pass
+ elif conditional is False:
+ f.write("\nOn {}, this setting is unavailable.\n".format(name))
+ else:
+ f.write("\nOn {}, this setting requires {} or newer.\n"
+ .format(name, conditional))
+ elif opt.backends == all_backends:
+ pass
+ elif opt.backends == [usertypes.Backend.QtWebKit]:
+ f.write("\nThis setting is only available with the QtWebKit "
+ "backend.\n")
+ elif opt.backends == [usertypes.Backend.QtWebEngine]:
+ f.write("\nThis setting is only available with the QtWebEngine "
+ "backend.\n")
+ else:
+ raise ValueError("Invalid value {!r} for opt.backends"
+ .format(opt.backends))
+
+
+def _generate_setting_option(f, opt):
+ """Generate documentation for a single section."""
+ f.write("\n")
+ f.write('[[{}]]'.format(opt.name) + "\n")
+ f.write("=== {}".format(opt.name) + "\n")
+ f.write(opt.description + "\n")
+ if opt.restart:
+ f.write("This setting requires a restart.\n")
+ if opt.supports_pattern:
+ f.write("\nThis setting supports URL patterns.\n")
+ if opt.no_autoconfig:
+ f.write("\nThis setting can only be set in config.py.\n")
+ f.write("\n")
+ typ = opt.typ.get_name().replace(',', '&#44;')
+ f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
+ f.write("\n")
+
+ valid_values = opt.typ.get_valid_values()
+ if valid_values is not None and valid_values.generate_docs:
+ f.write("Valid values:\n")
+ f.write("\n")
+ for val in valid_values:
+ try:
+ desc = valid_values.descriptions[val]
+ f.write(" * +{}+: {}".format(val, desc) + "\n")
+ except KeyError:
+ f.write(" * +{}+".format(val) + "\n")
+ f.write("\n")
+
+ f.write("Default: {}\n".format(opt.typ.to_doc(opt.default)))
+ _generate_setting_backend_info(f, opt)
+
+
+def generate_settings(filename):
+ """Generate the complete settings section."""
+ configdata.init()
+ with _open_file(filename) as f:
+ f.write(FILE_HEADER)
+ f.write("= Setting reference\n\n")
+ f.write("== All settings\n")
+ f.write(_get_setting_quickref() + "\n")
+ for opt in sorted(configdata.DATA.values()):
+ _generate_setting_option(f, opt)
+ f.write("\n== Setting types\n")
+ f.write(_get_setting_types_quickref() + "\n")
+
+
+def _format_block(filename, what, data):
+ """Format a block in a file.
+
+ The block is delimited by markers like these:
+ // QUTE_*_START
+ ...
+ // QUTE_*_END
+
+ The * part is the part which should be given as 'what'.
+
+ Args:
+ filename: The file to change.
+ what: What to change (authors, options, etc.)
+ data; A list of strings which is the new data.
+ """
+ what = what.upper()
+ oshandle, tmpname = tempfile.mkstemp()
+ try:
+ with _open_file(filename, mode='r') as infile, \
+ _open_file(oshandle, mode='w') as temp:
+ found_start = False
+ found_end = False
+ for line in infile:
+ if line.strip() == '// QUTE_{}_START'.format(what):
+ temp.write(line)
+ temp.write(''.join(data))
+ found_start = True
+ elif line.strip() == '// QUTE_{}_END'.format(what.upper()):
+ temp.write(line)
+ found_end = True
+ elif (not found_start) or found_end:
+ temp.write(line)
+ if not found_start:
+ raise Exception("Marker '// QUTE_{}_START' not found in "
+ "'{}'!".format(what, filename))
+ elif not found_end:
+ raise Exception("Marker '// QUTE_{}_END' not found in "
+ "'{}'!".format(what, filename))
+ except:
+ os.remove(tmpname)
+ raise
+ else:
+ os.remove(filename)
+ shutil.move(tmpname, filename)
+
+
+def regenerate_manpage(filename):
+ """Update manpage OPTIONS using an argparse parser."""
+ parser = qutebrowser.get_argparser()
+ groups = []
+ # positionals, optionals and user-defined groups
+ # pylint: disable=protected-access
+ for group in parser._action_groups:
+ groupdata = []
+ groupdata.append('=== {}'.format(group.title))
+ if group.description is not None:
+ groupdata.append(group.description)
+ for action in group._group_actions:
+ action_data = _format_action(action)
+ if action_data is not None:
+ groupdata.append(action_data)
+ groups.append('\n'.join(groupdata))
+ # pylint: enable=protected-access
+ options = '\n'.join(groups)
+ # epilog
+ if parser.epilog is not None:
+ options += parser.epilog
+ _format_block(filename, 'options', options)
+
+
+def regenerate_cheatsheet():
+ """Generate cheatsheet PNGs based on the SVG."""
+ files = [
+ ('doc/img/cheatsheet-small.png', 300, 185),
+ ('doc/img/cheatsheet-big.png', 3342, 2060),
+ ]
+
+ for filename, x, y in files:
+ subprocess.run(['inkscape', '-e', filename, '-b', 'white',
+ '-w', str(x), '-h', str(y),
+ 'misc/cheatsheet.svg'], check=True)
+
+
+def main():
+ """Regenerate all documentation."""
+ utils.change_cwd()
+ print("Generating manpage...")
+ regenerate_manpage('doc/qutebrowser.1.asciidoc')
+ print("Generating settings help...")
+ generate_settings('doc/help/settings.asciidoc')
+ print("Generating command help...")
+ generate_commands('doc/help/commands.asciidoc')
+ if '--cheatsheet' in sys.argv:
+ print("Regenerating cheatsheet .pngs")
+ regenerate_cheatsheet()
+ if '--html' in sys.argv:
+ asciidoc2html.main()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/standardpaths_tester.py b/.config/qutebrowser/scripts/dev/standardpaths_tester.py
new file mode 100644
index 0000000..27b8382
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/standardpaths_tester.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Show various QStandardPath paths."""
+
+import os
+import sys
+
+from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion,
+ QStandardPaths, QCoreApplication)
+
+
+def print_header():
+ """Show system information."""
+ print("Python {}".format(sys.version))
+ print("os.name: {}".format(os.name))
+ print("sys.platform: {}".format(sys.platform))
+ print()
+
+ print("Qt {}, compiled {}".format(qVersion(), QT_VERSION_STR))
+ print("PyQt {}".format(PYQT_VERSION_STR))
+ print()
+
+
+def print_paths():
+ """Print all QStandardPaths.StandardLocation members."""
+ for name, obj in vars(QStandardPaths).items():
+ if isinstance(obj, QStandardPaths.StandardLocation):
+ location = QStandardPaths.writableLocation(obj)
+ print("{:25} {}".format(name, location))
+
+
+def main():
+ print_header()
+
+ print("No QApplication")
+ print("===============")
+ print()
+ print_paths()
+
+ app = QCoreApplication(sys.argv)
+ app.setApplicationName("qapp_name")
+
+ print()
+ print("With QApplication")
+ print("=================")
+ print()
+ print_paths()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/strip_whitespace.sh b/.config/qutebrowser/scripts/dev/strip_whitespace.sh
new file mode 100644
index 0000000..ee14278
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/strip_whitespace.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Strip trailing whitespace from files in this repo
+
+find qutebrowser scripts tests \
+ -type f \( \
+ -name '*.py' -o \
+ -name '*.feature' -o \
+ -name '*.sh' \
+ \) -exec sed -i 's/ \+$//' {} +
diff --git a/.config/qutebrowser/scripts/dev/ua_fetch.py b/.config/qutebrowser/scripts/dev/ua_fetch.py
new file mode 100755
index 0000000..75ce4c2
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/ua_fetch.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2015-2018 lamarpavel
+# Copyright 2015-2018 Alexey Nabrodov (Averrin)
+# Copyright 2015-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+
+"""Fetch list of popular user-agents.
+
+The script is based on a gist posted by github.com/averrin, the output of this
+script is formatted to be pasted into configdata.yml
+"""
+
+import requests
+from lxml import html # pylint: disable=import-error
+
+
+def fetch():
+ """Fetch list of popular user-agents.
+
+ Return:
+ List of relevant strings.
+ """
+ url = 'https://techblog.willshouse.com/2012/01/03/most-common-user-agents/'
+ page = requests.get(url)
+ page = html.fromstring(page.text)
+ path = '//*[@id="post-2229"]/div[2]/table/tbody'
+ return page.xpath(path)[0]
+
+
+def filter_list(complete_list, browsers):
+ """Filter the received list based on a look up table.
+
+ The LUT should be a dictionary of the format {browser: versions}, where
+ 'browser' is the name of the browser (eg. "Firefox") as string and
+ 'versions' is a set of different versions of this browser that should be
+ included when found (eg. {"Linux", "MacOSX"}). This function returns a
+ dictionary with the same keys as the LUT, but storing lists of tuples
+ (user_agent, browser_description) as values.
+ """
+ # pylint: disable=too-many-nested-blocks
+ table = {}
+ for entry in complete_list:
+ # Tuple of (user_agent, browser_description)
+ candidate = (entry[1].text_content(), entry[2].text_content())
+ for name in browsers:
+ found = False
+ if name.lower() in candidate[1].lower():
+ for version in browsers[name]:
+ if version.lower() in candidate[1].lower():
+ if table.get(name) is None:
+ table[name] = []
+ table[name].append(candidate)
+ browsers[name].remove(version)
+ found = True
+ break
+ if found:
+ break
+ return table
+
+
+def add_diversity(table):
+ """Insert a few additional entries for diversity into the dict.
+
+ (as returned by filter_list())
+ """
+ table["Obscure"] = [
+ ('Mozilla/5.0 (compatible; Googlebot/2.1; '
+ '+http://www.google.com/bot.html',
+ "Google Bot"),
+ ('Wget/1.16.1 (linux-gnu)',
+ "wget 1.16.1"),
+ ('curl/7.40.0',
+ "curl 7.40.0"),
+ ('Mozilla/5.0 (Linux; U; Android 7.1.2) AppleWebKit/534.30 '
+ '(KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ "Mobile Generic Android"),
+ ('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
+ 'Gecko',
+ "IE 11.0 for Desktop Win7 64-bit"),
+ ]
+ return table
+
+
+def main():
+ """Generate user agent code."""
+ fetched = fetch()
+ lut = {
+ "Firefox": {"Win", "MacOSX", "Linux", "Android"},
+ "Chrome": {"Win", "MacOSX", "Linux"},
+ "Safari": {"MacOSX", "iOS"}
+ }
+ filtered = filter_list(fetched, lut)
+ filtered = add_diversity(filtered)
+
+ tab = " "
+ for browser in ["Firefox", "Safari", "Chrome", "Obscure"]:
+ for it in filtered[browser]:
+ print('{}- - "{}"'.format(3 * tab, it[0]))
+ desc = it[1].replace('\xa0', ' ').replace(' ', ' ')
+ print("{}- {}".format(4 * tab, desc))
+ print("")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dev/update_3rdparty.py b/.config/qutebrowser/scripts/dev/update_3rdparty.py
new file mode 100755
index 0000000..c40015d
--- /dev/null
+++ b/.config/qutebrowser/scripts/dev/update_3rdparty.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2015 Daniel Schadt
+# Copyright 2016-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Update all third-party-modules."""
+
+import argparse
+import urllib.request
+import urllib.error
+import shutil
+import json
+import os
+import sys
+
+sys.path.insert(
+ 0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+from scripts import dictcli
+from qutebrowser.config import configdata
+
+
+def get_latest_pdfjs_url():
+ """Get the URL of the latest pdf.js prebuilt package.
+
+ Returns a (version, url)-tuple.
+ """
+ github_api = 'https://api.github.com'
+ endpoint = 'repos/mozilla/pdf.js/releases/latest'
+ request_url = '{}/{}'.format(github_api, endpoint)
+ with urllib.request.urlopen(request_url) as fp:
+ data = json.loads(fp.read().decode('utf-8'))
+
+ download_url = data['assets'][0]['browser_download_url']
+ version_name = data['name']
+ return (version_name, download_url)
+
+
+def update_pdfjs(target_version=None):
+ """Download and extract the latest pdf.js version.
+
+ If target_version is not None, download the given version instead.
+
+ Args:
+ target_version: None or version string ('x.y.z')
+ """
+ if target_version is None:
+ version, url = get_latest_pdfjs_url()
+ else:
+ # We need target_version as x.y.z, without the 'v' prefix, though the
+ # user might give it on the command line
+ if target_version.startswith('v'):
+ target_version = target_version[1:]
+ # version should have the prefix to be consistent with the return value
+ # of get_latest_pdfjs_url()
+ version = 'v' + target_version
+ url = ('https://github.com/mozilla/pdf.js/releases/download/'
+ 'v{0}/pdfjs-{0}-dist.zip').format(target_version)
+
+ os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ '..', '..'))
+ target_path = os.path.join('qutebrowser', '3rdparty', 'pdfjs')
+ print("=> Downloading pdf.js {}".format(version))
+ try:
+ (archive_path, _headers) = urllib.request.urlretrieve(url)
+ except urllib.error.HTTPError as error:
+ print("Could not retrieve pdfjs {}: {}".format(version, error))
+ return
+ if os.path.isdir(target_path):
+ print("Removing old version in {}".format(target_path))
+ shutil.rmtree(target_path)
+ os.makedirs(target_path)
+ print("Extracting new version")
+ with open(archive_path, 'rb') as archive:
+ shutil.unpack_archive(archive, target_path, 'zip')
+ urllib.request.urlcleanup()
+
+
+def update_dmg_makefile():
+ """Update fancy-dmg Makefile.
+
+ See https://el-tramo.be/blog/fancy-dmg/
+ """
+ print("Updating fancy-dmg Makefile...")
+ url = 'https://raw.githubusercontent.com/remko/fancy-dmg/master/Makefile'
+ target_path = os.path.join('scripts', 'dev', 'Makefile-dmg')
+ urllib.request.urlretrieve(url, target_path)
+ urllib.request.urlcleanup()
+
+
+def update_ace():
+ """Update ACE.
+
+ See https://ace.c9.io/ and https://github.com/ajaxorg/ace-builds/
+ """
+ print("Updating ACE...")
+ url = 'https://raw.githubusercontent.com/ajaxorg/ace-builds/master/src/ace.js'
+ target_path = os.path.join('tests', 'end2end', 'data', 'hints', 'ace',
+ 'ace.js')
+ urllib.request.urlretrieve(url, target_path)
+ urllib.request.urlcleanup()
+
+
+def test_dicts():
+ """Test available dictionaries."""
+ configdata.init()
+ for lang in dictcli.available_languages():
+ print('Testing dictionary {}... '.format(lang.code), end='')
+ lang_url = urllib.parse.urljoin(dictcli.API_URL, lang.remote_path)
+ request = urllib.request.Request(lang_url, method='HEAD')
+ response = urllib.request.urlopen(request)
+ if response.status == 200:
+ print('OK')
+ else:
+ print('ERROR: {}'.format(response.status))
+
+
+def run(ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None,
+ dicts=False):
+ """Update components based on the given arguments."""
+ if pdfjs:
+ update_pdfjs(pdfjs_version)
+ if ace:
+ update_ace()
+ if fancy_dmg:
+ update_dmg_makefile()
+ if dicts:
+ test_dicts()
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--pdfjs', '-p',
+ help='Specify pdfjs version. If not given, '
+ 'the latest version is used.',
+ required=False, metavar='VERSION')
+ parser.add_argument('--fancy-dmg', help="Update fancy-dmg Makefile",
+ action='store_true')
+ parser.add_argument(
+ '--dicts', '-d',
+ help='Test whether all available dictionaries '
+ 'can be reached at the remote repository.',
+ required=False, action='store_true')
+ args = parser.parse_args()
+ run(ace=True, pdfjs=True, fancy_dmg=args.fancy_dmg,
+ pdfjs_version=args.pdfjs, dicts=args.dicts)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/dictcli.py b/.config/qutebrowser/scripts/dictcli.py
new file mode 100755
index 0000000..4017159
--- /dev/null
+++ b/.config/qutebrowser/scripts/dictcli.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017-2018 Michal Siedlaczek <michal.siedlaczek@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 <http://www.gnu.org/licenses/>.
+
+"""A script installing Hunspell dictionaries.
+
+Use: python -m scripts.dictcli [-h] {list,update,remove-old,install} ...
+"""
+
+import argparse
+import base64
+import json
+import os
+import sys
+import re
+import urllib.request
+
+import attr
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
+from qutebrowser.browser.webengine import spell
+from qutebrowser.config import configdata
+from qutebrowser.utils import standarddir
+
+
+API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/master/'
+
+
+class InvalidLanguageError(Exception):
+
+ """Raised when requesting invalid languages."""
+
+ def __init__(self, invalid_langs):
+ msg = 'invalid languages: {}'.format(', '.join(invalid_langs))
+ super().__init__(msg)
+
+
+@attr.s
+class Language:
+
+ """Dictionary language specs."""
+
+ code = attr.ib()
+ name = attr.ib()
+ remote_filename = attr.ib()
+ local_filename = attr.ib(default=None)
+ _file_extension = attr.ib('bdic', init=False)
+
+ def __attrs_post_init__(self):
+ if self.local_filename is None:
+ self.local_filename = spell.local_filename(self.code)
+
+ @property
+ def remote_path(self):
+ """Resolve the filename with extension the remote dictionary."""
+ return '.'.join([self.remote_filename, self._file_extension])
+
+ @property
+ def local_path(self):
+ """Resolve the filename with extension the local dictionary."""
+ if self.local_filename is None:
+ return None
+ return '.'.join([self.local_filename, self._file_extension])
+
+ @property
+ def remote_version(self):
+ """Resolve the version of the local dictionary."""
+ return spell.version(self.remote_path)
+
+ @property
+ def local_version(self):
+ """Resolve the version of the local dictionary."""
+ local_path = self.local_path
+ if local_path is None:
+ return None
+ return spell.version(local_path)
+
+
+def get_argparser():
+ """Get the argparse parser."""
+ desc = 'Install and manage Hunspell dictionaries for QtWebEngine.'
+ parser = argparse.ArgumentParser(prog='dictcli',
+ description=desc)
+ subparsers = parser.add_subparsers(help='Command', dest='cmd')
+ subparsers.required = True
+ subparsers.add_parser('list',
+ help='Display the list of available languages.')
+ subparsers.add_parser('update',
+ help='Update dictionaries')
+ subparsers.add_parser('remove-old',
+ help='Remove old versions of dictionaries.')
+
+ install_parser = subparsers.add_parser('install',
+ help='Install dictionaries')
+ install_parser.add_argument('language',
+ nargs='*',
+ help="A list of languages to install.")
+
+ return parser
+
+
+def version_str(version):
+ return '.'.join(str(n) for n in version)
+
+
+def print_list(languages):
+ """Print the list of available languages."""
+ pat = '{:<7}{:<26}{:<8}{:<5}'
+ print(pat.format('Code', 'Name', 'Version', 'Installed'))
+ for lang in languages:
+ remote_version = version_str(lang.remote_version)
+ local_version = '-'
+ if lang.local_version is not None:
+ local_version = version_str(lang.local_version)
+ if lang.local_version < lang.remote_version:
+ local_version += ' - update available!'
+ print(pat.format(lang.code, lang.name, remote_version, local_version))
+
+
+def valid_languages():
+ """Return a mapping from valid language codes to their names."""
+ option = configdata.DATA['spellcheck.languages']
+ return option.typ.valtype.valid_values.descriptions
+
+
+def parse_entry(entry):
+ """Parse an entry from the remote API."""
+ dict_re = re.compile(r"""
+ (?P<filename>(?P<code>[a-z]{2}(-[A-Z]{2})?).*)\.bdic
+ """, re.VERBOSE)
+ match = dict_re.fullmatch(entry['name'])
+ if match is not None:
+ return match.group('code'), match.group('filename')
+ else:
+ return None
+
+
+def language_list_from_api():
+ """Return a JSON with a list of available languages from Google API."""
+ listurl = API_URL + '?format=JSON'
+ response = urllib.request.urlopen(listurl)
+ # A special 5-byte prefix must be stripped from the response content
+ # See: https://github.com/google/gitiles/issues/22
+ # https://github.com/google/gitiles/issues/82
+ json_content = response.read()[5:]
+ entries = json.loads(json_content.decode('utf-8'))['entries']
+ parsed_entries = [parse_entry(entry) for entry in entries]
+ return [entry for entry in parsed_entries if entry is not None]
+
+
+def latest_yet(code2file, code, filename):
+ """Determine whether the latest version so far."""
+ if code not in code2file:
+ return True
+ return spell.version(code2file[code]) < spell.version(filename)
+
+
+def available_languages():
+ """Return a list of Language objects of all available languages."""
+ lang_map = valid_languages()
+ api_list = language_list_from_api()
+ code2file = {}
+ for code, filename in api_list:
+ if latest_yet(code2file, code, filename):
+ code2file[code] = filename
+ return [
+ Language(code, name, code2file[code])
+ for code, name in lang_map.items()
+ if code in code2file
+ ]
+
+
+def download_dictionary(url, dest):
+ """Download a decoded dictionary file."""
+ response = urllib.request.urlopen(url)
+ decoded = base64.decodebytes(response.read())
+ with open(dest, 'bw') as dict_file:
+ dict_file.write(decoded)
+
+
+def filter_languages(languages, selected):
+ """Filter a list of languages based on an inclusion list.
+
+ Args:
+ languages: a list of languages to filter
+ selected: a list of keys to select
+ """
+ filtered_languages = []
+ for language in languages:
+ if language.code in selected:
+ filtered_languages.append(language)
+ selected.remove(language.code)
+ if selected:
+ raise InvalidLanguageError(selected)
+ return filtered_languages
+
+
+def install_lang(lang):
+ """Install a single lang given by the argument."""
+ lang_url = API_URL + lang.remote_path + '?format=TEXT'
+ if not os.path.isdir(spell.dictionary_dir()):
+ msg = '{} does not exist, creating the directory'
+ print(msg.format(spell.dictionary_dir()))
+ os.makedirs(spell.dictionary_dir())
+ print('Downloading {}'.format(lang_url))
+ dest = os.path.join(spell.dictionary_dir(), lang.remote_path)
+ download_dictionary(lang_url, dest)
+ print('Done.')
+
+
+def install(languages):
+ """Install languages."""
+ for lang in languages:
+ try:
+ print('Installing {}: {}'.format(lang.code, lang.name))
+ install_lang(lang)
+ except PermissionError as e:
+ sys.exit(str(e))
+
+
+def update(languages):
+ """Update the given languages."""
+ installed = [lang for lang in languages if lang.local_version is not None]
+ for lang in installed:
+ if lang.local_version < lang.remote_version:
+ print('Upgrading {} from {} to {}'.format(
+ lang.code,
+ version_str(lang.local_version),
+ version_str(lang.remote_version)))
+ install_lang(lang)
+
+
+def remove_old(languages):
+ """Remove old versions of languages."""
+ installed = [lang for lang in languages if lang.local_version is not None]
+ for lang in installed:
+ local_files = spell.local_files(lang.code)
+ for old_file in local_files[1:]:
+ os.remove(os.path.join(spell.dictionary_dir(), old_file))
+
+
+def main():
+ if configdata.DATA is None:
+ configdata.init()
+ standarddir.init(None)
+
+ parser = get_argparser()
+ argv = sys.argv[1:]
+ args = parser.parse_args(argv)
+ languages = available_languages()
+ if args.cmd == 'list':
+ print_list(languages)
+ elif args.cmd == 'update':
+ update(languages)
+ elif args.cmd == 'remove-old':
+ remove_old(languages)
+ elif not args.language:
+ sys.exit('You must provide a list of languages to install.')
+ else:
+ try:
+ install(filter_languages(languages, args.language))
+ except InvalidLanguageError as e:
+ print(e)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/hist_importer.py b/.config/qutebrowser/scripts/hist_importer.py
new file mode 100755
index 0000000..914701a
--- /dev/null
+++ b/.config/qutebrowser/scripts/hist_importer.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+# Copyright 2017-2018 Josefson Souza <josefson.br@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 <http://www.gnu.org/licenses/>.
+
+
+"""Tool to import browser history from other browsers."""
+
+
+import argparse
+import sqlite3
+import sys
+import os
+
+
+class Error(Exception):
+
+ """Exception for errors in this module."""
+
+ pass
+
+
+def parse():
+ """Parse command line arguments."""
+ description = ("This program is meant to extract browser history from your"
+ " previous browser and import them into qutebrowser.")
+ epilog = ("Databases:\n\n\tqutebrowser: Is named 'history.sqlite' and can "
+ "be found at your --basedir. In order to find where your "
+ "basedir is you can run ':open qute:version' inside qutebrowser."
+ "\n\n\tFirefox: Is named 'places.sqlite', and can be found at "
+ "your system's profile folder. Check this link for where it is "
+ "located: http://kb.mozillazine.org/Profile_folder"
+ "\n\n\tChrome: Is named 'History', and can be found at the "
+ "respective User Data Directory. Check this link for where it is"
+ "located: https://chromium.googlesource.com/chromium/src/+/"
+ "master/docs/user_data_dir.md\n\n"
+ "Example: hist_importer.py -b firefox -s /Firefox/Profile/"
+ "places.sqlite -d /qutebrowser/data/history.sqlite")
+ parser = argparse.ArgumentParser(
+ description=description, epilog=epilog,
+ formatter_class=argparse.RawTextHelpFormatter
+ )
+ parser.add_argument('-b', '--browser', dest='browser', required=True,
+ type=str, help='Browsers: {firefox, chrome}')
+ parser.add_argument('-s', '--source', dest='source', required=True,
+ type=str, help='Source: Full path to the sqlite data'
+ 'base file from the source browser.')
+ parser.add_argument('-d', '--dest', dest='dest', required=True, type=str,
+ help='\nDestination: Full path to the qutebrowser '
+ 'sqlite database')
+ return parser.parse_args()
+
+
+def open_db(data_base):
+ """Open connection with database."""
+ if os.path.isfile(data_base):
+ return sqlite3.connect(data_base)
+ raise Error('The file {} does not exist.'.format(data_base))
+
+
+def extract(source, query):
+ """Get records from source database.
+
+ Args:
+ source: File path to the source database where we want to extract the
+ data from.
+ query: The query string to be executed in order to retrieve relevant
+ attributes as (datetime, url, time) from the source database according
+ to the browser chosen.
+ """
+ try:
+ conn = open_db(source)
+ cursor = conn.cursor()
+ cursor.execute(query)
+ history = cursor.fetchall()
+ conn.close()
+ return history
+ except sqlite3.OperationalError as op_e:
+ raise Error('Could not perform queries on the source database: '
+ '{}'.format(op_e))
+
+
+def clean(history):
+ """Clean up records from source database.
+
+ Receives a list of record and sanityze them in order for them to be
+ properly imported to qutebrowser. Sanitation requires adding a 4th
+ attribute 'redirect' which is filled with '0's, and also purging all
+ records that have a NULL/None datetime attribute.
+
+ Args:
+ history: List of records (datetime, url, title) from source database.
+ """
+ # replace missing titles with an empty string
+ for index, record in enumerate(history):
+ if record[1] is None:
+ cleaned = list(record)
+ cleaned[1] = ''
+ history[index] = tuple(cleaned)
+
+ nulls = [record for record in history if None in record]
+ for null_record in nulls:
+ history.remove(null_record)
+ history = [list(record) for record in history]
+ for record in history:
+ record.append('0')
+ return history
+
+
+def insert_qb(history, dest):
+ """Insert history into dest database.
+
+ Args:
+ history: List of records.
+ dest: File path to the destination database, where history will be
+ inserted.
+ """
+ conn = open_db(dest)
+ cursor = conn.cursor()
+ cursor.executemany(
+ 'INSERT INTO History (url,title,atime,redirect) VALUES (?,?,?,?)',
+ history
+ )
+ cursor.execute('DROP TABLE CompletionHistory')
+ conn.commit()
+ conn.close()
+
+
+def run():
+ """Main control flux of the script."""
+ args = parse()
+ browser = args.browser.lower()
+ source, dest = args.source, args.dest
+ query = {
+ 'firefox': 'select url,title,last_visit_date/1000000 as date '
+ 'from moz_places where url like "http%" or url '
+ 'like "ftp%" or url like "file://%"',
+ 'chrome': 'select url,title,last_visit_time/10000000 as date '
+ 'from urls',
+ }
+ if browser not in query:
+ raise Error('Sorry, the selected browser: "{}" is not '
+ 'supported.'.format(browser))
+ else:
+ history = extract(source, query[browser])
+ history = clean(history)
+ insert_qb(history, dest)
+
+
+def main():
+ try:
+ run()
+ except Error as e:
+ sys.exit(str(e))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.config/qutebrowser/scripts/hostblock_blame.py b/.config/qutebrowser/scripts/hostblock_blame.py
new file mode 100755
index 0000000..2f68d29
--- /dev/null
+++ b/.config/qutebrowser/scripts/hostblock_blame.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Check by which hostblock list a host was blocked."""
+
+import sys
+import io
+import os
+import os.path
+import urllib.request
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
+from qutebrowser.browser import adblock
+from qutebrowser.config import configdata
+
+
+def main():
+ """Check by which hostblock list a host was blocked."""
+ if len(sys.argv) != 2:
+ print("Usage: {} <host>".format(sys.argv[0]), file=sys.stderr)
+ sys.exit(1)
+
+ configdata.init()
+
+ for url in configdata.DATA['content.host_blocking.lists'].default:
+ print("checking {}...".format(url))
+ raw_file = urllib.request.urlopen(url)
+ byte_io = io.BytesIO(raw_file.read())
+ f = adblock.get_fileobj(byte_io)
+ for line in f:
+ line = line.decode('utf-8')
+ if sys.argv[1] in line:
+ print("FOUND {} in {}:".format(sys.argv[1], url))
+ print(" " + line.rstrip())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/importer.py b/.config/qutebrowser/scripts/importer.py
new file mode 100755
index 0000000..eb808a6
--- /dev/null
+++ b/.config/qutebrowser/scripts/importer.py
@@ -0,0 +1,349 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Claude (longneck) <longneck@scratchbook.ch>
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+
+"""Tool to import data from other browsers.
+
+Currently importing bookmarks from Netscape Bookmark files and Mozilla
+profiles is supported.
+"""
+
+
+import argparse
+import sqlite3
+import os
+import urllib.parse
+import json
+import string
+
+browser_default_input_format = {
+ 'chromium': 'chrome',
+ 'chrome': 'chrome',
+ 'ie': 'netscape',
+ 'firefox': 'mozilla',
+ 'seamonkey': 'mozilla',
+ 'palemoon': 'mozilla',
+}
+
+
+def main():
+ args = get_args()
+ bookmark_types = []
+ output_format = None
+ input_format = args.input_format
+ if args.search_output:
+ bookmark_types = ['search']
+ if args.oldconfig:
+ output_format = 'oldsearch'
+ else:
+ output_format = 'search'
+ else:
+ if args.bookmark_output:
+ output_format = 'bookmark'
+ elif args.quickmark_output:
+ output_format = 'quickmark'
+ if args.import_bookmarks:
+ bookmark_types.append('bookmark')
+ if args.import_keywords:
+ bookmark_types.append('keyword')
+ if not bookmark_types:
+ bookmark_types = ['bookmark', 'keyword']
+ if not output_format:
+ output_format = 'quickmark'
+ if not input_format:
+ if args.browser:
+ input_format = browser_default_input_format[args.browser]
+ else:
+ #default to netscape
+ input_format = 'netscape'
+
+ import_function = {
+ 'netscape': import_netscape_bookmarks,
+ 'mozilla': import_moz_places,
+ 'chrome': import_chrome,
+ }
+ import_function[input_format](args.bookmarks, bookmark_types,
+ output_format)
+
+
+def get_args():
+ """Get the argparse parser."""
+ parser = argparse.ArgumentParser(
+ epilog="To import bookmarks from Chromium, Firefox or IE, "
+ "export them to HTML in your browsers bookmark manager. ")
+ parser.add_argument(
+ 'browser',
+ help="Which browser? {%(choices)s}",
+ choices=browser_default_input_format.keys(),
+ nargs='?',
+ metavar='browser')
+ parser.add_argument(
+ '-i',
+ '--input-format',
+ help='Which input format? (overrides browser default; "netscape" if '
+ 'neither given)',
+ choices=set(browser_default_input_format.values()),
+ required=False)
+ parser.add_argument(
+ '-b',
+ '--bookmark-output',
+ help="Output in bookmark format.",
+ action='store_true',
+ default=False,
+ required=False)
+ parser.add_argument(
+ '-q',
+ '--quickmark-output',
+ help="Output in quickmark format (default).",
+ action='store_true',
+ default=False,
+ required=False)
+ parser.add_argument(
+ '-s',
+ '--search-output',
+ help="Output config.py search engine format (negates -B and -K)",
+ action='store_true',
+ default=False,
+ required=False)
+ parser.add_argument(
+ '--oldconfig',
+ help="Output search engine format for old qutebrowser.conf format",
+ default=False,
+ action='store_true',
+ required=False)
+ parser.add_argument(
+ '-B',
+ '--import-bookmarks',
+ help="Import plain bookmarks (can be combiend with -K)",
+ action='store_true',
+ default=False,
+ required=False)
+ parser.add_argument(
+ '-K',
+ '--import-keywords',
+ help="Import keywords (can be combined with -B)",
+ action='store_true',
+ default=False,
+ required=False)
+ parser.add_argument(
+ 'bookmarks',
+ help="Bookmarks file (html format) or "
+ "profile folder (Mozilla format)")
+ args = parser.parse_args()
+ return args
+
+
+def search_escape(url):
+ """Escape URLs such that preexisting { and } are handled properly.
+
+ Will obviously trash a properly-formatted qutebrowser URL.
+ """
+ return url.replace('{', '{{').replace('}', '}}')
+
+
+def opensearch_convert(url):
+ """Convert a basic OpenSearch URL into something qutebrowser can use.
+
+ Exceptions:
+ KeyError:
+ An unknown and required parameter is present in the URL. This
+ usually means there's browser/addon specific functionality needed
+ to build the URL (I'm looking at you and your browser, Google) that
+ obviously won't be present here.
+ """
+ subst = {
+ 'searchTerms': '%s', # for proper escaping later
+ 'language': '*',
+ 'inputEncoding': 'UTF-8',
+ 'outputEncoding': 'UTF-8'
+ }
+
+ # remove optional parameters (even those we don't support)
+ for param in string.Formatter().parse(url):
+ if param[1]:
+ if param[1].endswith('?'):
+ url = url.replace('{' + param[1] + '}', '')
+ elif param[2] and param[2].endswith('?'):
+ url = url.replace('{' + param[1] + ':' + param[2] + '}', '')
+ return search_escape(url.format(**subst)).replace('%s', '{}')
+
+
+def import_netscape_bookmarks(bookmarks_file, bookmark_types, output_format):
+ """Import bookmarks from a NETSCAPE-Bookmark-file v1.
+
+ Generated by Chromium, Firefox, IE and possibly more browsers. Not all
+ export all possible bookmark types:
+ - Firefox mostly works with everything
+ - Chrome doesn't support keywords at all; searches are a separate
+ database
+ """
+ import bs4
+ with open(bookmarks_file, encoding='utf-8') as f:
+ soup = bs4.BeautifulSoup(f, 'html.parser')
+ bookmark_query = {
+ 'search': lambda tag: (
+ (tag.name == 'a') and
+ ('shortcuturl' in tag.attrs) and
+ ('%s' in tag['href'])),
+ 'keyword': lambda tag: (
+ (tag.name == 'a') and
+ ('shortcuturl' in tag.attrs) and
+ ('%s' not in tag['href'])),
+ 'bookmark': lambda tag: (
+ (tag.name == 'a') and
+ ('shortcuturl' not in tag.attrs) and
+ (tag.string)),
+ }
+ output_template = {
+ 'search': {
+ 'search':
+ "c.url.searchengines['{tag[shortcuturl]}'] = "
+ "'{tag[href]}' #{tag.string}"
+ },
+ 'oldsearch': {
+ 'search': '{tag[shortcuturl]} = {tag[href]} #{tag.string}',
+ },
+ 'bookmark': {
+ 'bookmark': '{tag[href]} {tag.string}',
+ 'keyword': '{tag[href]} {tag.string}'
+ },
+ 'quickmark': {
+ 'bookmark': '{tag.string} {tag[href]}',
+ 'keyword': '{tag[shortcuturl]} {tag[href]}'
+ }
+ }
+ bookmarks = []
+ for typ in bookmark_types:
+ tags = soup.findAll(bookmark_query[typ])
+ for tag in tags:
+ if typ == 'search':
+ tag['href'] = search_escape(tag['href']).replace('%s', '{}')
+ if tag['href'] not in bookmarks:
+ bookmarks.append(
+ output_template[output_format][typ].format(tag=tag))
+ for bookmark in bookmarks:
+ print(bookmark)
+
+
+def import_moz_places(profile, bookmark_types, output_format):
+ """Import bookmarks from a Mozilla profile's places.sqlite database."""
+ place_query = {
+ 'bookmark': (
+ "SELECT DISTINCT moz_bookmarks.title,moz_places.url "
+ "FROM moz_bookmarks,moz_places "
+ "WHERE moz_places.id=moz_bookmarks.fk "
+ "AND moz_places.id NOT IN (SELECT place_id FROM moz_keywords) "
+ "AND moz_places.url NOT LIKE 'place:%';"
+ ), # Bookmarks with no keywords assigned
+ 'keyword': (
+ "SELECT moz_keywords.keyword,moz_places.url "
+ "FROM moz_keywords,moz_places,moz_bookmarks "
+ "WHERE moz_places.id=moz_bookmarks.fk "
+ "AND moz_places.id=moz_keywords.place_id "
+ "AND moz_places.url NOT LIKE '%!%s%' ESCAPE '!';"
+ ), # Bookmarks with keywords assigned but no %s substitution
+ 'search': (
+ "SELECT moz_keywords.keyword, "
+ " moz_bookmarks.title, "
+ " search_conv(moz_places.url) AS url "
+ "FROM moz_keywords,moz_places,moz_bookmarks "
+ "WHERE moz_places.id=moz_bookmarks.fk "
+ "AND moz_places.id=moz_keywords.place_id "
+ "AND moz_places.url LIKE '%!%s%' ESCAPE '!';"
+ ) # bookmarks with keyword and %s substitution
+ }
+ out_template = {
+ 'bookmark': {
+ 'bookmark': '{url} {title}',
+ 'keyword': '{url} {keyword}'
+ },
+ 'quickmark': {
+ 'bookmark': '{title} {url}',
+ 'keyword': '{keyword} {url}'
+ },
+ 'oldsearch': {
+ 'search': '{keyword} {url} #{title}'
+ },
+ 'search': {
+ 'search': "c.url.searchengines['{keyword}'] = '{url}' #{title}"
+ }
+ }
+
+ def search_conv(url):
+ return search_escape(url).replace('%s', '{}')
+
+ places = sqlite3.connect(os.path.join(profile, "places.sqlite"))
+ places.create_function('search_conv', 1, search_conv)
+ places.row_factory = sqlite3.Row
+ c = places.cursor()
+ for typ in bookmark_types:
+ c.execute(place_query[typ])
+ for row in c:
+ print(out_template[output_format][typ].format(**row))
+
+
+def import_chrome(profile, bookmark_types, output_format):
+ """Import bookmarks and search keywords from Chrome-type profiles.
+
+ On Chrome, keywords and search engines are the same thing and handled in
+ their own database table; bookmarks cannot have associated keywords. This
+ is why the dictionary lookups here are much simpler.
+ """
+ out_template = {
+ 'bookmark': '{url} {name}',
+ 'quickmark': '{name} {url}',
+ 'search': "c.url.searchengines['{keyword}'] = '{url}'",
+ 'oldsearch': '{keyword} {url}'
+ }
+
+ if 'search' in bookmark_types:
+ webdata = sqlite3.connect(os.path.join(profile, 'Web Data'))
+ c = webdata.cursor()
+ c.execute('SELECT keyword,url FROM keywords;')
+ for keyword, url in c:
+ try:
+ url = opensearch_convert(url)
+ print(out_template[output_format].format(
+ keyword=keyword, url=url))
+ except KeyError:
+ print('# Unsupported parameter in url for {}; skipping....'.
+ format(keyword))
+
+ else:
+ with open(os.path.join(profile, 'Bookmarks'), encoding='utf-8') as f:
+ bookmarks = json.load(f)
+
+ def bm_tree_walk(bm, template):
+ """Recursive function to walk through bookmarks."""
+ assert 'type' in bm, bm
+ if bm['type'] == 'url':
+ if urllib.parse.urlparse(bm['url']).scheme != 'chrome':
+ print(template.format(**bm))
+ elif bm['type'] == 'folder':
+ for child in bm['children']:
+ bm_tree_walk(child, template)
+
+ for root in bookmarks['roots'].values():
+ bm_tree_walk(root, out_template[output_format])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.config/qutebrowser/scripts/keytester.py b/.config/qutebrowser/scripts/keytester.py
new file mode 100755
index 0000000..ee5eb34
--- /dev/null
+++ b/.config/qutebrowser/scripts/keytester.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Small test script to show key presses.
+
+Use python3 -m scripts.keytester to launch it.
+"""
+
+from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
+
+from qutebrowser.keyinput import keyutils
+
+
+class KeyWidget(QWidget):
+
+ """Widget displaying key presses."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._layout = QHBoxLayout(self)
+ self._label = QLabel(text="Waiting for keypress...")
+ self._layout.addWidget(self._label)
+
+ def keyPressEvent(self, e):
+ """Show pressed keys."""
+ lines = [
+ str(keyutils.KeyInfo.from_event(e)),
+ '',
+ 'key: 0x{:x}'.format(int(e.key())),
+ 'modifiers: 0x{:x}'.format(int(e.modifiers())),
+ 'text: {!r}'.format(e.text()),
+ ]
+ self._label.setText('\n'.join(lines))
+
+
+app = QApplication([])
+w = KeyWidget()
+w.show()
+app.exec_()
diff --git a/.config/qutebrowser/scripts/link_pyqt.py b/.config/qutebrowser/scripts/link_pyqt.py
new file mode 100755
index 0000000..ae7eaa6
--- /dev/null
+++ b/.config/qutebrowser/scripts/link_pyqt.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Symlink PyQt into a given virtualenv."""
+
+import os
+import os.path
+import argparse
+import shutil
+import sys
+import subprocess
+import tempfile
+import filecmp
+
+
+class Error(Exception):
+
+ """Exception raised when linking fails."""
+
+ pass
+
+
+def run_py(executable, *code):
+ """Run the given python code with the given executable."""
+ if os.name == 'nt' and len(code) > 1:
+ # Windows can't do newlines in arguments...
+ oshandle, filename = tempfile.mkstemp()
+ with os.fdopen(oshandle, 'w') as f:
+ f.write('\n'.join(code))
+ cmd = [executable, filename]
+ try:
+ ret = subprocess.run(cmd, universal_newlines=True, check=True,
+ stdout=subprocess.PIPE).stdout
+ finally:
+ os.remove(filename)
+ else:
+ cmd = [executable, '-c', '\n'.join(code)]
+ ret = subprocess.run(cmd, universal_newlines=True, check=True,
+ stdout=subprocess.PIPE).stdout
+ return ret.rstrip()
+
+
+def verbose_copy(src, dst, *, follow_symlinks=True):
+ """Copy function for shutil.copytree which prints copied files."""
+ if '-v' in sys.argv:
+ print('{} -> {}'.format(src, dst))
+ shutil.copy(src, dst, follow_symlinks=follow_symlinks)
+
+
+def get_ignored_files(directory, files):
+ """Get the files which should be ignored for link_pyqt() on Windows."""
+ needed_exts = ('.py', '.dll', '.pyd', '.so')
+ ignored_dirs = ('examples', 'qml', 'uic', 'doc')
+ filtered = []
+ for f in files:
+ ext = os.path.splitext(f)[1]
+ full_path = os.path.join(directory, f)
+ if os.path.isdir(full_path) and f in ignored_dirs:
+ filtered.append(f)
+ elif (ext not in needed_exts) and os.path.isfile(full_path):
+ filtered.append(f)
+ return filtered
+
+
+def needs_update(source, dest):
+ """Check if a file to be linked/copied needs to be updated."""
+ if os.path.islink(dest):
+ # No need to delete a link and relink -> skip this
+ return False
+ elif os.path.isdir(dest):
+ diffs = filecmp.dircmp(source, dest)
+ ignored = get_ignored_files(source, diffs.left_only)
+ has_new_files = set(ignored) != set(diffs.left_only)
+ return (has_new_files or diffs.right_only or diffs.common_funny or
+ diffs.diff_files or diffs.funny_files)
+ else:
+ return not filecmp.cmp(source, dest)
+
+
+def get_lib_path(executable, name, required=True):
+ """Get the path of a python library.
+
+ Args:
+ executable: The Python executable to use.
+ name: The name of the library to get the path for.
+ required: Whether Error should be raised if the lib was not found.
+ """
+ code = [
+ 'try:',
+ ' import {}'.format(name),
+ 'except ImportError as e:',
+ ' print("ImportError: " + str(e))',
+ 'else:',
+ ' print("path: " + {}.__file__)'.format(name)
+ ]
+ output = run_py(executable, *code)
+
+ try:
+ prefix, data = output.split(': ')
+ except ValueError:
+ raise ValueError("Unexpected output: {!r}".format(output))
+
+ if prefix == 'path':
+ return data
+ elif prefix == 'ImportError':
+ if required:
+ raise Error("Could not import {} with {}: {}!".format(
+ name, executable, data))
+ else:
+ return None
+ else:
+ raise ValueError("Unexpected output: {!r}".format(output))
+
+
+def link_pyqt(executable, venv_path):
+ """Symlink the systemwide PyQt/sip into the venv.
+
+ Args:
+ executable: The python executable where the source files are present.
+ venv_path: The path to the virtualenv site-packages.
+ """
+ try:
+ get_lib_path(executable, 'PyQt5.sip')
+ except Error:
+ # There is no PyQt5.sip, so we need to copy the toplevel sip.
+ sip_file = get_lib_path(executable, 'sip')
+ else:
+ # There is a PyQt5.sip, it'll get copied with the PyQt5 dir.
+ sip_file = None
+
+ sipconfig_file = get_lib_path(executable, 'sipconfig', required=False)
+ pyqt_dir = os.path.dirname(get_lib_path(executable, 'PyQt5.QtCore'))
+
+ for path in [sip_file, sipconfig_file, pyqt_dir]:
+ if path is None:
+ continue
+
+ fn = os.path.basename(path)
+ dest = os.path.join(venv_path, fn)
+
+ if os.path.exists(dest):
+ if needs_update(path, dest):
+ remove(dest)
+ else:
+ continue
+
+ copy_or_link(path, dest)
+
+
+def copy_or_link(source, dest):
+ """Copy or symlink source to dest."""
+ if os.name == 'nt':
+ if os.path.isdir(source):
+ print('{} -> {}'.format(source, dest))
+ shutil.copytree(source, dest, ignore=get_ignored_files,
+ copy_function=verbose_copy)
+ else:
+ print('{} -> {}'.format(source, dest))
+ shutil.copy(source, dest)
+ else:
+ print('{} -> {}'.format(source, dest))
+ os.symlink(source, dest)
+
+
+def remove(filename):
+ """Remove a given filename, regardless of whether it's a file or dir."""
+ if os.path.isdir(filename):
+ shutil.rmtree(filename)
+ else:
+ os.unlink(filename)
+
+
+def get_venv_lib_path(path):
+ """Get the library path of a virtualenv."""
+ subdir = 'Scripts' if os.name == 'nt' else 'bin'
+ executable = os.path.join(path, subdir, 'python')
+ return run_py(executable,
+ 'from distutils.sysconfig import get_python_lib',
+ 'print(get_python_lib())')
+
+
+def get_tox_syspython(tox_path):
+ """Get the system python based on a virtualenv created by tox."""
+ path = os.path.join(tox_path, '.tox-config1')
+ with open(path, encoding='ascii') as f:
+ line = f.readline()
+ _md5, sys_python = line.rstrip().split(' ', 1)
+ return sys_python
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('path', help="Base path to the venv.")
+ parser.add_argument('--tox', help="Add when called via tox.",
+ action='store_true')
+ args = parser.parse_args()
+
+ if args.tox:
+ # Workaround for the lack of negative factors in tox.ini
+ if 'LINK_PYQT_SKIP' in os.environ:
+ print('LINK_PYQT_SKIP set, exiting...')
+ sys.exit(0)
+ executable = get_tox_syspython(args.path)
+ else:
+ executable = sys.executable
+
+ venv_path = get_venv_lib_path(args.path)
+ link_pyqt(executable, venv_path)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except Error as e:
+ print(str(e), file=sys.stderr)
+ sys.exit(1)
diff --git a/.config/qutebrowser/scripts/open_url_in_instance.sh b/.config/qutebrowser/scripts/open_url_in_instance.sh
new file mode 100755
index 0000000..a6ce0ed
--- /dev/null
+++ b/.config/qutebrowser/scripts/open_url_in_instance.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# initial idea: Florian Bruhin (The-Compiler)
+# author: Thore Bödecker (foxxx0)
+
+_url="$1"
+_qb_version='1.0.4'
+_proto_version=1
+_ipc_socket="${XDG_RUNTIME_DIR}/qutebrowser/ipc-$(echo -n "$USER" | md5sum | cut -d' ' -f1)"
+_qute_bin="/usr/bin/qutebrowser"
+
+printf '{"args": ["%s"], "target_arg": null, "version": "%s", "protocol_version": %d, "cwd": "%s"}\n' \
+ "${_url}" \
+ "${_qb_version}" \
+ "${_proto_version}" \
+ "${PWD}" | socat - UNIX-CONNECT:"${_ipc_socket}" 2>/dev/null || "$_qute_bin" "$@" &
diff --git a/.config/qutebrowser/scripts/setupcommon.py b/.config/qutebrowser/scripts/setupcommon.py
new file mode 100755
index 0000000..50eabac
--- /dev/null
+++ b/.config/qutebrowser/scripts/setupcommon.py
@@ -0,0 +1,74 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+
+"""Data used by setup.py and the PyInstaller qutebrowser.spec."""
+
+import sys
+import os
+import os.path
+import subprocess
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
+
+
+if sys.hexversion >= 0x03000000:
+ open_file = open
+else:
+ import codecs
+ open_file = codecs.open
+
+
+BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ os.path.pardir)
+
+
+def _git_str():
+ """Try to find out git version.
+
+ Return:
+ string containing the git commit ID and timestamp.
+ None if there was an error or we're not in a git repo.
+ """
+ if BASEDIR is None:
+ return None
+ if not os.path.isdir(os.path.join(BASEDIR, ".git")):
+ return None
+ try:
+ # https://stackoverflow.com/questions/21017300/21017394#21017394
+ commit_hash = subprocess.run(
+ ['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
+ cwd=BASEDIR, check=True,
+ stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
+ date = subprocess.run(
+ ['git', 'show', '-s', '--format=%ci', 'HEAD'],
+ cwd=BASEDIR, check=True,
+ stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
+ return '{} ({})'.format(commit_hash, date)
+ except (subprocess.CalledProcessError, OSError):
+ return None
+
+
+def write_git_file():
+ """Write the git-commit-id file with the current commit."""
+ gitstr = _git_str()
+ if gitstr is None:
+ gitstr = ''
+ path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
+ with open_file(path, 'w', encoding='ascii') as f:
+ f.write(gitstr)
diff --git a/.config/qutebrowser/scripts/testbrowser/cpp/webengine/main.cpp b/.config/qutebrowser/scripts/testbrowser/cpp/webengine/main.cpp
new file mode 100644
index 0000000..311432e
--- /dev/null
+++ b/.config/qutebrowser/scripts/testbrowser/cpp/webengine/main.cpp
@@ -0,0 +1,13 @@
+#include <QApplication>
+#include <QWebEngineView>
+#include <QUrl>
+
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ QWebEngineView view;
+ view.load(QUrl(argv[1]));
+ view.show();
+ return app.exec();
+}
diff --git a/.config/qutebrowser/scripts/testbrowser/cpp/webengine/testbrowser.pro b/.config/qutebrowser/scripts/testbrowser/cpp/webengine/testbrowser.pro
new file mode 100644
index 0000000..12a1cf7
--- /dev/null
+++ b/.config/qutebrowser/scripts/testbrowser/cpp/webengine/testbrowser.pro
@@ -0,0 +1,6 @@
+QT += core widgets webenginewidgets
+
+TARGET = testbrowser
+TEMPLATE = app
+
+SOURCES += main.cpp
diff --git a/.config/qutebrowser/scripts/testbrowser/cpp/webkit/main.cpp b/.config/qutebrowser/scripts/testbrowser/cpp/webkit/main.cpp
new file mode 100644
index 0000000..06c3d1a
--- /dev/null
+++ b/.config/qutebrowser/scripts/testbrowser/cpp/webkit/main.cpp
@@ -0,0 +1,13 @@
+#include <QApplication>
+#include <QWebView>
+#include <QUrl>
+
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ QWebView view;
+ view.load(QUrl(argv[1]));
+ view.show();
+ return app.exec();
+}
diff --git a/.config/qutebrowser/scripts/testbrowser/cpp/webkit/testbrowser.pro b/.config/qutebrowser/scripts/testbrowser/cpp/webkit/testbrowser.pro
new file mode 100644
index 0000000..59f55dd
--- /dev/null
+++ b/.config/qutebrowser/scripts/testbrowser/cpp/webkit/testbrowser.pro
@@ -0,0 +1,6 @@
+QT += core widgets webkit webkitwidgets
+
+TARGET = testbrowser
+TEMPLATE = app
+
+SOURCES += main.cpp
diff --git a/.config/qutebrowser/scripts/testbrowser/testbrowser_webengine.py b/.config/qutebrowser/scripts/testbrowser/testbrowser_webengine.py
new file mode 100755
index 0000000..fdf6728
--- /dev/null
+++ b/.config/qutebrowser/scripts/testbrowser/testbrowser_webengine.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Very simple browser for testing purposes."""
+
+import sys
+import argparse
+
+from PyQt5.QtCore import QUrl
+from PyQt5.QtWidgets import QApplication
+from PyQt5.QtWebEngineWidgets import QWebEngineView
+
+
+def parse_args():
+ """Parse commandline arguments."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument('url', help='The URL to open')
+ return parser.parse_known_args()[0]
+
+
+if __name__ == '__main__':
+ args = parse_args()
+ app = QApplication(sys.argv)
+ wv = QWebEngineView()
+
+ wv.loadStarted.connect(lambda: print("Loading started"))
+ wv.loadProgress.connect(lambda p: print("Loading progress: {}%".format(p)))
+ wv.loadFinished.connect(lambda: print("Loading finished"))
+
+ wv.load(QUrl.fromUserInput(args.url))
+ wv.show()
+
+ app.exec_()
diff --git a/.config/qutebrowser/scripts/testbrowser/testbrowser_webkit.py b/.config/qutebrowser/scripts/testbrowser/testbrowser_webkit.py
new file mode 100755
index 0000000..73cae08
--- /dev/null
+++ b/.config/qutebrowser/scripts/testbrowser/testbrowser_webkit.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Very simple browser for testing purposes."""
+
+import sys
+import argparse
+
+from PyQt5.QtCore import QUrl
+from PyQt5.QtWidgets import QApplication
+from PyQt5.QtWebKit import QWebSettings
+from PyQt5.QtWebKitWidgets import QWebView
+
+
+def parse_args():
+ """Parse commandline arguments."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument('url', help='The URL to open')
+ parser.add_argument('--plugins', '-p', help='Enable plugins',
+ default=False, action='store_true')
+ return parser.parse_known_args()[0]
+
+
+if __name__ == '__main__':
+ args = parse_args()
+ app = QApplication(sys.argv)
+ wv = QWebView()
+
+ wv.loadStarted.connect(lambda: print("Loading started"))
+ wv.loadProgress.connect(lambda p: print("Loading progress: {}%".format(p)))
+ wv.loadFinished.connect(lambda: print("Loading finished"))
+
+ if args.plugins:
+ wv.settings().setAttribute(QWebSettings.PluginsEnabled, True)
+
+ wv.load(QUrl.fromUserInput(args.url))
+ wv.show()
+
+ app.exec_()
diff --git a/.config/qutebrowser/scripts/utils.py b/.config/qutebrowser/scripts/utils.py
new file mode 100755
index 0000000..9a1a751
--- /dev/null
+++ b/.config/qutebrowser/scripts/utils.py
@@ -0,0 +1,103 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Utility functions for scripts."""
+
+import os
+import os.path
+
+
+# Import side-effects are an evil thing, but here it's okay so scripts using
+# colors work on Windows as well.
+try:
+ import colorama
+except ImportError:
+ colorama = None
+else:
+ colorama.init()
+
+
+use_color = os.name != 'nt' or colorama
+
+
+fg_colors = {
+ 'black': 30,
+ 'red': 31,
+ 'green': 32,
+ 'yellow': 33,
+ 'blue': 34,
+ 'magenta': 35,
+ 'cyan': 36,
+ 'white': 37,
+ 'reset': 39,
+}
+
+
+bg_colors = {name: col + 10 for name, col in fg_colors.items()}
+
+
+term_attributes = {
+ 'bright': 1,
+ 'dim': 2,
+ 'normal': 22,
+ 'reset': 0,
+}
+
+
+def _esc(code):
+ """Get an ANSI color code based on a color number."""
+ return '\033[{}m'.format(code)
+
+
+def print_col(text, color):
+ """Print a colorized text."""
+ if use_color:
+ fg = _esc(fg_colors[color.lower()])
+ reset = _esc(fg_colors['reset'])
+ print(''.join([fg, text, reset]))
+ else:
+ print(text)
+
+
+def print_title(text):
+ """Print a title."""
+ print_col("==================== {} ====================".format(text),
+ 'yellow')
+
+
+def print_subtitle(text):
+ """Print a subtitle."""
+ print_col("------ {} ------".format(text), 'cyan')
+
+
+def print_bold(text):
+ """Print a bold text."""
+ if use_color:
+ bold = _esc(term_attributes['bright'])
+ reset = _esc(term_attributes['reset'])
+ print(''.join([bold, text, reset]))
+ else:
+ print(text)
+
+
+def change_cwd():
+ """Change the scripts cwd if it was started inside the script folder."""
+ cwd = os.getcwd()
+ if os.path.split(cwd)[1] == 'scripts':
+ os.chdir(os.path.join(cwd, os.pardir))