]> Freerunner's - dotfiles.git/commitdiff
qutebrowser: remove symlink and move userscripts to local
authorAndre Ramnitz <tux.rising@gmail.com>
Mon, 15 Jan 2024 07:42:23 +0000 (08:42 +0100)
committerAndre Ramnitz <tux.rising@gmail.com>
Sun, 18 Aug 2024 16:18:53 +0000 (18:18 +0200)
config/qutebrowser/config.py
config/qutebrowser/userscripts/qr [deleted file]
config/qutebrowser/userscripts/qute-keepassxc [deleted file]
config/qutebrowser/userscripts/view_in_mpv [deleted file]
local/share/qutebrowser/userscripts/dmenu_qutebrowser [new file with mode: 0755]
local/share/qutebrowser/userscripts/qr [new file with mode: 0755]
local/share/qutebrowser/userscripts/qute-keepassxc [new file with mode: 0755]
local/share/qutebrowser/userscripts/qutedmenu [new file with mode: 0755]
local/share/qutebrowser/userscripts/view_in_mpv [new file with mode: 0755]

index 19a42a87d98bb1be0c46b2ef5ffe97ad3bc0547b..d37575c607f7d19218535d9cd5ad04c43dc7f370 100644 (file)
@@ -1232,6 +1232,9 @@ c.fonts.web.size.minimum_logical = 7
 
 # Bindings for normal mode
 config.unbind('+')
+config.unbind('q')
+config.bind('q', 'spawn --userscript dmenu_qutebrowser')
+config.bind('Q', 'spawn --userscript qr')
 config.bind('j', 'cmd-run-with-count 3 scroll down')
 config.bind('k', 'cmd-run-with-count 3 scroll up')
 config.bind(',M', 'hint links spawn vlc {hint-url}')
diff --git a/config/qutebrowser/userscripts/qr b/config/qutebrowser/userscripts/qr
deleted file mode 100755 (executable)
index 8421524..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-pngfile=$(mktemp --suffix=.png)
-trap 'rm -f "$pngfile"' EXIT
-
-qrencode -t PNG -o "$pngfile" -s 10 "$QUTE_URL"
-echo ":open -t file:///$pngfile" >> "$QUTE_FIFO"
-sleep 1  # give qutebrowser time to open the file before it gets removed
diff --git a/config/qutebrowser/userscripts/qute-keepassxc b/config/qutebrowser/userscripts/qute-keepassxc
deleted file mode 100755 (executable)
index d5970cf..0000000
+++ /dev/null
@@ -1,435 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.com>
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-"""
-# Introduction
-
-This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database.
-
-
-# Installation
-
-First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config.
-
-
-Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3].
-
-
-Third, install the python module `pynacl`.
-
-
-Finally, adapt your qutebrowser config.
-You can e.g. add the following lines to your `~/.config/qutebrowser/config.py`
-Remember to replace `ABC1234` with your actual GPG key.
-
-```python
-config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert')
-config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
-```
-
-To manage multiple accounts you also need [rofi](https://github.com/davatorium/rofi) installed.
-
-
-# Usage
-
-If you are on a webpage with a login form, simply activate one of the configured key-bindings.
-
-The first time you run this script, KeepassXC will ask you for authentication like with any other browser extension.
-Just provide a name of your choice and accept the request if nothing looks fishy.
-
-
-# How it works
-
-This script will talk to KeepassXC using the native [KeepassXC-Browser protocol][4].
-
-
-This script needs to store the key used to associate with your KeepassXC instance somewhere.
-Unlike most browser extensions which only use plain local storage, this one attempts to do so in a safe way
-by storing the key in encrypted form using GPG.
-Therefore you need to have a public-key-pair readily set up.
-
-GPG might then ask for your private-key password whenever you query the database for login credentials.
-
-
-# TOTP
-
-This script recently received experimental TOTP support.
-To use it, you need to have working TOTP authentication within KeepassXC.
-Then call `qute-keepassxc` with the `--totp` flags.
-
-For example, I have the following line in my `config.py`:
-
-```python
-config.bind('pt', 'spawn --userscript qute-keepassxc --key ABC1234 --totp', mode='normal')
-```
-
-For now this script will simply insert the TOTP-token into the currently selected
-input field, since I have not yet found a reliable way to identify the correct field
-within all existing login forms.
-Thus you need to manually select the TOTP input field, press escape to leave input
-mode and then enter `pt` to fill in the token (or configure another key-binding for
-insert mode if you prefer that).
-
-
-[1]: https://keepassxc.org/
-[2]: https://qutebrowser.org/
-[3]: https://gnupg.org/
-[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
-[5]: https://github.com/qutebrowser/qutebrowser/blob/main/doc/userscripts.asciidoc
-[6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration
-"""
-
-import sys
-import os
-import socket
-import json
-import base64
-import subprocess
-import argparse
-
-import nacl.utils
-import nacl.public
-
-
-def parse_args():
-    parser = argparse.ArgumentParser(description="Full passwords from KeepassXC")
-    parser.add_argument('url', nargs='?', default=os.environ.get('QUTE_URL'))
-    parser.add_argument('--totp', action='store_true',
-                        help="Fill in current TOTP field instead of username/password")
-    parser.add_argument('--socket', '-s', default='/run/user/{}/org.keepassxc.KeePassXC.BrowserServer'.format(os.getuid()),
-                        help='Path to KeepassXC browser socket')
-    parser.add_argument('--key', '-k', default='alice@example.com',
-                        help='GPG key to encrypt KeepassXC auth key with')
-    parser.add_argument('--insecure', action='store_true',
-                        help="Do not encrypt auth key")
-    return parser.parse_args()
-
-
-class KeepassError(Exception):
-    def __init__(self, code, desc):
-        self.code = code
-        self.description = desc
-
-    def __str__(self):
-        return f"KeepassXC Error [{self.code}]: {self.description}"
-
-
-class KeepassXC:
-    """ Wrapper around the KeepassXC socket API """
-    def __init__(self, id=None, *, key, socket_path):
-        self.sock        = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        self.id          = id
-        self.socket_path = socket_path
-        self.client_key  = nacl.public.PrivateKey.generate()
-        self.id_key      = nacl.public.PrivateKey.from_seed(key)
-        self.cryptobox   = None
-
-    def connect(self):
-        if not os.path.exists(self.socket_path):
-            raise KeepassError(-1, "KeepassXC Browser socket does not exists")
-        self.client_id = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8')
-        self.sock.connect(self.socket_path)
-
-        self.send_raw_msg(dict(
-            action    = 'change-public-keys',
-            publicKey = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
-            nonce     = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8'),
-            clientID  = self.client_id
-        ))
-
-        resp = self.recv_raw_msg()
-        assert resp['action'] == 'change-public-keys'
-        assert resp['success'] == 'true'
-        assert resp['nonce']
-        self.cryptobox = nacl.public.Box(
-            self.client_key,
-            nacl.public.PublicKey(base64.b64decode(resp['publicKey']))
-        )
-
-    def get_databasehash(self):
-        self.send_msg(dict(action='get-databasehash'))
-        return self.recv_msg()['hash']
-
-    def lock_database(self):
-        self.send_msg(dict(action='lock-database'))
-        try:
-            self.recv_msg()
-        except KeepassError as e:
-            if e.code == 1:
-                return True
-            raise
-        return False
-
-
-    def test_associate(self):
-        if not self.id:
-            return False
-        self.send_msg(dict(
-            action = 'test-associate',
-            id     = self.id,
-            key    = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
-        ), triggerUnlock = 'true')
-        return self.recv_msg()['success'] == 'true'
-
-    def associate(self):
-        self.send_msg(dict(
-            action = 'associate',
-            key    = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
-            idKey  = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
-        ))
-        resp = self.recv_msg()
-        self.id = resp['id']
-
-    def get_logins(self, url):
-        self.send_msg(dict(
-            action = 'get-logins',
-            url    = url,
-            keys   = [{ 'id': self.id, 'key': base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') }]
-        ))
-        return self.recv_msg()['entries']
-
-    def get_totp(self, uuid):
-        self.send_msg(dict(
-            action = 'get-totp',
-            uuid = uuid
-        ))
-        response = self.recv_msg()
-        if response['success'] != 'true' or not response['totp']:
-            return None
-        return response['totp']
-
-    def send_raw_msg(self, msg):
-        self.sock.send( json.dumps(msg).encode('utf-8') )
-
-    def recv_raw_msg(self):
-        return json.loads( self.sock.recv(4096).decode('utf-8') )
-
-    def send_msg(self, msg, **extra):
-        nonce = nacl.utils.random(nacl.public.Box.NONCE_SIZE)
-        self.send_raw_msg(dict(
-            action   = msg['action'],
-            message  = base64.b64encode(self.cryptobox.encrypt(json.dumps(msg).encode('utf-8'), nonce).ciphertext).decode('utf-8'),
-            nonce    = base64.b64encode(nonce).decode('utf-8'),
-            clientID = self.client_id,
-            **extra
-        ))
-
-    def recv_msg(self):
-        resp = self.recv_raw_msg()
-        if 'error' in resp:
-            raise KeepassError(resp['errorCode'], resp['error'])
-        assert resp['action']
-        return json.loads(self.cryptobox.decrypt(base64.b64decode(resp['message']), base64.b64decode(resp['nonce'])).decode('utf-8'))
-
-
-
-class SecretKeyStore:
-    def __init__(self, gpgkey, insecure):
-        self.gpgkey = gpgkey
-        self.insecure = insecure
-        if self.insecure:
-            self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key')
-        else:
-            self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key.gpg')
-
-    def load(self):
-        "Load existing association key from file"
-        if self.insecure:
-            jsondata = open(self.path, 'r').read()
-        else:
-            jsondata = subprocess.check_output(['gpg', '--decrypt', self.path]).decode('utf-8')
-        data = json.loads(jsondata)
-        self.id = data['id']
-        self.key = base64.b64decode(data['key'])
-
-    def create(self):
-        "Create new association key"
-        self.key = nacl.utils.random(32)
-        self.id = None
-
-    def store(self, id):
-        "Store newly created association key in file"
-        self.id = id
-        jsondata = json.dumps({'id':self.id, 'key':base64.b64encode(self.key).decode('utf-8')})
-        if self.insecure:
-            open(self.path, "w").write(jsondata)
-        else:
-            subprocess.run(['gpg', '--encrypt', '-o', self.path, '-r', self.gpgkey], input=jsondata.encode('utf-8'), check=True)
-
-
-def qute(cmd):
-    with open(os.environ['QUTE_FIFO'], 'w') as fifo:
-        fifo.write(cmd)
-        fifo.write('\n')
-        fifo.flush()
-
-def error(msg):
-    print(msg, file=sys.stderr)
-    qute('message-error "{}"'.format(msg))
-
-
-def connect_to_keepassxc(args):
-    if not args.insecure and not args.key:
-        error("Missing GPG key to use for auth key encryption")
-        return
-    keystore = SecretKeyStore(args.key, args.insecure)
-    if os.path.isfile(keystore.path):
-        keystore.load()
-        kp = KeepassXC(keystore.id, key=keystore.key, socket_path=args.socket)
-        kp.connect()
-        if not kp.test_associate():
-            error('No KeepassXC association')
-            return None
-    else:
-        keystore.create()
-        kp = KeepassXC(key=keystore.key, socket_path=args.socket)
-        kp.connect()
-        kp.associate()
-        if not kp.test_associate():
-            error('No KeepassXC association')
-            return None
-        keystore.store(kp.id)
-    return kp
-
-
-def select_account(creds):
-    try:
-        if len(creds) == 1:
-            return creds[0]
-        idx = subprocess.check_output(
-                ['rofi', '-dmenu', '-format', 'i', '-matching', 'fuzzy',
-                '-p', 'Search',
-                '-mesg', '<b>qute-keepassxc</b>: select an account, please!'],
-                input=b"\n".join(c['login'].encode('utf-8') for c in creds)
-        )
-        idx = int(idx)
-        if idx < 0:
-            return None
-        return creds[idx]
-    except subprocess.CalledProcessError:
-        return None
-    except FileNotFoundError:
-        error("rofi not found. Please install rofi to select from multiple credentials")
-        return creds[0]
-    except Exception as e:
-        error(f"Error while picking account: {e}")
-        return None
-
-
-def make_js_code(username, password):
-    return ' '.join("""
-        function isVisible(elem) {
-            var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
-
-            if (style.getPropertyValue("visibility") !== "visible" ||
-                style.getPropertyValue("display") === "none" ||
-                style.getPropertyValue("opacity") === "0") {
-                return false;
-            }
-
-            return elem.offsetWidth > 0 && elem.offsetHeight > 0;
-        };
-
-        function hasPasswordField(form) {
-            var inputs = form.getElementsByTagName("input");
-            for (var j = 0; j < inputs.length; j++) {
-                var input = inputs[j];
-                if (input.type === "password") {
-                    return true;
-                }
-            }
-            return false;
-        };
-
-        function loadData2Form (form) {
-            var inputs = form.getElementsByTagName("input");
-            for (var j = 0; j < inputs.length; j++) {
-                var input = inputs[j];
-                if (isVisible(input) && (input.type === "text" || input.type === "email")) {
-                    input.focus();
-                    input.value = %s;
-                    input.dispatchEvent(new Event('input', { 'bubbles': true }));
-                    input.dispatchEvent(new Event('change', { 'bubbles': true }));
-                    input.blur();
-                }
-                if (input.type === "password") {
-                    input.focus();
-                    input.value = %s;
-                    input.dispatchEvent(new Event('input', { 'bubbles': true }));
-                    input.dispatchEvent(new Event('change', { 'bubbles': true }));
-                    input.blur();
-                }
-            }
-        };
-
-        function fillFirstForm() {
-            var forms = document.getElementsByTagName("form");
-            for (i = 0; i < forms.length; i++) {
-                if (hasPasswordField(forms[i])) {
-                    loadData2Form(forms[i]);
-                    return;
-                }
-            }
-            alert("No Credentials Form found");
-        };
-
-        fillFirstForm()
-    """.splitlines()) % (json.dumps(username), json.dumps(password))
-
-
-def make_js_totp_code(totp):
-    return ' '.join("""
-        (function () {
-            var input = document.activeElement;
-            if (!input || input.tagName !== "INPUT") {
-                alert("No TOTP input field selected");
-                return;
-            }
-            input.value = %s;
-            input.dispatchEvent(new Event('input', { 'bubbles': true }));
-            input.dispatchEvent(new Event('change', { 'bubbles': true }));
-        })();
-    """.splitlines()) % (json.dumps(totp),)
-
-
-def main():
-    if 'QUTE_FIFO' not in os.environ:
-        print(f"No QUTE_FIFO found - {sys.argv[0]} must be run as a qutebrowser userscript")
-        sys.exit(-1)
-
-    try:
-        args = parse_args()
-        assert args.url, "Missing URL"
-        kp = connect_to_keepassxc(args)
-        if not kp:
-            error('Could not connect to KeepassXC')
-            return
-        creds = kp.get_logins(args.url)
-        if not creds:
-            error('No credentials found')
-            return
-        cred = select_account(creds)
-        if not cred:
-            error('No credentials selected')
-            return
-        if args.totp:
-            uuid = cred['uuid']
-            totp = kp.get_totp(uuid)
-            if not totp:
-                error('No TOTP key found')
-                return
-            qute('jseval -q ' + make_js_totp_code(totp))
-        else:
-            name, pw = cred['login'], cred['password']
-            if name and pw:
-                qute('jseval -q ' + make_js_code(name, pw))
-    except Exception as e:
-        error(str(e))
-
-
-if __name__ == '__main__':
-    main()
-
diff --git a/config/qutebrowser/userscripts/view_in_mpv b/config/qutebrowser/userscripts/view_in_mpv
deleted file mode 100755 (executable)
index 4f371c6..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/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 <video>, <embed>,
-#   and <object> tags in the original page are temporarily removed. Clicking on
-#   such a removed video restores the respective video.
-#
-#   In order to use this script, just start it using `spawn --userscript` from
-#   qutebrowser. I recommend using an alias, e.g. put this in the
-#   [alias]-section of qutebrowser.conf:
-#
-#     mpv = spawn --userscript /path/to/view_in_mpv
-#
-# Background:
-#   Most of my machines are too slow to play youtube videos using html5, but
-#   they work fine in mpv (and mpv has further advantages like video scaling,
-#   etc). Of course, I don't want the video to be played (or even to be
-#   downloaded) twice — in MPV and in qwebkit. So I often close the tab after
-#   opening it in mpv. However, I actually want to keep the rest of the page
-#   (comments and video suggestions), i.e. only the videos should disappear
-#   when mpv is started. And that's precisely what the present script does.
-#
-# Thorsten Wißmann, 2015 (thorsten` on Libera Chat)
-# Any feedback is welcome!
-
-set -e
-
-if [ -z "$QUTE_FIFO" ] ; then
-    cat 1>&2 <<EOF
-Error: $0 can not be run as a standalone script.
-
-It is a qutebrowser userscript. In order to use it, call it using
-'spawn --userscript' as described in qute://help/userscripts.html
-EOF
-    exit 1
-fi
-
-msg() {
-    local cmd="$1"
-    shift
-    local msg="$*"
-    if [ -z "$QUTE_FIFO" ] ; then
-        echo "$cmd: $msg" >&2
-    else
-        echo "message-$cmd '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
-    fi
-}
-
-MPV_COMMAND=${MPV_COMMAND:-mpv}
-# Warning: spaces in single flags are not supported
-MPV_FLAGS=${MPV_FLAGS:- --force-window --quiet --keep-open=yes --ytdl}
-IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS"
-
-js() {
-cat <<EOF
-
-    function descendantOfTagName(child, ancestorTagName) {
-        // tells whether child has some (proper) ancestor
-        // with the tag name ancestorTagName
-        while (child.parentNode != null) {
-            child = child.parentNode;
-            if (typeof child.tagName === 'undefined') break;
-            if (child.tagName.toUpperCase() == ancestorTagName.toUpperCase()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    var App = {};
-
-    var all_videos = [];
-    all_videos.push.apply(all_videos, document.getElementsByTagName("video"));
-    all_videos.push.apply(all_videos, document.getElementsByTagName("object"));
-    all_videos.push.apply(all_videos, document.getElementsByTagName("embed"));
-    App.backup_videos = Array();
-    App.all_replacements = Array();
-    for (i = 0; i < all_videos.length; i++) {
-        var video = all_videos[i];
-        if (descendantOfTagName(video, "object")) {
-            // skip tags that are contained in an object, because we hide
-            // the object anyway.
-            continue;
-        }
-        var replacement = document.createElement("div");
-        replacement.innerHTML = "
-            <p style=\\"margin-bottom: 0.5em\\">
-            Opening page with:
-            <span style=\\"font-family: monospace;\\">${video_command[*]}</span>
-            </p>
-            <p>
-            In order to restore this particular video
-            <a style=\\"font-weight: bold;
-                        color: white;
-                        background: transparent;
-                        cursor: pointer;
-                     \\"
-               onClick=\\"restore_video(this, " + i + ");\\"
-              >click here</a>.
-            </p>
-        ";
-        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"
diff --git a/local/share/qutebrowser/userscripts/dmenu_qutebrowser b/local/share/qutebrowser/userscripts/dmenu_qutebrowser
new file mode 100755 (executable)
index 0000000..addf63b
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Zach-Button <zachrey.button@gmail.com>
+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Pipes history, quickmarks, and URL into dmenu.
+#
+# If run from qutebrowser as a userscript, it runs :open on the URL
+# If not, it opens a new qutebrowser window at the URL
+#
+# Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
+#      :bind o spawn --userscript dmenu_qutebrowser
+#
+# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window
+# You can simulate "go" by pressing "o<tab>", as the current URL is always first in the list
+#
+# I personally use "<Mod4>o" to launch this script. For me, my workflow is:
+#      Default keys            Keys with this script
+#      O                       <Mod4>o
+#      o                       o
+#      go                      o<Tab>
+#      gO                      gC, then o<Tab>
+#                              (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
+#
+
+
+[ -z "$QUTE_URL" ] && QUTE_URL='https://duckduckgo.com'
+
+url=$(printf "%s\n%s" "$QUTE_URL" "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')" | cat "$QUTE_CONFIG_DIR/quickmarks" - | fuzzel -d -l 15 -w 150 -p qutebrowser)
+url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url")
+
+[ -z "${url// }" ] && exit
+
+echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"
diff --git a/local/share/qutebrowser/userscripts/qr b/local/share/qutebrowser/userscripts/qr
new file mode 100755 (executable)
index 0000000..8421524
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+pngfile=$(mktemp --suffix=.png)
+trap 'rm -f "$pngfile"' EXIT
+
+qrencode -t PNG -o "$pngfile" -s 10 "$QUTE_URL"
+echo ":open -t file:///$pngfile" >> "$QUTE_FIFO"
+sleep 1  # give qutebrowser time to open the file before it gets removed
diff --git a/local/share/qutebrowser/userscripts/qute-keepassxc b/local/share/qutebrowser/userscripts/qute-keepassxc
new file mode 100755 (executable)
index 0000000..d5970cf
--- /dev/null
@@ -0,0 +1,435 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.com>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""
+# Introduction
+
+This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database.
+
+
+# Installation
+
+First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config.
+
+
+Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3].
+
+
+Third, install the python module `pynacl`.
+
+
+Finally, adapt your qutebrowser config.
+You can e.g. add the following lines to your `~/.config/qutebrowser/config.py`
+Remember to replace `ABC1234` with your actual GPG key.
+
+```python
+config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert')
+config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
+```
+
+To manage multiple accounts you also need [rofi](https://github.com/davatorium/rofi) installed.
+
+
+# Usage
+
+If you are on a webpage with a login form, simply activate one of the configured key-bindings.
+
+The first time you run this script, KeepassXC will ask you for authentication like with any other browser extension.
+Just provide a name of your choice and accept the request if nothing looks fishy.
+
+
+# How it works
+
+This script will talk to KeepassXC using the native [KeepassXC-Browser protocol][4].
+
+
+This script needs to store the key used to associate with your KeepassXC instance somewhere.
+Unlike most browser extensions which only use plain local storage, this one attempts to do so in a safe way
+by storing the key in encrypted form using GPG.
+Therefore you need to have a public-key-pair readily set up.
+
+GPG might then ask for your private-key password whenever you query the database for login credentials.
+
+
+# TOTP
+
+This script recently received experimental TOTP support.
+To use it, you need to have working TOTP authentication within KeepassXC.
+Then call `qute-keepassxc` with the `--totp` flags.
+
+For example, I have the following line in my `config.py`:
+
+```python
+config.bind('pt', 'spawn --userscript qute-keepassxc --key ABC1234 --totp', mode='normal')
+```
+
+For now this script will simply insert the TOTP-token into the currently selected
+input field, since I have not yet found a reliable way to identify the correct field
+within all existing login forms.
+Thus you need to manually select the TOTP input field, press escape to leave input
+mode and then enter `pt` to fill in the token (or configure another key-binding for
+insert mode if you prefer that).
+
+
+[1]: https://keepassxc.org/
+[2]: https://qutebrowser.org/
+[3]: https://gnupg.org/
+[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
+[5]: https://github.com/qutebrowser/qutebrowser/blob/main/doc/userscripts.asciidoc
+[6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration
+"""
+
+import sys
+import os
+import socket
+import json
+import base64
+import subprocess
+import argparse
+
+import nacl.utils
+import nacl.public
+
+
+def parse_args():
+    parser = argparse.ArgumentParser(description="Full passwords from KeepassXC")
+    parser.add_argument('url', nargs='?', default=os.environ.get('QUTE_URL'))
+    parser.add_argument('--totp', action='store_true',
+                        help="Fill in current TOTP field instead of username/password")
+    parser.add_argument('--socket', '-s', default='/run/user/{}/org.keepassxc.KeePassXC.BrowserServer'.format(os.getuid()),
+                        help='Path to KeepassXC browser socket')
+    parser.add_argument('--key', '-k', default='alice@example.com',
+                        help='GPG key to encrypt KeepassXC auth key with')
+    parser.add_argument('--insecure', action='store_true',
+                        help="Do not encrypt auth key")
+    return parser.parse_args()
+
+
+class KeepassError(Exception):
+    def __init__(self, code, desc):
+        self.code = code
+        self.description = desc
+
+    def __str__(self):
+        return f"KeepassXC Error [{self.code}]: {self.description}"
+
+
+class KeepassXC:
+    """ Wrapper around the KeepassXC socket API """
+    def __init__(self, id=None, *, key, socket_path):
+        self.sock        = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self.id          = id
+        self.socket_path = socket_path
+        self.client_key  = nacl.public.PrivateKey.generate()
+        self.id_key      = nacl.public.PrivateKey.from_seed(key)
+        self.cryptobox   = None
+
+    def connect(self):
+        if not os.path.exists(self.socket_path):
+            raise KeepassError(-1, "KeepassXC Browser socket does not exists")
+        self.client_id = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8')
+        self.sock.connect(self.socket_path)
+
+        self.send_raw_msg(dict(
+            action    = 'change-public-keys',
+            publicKey = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+            nonce     = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8'),
+            clientID  = self.client_id
+        ))
+
+        resp = self.recv_raw_msg()
+        assert resp['action'] == 'change-public-keys'
+        assert resp['success'] == 'true'
+        assert resp['nonce']
+        self.cryptobox = nacl.public.Box(
+            self.client_key,
+            nacl.public.PublicKey(base64.b64decode(resp['publicKey']))
+        )
+
+    def get_databasehash(self):
+        self.send_msg(dict(action='get-databasehash'))
+        return self.recv_msg()['hash']
+
+    def lock_database(self):
+        self.send_msg(dict(action='lock-database'))
+        try:
+            self.recv_msg()
+        except KeepassError as e:
+            if e.code == 1:
+                return True
+            raise
+        return False
+
+
+    def test_associate(self):
+        if not self.id:
+            return False
+        self.send_msg(dict(
+            action = 'test-associate',
+            id     = self.id,
+            key    = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+        ), triggerUnlock = 'true')
+        return self.recv_msg()['success'] == 'true'
+
+    def associate(self):
+        self.send_msg(dict(
+            action = 'associate',
+            key    = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+            idKey  = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+        ))
+        resp = self.recv_msg()
+        self.id = resp['id']
+
+    def get_logins(self, url):
+        self.send_msg(dict(
+            action = 'get-logins',
+            url    = url,
+            keys   = [{ 'id': self.id, 'key': base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') }]
+        ))
+        return self.recv_msg()['entries']
+
+    def get_totp(self, uuid):
+        self.send_msg(dict(
+            action = 'get-totp',
+            uuid = uuid
+        ))
+        response = self.recv_msg()
+        if response['success'] != 'true' or not response['totp']:
+            return None
+        return response['totp']
+
+    def send_raw_msg(self, msg):
+        self.sock.send( json.dumps(msg).encode('utf-8') )
+
+    def recv_raw_msg(self):
+        return json.loads( self.sock.recv(4096).decode('utf-8') )
+
+    def send_msg(self, msg, **extra):
+        nonce = nacl.utils.random(nacl.public.Box.NONCE_SIZE)
+        self.send_raw_msg(dict(
+            action   = msg['action'],
+            message  = base64.b64encode(self.cryptobox.encrypt(json.dumps(msg).encode('utf-8'), nonce).ciphertext).decode('utf-8'),
+            nonce    = base64.b64encode(nonce).decode('utf-8'),
+            clientID = self.client_id,
+            **extra
+        ))
+
+    def recv_msg(self):
+        resp = self.recv_raw_msg()
+        if 'error' in resp:
+            raise KeepassError(resp['errorCode'], resp['error'])
+        assert resp['action']
+        return json.loads(self.cryptobox.decrypt(base64.b64decode(resp['message']), base64.b64decode(resp['nonce'])).decode('utf-8'))
+
+
+
+class SecretKeyStore:
+    def __init__(self, gpgkey, insecure):
+        self.gpgkey = gpgkey
+        self.insecure = insecure
+        if self.insecure:
+            self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key')
+        else:
+            self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key.gpg')
+
+    def load(self):
+        "Load existing association key from file"
+        if self.insecure:
+            jsondata = open(self.path, 'r').read()
+        else:
+            jsondata = subprocess.check_output(['gpg', '--decrypt', self.path]).decode('utf-8')
+        data = json.loads(jsondata)
+        self.id = data['id']
+        self.key = base64.b64decode(data['key'])
+
+    def create(self):
+        "Create new association key"
+        self.key = nacl.utils.random(32)
+        self.id = None
+
+    def store(self, id):
+        "Store newly created association key in file"
+        self.id = id
+        jsondata = json.dumps({'id':self.id, 'key':base64.b64encode(self.key).decode('utf-8')})
+        if self.insecure:
+            open(self.path, "w").write(jsondata)
+        else:
+            subprocess.run(['gpg', '--encrypt', '-o', self.path, '-r', self.gpgkey], input=jsondata.encode('utf-8'), check=True)
+
+
+def qute(cmd):
+    with open(os.environ['QUTE_FIFO'], 'w') as fifo:
+        fifo.write(cmd)
+        fifo.write('\n')
+        fifo.flush()
+
+def error(msg):
+    print(msg, file=sys.stderr)
+    qute('message-error "{}"'.format(msg))
+
+
+def connect_to_keepassxc(args):
+    if not args.insecure and not args.key:
+        error("Missing GPG key to use for auth key encryption")
+        return
+    keystore = SecretKeyStore(args.key, args.insecure)
+    if os.path.isfile(keystore.path):
+        keystore.load()
+        kp = KeepassXC(keystore.id, key=keystore.key, socket_path=args.socket)
+        kp.connect()
+        if not kp.test_associate():
+            error('No KeepassXC association')
+            return None
+    else:
+        keystore.create()
+        kp = KeepassXC(key=keystore.key, socket_path=args.socket)
+        kp.connect()
+        kp.associate()
+        if not kp.test_associate():
+            error('No KeepassXC association')
+            return None
+        keystore.store(kp.id)
+    return kp
+
+
+def select_account(creds):
+    try:
+        if len(creds) == 1:
+            return creds[0]
+        idx = subprocess.check_output(
+                ['rofi', '-dmenu', '-format', 'i', '-matching', 'fuzzy',
+                '-p', 'Search',
+                '-mesg', '<b>qute-keepassxc</b>: select an account, please!'],
+                input=b"\n".join(c['login'].encode('utf-8') for c in creds)
+        )
+        idx = int(idx)
+        if idx < 0:
+            return None
+        return creds[idx]
+    except subprocess.CalledProcessError:
+        return None
+    except FileNotFoundError:
+        error("rofi not found. Please install rofi to select from multiple credentials")
+        return creds[0]
+    except Exception as e:
+        error(f"Error while picking account: {e}")
+        return None
+
+
+def make_js_code(username, password):
+    return ' '.join("""
+        function isVisible(elem) {
+            var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
+
+            if (style.getPropertyValue("visibility") !== "visible" ||
+                style.getPropertyValue("display") === "none" ||
+                style.getPropertyValue("opacity") === "0") {
+                return false;
+            }
+
+            return elem.offsetWidth > 0 && elem.offsetHeight > 0;
+        };
+
+        function hasPasswordField(form) {
+            var inputs = form.getElementsByTagName("input");
+            for (var j = 0; j < inputs.length; j++) {
+                var input = inputs[j];
+                if (input.type === "password") {
+                    return true;
+                }
+            }
+            return false;
+        };
+
+        function loadData2Form (form) {
+            var inputs = form.getElementsByTagName("input");
+            for (var j = 0; j < inputs.length; j++) {
+                var input = inputs[j];
+                if (isVisible(input) && (input.type === "text" || input.type === "email")) {
+                    input.focus();
+                    input.value = %s;
+                    input.dispatchEvent(new Event('input', { 'bubbles': true }));
+                    input.dispatchEvent(new Event('change', { 'bubbles': true }));
+                    input.blur();
+                }
+                if (input.type === "password") {
+                    input.focus();
+                    input.value = %s;
+                    input.dispatchEvent(new Event('input', { 'bubbles': true }));
+                    input.dispatchEvent(new Event('change', { 'bubbles': true }));
+                    input.blur();
+                }
+            }
+        };
+
+        function fillFirstForm() {
+            var forms = document.getElementsByTagName("form");
+            for (i = 0; i < forms.length; i++) {
+                if (hasPasswordField(forms[i])) {
+                    loadData2Form(forms[i]);
+                    return;
+                }
+            }
+            alert("No Credentials Form found");
+        };
+
+        fillFirstForm()
+    """.splitlines()) % (json.dumps(username), json.dumps(password))
+
+
+def make_js_totp_code(totp):
+    return ' '.join("""
+        (function () {
+            var input = document.activeElement;
+            if (!input || input.tagName !== "INPUT") {
+                alert("No TOTP input field selected");
+                return;
+            }
+            input.value = %s;
+            input.dispatchEvent(new Event('input', { 'bubbles': true }));
+            input.dispatchEvent(new Event('change', { 'bubbles': true }));
+        })();
+    """.splitlines()) % (json.dumps(totp),)
+
+
+def main():
+    if 'QUTE_FIFO' not in os.environ:
+        print(f"No QUTE_FIFO found - {sys.argv[0]} must be run as a qutebrowser userscript")
+        sys.exit(-1)
+
+    try:
+        args = parse_args()
+        assert args.url, "Missing URL"
+        kp = connect_to_keepassxc(args)
+        if not kp:
+            error('Could not connect to KeepassXC')
+            return
+        creds = kp.get_logins(args.url)
+        if not creds:
+            error('No credentials found')
+            return
+        cred = select_account(creds)
+        if not cred:
+            error('No credentials selected')
+            return
+        if args.totp:
+            uuid = cred['uuid']
+            totp = kp.get_totp(uuid)
+            if not totp:
+                error('No TOTP key found')
+                return
+            qute('jseval -q ' + make_js_totp_code(totp))
+        else:
+            name, pw = cred['login'], cred['password']
+            if name and pw:
+                qute('jseval -q ' + make_js_code(name, pw))
+    except Exception as e:
+        error(str(e))
+
+
+if __name__ == '__main__':
+    main()
+
diff --git a/local/share/qutebrowser/userscripts/qutedmenu b/local/share/qutebrowser/userscripts/qutedmenu
new file mode 100755 (executable)
index 0000000..e2ca47a
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+# Handle open -s && open -t with bemenu
+
+#:bind o spawn --userscript /path/to/userscripts/qutedmenu open
+#:bind O spawn --userscript /path/to/userscripts/qutedmenu tab
+
+# If you would like to set a custom colorscheme/font use these dirs.
+# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
+
+
+readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
+readonly optsfile=$confdir/dmenu/bemenucolors
+
+create_menu() {
+    # Check quickmarks
+    while read -r url; do
+        printf -- '%s\n' "$url"
+    done < "$QUTE_CONFIG_DIR"/quickmarks
+
+    # Next bookmarks
+    while read -r url _; do
+        printf -- '%s\n' "$url"
+    done < "$QUTE_CONFIG_DIR"/bookmarks/urls
+
+    # Finally history
+        printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')"
+    }
+
+get_selection() {
+    opts+=(-p qutebrowser)
+    create_menu | fuzzel -d -l 10 "${opts[@]}"
+    #create_menu | dmenu -l 10 "${opts[@]}"
+    #create_menu | bemenu -l 10 "${opts[@]}"
+}
+
+# Main
+# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
+[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font
+
+[[ -n $font ]] && opts+=(-fn "$font")
+
+# shellcheck source=/dev/null
+[[ -s $optsfile ]] && source "$optsfile"
+
+url=$(get_selection)
+url=${url/*http/http}
+
+# If no selection is made, exit (escape pressed, e.g.)
+[[ -z $url ]] && exit 0
+
+case $1 in
+    open)    printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
+    tab)     printf '%s' "open -t $url" >> "$QUTE_FIFO" || qutebrowser "$url"  ;;
+    window)  printf '%s' "open -w $url" >> "$QUTE_FIFO" || qutebrowser "$url --target window"  ;;
+    private) printf '%s' "open -p $url" >> "$QUTE_FIFO" || qutebrowser "$url --target private-window"  ;;
+esac
diff --git a/local/share/qutebrowser/userscripts/view_in_mpv b/local/share/qutebrowser/userscripts/view_in_mpv
new file mode 100755 (executable)
index 0000000..4f371c6
--- /dev/null
@@ -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 <video>, <embed>,
+#   and <object> tags in the original page are temporarily removed. Clicking on
+#   such a removed video restores the respective video.
+#
+#   In order to use this script, just start it using `spawn --userscript` from
+#   qutebrowser. I recommend using an alias, e.g. put this in the
+#   [alias]-section of qutebrowser.conf:
+#
+#     mpv = spawn --userscript /path/to/view_in_mpv
+#
+# Background:
+#   Most of my machines are too slow to play youtube videos using html5, but
+#   they work fine in mpv (and mpv has further advantages like video scaling,
+#   etc). Of course, I don't want the video to be played (or even to be
+#   downloaded) twice — in MPV and in qwebkit. So I often close the tab after
+#   opening it in mpv. However, I actually want to keep the rest of the page
+#   (comments and video suggestions), i.e. only the videos should disappear
+#   when mpv is started. And that's precisely what the present script does.
+#
+# Thorsten Wißmann, 2015 (thorsten` on Libera Chat)
+# Any feedback is welcome!
+
+set -e
+
+if [ -z "$QUTE_FIFO" ] ; then
+    cat 1>&2 <<EOF
+Error: $0 can not be run as a standalone script.
+
+It is a qutebrowser userscript. In order to use it, call it using
+'spawn --userscript' as described in qute://help/userscripts.html
+EOF
+    exit 1
+fi
+
+msg() {
+    local cmd="$1"
+    shift
+    local msg="$*"
+    if [ -z "$QUTE_FIFO" ] ; then
+        echo "$cmd: $msg" >&2
+    else
+        echo "message-$cmd '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
+    fi
+}
+
+MPV_COMMAND=${MPV_COMMAND:-mpv}
+# Warning: spaces in single flags are not supported
+MPV_FLAGS=${MPV_FLAGS:- --force-window --quiet --keep-open=yes --ytdl}
+IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS"
+
+js() {
+cat <<EOF
+
+    function descendantOfTagName(child, ancestorTagName) {
+        // tells whether child has some (proper) ancestor
+        // with the tag name ancestorTagName
+        while (child.parentNode != null) {
+            child = child.parentNode;
+            if (typeof child.tagName === 'undefined') break;
+            if (child.tagName.toUpperCase() == ancestorTagName.toUpperCase()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    var App = {};
+
+    var all_videos = [];
+    all_videos.push.apply(all_videos, document.getElementsByTagName("video"));
+    all_videos.push.apply(all_videos, document.getElementsByTagName("object"));
+    all_videos.push.apply(all_videos, document.getElementsByTagName("embed"));
+    App.backup_videos = Array();
+    App.all_replacements = Array();
+    for (i = 0; i < all_videos.length; i++) {
+        var video = all_videos[i];
+        if (descendantOfTagName(video, "object")) {
+            // skip tags that are contained in an object, because we hide
+            // the object anyway.
+            continue;
+        }
+        var replacement = document.createElement("div");
+        replacement.innerHTML = "
+            <p style=\\"margin-bottom: 0.5em\\">
+            Opening page with:
+            <span style=\\"font-family: monospace;\\">${video_command[*]}</span>
+            </p>
+            <p>
+            In order to restore this particular video
+            <a style=\\"font-weight: bold;
+                        color: white;
+                        background: transparent;
+                        cursor: pointer;
+                     \\"
+               onClick=\\"restore_video(this, " + i + ");\\"
+              >click here</a>.
+            </p>
+        ";
+        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"