From f4b83f2b5adc947528dd3b3e63b0279f3c2f8f19 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 3 Jun 2025 19:03:18 -0400 Subject: [PATCH 01/71] AutoYADM commit: 2025-06-03 19:03:18 --- .config/fish/config.fish | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.config/fish/config.fish b/.config/fish/config.fish index 458feeae..c09fcb70 100644 --- a/.config/fish/config.fish +++ b/.config/fish/config.fish @@ -8,21 +8,21 @@ if status is-login " end - if status is-interactive - - # source ~/.config/fish/custom_cd.fish - - starship init fish | source - direnv hook fish | source - zoxide init fish | source - thefuck --alias | source - fzf --fish | source && fzf_configure_bindings - if test "$SSH_TTY" - set -gx COLORTERM truecolor - set -gx TERM xterm-256color - end - end - set -gx EDITOR kak - - set -e __sourced_profile end +if status is-interactive + + # source ~/.config/fish/custom_cd.fish + + starship init fish | source + direnv hook fish | source + zoxide init fish | source + thefuck --alias | source + fzf --fish | source && fzf_configure_bindings + if test "$SSH_TTY" + set -gx COLORTERM truecolor + set -gx TERM xterm-256color + end +end +set -gx EDITOR kak + +set -e __sourced_profile From d962ff6dc2b43e2fba034181337c0925c1e24fdd Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 3 Jun 2025 19:50:18 -0400 Subject: [PATCH 02/71] AutoYADM commit: 2025-06-03 19:50:18 --- .config/qutebrowser/config.py | 12 +++-- .config/qutebrowser/userscripts/getbib | 68 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 .config/qutebrowser/userscripts/getbib diff --git a/.config/qutebrowser/config.py b/.config/qutebrowser/config.py index 0771ed53..2f27d134 100644 --- a/.config/qutebrowser/config.py +++ b/.config/qutebrowser/config.py @@ -36,14 +36,15 @@ c.colors.webpage.preferred_color_scheme = "dark" # searches c.url.searchengines["DEFAULT"] = "https://www.startpage.com/sp/search?query={}" -c.url.searchengines["!d"] = "https://duckduckgo.com/?q={}" -c.url.searchengines["!aw"] = "https://wiki.archlinux.org/?search={}" -c.url.searchengines["!g"] = ( +c.url.searchengines["d"] = "https://duckduckgo.com/?q={}" +c.url.searchengines["aw"] = "https://wiki.archlinux.org/?search={}" +c.url.searchengines["g"] = ( "http://www.google.com/search?hl=en&source=hp&ie=ISO-8859-l&q={}" ) c.url.searchengines["ap"] = "https://www.archlinux.org/packages/?sort=&q={}" -# with config.pattern("chatgpt.com") as p: -# p.bindings.commands["normal"][""] = "click-element css main" +c.url.searchengines["w"] = ( + "https://en.wikipedia.org/w/index.php?title=Special:Search&search={}" +) config.bind( "", "mode-leave ;; jseval -q document.activeElement.blur()", @@ -58,6 +59,7 @@ config.bind( sets = { "normal": [ + ["\\", "mode-enter passthrough"], ["m", "scroll left"], ["n", "scroll down"], ["e", "scroll up"], diff --git a/.config/qutebrowser/userscripts/getbib b/.config/qutebrowser/userscripts/getbib new file mode 100644 index 00000000..0ab0ba54 --- /dev/null +++ b/.config/qutebrowser/userscripts/getbib @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Qutebrowser userscript scraping the current web page for DOIs and downloading +corresponding bibtex information. + +Set the environment variable 'QUTE_BIB_FILEPATH' to indicate the path to +download to. Otherwise, bibtex information is downloaded to '/tmp' and hence +deleted at reboot. + +Installation: see qute://help/userscripts.html + +Inspired by +https://ocefpaf.github.io/python4oceanographers/blog/2014/05/19/doi2bibtex/ +""" + +import os +import sys +import re +from collections import Counter +from urllib import parse as url_parse +from urllib import request as url_request + + +FIFO_PATH = os.getenv("QUTE_FIFO") + +def message_fifo(message, level="warning"): + """Send message to qutebrowser FIFO. The level must be one of 'info', + 'warning' (default) or 'error'.""" + with open(FIFO_PATH, "w") as fifo: + fifo.write("message-{} '{}'".format(level, message)) + + +source = os.getenv("QUTE_TEXT") +with open(source) as f: + text = f.read() + +# find DOIs on page using regex +dval = re.compile(r'(10\.(\d)+/([^(\s\>\"\<)])+)') +# https://stackoverflow.com/a/10324802/3865876, too strict +# dval = re.compile(r'\b(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?!["&\'<>])\S)+)\b') +dois = dval.findall(text) +dois = Counter(e[0] for e in dois) +try: + doi = dois.most_common(1)[0][0] +except IndexError: + message_fifo("No DOIs found on page") + sys.exit() +message_fifo("Found {} DOIs on page, selecting {}".format(len(dois), doi), + level="info") + +# get bibtex data corresponding to DOI +url = "https://dx.doi.org/" + url_parse.quote(doi) +headers = dict(Accept='text/bibliography; style=bibtex') +request = url_request.Request(url, headers=headers) +response = url_request.urlopen(request) +status_code = response.getcode() +if status_code >= 400: + message_fifo("Request returned {}".format(status_code)) + sys.exit() + +# obtain content and format it +bibtex = response.read().decode("utf-8").strip() +bibtex = bibtex.replace(" ", "\n ", 1).\ + replace("}, ", "},\n ").replace("}}", "}\n}") + +# append to file +bib_filepath = os.getenv("QUTE_BIB_FILEPATH", "/tmp/qute.bib") +with open(bib_filepath, "a") as f: + f.write(bibtex + "\n\n") From e889b84eca22c9f435c822246711b57121971823 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 3 Jun 2025 20:05:22 -0400 Subject: [PATCH 03/71] AutoYADM commit: 2025-06-03 20:05:22 --- .config/qutebrowser/config.py | 7 +- .../qutebrowser/userscripts/code_select.py | 64 ++++ .config/qutebrowser/userscripts/localhost | 9 + .../qutebrowser/userscripts/qute-bitwarden | 308 ++++++++++++++++++ .config/qutebrowser/userscripts/readability | 66 ++++ .config/qutebrowser/userscripts/view_in_mpv | 142 ++++++++ 6 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 .config/qutebrowser/userscripts/code_select.py create mode 100644 .config/qutebrowser/userscripts/localhost create mode 100644 .config/qutebrowser/userscripts/qute-bitwarden create mode 100644 .config/qutebrowser/userscripts/readability create mode 100644 .config/qutebrowser/userscripts/view_in_mpv 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