diff --git a/.config/qutebrowser/config.py b/.config/qutebrowser/config.py
index 2f27d134..cfaa9d6e 100644
--- a/.config/qutebrowser/config.py
+++ b/.config/qutebrowser/config.py
@@ -74,7 +74,12 @@ sets = {
["k", "quickmark-save"],
["J", "search-prev"],
["j", "search-next"],
- ["", "hint links spawn --detach mpv {hint-url}"],
+ [
+ "",
+ "hint links spawn --detach mpv --force-window --quiet --keep-open=yes --ytdl",
+ ],
+ ["", "spawn --userscript view_in_mpv"],
+ # ["", "hint links spawn --userscript view_in_mpv"],
["gm", "tab-focus 1"],
["gi", "tab-focus -1"],
["gN", "tab-move +"],
diff --git a/.config/qutebrowser/userscripts/code_select.py b/.config/qutebrowser/userscripts/code_select.py
new file mode 100644
index 00000000..8f7fc312
--- /dev/null
+++ b/.config/qutebrowser/userscripts/code_select.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+import os
+import html
+import re
+import sys
+import xml.etree.ElementTree as ET
+try:
+ import pyperclip
+except ImportError:
+ try:
+ import pyclip as pyperclip
+ except ImportError:
+ PYPERCLIP = False
+ else:
+ PYPERCLIP = True
+else:
+ PYPERCLIP = True
+
+
+def parse_text_content(element):
+ # https://stackoverflow.com/a/35591507/15245191
+ magic = '''
+ ]>'''
+ root = ET.fromstring(magic + element)
+ text = ET.tostring(root, encoding="unicode", method="text")
+ text = html.unescape(text)
+ return text
+
+
+def send_command_to_qute(command):
+ with open(os.environ.get("QUTE_FIFO"), "w") as f:
+ f.write(command)
+
+
+def main():
+ delimiter = sys.argv[1] if len(sys.argv) > 1 else ";"
+ # For info on qute environment vairables, see
+ # https://github.com/qutebrowser/qutebrowser/blob/master/doc/userscripts.asciidoc
+ element = os.environ.get("QUTE_SELECTED_HTML")
+ code_text = parse_text_content(element)
+ re_remove_dollars = re.compile(r"^(\$ )", re.MULTILINE)
+ code_text = re.sub(re_remove_dollars, '', code_text)
+ if PYPERCLIP:
+ pyperclip.copy(code_text)
+ send_command_to_qute(
+ "message-info 'copied to clipboard: {info}{suffix}'".format(
+ info=code_text.splitlines()[0].replace("'", "\""),
+ suffix="..." if len(code_text.splitlines()) > 1 else ""
+ )
+ )
+ else:
+ # Qute's yank command won't copy accross multiple lines so we
+ # compromise by placing lines on a single line seperated by the
+ # specified delimiter
+ code_text = re.sub("(\n)+", delimiter, code_text)
+ code_text = code_text.replace("'", "\"")
+ send_command_to_qute("yank inline '{code}'\n".format(code=code_text))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.config/qutebrowser/userscripts/localhost b/.config/qutebrowser/userscripts/localhost
new file mode 100644
index 00000000..1715c10a
--- /dev/null
+++ b/.config/qutebrowser/userscripts/localhost
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+if [[ $1 -eq 'list' ]] && [[ -z $QUTE_COUNT ]];
+then
+ PORTS="$(ss -nltp | tail -n +2 | awk '{print $4}' | awk -F: '{print $2}')"
+ QUTE_COUNT=$(echo "$PORTS" | dmenu )
+fi
+
+echo open -t localhost:${QUTE_COUNT:-8080} > $QUTE_FIFO
diff --git a/.config/qutebrowser/userscripts/qute-bitwarden b/.config/qutebrowser/userscripts/qute-bitwarden
new file mode 100644
index 00000000..4e755772
--- /dev/null
+++ b/.config/qutebrowser/userscripts/qute-bitwarden
@@ -0,0 +1,308 @@
+#!/usr/bin/env python3
+
+# SPDX-FileCopyrightText: Chris Braun (cryzed)
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""
+Insert login information using Bitwarden CLI and a dmenu-compatible application
+(e.g. dmenu, rofi -dmenu, ...).
+"""
+
+USAGE = """The domain of the site has to be in the name of the Bitwarden entry, for example: "github.com/cryzed" or
+"websites/github.com". The login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
+[USERNAME][PASSWORD], which is compatible with almost all login forms.
+
+If enabled, with the `--totp` flag, it will also move the TOTP code to the
+clipboard, much like the Firefox add-on.
+
+You must log into Bitwarden CLI using `bw login` prior to use of this script.
+The session key will be stored using keyctl for the number of seconds passed to
+the --auto-lock option.
+
+To use in qutebrowser, run: `spawn --userscript qute-bitwarden`
+"""
+
+EPILOG = """Dependencies: tldextract (Python 3 module), pyperclip (optional
+Python module, used for TOTP codes), Bitwarden CLI (1.7.4 is known to work
+but older versions may well also work)
+
+WARNING: The login details are viewable as plaintext in qutebrowser's debug log
+(qute://log) and might be shared if you decide to submit a crash report!"""
+
+import argparse
+import enum
+import functools
+import os
+import shlex
+import subprocess
+import sys
+import json
+import tldextract
+
+argument_parser = argparse.ArgumentParser(
+ description=__doc__,
+ usage=USAGE,
+ epilog=EPILOG,
+)
+argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
+argument_parser.add_argument('--dmenu-invocation', '-d', default='rofi -dmenu -i -p Bitwarden',
+ help='Invocation used to execute a dmenu-provider')
+argument_parser.add_argument('--password-prompt-invocation', '-p', default='rofi -dmenu -p "Master Password" -password -lines 0',
+ help='Invocation used to prompt the user for their Bitwarden password')
+argument_parser.add_argument('--no-insert-mode', '-n', dest='insert_mode', action='store_false',
+ help="Don't automatically enter insert mode")
+argument_parser.add_argument('--totp', '-t', action='store_true',
+ help="Copy TOTP key to clipboard")
+argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
+ help='Encoding used to communicate with subprocesses')
+argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
+ help='Merge pass candidates for fully-qualified and registered domain name')
+argument_parser.add_argument('--auto-lock', type=int, default=900,
+ help='Automatically lock the vault after this many seconds')
+group = argument_parser.add_mutually_exclusive_group()
+group.add_argument('--username-only', '-e',
+ action='store_true', help='Only insert username')
+group.add_argument('--password-only', '-w',
+ action='store_true', help='Only insert password')
+group.add_argument('--totp-only', '-T',
+ action='store_true', help='Only insert totp code')
+
+stderr = functools.partial(print, file=sys.stderr)
+
+
+class ExitCodes(enum.IntEnum):
+ SUCCESS = 0
+ FAILURE = 1
+ # 1 is automatically used if Python throws an exception
+ NO_PASS_CANDIDATES = 2
+ COULD_NOT_MATCH_USERNAME = 3
+ COULD_NOT_MATCH_PASSWORD = 4
+
+
+def qute_command(command):
+ with open(os.environ['QUTE_FIFO'], 'w') as fifo:
+ fifo.write(command + '\n')
+ fifo.flush()
+
+
+def ask_password(password_prompt_invocation):
+ process = subprocess.run(
+ shlex.split(password_prompt_invocation),
+ text=True,
+ stdout=subprocess.PIPE,
+ )
+ if process.returncode > 0:
+ raise Exception('Could not unlock vault')
+ master_pass = process.stdout.strip()
+ return subprocess.check_output(
+ ['bw', 'unlock', '--raw', '--passwordenv', 'BW_MASTERPASS'],
+ env={**os.environ, 'BW_MASTERPASS': master_pass},
+ text=True,
+ ).strip()
+
+
+def get_session_key(auto_lock, password_prompt_invocation):
+ if auto_lock == 0:
+ subprocess.call(['keyctl', 'purge', 'user', 'bw_session'])
+ return ask_password(password_prompt_invocation)
+ else:
+ process = subprocess.run(
+ ['keyctl', 'request', 'user', 'bw_session'],
+ text=True,
+ stdout=subprocess.PIPE,
+ )
+ key_id = process.stdout.strip()
+ if process.returncode > 0:
+ session = ask_password(password_prompt_invocation)
+ if not session:
+ raise Exception('Could not unlock vault')
+ key_id = subprocess.check_output(
+ ['keyctl', 'add', 'user', 'bw_session', session, '@u'],
+ text=True,
+ ).strip()
+
+ if auto_lock > 0:
+ subprocess.call(['keyctl', 'timeout', str(key_id), str(auto_lock)])
+ return subprocess.check_output(
+ ['keyctl', 'pipe', str(key_id)],
+ text=True,
+ ).strip()
+
+
+def pass_(domain, encoding, auto_lock, password_prompt_invocation):
+ session_key = get_session_key(auto_lock, password_prompt_invocation)
+ process = subprocess.run(
+ ['bw', 'list', 'items', '--nointeraction', '--session', session_key, '--url', domain],
+ capture_output=True,
+ )
+
+ err = process.stderr.decode(encoding).strip()
+ if err:
+ msg = 'Bitwarden CLI returned for {:s} - {:s}'.format(domain, err)
+ stderr(msg)
+
+ if "Vault is locked" in err:
+ stderr("Bitwarden Vault got locked, trying again with clean session")
+ return pass_(domain, encoding, 0, password_prompt_invocation)
+
+ if process.returncode:
+ return '[]'
+
+ out = process.stdout.decode(encoding).strip()
+
+ return out
+
+
+def get_totp_code(selection_id, domain_name, encoding, auto_lock, password_prompt_invocation):
+ session_key = get_session_key(auto_lock, password_prompt_invocation)
+ process = subprocess.run(
+ ['bw', 'get', 'totp', '--nointeraction', '--session', session_key, selection_id],
+ capture_output=True,
+ )
+
+ err = process.stderr.decode(encoding).strip()
+ if err:
+ # domain_name instead of selection_id to make it more user-friendly
+ msg = 'Bitwarden CLI returned for {:s} - {:s}'.format(domain_name, err)
+ stderr(msg)
+
+ if "Vault is locked" in err:
+ stderr("Bitwarden Vault got locked, trying again with clean session")
+ return get_totp_code(selection_id, domain_name, encoding, 0, password_prompt_invocation)
+
+ if process.returncode:
+ return '[]'
+
+ out = process.stdout.decode(encoding).strip()
+
+ return out
+
+
+def dmenu(items, invocation, encoding):
+ command = shlex.split(invocation)
+ process = subprocess.run(command, input='\n'.join(
+ items).encode(encoding), stdout=subprocess.PIPE)
+ return process.stdout.decode(encoding).strip()
+
+
+def fake_key_raw(text):
+ for character in text:
+ # Escape all characters by default, space requires special handling
+ sequence = '" "' if character == ' ' else r'\{}'.format(character)
+ qute_command('fake-key {}'.format(sequence))
+
+
+def main(arguments):
+ if not arguments.url:
+ argument_parser.print_help()
+ return ExitCodes.FAILURE
+
+ extract_result = tldextract.extract(arguments.url)
+
+ # Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
+ # the registered domain name and finally: the IPv4 address if that's what
+ # the URL represents
+ candidates = []
+ for target in filter(
+ None,
+ [
+ extract_result.fqdn,
+ (
+ extract_result.top_domain_under_public_suffix
+ if hasattr(extract_result, "top_domain_under_public_suffix")
+ else extract_result.registered_domain
+ ),
+ extract_result.subdomain + "." + extract_result.domain,
+ extract_result.domain,
+ extract_result.ipv4,
+ ],
+ ):
+ target_candidates = json.loads(
+ pass_(
+ target,
+ arguments.io_encoding,
+ arguments.auto_lock,
+ arguments.password_prompt_invocation,
+ )
+ )
+ if not target_candidates:
+ continue
+
+ candidates = candidates + target_candidates
+ if not arguments.merge_candidates:
+ break
+ else:
+ if not candidates:
+ stderr('No pass candidates for URL {!r} found!'.format(
+ arguments.url))
+ return ExitCodes.NO_PASS_CANDIDATES
+
+ if len(candidates) == 1:
+ selection = candidates.pop()
+ else:
+ choices = ['{:s} | {:s}'.format(c['name'], c['login']['username']) for c in candidates]
+ choice = dmenu(choices, arguments.dmenu_invocation, arguments.io_encoding)
+ choice_tokens = choice.split('|')
+ choice_name = choice_tokens[0].strip()
+ choice_username = choice_tokens[1].strip()
+ selection = next((c for (i, c) in enumerate(candidates)
+ if c['name'] == choice_name
+ and c['login']['username'] == choice_username),
+ None)
+
+ # Nothing was selected, simply return
+ if not selection:
+ return ExitCodes.SUCCESS
+
+ username = selection['login']['username']
+ password = selection['login']['password']
+ totp = selection['login']['totp']
+
+ if arguments.username_only:
+ fake_key_raw(username)
+ elif arguments.password_only:
+ fake_key_raw(password)
+ elif arguments.totp_only:
+ # No point in moving it to the clipboard in this case
+ fake_key_raw(
+ get_totp_code(
+ selection['id'],
+ selection['name'],
+ arguments.io_encoding,
+ arguments.auto_lock,
+ arguments.password_prompt_invocation,
+ )
+ )
+ else:
+ # Enter username and password using fake-key and (which seems to work almost universally), then switch
+ # back into insert-mode, so the form can be directly submitted by
+ # hitting enter afterwards
+ fake_key_raw(username)
+ qute_command('fake-key ')
+ fake_key_raw(password)
+
+ if arguments.insert_mode:
+ qute_command('mode-enter insert')
+
+ # If it finds a TOTP code, it copies it to the clipboard,
+ # which is the same behavior as the Firefox add-on.
+ if not arguments.totp_only and totp and arguments.totp:
+ # The import is done here, to make pyperclip an optional dependency
+ import pyperclip
+ pyperclip.copy(
+ get_totp_code(
+ selection['id'],
+ selection['name'],
+ arguments.io_encoding,
+ arguments.auto_lock,
+ arguments.password_prompt_invocation,
+ )
+ )
+
+ return ExitCodes.SUCCESS
+
+
+if __name__ == '__main__':
+ arguments = argument_parser.parse_args()
+ sys.exit(main(arguments))
diff --git a/.config/qutebrowser/userscripts/readability b/.config/qutebrowser/userscripts/readability
new file mode 100644
index 00000000..07095a5b
--- /dev/null
+++ b/.config/qutebrowser/userscripts/readability
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+#
+# Executes python-readability on current page and opens the summary as new tab.
+#
+# Depends on the python-readability package, or its fork:
+#
+# - https://github.com/buriy/python-readability
+# - https://github.com/bookieio/breadability
+#
+# Usage:
+# :spawn --userscript readability
+#
+import codecs, os
+
+tmpfile = os.path.join(
+ os.environ.get('QUTE_DATA_DIR',
+ os.path.expanduser('~/.local/share/qutebrowser')),
+ 'userscripts/readability.html')
+
+if not os.path.exists(os.path.dirname(tmpfile)):
+ os.makedirs(os.path.dirname(tmpfile))
+
+# Styling for dynamic window margin scaling and line height
+HEADER = """
+
+
+
+
+ %s
+
+
+
+"""
+
+with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source:
+ data = source.read()
+
+ try:
+ from breadability.readable import Article as reader
+ doc = reader(data, os.environ['QUTE_URL'])
+ title = doc._original_document.title
+ content = HEADER % title + doc.readable + ""
+ except ImportError:
+ from readability import Document
+ doc = Document(data)
+ title = doc.title()
+ content = doc.summary().replace('', HEADER % title)
+
+ # add a class to make styling the page easier
+ content = content.replace('', '')
+
+ with codecs.open(tmpfile, 'w', 'utf-8') as target:
+ target.write(content.lstrip())
+
+ with open(os.environ['QUTE_FIFO'], 'w') as fifo:
+ fifo.write('open -t %s' % tmpfile)
diff --git a/.config/qutebrowser/userscripts/view_in_mpv b/.config/qutebrowser/userscripts/view_in_mpv
new file mode 100644
index 00000000..4f371c6b
--- /dev/null
+++ b/.config/qutebrowser/userscripts/view_in_mpv
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+#
+# Behavior:
+# Userscript for qutebrowser which views the current web page in mpv using
+# sensible mpv-flags. While viewing the page in MPV, all
+
+ In order to restore this particular video
+ click here.
+
+ ";
+ replacement.style.position = "relative";
+ replacement.style.zIndex = "100003000000";
+ replacement.style.fontSize = "1rem";
+ replacement.style.textAlign = "center";
+ replacement.style.verticalAlign = "middle";
+ replacement.style.height = "100%";
+ replacement.style.background = "#101010";
+ replacement.style.color = "white";
+ replacement.style.border = "4px dashed #545454";
+ replacement.style.padding = "2em";
+ replacement.style.margin = "auto";
+ App.all_replacements[i] = replacement;
+ App.backup_videos[i] = video;
+ video.parentNode.replaceChild(replacement, video);
+ }
+
+ function restore_video(obj, index) {
+ obj = App.all_replacements[index];
+ video = App.backup_videos[index];
+ obj.parentNode.replaceChild(video, obj);
+ }
+
+ /** force repainting the video, thanks to:
+ * http://web.archive.org/web/20151029064649/https://martinwolf.org/2014/06/10/force-repaint-of-an-element-with-javascript/
+ */
+ var siteHeader = document.getElementById('header');
+ siteHeader.style.display='none';
+ siteHeader.offsetHeight; // no need to store this anywhere, the reference is enough
+ siteHeader.style.display='block';
+
+EOF
+}
+
+printjs() {
+ js | sed 's,//.*$,,' | tr '\n' ' '
+}
+echo "jseval -q -w main $(printjs)" >> "$QUTE_FIFO"
+
+msg info "Opening $QUTE_URL with mpv"
+"${video_command[@]}" "$@" "$QUTE_URL"