]> Freerunner's - dotfiles.git/commitdiff
qutebrowser add config
authorAndre Ramnitz <tux.rising@gmail.com>
Mon, 4 Dec 2023 23:08:20 +0000 (00:08 +0100)
committerAndre Ramnitz <tux.rising@gmail.com>
Thu, 7 Dec 2023 12:34:57 +0000 (13:34 +0100)
qutebrowser/autoconfig.yml [new file with mode: 0644]
qutebrowser/config.py [new file with mode: 0644]
qutebrowser/userscripts/qr [new file with mode: 0755]
qutebrowser/userscripts/qute-keepassxc [new file with mode: 0755]

diff --git a/qutebrowser/autoconfig.yml b/qutebrowser/autoconfig.yml
new file mode 100644 (file)
index 0000000..7825b1d
--- /dev/null
@@ -0,0 +1,297 @@
+# If a config.py file exists, this file is ignored unless it's explicitly loaded
+# via config.load_autoconfig(). For more information, see:
+# https://github.com/qutebrowser/qutebrowser/blob/main/doc/help/configuring.asciidoc#loading-autoconfigyml
+# DO NOT edit this file by hand, qutebrowser will overwrite it.
+# Instead, create a config.py - see :help for details.
+
+config_version: 2
+settings:
+  backend:
+    global: webengine
+  bindings.commands:
+    global:
+      insert:
+        <Alt+Shift+u>: spawn --userscript qute-keepassxc --key 285D16B66B11BD45BFD57B6CB22C3113D0E83104
+      normal:
+        '''gh''': '''open duckduckgo.com'''
+        +: null
+        ',M': hint links spawn vlc {hint-url}
+        ',m': spawn vlc {url}
+        '-': null
+        <Alt+h>: home
+        <Ctrl++>: zoom-in
+        <Ctrl+->: zoom-out
+        <Ctrl+h>: back
+        <Ctrl+l>: forward
+        Ctrl-+: zoom-in
+        Ctrl--: zoom-out
+        E: edit-url
+        d: null
+        dd: tab-close
+        dlA: spawn -u yt-dlp-video {url}
+        dlV: spawn -u yt-dlp-video {url}
+        dla: hint links userscript yt-dlp-audio
+        dlv: hint links userscript yt-dlp-video
+        eu: edit-url
+        ga: null
+        ge: scroll-to-perc 100
+        gg: scroll-to-perc 0
+        gh: open duckduckgo.com
+        pw: spawn --userscript qute-keepassxc --key 285D16B66B11BD45BFD57B6CB22C3113D0E83104
+        tG: home
+        tda: tab-only
+        tg1: tab-give 1
+        tg2: tab-give 2
+        tg3: tab-give 3
+        tgg: tab-give 0
+        tp: tab-pin
+        ttb: set tabs.position bottom
+        ttl: set tabs.position left
+        ttr: set tabs.position right
+        ttt: set tabs.position top
+        tw1: set tabs.width  48
+        tw2: set tabs.width  96
+        tw3: set tabs.width 160
+        tw4: set tabs.width 240
+        tw5: set tabs.width 320
+        tw6: set tabs.width 480
+  changelog_after_upgrade:
+    global: patch
+  colors.statusbar.private.bg:
+    global: '#2a2e32'
+  colors.statusbar.private.fg:
+    global: '#9b59b6'
+  colors.tabs.indicator.system:
+    global: rgb
+  colors.tabs.pinned.selected.odd.fg:
+    global: '#232629'
+  colors.webpage.bg:
+    global: white
+  colors.webpage.darkmode.enabled:
+    global: false
+  completion.open_categories:
+    global:
+    - searchengines
+    - quickmarks
+    - bookmarks
+    - history
+    - filesystem
+  completion.scrollbar.width:
+    global: 16
+  confirm_quit:
+    global:
+    - never
+  content.autoplay:
+    global: false
+  content.blocking.adblock.lists:
+    global:
+    - https://easylist.to/easylist/easylist.txt
+    - https://easylist.to/easylist/easyprivacy.txt
+  content.blocking.enabled:
+    global: true
+  content.blocking.hosts.block_subdomains:
+    global: true
+  content.blocking.whitelist:
+    global:
+    - piwik.org
+    - linuxnews.de
+  content.cache.size:
+    global: 0
+  content.canvas_reading:
+    global: true
+  content.default_encoding:
+    global: _utf-8_,iso-8859-1
+  content.dns_prefetch:
+    global: false
+  content.geolocation:
+    https://www.clubderdampfer.de: false
+    https://www.r-m.de: false
+  content.headers.accept_language:
+    global: en-US,en;q=0.9
+  content.headers.do_not_track:
+    global: false
+  content.javascript.clipboard:
+    wago.io: access
+    wowhead.com: access
+  content.javascript.enabled:
+    global: true
+  content.javascript.log_message.excludes:
+    global:
+      userscript:_qute_stylesheet:
+      - '*Refused to apply inline style because it violates the following Content
+        Security Policy directive: *'
+  content.notifications.enabled:
+    global: ask
+  content.notifications.presenter:
+    global: auto
+  content.pdfjs:
+    global: false
+  content.plugins:
+    global: true
+  content.register_protocol_handler:
+    https://mail.google.com?extsrc=mailto&url=%25s: true
+  content.webgl:
+    global: true
+  content.xss_auditing:
+    global: false
+  downloads.location.directory:
+    global: null
+  downloads.location.prompt:
+    global: false
+  downloads.location.remember:
+    global: true
+  downloads.location.suggestion:
+    global: both
+  downloads.prevent_mixed_content:
+    global: true
+  editor.command:
+    global:
+    - konsole
+    - --separate
+    - -e
+    - kak
+    - '{file}'
+  fileselect.folder.command:
+    global:
+    - kitty
+    - nnn
+    - -p
+    - '-'
+  fileselect.handler:
+    global: external
+  fileselect.multiple_files.command:
+    global:
+    - kitty
+    - nnn
+    - -p
+    - '-'
+  fileselect.single_file.command:
+    global:
+    - kitty
+    - nnn
+    - -p
+    - '-'
+  fonts.completion.category:
+    global: bold 12pt LiterationMono Nerd Font
+  fonts.completion.entry:
+    global: 12pt LiterationMono Nerd Font
+  fonts.contextmenu:
+    global: 12pt LiterationMono Nerd Font
+  fonts.debug_console:
+    global: default_size default_family
+  fonts.default_family:
+    global: 12pt LiterationMono Nerd Font
+  fonts.default_size:
+    global: 12pt
+  fonts.downloads:
+    global: 12pt LiterationMono Nerd Font
+  fonts.hints:
+    global: bold 12pt LiterationMono Nerd Font
+  fonts.keyhint:
+    global: 12pt LiterationMono Nerd Font
+  fonts.messages.error:
+    global: 12pt LiterationMono Nerd Font
+  fonts.messages.info:
+    global: 12pt LiterationMono Nerd Font
+  fonts.messages.warning:
+    global: 12pt LiterationMono Nerd Font
+  fonts.prompts:
+    global: 12pt LiterationMono Nerd Font
+  fonts.statusbar:
+    global: 12pt LiterationMono Nerd Font
+  fonts.tabs.selected:
+    global: 12pt LiterationMono Nerd Font
+  fonts.tabs.unselected:
+    global: 12pt LiterationMono Nerd Font
+  fonts.tooltip:
+    global: 12pt LiterationMono Nerd Font
+  fonts.web.family.cursive:
+    global: Noto Sans Old Italic
+  fonts.web.family.fantasy:
+    global: FantasqueSansM Nerd Font
+  fonts.web.family.fixed:
+    global: LiterationMono Nerd Font
+  fonts.web.family.sans_serif:
+    global: Noto Sans
+  fonts.web.family.serif:
+    global: Noto Serif
+  fonts.web.family.standard:
+    global: Noto Sans
+  fonts.web.size.default:
+    global: 15
+  fonts.web.size.default_fixed:
+    global: 15
+  fonts.web.size.minimum:
+    global: 6
+  fonts.web.size.minimum_logical:
+    global: 7
+  hints.min_chars:
+    global: 2
+  hints.scatter:
+    global: true
+  input.insert_mode.auto_enter:
+    global: true
+  input.insert_mode.auto_load:
+    global: false
+  input.insert_mode.leave_on_load:
+    global: true
+  input.mode_override:
+    global: normal
+  qt.chromium.process_model:
+    global: process-per-site
+  qt.force_platform:
+    global: null
+  qt.force_platformtheme:
+    global: qt6ct
+  qt.force_software_rendering:
+    global: chromium
+  qt.highdpi:
+    global: false
+  scrolling.bar:
+    global: overlay
+  scrolling.smooth:
+    global: true
+  session.lazy_restore:
+    global: true
+  statusbar.show:
+    global: always
+  tabs.favicons.scale:
+    global: 1.0
+  tabs.indicator.padding:
+    global:
+      bottom: 1
+      left: 0
+      right: 6
+      top: 1
+  tabs.indicator.width:
+    global: 10
+  tabs.min_width:
+    global: -1
+  tabs.padding:
+    global:
+      bottom: 4
+      left: 5
+      right: 5
+      top: 4
+  tabs.pinned.shrink:
+    global: true
+  tabs.position:
+    global: left
+  tabs.title.alignment:
+    global: left
+  tabs.title.elide:
+    global: right
+  tabs.title.format:
+    global: '{audio}{index}: {current_title}'
+  tabs.title.format_pinned:
+    global: '{audio}{index}: ** {current_title}'
+  tabs.undo_stack_size:
+    global: 50
+  tabs.width:
+    global: 240
+  window.hide_decoration:
+    global: false
+  zoom.default:
+    global: 110%
+  zoom.mouse_divider:
+    global: 1024
diff --git a/qutebrowser/config.py b/qutebrowser/config.py
new file mode 100644 (file)
index 0000000..6047741
--- /dev/null
@@ -0,0 +1,368 @@
+import subprocess
+config.load_autoconfig()
+
+## testing
+#
+config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key 285D16B66B11BD45BFD57B6CB22C3113D0E83104', mode='insert')
+config.bind('pw', 'spawn --userscript qute-keepassxc --key 285D16B66B11BD45BFD57B6CB22C3113D0E83104', mode='normal')
+
+## Generic
+#
+c.auto_save.session = True
+c.session.lazy_restore = True
+c.content.fullscreen.window = True
+c.content.notifications.enabled = True
+c.content.cookies.accept = 'no-3rdparty'
+c.content.blocking.whitelist = ["piwik.org"]
+c.downloads.position = "bottom"
+c.editor.command = ["konsole", "--separate", "-e", "kak", "{file}"]
+c.statusbar.widgets = ["url", "progress", "scroll"]
+c.zoom.default = "123%"
+c.completion.web_history.max_items = 1000
+c.input.insert_mode.auto_load = True
+c.new_instance_open_target = "tab-bg"
+
+## Key bindings
+#
+config.unbind('d')
+config.unbind('ga')
+config.unbind('+')
+config.unbind('-')
+config.bind ('gg', 'scroll-to-perc 0')
+config.bind ('ge', 'scroll-to-perc 100')
+config.bind ('gh', 'open duckduckgo.com')
+config.bind('dd', 'tab-close')
+config.bind('tda', 'tab-only')
+config.bind('tgg', 'tab-give 0')
+config.bind('tg1', 'tab-give 1')
+config.bind('tg2', 'tab-give 2')
+config.bind('tg3', 'tab-give 3')
+config.bind('tp', 'tab-pin')
+config.bind('E', 'edit-url')
+config.bind('eu', 'edit-url')
+config.bind('ttl', 'set tabs.position left')
+config.bind('ttr', 'set tabs.position right')
+config.bind('ttt', 'set tabs.position top')
+config.bind('ttb', 'set tabs.position bottom')
+config.bind('tw1', 'set tabs.width  48')
+config.bind('tw2', 'set tabs.width  96')
+config.bind('tw3', 'set tabs.width 160')
+config.bind('tw4', 'set tabs.width 240')
+config.bind('tw5', 'set tabs.width 320')
+config.bind('tw6', 'set tabs.width 480')
+config.unbind('<Ctrl-h>')
+config.bind('<Ctrl-h>', 'back')
+config.bind('<Ctrl-l>', 'forward')
+config.bind('<Alt-h>', 'home')
+config.bind('tG', 'home')
+config.bind('dlV', 'spawn -u yt-dlp-video {url}')
+config.bind('dlA', 'spawn -u yt-dlp-video {url}')
+config.bind('dlv', 'hint links userscript yt-dlp-video')
+config.bind('dla', 'hint links userscript yt-dlp-audio')
+config.bind(',m', 'spawn vlc {url}')
+config.bind(',M', 'hint links spawn vlc {hint-url}')
+config.bind('<Ctrl-+>', 'zoom-in')
+config.bind('<Ctrl-->', 'zoom-out')
+
+## Font config
+#
+c.fonts.completion.category = 'bold 12pt LiterationMono\ Nerd\ Font'
+c.fonts.completion.entry = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.downloads = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.hints = 'bold 12pt LiterationMono\ Nerd\ Font'
+c.fonts.keyhint = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.messages.error = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.messages.info = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.messages.warning = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.prompts = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.statusbar = '12pt LiterationMono\ Nerd\ Font'
+c.fonts.tabs.selected= '12pt LiterationMono\ Nerd\ Font'
+c.fonts.tabs.unselected= '12pt LiterationMono\ Nerd\ Font'
+
+## Tabs config
+#
+c.tabs.position = "left"
+c.tabs.indicator.width = 12
+c.tabs.favicons.show = "always"
+c.tabs.select_on_remove = 'last-used'
+c.tabs.background = True
+
+## Color config
+#
+base00 = '#2a2e32' #black
+base01 = '#da4453' #red
+base02 = '#27ae60' #green
+base03 = '#fdbc4b' #yellow
+base04 = '#1d99f3' #blue
+base05 = '#9b59b6' #magenta
+base06 = '#1cdc9a' #cyan
+base07 = '#eff0f1' #white
+base08 = '#4d4d4d' #brightblack
+base09 = '#da4453' #brightred
+base0A = '#27ae60' #brightgreen
+base0B = '#fdbc4b' #brightyellow
+base0C = '#1d99f3' #brightblue
+base0D = '#9b59b6' #brightmagenta
+base0E = '#1cdc9a' #brightcyan
+base0F = '#fcfcfc' #brightwhite
+base10 = '#3b4045'  #backgroundalternate1
+
+# Text color of the completion widget. May be a single color to use for
+# all columns or a list of three colors, one for each column.
+c.colors.completion.fg = base07
+
+# Background color of the completion widget for odd rows.
+c.colors.completion.odd.bg = base10
+
+# Background color of the completion widget for even rows.
+c.colors.completion.even.bg = base00
+
+# Foreground color of completion widget category headers.
+c.colors.completion.category.fg = base04
+
+# Background color of the completion widget category headers.
+c.colors.completion.category.bg = base00
+
+# Top border color of the completion widget category headers.
+c.colors.completion.category.border.top = base00
+
+# Bottom border color of the completion widget category headers.
+c.colors.completion.category.border.bottom = base00
+
+# Foreground color of the selected completion item.
+c.colors.completion.item.selected.fg = base0F
+
+# Background color of the selected completion item.
+c.colors.completion.item.selected.bg = base04
+
+# Top border color of the completion widget category headers.
+c.colors.completion.item.selected.border.top = base10
+
+# Bottom border color of the selected completion item.
+c.colors.completion.item.selected.border.bottom = base10
+
+# Foreground color of the matched text in the selected completion item.
+c.colors.completion.item.selected.match.fg = base10
+
+# Foreground color of the matched text in the completion.
+c.colors.completion.match.fg = base0B
+
+# Color of the scrollbar handle in the completion view.
+c.colors.completion.scrollbar.fg = base05
+
+# Color of the scrollbar in the completion view.
+c.colors.completion.scrollbar.bg = base00
+
+# Background color for the download bar.
+c.colors.downloads.bar.bg = base00
+
+# Color gradient start for download text.
+c.colors.downloads.start.fg = base00
+
+# Color gradient start for download backgrounds.
+c.colors.downloads.start.bg = base0D
+
+# Color gradient end for download text.
+c.colors.downloads.stop.fg = base00
+
+# Color gradient stop for download backgrounds.
+c.colors.downloads.stop.bg = base0C
+
+# Background color for downloads with errors.
+c.colors.downloads.error.bg = base00
+
+# Foreground color for downloads with errors.
+c.colors.downloads.error.fg = base02
+
+# Font color for hints.
+c.colors.hints.fg = base03
+
+# Background color for hints. Note that you can use a `rgba(...)` value
+# for transparency.
+c.colors.hints.bg = base08
+
+# Font color for the matched part of hints.
+c.colors.hints.match.fg = base05
+
+# Text color for the keyhint widget.
+c.colors.keyhint.fg = base05
+
+# Highlight color for keys to complete the current keychain.
+c.colors.keyhint.suffix.fg = base05
+
+# Background color of the keyhint widget.
+c.colors.keyhint.bg = base00
+
+# Foreground color of an error message.
+c.colors.messages.error.fg = base00
+
+# Background color of an error message.
+c.colors.messages.error.bg = base01
+
+# Border color of an error message.
+c.colors.messages.error.border = base01
+
+# Foreground color of a warning message.
+c.colors.messages.warning.fg = base09
+
+# Background color of a warning message.
+c.colors.messages.warning.bg = base03
+
+# Border color of a warning message.
+c.colors.messages.warning.border = base03
+
+# Foreground color of an info message.
+c.colors.messages.info.fg = base0F
+
+# Background color of an info message.
+c.colors.messages.info.bg = base02
+
+# Border color of an info message.
+c.colors.messages.info.border = base02
+
+# Foreground color for prompts.
+c.colors.prompts.fg = base05
+
+# Border used around UI elements in prompts.
+c.colors.prompts.border = base00
+
+# Background color for prompts.
+c.colors.prompts.bg = base00
+
+# Background color for the selected item in filename prompts.
+c.colors.prompts.selected.bg = base10
+
+# Foreground color of the statusbar.
+c.colors.statusbar.normal.fg = base0B
+
+# Background color of the statusbar.
+c.colors.statusbar.normal.bg = base00
+
+# Foreground color of the statusbar in insert mode.
+c.colors.statusbar.insert.fg = base00
+
+# Background color of the statusbar in insert mode.
+c.colors.statusbar.insert.bg = base0D
+
+# Foreground color of the statusbar in passthrough mode.
+c.colors.statusbar.passthrough.fg = base00
+
+# Background color of the statusbar in passthrough mode.
+c.colors.statusbar.passthrough.bg = base0C
+
+# Foreground color of the statusbar in private browsing mode.
+c.colors.statusbar.private.fg = base05
+
+# Background color of the statusbar in private browsing mode.
+c.colors.statusbar.private.bg = base00
+
+# Foreground color of the statusbar in command mode.
+c.colors.statusbar.command.fg = base0F
+
+# Background color of the statusbar in command mode.
+c.colors.statusbar.command.bg = base04
+
+# Foreground color of the statusbar in private browsing + command mode.
+c.colors.statusbar.command.private.fg = base05
+
+# Background color of the statusbar in private browsing + command mode.
+c.colors.statusbar.command.private.bg = base00
+
+# Foreground color of the statusbar in caret mode.
+c.colors.statusbar.caret.fg = base00
+
+# Background color of the statusbar in caret mode.
+c.colors.statusbar.caret.bg = base0E
+
+# Foreground color of the statusbar in caret mode with a selection.
+c.colors.statusbar.caret.selection.fg = base00
+
+# Background color of the statusbar in caret mode with a selection.
+c.colors.statusbar.caret.selection.bg = base0D
+
+# Background color of the progress bar.
+c.colors.statusbar.progress.bg = base0B
+
+# Default foreground color of the URL in the statusbar.
+c.colors.statusbar.url.fg = base05
+
+# Foreground color of the URL in the statusbar on error.
+c.colors.statusbar.url.error.fg = base10
+
+# Foreground color of the URL in the statusbar for hovered links.
+c.colors.statusbar.url.hover.fg = base05
+
+# Foreground color of the URL in the statusbar on successful load
+# (http).
+c.colors.statusbar.url.success.http.fg = base0C
+
+# Foreground color of the URL in the statusbar on successful load
+# (https).
+c.colors.statusbar.url.success.https.fg = base0F
+
+# Foreground color of the URL in the statusbar when there's a warning.
+c.colors.statusbar.url.warn.fg = base01
+
+# Background color of the tab bar.
+c.colors.tabs.bar.bg = base00
+
+# Color gradient start for the tab indicator.
+c.colors.tabs.indicator.start = base03
+
+# Color gradient end for the tab indicator.
+c.colors.tabs.indicator.stop = base02
+
+# Color for the tab indicator on errors.
+c.colors.tabs.indicator.error = base01
+
+# Foreground color of unselected odd tabs.
+c.colors.tabs.odd.fg = base07
+
+# Background color of unselected odd tabs.
+c.colors.tabs.odd.bg = base10
+
+# Foreground color of unselected even tabs.
+c.colors.tabs.even.fg = base07
+
+# Background color of unselected even tabs.
+c.colors.tabs.even.bg = base00
+
+# Background color of pinned unselected even tabs.
+c.colors.tabs.pinned.even.bg = base03
+
+# Foreground color of pinned unselected even tabs.
+c.colors.tabs.pinned.even.fg = base00
+
+# Background color of pinned unselected odd tabs.
+c.colors.tabs.pinned.odd.bg = base03
+
+# Foreground color of pinned unselected odd tabs.
+c.colors.tabs.pinned.odd.fg = base00
+
+# Background color of pinned selected even tabs.
+c.colors.tabs.pinned.selected.even.bg = base03
+
+# Foreground color of pinned selected even tabs.
+c.colors.tabs.pinned.selected.even.fg = base04
+
+# Background color of pinned selected odd tabs.
+c.colors.tabs.pinned.selected.odd.bg = base03
+
+# Foreground color of pinned selected odd tabs.
+c.colors.tabs.pinned.selected.odd.fg = base04
+
+# Foreground color of selected odd tabs.
+c.colors.tabs.selected.odd.fg = base07
+
+# Background color of selected odd tabs.
+c.colors.tabs.selected.odd.bg = base04
+
+# Foreground color of selected even tabs.
+c.colors.tabs.selected.even.fg = base07
+
+# Background color of selected even tabs.
+c.colors.tabs.selected.even.bg = base04
+
+# Background color for webpages if unset (or empty to use the theme's
+# color).
+# c.colors.webpage.bg = base00
diff --git a/qutebrowser/userscripts/qr b/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/qutebrowser/userscripts/qute-keepassxc b/qutebrowser/userscripts/qute-keepassxc
new file mode 100755 (executable)
index 0000000..61a6c7b
--- /dev/null
@@ -0,0 +1,448 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.com>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.
+
+"""
+# Introduction
+
+This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database.
+
+
+# Installation
+
+First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config.
+
+
+Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3].
+
+
+Third, install the python module `pynacl`.
+
+
+Finally, adapt your qutebrowser config.
+You can e.g. add the following lines to your `~/.config/qutebrowser/config.py`
+Remember to replace `ABC1234` with your actual GPG key.
+
+```python
+config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert')
+config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
+```
+
+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/master/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()
+