diff options
Diffstat (limited to 'mac/.config/qutebrowser/userscripts')
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/add-nextcloud-bookmarks | 173 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/add-nextcloud-cookbook | 131 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/code_select | 64 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/gitclone | 74 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/qr | 8 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/qute-pass | 415 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/substiqute | 61 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/tab-manager | 529 | ||||
| -rwxr-xr-x | mac/.config/qutebrowser/userscripts/translate | 116 | ||||
| -rw-r--r-- | mac/.config/qutebrowser/userscripts/usage.txt | 99 |
10 files changed, 1670 insertions, 0 deletions
diff --git a/mac/.config/qutebrowser/userscripts/add-nextcloud-bookmarks b/mac/.config/qutebrowser/userscripts/add-nextcloud-bookmarks new file mode 100755 index 0000000..2a480cc --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/add-nextcloud-bookmarks @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 + +""" +Behavior: + A qutebrowser userscript that creates bookmarks in Nextcloud's Bookmarks app. + +Requirements: + requests + +userscript setup: + Optionally create ~/.config/qutebrowser/add-nextcloud-bookmarks.ini like: + +[nextcloud] +HOST=https://nextcloud.example.com +USER=username +;PASSWORD=lamepassword +DESCRIPTION=None +;TAGS=just-one +TAGS=read-me-later,added-by-qutebrowser, Another-One + + If settings aren't in the configuration file, the user will be prompted during + bookmark creation. If DESCRIPTION and TAGS are set to None, they will be left + blank. If the user does not want to be prompted for a password, it is recommended + to set up an 'app password'. See the following for instructions: + https://docs.nextcloud.com/server/latest/user_manual/en/session_management.html#managing-devices # noqa: E501 + +qutebrowser setup: + add bookmark via hints + config.bind('X', 'hint links userscript add-nextcloud-bookmarks') + + add bookmark of current URL + config.bind('X', 'spawn --userscript add-nextcloud-bookmarks') + +troubleshooting: + Errors detected within this userscript will have an exit of 231. All other + exit codes will come from requests. +""" + +import configparser +from json import dumps +from os import environ, path +from sys import argv, exit + +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit +from requests import get, post +from requests.auth import HTTPBasicAuth + + +def get_text(name, info): + """Get input from the user.""" + _app = QApplication(argv) # noqa: F841 + if name == "password": + text, ok = QInputDialog.getText( + None, + "add-nextcloud-bookmarks userscript", + "Please enter {}".format(info), + QLineEdit.EchoMode.Password, + ) + else: + text, ok = QInputDialog.getText( + None, "add-nextcloud-bookmarks userscript", "Please enter {}".format(info) + ) + if not ok: + message("info", "Dialog box canceled.") + exit(0) + return text + + +def message(level, text): + """display message""" + with open(environ["QUTE_FIFO"], "w") as fifo: + fifo.write( + 'message-{} "add-nextcloud-bookmarks userscript: {}"\n'.format(level, text) + ) + fifo.flush() + + +if "QUTE_FIFO" not in environ: + print( + "This script is designed to run as a qutebrowser userscript, " + "not as a standalone script." + ) + exit(231) + +if "QUTE_CONFIG_DIR" not in environ: + if "XDG_CONFIG_HOME" in environ: + QUTE_CONFIG_DIR = environ["XDG_CONFIG_HOME"] + "/qutebrowser" + else: + QUTE_CONFIG_DIR = environ["HOME"] + "/.config/qutebrowser" +else: + QUTE_CONFIG_DIR = environ["QUTE_CONFIG_DIR"] + +config_file = QUTE_CONFIG_DIR + "/add-nextcloud-bookmarks.ini" +if path.isfile(config_file): + config = configparser.ConfigParser() + config.read(config_file) + settings = dict(config.items("nextcloud")) +else: + settings = {} + +settings_info = [ + ("host", "host information.", "required"), + ("user", "username.", "required"), + ("password", "password.", "required"), + ("description", "description or leave blank", "optional"), + ("tags", "tags (comma separated) or leave blank", "optional"), +] + +# check for settings that need user interaction and clear optional setting if need be +for setting in settings_info: + if setting[0] not in settings: + userInput = get_text(setting[0], setting[1]) + settings[setting[0]] = userInput + if setting[2] == "optional": + if settings[setting[0]] == "None": + settings[setting[0]] = "" + +tags = settings["tags"].split(",") + +QUTE_URL = environ["QUTE_URL"] +api_url = settings["host"] + "/index.php/apps/bookmarks/public/rest/v2/bookmark" + +auth = HTTPBasicAuth(settings["user"], settings["password"]) +headers = {"Content-Type": "application/json"} +params = {"url": QUTE_URL} + +# check if there is already a bookmark for the URL +r = get( + api_url, + auth=auth, + headers=headers, + params=params, + timeout=(3.05, 27), +) +if r.status_code != 200: + message( + "error", + "Could not connect to {} with status code {}".format( + settings["host"], r.status_code + ), + ) + exit(r.status_code) + +try: + r.json()["data"][0]["id"] +except IndexError: + pass +else: + message("info", "bookmark already exists for {}".format(QUTE_URL)) + exit(0) + +if environ["QUTE_MODE"] == "hints": + QUTE_TITLE = QUTE_URL +else: + QUTE_TITLE = environ["QUTE_TITLE"] + +# JSON format +# https://nextcloud-bookmarks.readthedocs.io/en/latest/bookmark.html#create-a-bookmark +dict = { + "url": QUTE_URL, + "title": QUTE_TITLE, + "description": settings["description"], + "tags": tags, +} +data = dumps(dict) + +r = post(api_url, data=data, headers=headers, auth=auth, timeout=(3.05, 27)) + +if r.status_code == 200: + message("info", "bookmark {} added".format(QUTE_URL)) +else: + message("error", "something went wrong {} bookmark not added".format(QUTE_URL)) + exit(r.status_code) diff --git a/mac/.config/qutebrowser/userscripts/add-nextcloud-cookbook b/mac/.config/qutebrowser/userscripts/add-nextcloud-cookbook new file mode 100755 index 0000000..1510907 --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/add-nextcloud-cookbook @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +""" +Behavior: + A qutebrowser userscript that adds recipes to Nextcloud's Cookbook app. + +Requirements: + requests + +userscript setup: + Optionally create ~/.config/qutebrowser/add-nextcloud-cookbook.ini like: + +[nextcloud] +HOST=https://nextcloud.example.com +USER=username +;PASSWORD=lamepassword + + If settings aren't in the configuration file, the user will be prompted. + If the user does not want to be prompted for a password, it is recommended + to set up an 'app password' with 'Allow filesystem access' enabled. + See the following for instructions: + https://docs.nextcloud.com/server/latest/user_manual/en/session_management.html#managing-devices # noqa: E501 + +qutebrowser setup: + add recipe via hints + config.bind('X', 'hint links userscript add-nextcloud-cookbook') + + add recipe of current URL + config.bind('X', 'spawn --userscript add-nextcloud-cookbook') + +troubleshooting: + Errors detected within this userscript will have an exit of 231. All other + exit codes will come from requests. +""" + +import configparser +from os import environ, path +from sys import argv, exit + +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit +from requests import post +from requests.auth import HTTPBasicAuth + + +def get_text(name, info): + """Get input from the user.""" + _app = QApplication(argv) # noqa: F841 + if name == "password": + text, ok = QInputDialog.getText( + None, + "add-nextcloud-cookbook userscript", + "Please enter {}".format(info), + QLineEdit.EchoMode.Password, + ) + else: + text, ok = QInputDialog.getText( + None, "add-nextcloud-cookbook userscript", "Please enter {}".format(info) + ) + if not ok: + message("info", "Dialog box canceled.") + exit(0) + return text + + +def message(level, text): + """display message""" + with open(environ["QUTE_FIFO"], "w") as fifo: + fifo.write( + "message-{} 'add-nextcloud-cookbook userscript: {}'\n".format(level, text) + ) + fifo.flush() + + +if "QUTE_FIFO" not in environ: + print( + "This script is designed to run as a qutebrowser userscript, " + "not as a standalone script." + ) + exit(231) + +if "QUTE_CONFIG_DIR" not in environ: + if "XDG_CONFIG_HOME" in environ: + QUTE_CONFIG_DIR = environ["XDG_CONFIG_HOME"] + "/qutebrowser" + else: + QUTE_CONFIG_DIR = environ["HOME"] + "/.config/qutebrowser" +else: + QUTE_CONFIG_DIR = environ["QUTE_CONFIG_DIR"] + +config_file = QUTE_CONFIG_DIR + "/add-nextcloud-cookbook.ini" +if path.isfile(config_file): + config = configparser.ConfigParser() + config.read(config_file) + settings = dict(config.items("nextcloud")) +else: + settings = {} + +settings_info = [ + ("host", "host information.", "required"), + ("user", "username.", "required"), + ("password", "password.", "required"), +] + +# check for settings that need user interaction +for setting in settings_info: + if setting[0] not in settings: + userInput = get_text(setting[0], setting[1]) + settings[setting[0]] = userInput + +api_url = settings["host"] + "/index.php/apps/cookbook/import" +headers = {"Content-Type": "application/x-www-form-urlencoded"} +auth = HTTPBasicAuth(settings["user"], settings["password"]) +data = "url=" + environ["QUTE_URL"] + +message("info", "starting to process {}".format(environ["QUTE_URL"])) + +r = post(api_url, data=data, headers=headers, auth=auth, timeout=(3.05, 27)) + +if r.status_code == 200: + message("info", "recipe from {} added.".format(environ["QUTE_URL"])) + exit(0) +elif r.status_code == 500: + message("warning", "Cookbook app reports {}".format(r.text)) + exit(0) +else: + message( + "error", + "Could not connect to {} with status code {}".format( + settings["host"], r.status_code + ), + ) + exit(r.status_code) diff --git a/mac/.config/qutebrowser/userscripts/code_select b/mac/.config/qutebrowser/userscripts/code_select new file mode 100755 index 0000000..8f7fc31 --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/code_select @@ -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 = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" [ + <!ENTITY nbsp ' '> + ]>''' + 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/mac/.config/qutebrowser/userscripts/gitclone b/mac/.config/qutebrowser/userscripts/gitclone new file mode 100755 index 0000000..ad62d17 --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/gitclone @@ -0,0 +1,74 @@ +#!/bin/sh +# +# Author: palb91 +# Date: 2022 +# +# Clone a git repository directly from qutebrowser +# +# In config.py: +# bind('gc', 'spawn -u -- gitclone') +# +# # Run a shell command after successful clone +# import os +# os.environ['QUTE_POST_CLONE'] = 'notify-send "git clone" "${QUTE_URL}"' + +set -e + +# Local storage +BASE_DIR="${HOME}"/Public/repos +TEMP_DIR="$(mktemp -d)" + +# Get informations from userscripts variables +QUTE_URL="${QUTE_URL%%\#*}" +QUTE_URL="${QUTE_URL%%\?*}" +QUTE_URL="${QUTE_URL%%\&*}" + +DOMAIN="${QUTE_URL#*//}" +DOMAIN="${DOMAIN%%/*}" + +REPO="${QUTE_URL#*"${DOMAIN}"/}" +REPO="${REPO%/}" +REPO="${REPO##*/}" +[ "${REPO#\.}" != "${REPO}" ] && REPO="_${REPO}" + +BASE_REPO="${BASE_DIR}/${DOMAIN}/${REPO}" +TEMP_REPO="${TEMP_DIR}/${DOMAIN}/${REPO}" + +# logging +info() { printf 'message-info "%s"\n' "${*}" >>"${QUTE_FIFO}"; } +warn() { printf 'message-warning "%s"\n' "${*}" >>"${QUTE_FIFO}"; } +err() { + printf 'message-error "%s"\n' "${*}" >>"${QUTE_FIFO}" + return 1 +} +clean() { + rm -rf "${TEMP_DIR}" + exit "${1:-0}" +} + +# Check repo exists +if [ -d "${BASE_REPO}"/.git ]; then + warn "${REPO} already cloned in ${BASE_REPO}" + clean 0 + +# Try cloning +else + info "Cloning ${DOMAIN}/${REPO}..." + git clone "${QUTE_URL}" "${TEMP_REPO}" || + err "Error while cloning ${DOMAIN}/${REPO}, is it a repository?" + + if [ ! -d "${TEMP_REPO}"/.git ]; then + err 'An error occured, cloning failed...' + clean 2 + fi +fi + +# Move the temp folder to its final destination +[ -d "${BASE_REPO%/*}" ] || mkdir -p "${BASE_REPO%/*}" +mv "${TEMP_REPO}" "${BASE_REPO}" +info "${REPO} successfully cloned in ${BASE_REPO}" + +# Run post hook +if [ -n "${QUTE_POST_CLONE}" ]; then + eval "${QUTE_POST_CLONE}" +fi diff --git a/mac/.config/qutebrowser/userscripts/qr b/mac/.config/qutebrowser/userscripts/qr new file mode 100755 index 0000000..8421524 --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/qr @@ -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/mac/.config/qutebrowser/userscripts/qute-pass b/mac/.config/qutebrowser/userscripts/qute-pass new file mode 100755 index 0000000..064bd88 --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/qute-pass @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Chris Braun (cryzed) <cryzed@googlemail.com> +# +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +Insert login information using pass and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...). A short +demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif. +""" + +USAGE = """The domain of the site has to appear as a segment in the pass path, +for example: "github.com/cryzed" or "websites/github.com". Alternatively the +parameter `--unfiltered` may be used to get a list of all passwords. How the +username and password are determined is freely configurable using the CLI +arguments. As an example, if you instead store the username as part of the +secret (and use a site's name as filename), instead of the default configuration, +use `--username-target secret` and `--username-pattern "username: (.+)"`. + +The login information is inserted by emulating key events using qutebrowser's +fake-key command in this manner: [USERNAME]<Tab>[PASSWORD], which is compatible +with almost all login forms. + +If you use gopass with multiple mounts, use the CLI switch --mode gopass to switch to gopass mode. + +Suggested bindings similar to Uzbl's `formfiller` script: + + config.bind('<z><l>', 'spawn --userscript qute-pass') + config.bind('<z><u><l>', 'spawn --userscript qute-pass --username-only') + config.bind('<z><p><l>', 'spawn --userscript qute-pass --password-only') + config.bind('<z><o><l>', 'spawn --userscript qute-pass --otp-only') +""" + +EPILOG = """Dependencies: tldextract (Python 3 module), pass, pass-otp (optional). + +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 fnmatch +import functools +import os +import re +import shlex +import subprocess +import sys +import unicodedata +from urllib.parse import urlparse + +import idna +import tldextract + + +def expanded_path(path): + # Expand potential ~ in paths, since this script won't be called from a shell that does it for us + expanded = os.path.expanduser(path) + # Add trailing slash if not present + return os.path.join(expanded, "") + + +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( + "--password-store", + "-p", + default=expanded_path(os.getenv("PASSWORD_STORE_DIR", default="~/.password-store")), + help="Path to your pass password-store (only used in pass-mode)", + type=expanded_path, +) +argument_parser.add_argument( + "--mode", + "-M", + choices=["pass", "gopass"], + default="pass", + help="Select mode [gopass] to use gopass instead of the standard pass.", +) +argument_parser.add_argument( + "--prefix", + type=str, + help="Search only the given subfolder of the store (only used in gopass-mode)", +) +argument_parser.add_argument( + "--username-pattern", + "-u", + default=r".*/(.+)", + help="Regular expression that matches the username", +) +argument_parser.add_argument( + "--username-target", + "-U", + choices=["path", "secret"], + default="path", + help="The target for the username regular expression", +) +argument_parser.add_argument( + "--password-pattern", + "-P", + default=r"(.*)", + help="Regular expression that matches the password", +) +argument_parser.add_argument( + "--dmenu-invocation", + "-d", + default="dmenu", + help="Invocation used to execute a dmenu-provider", +) +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( + "--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( + "--extra-url-suffixes", + "-s", + default="", + help="Comma-separated string containing extra suffixes (e.g local)", +) +argument_parser.add_argument( + "--unfiltered", + dest="unfiltered", + action="store_true", + help="Show an unfiltered selection of all passwords in the store", +) +argument_parser.add_argument( + "--always-show-selection", + dest="always_show_selection", + action="store_true", + help="Always show selection, even if there is only a single match", +) +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("--otp-only", "-o", action="store_true", help="Only insert OTP 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 + + +class CouldNotMatchUsername(Exception): + pass + + +class CouldNotMatchPassword(Exception): + pass + + +def qute_command(command): + with open(os.environ["QUTE_FIFO"], "w") as fifo: + fifo.write(command + "\n") + fifo.flush() + + +# Encode candidate string parts as Internationalized Domain Name, doing +# Unicode normalization before. This allows to properly match (non-ASCII) +# pass entries with the corresponding domain names. +def idna_encode(name): + # Do Unicode normalization first, we use form NFKC because: + # 1. Use the compatibility normalization because these sequences have "the same meaning in some contexts" + # 2. idna.encode() below requires the Unicode strings to be in normalization form C + # See https://en.wikipedia.org/wiki/Unicode_equivalence#Normal_forms + unicode_normalized = unicodedata.normalize("NFKC", name) + # Empty strings can not be encoded, they appear for example as empty + # parts in split_path. If something like this happens, we just fall back + # to the unicode representation (which may already be ASCII then). + try: + idna_encoded = idna.encode(unicode_normalized) + except idna.IDNAError: + idna_encoded = unicode_normalized + return idna_encoded + + +def find_pass_candidates(domain, unfiltered=False): + candidates = [] + + if arguments.mode == "gopass": + gopass_args = ["gopass", "list", "--flat"] + if arguments.prefix: + gopass_args.append(arguments.prefix) + all_passwords = ( + subprocess.run(gopass_args, stdout=subprocess.PIPE) + .stdout.decode("UTF-8") + .splitlines() + ) + + for password in all_passwords: + if unfiltered or domain in password: + candidates.append(password) + else: + idna_domain = idna_encode(domain) + for path, directories, file_names in os.walk( + arguments.password_store, followlinks=True + ): + secrets = fnmatch.filter(file_names, "*.gpg") + if not secrets: + continue + + # Strip password store path prefix to get the relative pass path + pass_path = path[len(arguments.password_store) :] + split_path = pass_path.split(os.path.sep) + idna_split_path = [idna_encode(part) for part in split_path] + for secret in secrets: + secret_base = os.path.splitext(secret)[0] + idna_secret_base = idna_encode(secret_base) + if not unfiltered and idna_domain not in ( + idna_split_path + [idna_secret_base] + ): + continue + + # Append the unencoded Unicode path/name since this is how pass uses them + candidates.append(os.path.join(pass_path, secret_base)) + return candidates + + +def _run_pass(pass_arguments): + # The executable is conveniently named after it's mode [pass|gopass]. + pass_command = [arguments.mode] + env = os.environ.copy() + env["PASSWORD_STORE_DIR"] = arguments.password_store + process = subprocess.run( + pass_command + pass_arguments, env=env, stdout=subprocess.PIPE + ) + return process.stdout.decode(arguments.io_encoding).strip() + + +def pass_(path): + return _run_pass(["show", path]) + + +def pass_otp(path): + if arguments.mode == "gopass": + return _run_pass(["otp", "-o", path]) + return _run_pass(["otp", path]) + + +def dmenu(items, invocation): + command = shlex.split(invocation) + process = subprocess.run( + command, + input="\n".join(items).encode(arguments.io_encoding), + stdout=subprocess.PIPE, + ) + return process.stdout.decode(arguments.io_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 extract_password(secret, pattern): + match = re.match(pattern, secret) + if not match: + raise CouldNotMatchPassword("Pattern did not match target") + try: + return match.group(1) + except IndexError: + raise CouldNotMatchPassword( + "Pattern did not contain capture group, please use capture group. Example: (.*)" + ) + + +def extract_username(target, pattern): + match = re.search(pattern, target, re.MULTILINE) + if not match: + raise CouldNotMatchUsername("Pattern did not match target") + try: + return match.group(1) + except IndexError: + raise CouldNotMatchUsername( + "Pattern did not contain capture group, please use capture group. Example: (.*)" + ) + + +def main(arguments): + if not arguments.url: + argument_parser.print_help() + return ExitCodes.FAILURE + + extractor = tldextract.TLDExtract( + extra_suffixes=arguments.extra_url_suffixes.split(",") + ) + extract_result = extractor(arguments.url) + + # Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains), + # the registered domain name, the IPv4 address if that's what the URL represents and finally the private domain + # (if a non-public suffix was used), and the URL netloc. + candidates = set() + attempted_targets = [] + + private_domain = "" + if not extract_result.suffix: + private_domain = ( + ".".join((extract_result.subdomain, extract_result.domain)) + if extract_result.subdomain + else extract_result.domain + ) + + netloc = urlparse(arguments.url).netloc + + 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.ipv4, + private_domain, + netloc, + ], + ): + attempted_targets.append(target) + target_candidates = find_pass_candidates( + target, unfiltered=arguments.unfiltered + ) + if not target_candidates: + continue + + candidates.update(target_candidates) + if not arguments.merge_candidates: + break + else: + if not candidates: + stderr( + "No pass candidates for URL {!r} found! (I tried {!r})".format( + arguments.url, attempted_targets + ) + ) + return ExitCodes.NO_PASS_CANDIDATES + + if len(candidates) == 1 and not arguments.always_show_selection: + selection = candidates.pop() + else: + selection = dmenu(sorted(candidates), arguments.dmenu_invocation) + + # Nothing was selected, simply return + if not selection: + return ExitCodes.SUCCESS + + # If username-target is path and user asked for username-only, we don't need to run pass. + # Or if using otp-only, it will run pass on its own. + secret = None + if ( + not (arguments.username_target == "path" and arguments.username_only) + and not arguments.otp_only + ): + secret = pass_(selection) + username_target = selection if arguments.username_target == "path" else secret + try: + if arguments.username_only: + fake_key_raw(extract_username(username_target, arguments.username_pattern)) + elif arguments.password_only: + fake_key_raw(extract_password(secret, arguments.password_pattern)) + elif arguments.otp_only: + otp = pass_otp(selection) + fake_key_raw(otp) + else: + # Enter username and password using fake-key and <Tab> (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(extract_username(username_target, arguments.username_pattern)) + qute_command("fake-key <Tab>") + fake_key_raw(extract_password(secret, arguments.password_pattern)) + except CouldNotMatchPassword as e: + stderr("Failed to match password, target: secret, error: {}".format(e)) + return ExitCodes.COULD_NOT_MATCH_PASSWORD + except CouldNotMatchUsername as e: + stderr( + "Failed to match username, target: {}, error: {}".format( + arguments.username_target, e + ) + ) + return ExitCodes.COULD_NOT_MATCH_USERNAME + + if arguments.insert_mode: + qute_command("mode-enter insert") + + return ExitCodes.SUCCESS + + +if __name__ == "__main__": + arguments = argument_parser.parse_args() + sys.exit(main(arguments)) diff --git a/mac/.config/qutebrowser/userscripts/substiqute b/mac/.config/qutebrowser/userscripts/substiqute new file mode 100755 index 0000000..e52e345 --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/substiqute @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Author: palb91 +# Date: 2022 +# +# Bash style quick substitution in URL +# +# Usage: +# substiqute [-t] <replace_string> <new_string> +# +# Option: +# -t Open in a new tab +# +# In config.py: +# bind('gs', 'set-cmd-text -s -- :spawn -u -- substiqute') +# bind('gS', 'set-cmd-text -s -- :spawn -u -- substiqute -t') +# +# Note: +# Don't forget to quote replace_string and new_string if there are spaces + +set -e + +OPEN_IN_TAB=false + +# logging +info() { printf 'message-info "%s"\n' "${*}" >>"${QUTE_FIFO}"; } +warn() { printf 'message-warning "%s"\n' "${*}" >>"${QUTE_FIFO}"; } +err() { printf 'message-error "%s"\n' "${*}" >>"${QUTE_FIFO}"; return 1; } + + +replace() { + "${OPEN_IN_TAB}" \ + && printf 'open -t %s\n' "${QUTE_URL/"${1}"/"${2}"}" >>"${QUTE_FIFO}" \ + || printf 'open %s\n' "${QUTE_URL/"${1}"/"${2}"}" >>"${QUTE_FIFO}" +} + + +# with a binding like '^', it is possible to do like in bash ^string1^string2 +split() { + case "${1}" in + *^*) set -- "${1%%\^*}" "${1#*\^}" ;; + *) err 'Unknown substitution format' ;; + esac + replace "${@}" +} + + +# -t open in a new tab... but to replace the string -t with another, use +# `substiqute -- -t anything_else` +case "${1}" in + -t) OPEN_IN_TAB=true; shift ;; + --) shift ;; +esac + + +case "${#}" in + 0) err "No substitution in command" ;; + 1) split "${1}" ;; + 2) replace "${@}" ;; + *) err "To many arguments" ;; +esac diff --git a/mac/.config/qutebrowser/userscripts/tab-manager b/mac/.config/qutebrowser/userscripts/tab-manager new file mode 100755 index 0000000..3104d2d --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/tab-manager @@ -0,0 +1,529 @@ +#!/usr/bin/python3 + +from datetime import datetime as datetime +from os import environ as environ +from os import listdir as listdir +from os import path as path +from os import remove as remove +from sys import argv as argv +from sys import exit as exit +from time import sleep as sleep + +from yaml import safe_load as yaml_load + + +# this splits all args passed to the script so they can be used. +# args are passed like this "/path/to/session/files/dir/ COMMAND flag <?filename> flag <?filename> flag <filename> <?filename>" +# since some flags can be given filenames, they must be split out to be parsed +def split_out_args(args): + split_args = [] + # flip args list so as to iterate backwards, saves iteration + args.reverse() + # split the list by flag + for arg in args: + if arg[0] == "-": + split_args.append(args[: (args.index(arg) + 1)]) + args = args[(args.index(arg) + 1) :] + # flip every list in the master list back to normal + for lst in split_args: + lst.reverse() + # reverse master list so that all elements are in order + split_args.reverse() + return split_args + + +# generic function to check for a specified flag and return it's arguments +def check_flags(args, flag): + flag_found = False + flag_args = [] + for arg in args: + if arg[0] == flag: + flag_found = True + flag_args = arg + break + else: + flag_args = [] + return flag_found, flag_args + + +# read HTML, return list of tabs +def parse_html(html): + # get the tabs from the body + tabs = ( + html.split('<p style="word-wrap: break-word; word-break: break-all;">')[1] + .split("</p>")[0] + .split("<br>\n<br>\n") + ) + tabs = tabs[:-1] + tabs_list = [] + # parse out the tabs from the body into list of tabs, [[url,title],[url,title]...] + for tab in tabs: + stuff = tab.split(" | ")[0].split(">") + title = stuff[1] + url = stuff[0].split('"')[1] + tab_info = [url, title] + tabs_list.append(tab_info) + return tabs_list + + +# open and read an HTML file +def read_html(session_path): + with open(session_path + ".html", "r") as html_file: + html = html_file.read() + return html + + +# build HTML from list of tabs +def build_html(session_path, open_tabs): + # get the file name from session path + doc_title = session_path.split("/")[-1].split(".")[0] + # build html document title from document title, number of tabs and date and time + title = str( + doc_title + + ", " + + str(len(open_tabs)) + + " tabs, last updated " + + str(datetime.now()).split(".")[0] + ) + doc_body = str() + # iterate over tabs, build HTML for the body of the HTML file + for tab in open_tabs: + tab_line = ( + str(open_tabs.index(tab) + 1) + + '. <a href="' + + tab[0] + + '">' + + tab[1] + + " | " + + tab[0] + + "</a>\n<br>\n<br>\n" + ) + doc_body = doc_body + tab_line + # build the HTML document + html_doc = ( + "<html>\n<head>\n<title>" + + title + + '</title>\n</head>\n<body>\n<p style="word-wrap: break-word; word-break: break-all;">\n' + + doc_body + + "</p>\n</body>\n</html>" + ) + return html_doc + + +# writes HTML to specified file +def write_html(session_path, html): + with open(session_path + ".html", "w+") as html_file: + html_file.write(html) + + +# takes 1 list of tabs, checks for duplicate tabs in the same list +# turns tabs list [[url,title],[url,title]...] into dict with URLs as keys, since duplicate keys are automatically removed from dicts +def check_for_duplicate_tabs(tabs_list): + tabs_list_dict = {} + tabs_list_new = [] + for tab in tabs_list: + tabs_list_dict[tab[0]] = tab[1] + for item in tabs_list_dict.items(): + tabs_list_new.append([item[0], item[1]]) + return tabs_list_new + + +# update the index file, to be called any time a modification to a tab session occurs +def update_index_file(session_path): + sessions_list = [] + for item in listdir(session_path): + filename = item.split(".") + if len(filename) > 1 and filename[1] == "html" and item != "index.html": + sessions_list.append( + [ + "file:///" + session_path + item, + read_html(session_path + filename[0]) + .split("</title>")[0] + .split("<title>")[1], + ] + ) + index_path = session_path + "index" + write_html(index_path, build_html(index_path, sessions_list)) + + +# inform of an error, open the usage.txt file in browser, exit the program +def inform_error(toast): + usage_file_path = script_path.replace("tab-manager", "usage.txt") + run_command("open -t file://" + usage_file_path) + run_command("message-error '" + toast + "'") + exit() + + +# run a command in qutebrowser +def run_command(command): + with open(environ["QUTE_FIFO"], "a") as output: + output.write(command) + sleep(wait_time) + + +# reads the qutebrowser session file, return the yaml inside +def read_qutebrowser_session_file(session_path): + with open(session_path, "r") as session: + session_data = session.read() + return session_data + + +# pass session data as yaml, "history" and "all_windows" as bool, return required tabs +# if export_history is true, returns history, otherwise only returns active tabs, if all_windows is true, returns all windows, otherwise only returns the active window +# yaml ugh, fucking hyphen delimited dict +def get_qbsession_tabs(session_data, export_history=False, all_windows=False): + # for history in ['windows']['tabs']['history'] + yaml_content = yaml_load(session_data) + tabs = [] + for window in yaml_content["windows"]: + if all_windows == False: + if "active" in window: + for tab in window["tabs"]: + for history in tab["history"]: + if export_history == False: + if "active" in history: + tabs.append([history["url"], history["title"]]) + else: + tabs.append([history["url"], history["title"]]) + else: + for tab in window["tabs"]: + for history in tab["history"]: + if export_history == False: + if "active" in history: + tabs.append([history["url"], history["title"]]) + else: + tabs.append([history["url"], history["title"]]) + return tabs + + +# saves focused tab to specified session file unless it is already in there +def save(session_path, args): + tab = [[environ["QUTE_URL"], environ["QUTE_TITLE"]]] + check_f = check_flags(args, "-f") + if not check_f[0] or len(check_f[1]) < 2: + inform_error("no -f or no output session specified!") + if "index" in check_f[1]: + inform_error("cannot modify index!") + file_path = session_path + check_f[1][1] + if path.exists(file_path + ".html"): + tabs_list = parse_html(read_html(file_path)) + tabs_list = tabs_list + tab + tabs_list = check_for_duplicate_tabs(tabs_list) + else: + tabs_list = tab + write_html(file_path, build_html(file_path, tabs_list)) + update_index_file(session_path) + run_command( + "message-info 'focused tab successfully saved to " + check_f[1][1] + "'" + ) + + +# saves all open tabs to a session file, removes duplicates +def save_all(session_path, args): + open_tabs = [] + check_f = check_flags(args, "-f") + if not check_f[0] or len(check_f[1]) < 2: + inform_error("no -f or no output session specified!") + if "index" in check_f[1]: + inform_error("cannot modify index!") + file_path = session_path + check_f[1][1] + close_tabs = check_flags(args, "-c")[0] + overwrite = check_flags(args, "-o")[0] + run_command("session-save " + file_path) + open_tabs = get_qbsession_tabs(read_qutebrowser_session_file(file_path)) + # remove the recently created qutebrowser session file that has no extension, not the .html file. + remove(file_path) + if overwrite == True: + open_tabs = check_for_duplicate_tabs(open_tabs) + write_html(file_path, build_html(file_path, open_tabs)) + run_command("message-info '-o found, overwriting specified session'") + else: + if not path.exists(file_path + ".html"): + run_command( + "message-info 'session " + + check_f[1][1] + + " does not exist; creating...'" + ) + with open(file_path + ".html", "w"): + pass + else: + open_tabs = parse_html(read_html(file_path)) + open_tabs + open_tabs = check_for_duplicate_tabs(open_tabs) + write_html(file_path, build_html(file_path, open_tabs)) + update_index_file(session_path) + run_command( + "message-info 'all open tabs sucessfully saved to " + check_f[1][1] + "'" + ) + if close_tabs == True: + run_command("tab-only") + run_command("open " + file_path + ".html") + run_command + else: + run_command("open -t " + file_path + ".html") + pass + + +# open command, opens one or more html files +def open_session_files(session_path, args): + check_f = check_flags(args, "-f") + files_specified = check_f[0] + if len(check_f[1]) < 2: + files_specified = False + if files_specified == True: + for file in check_f[1][1:]: + run_command("open -t " + "file:///" + session_path + file + ".html") + run_command("message-info 'successfully opened " + file + "'") + else: + run_command("open -t " + "file:///" + session_path + "index.html") + run_command("message-info 'no session file specified, opening index.'") + + +# restore command, restores one or more sessions to one window +def restore_sessions(session_path, args): + check_f = check_flags(args, "-f") + files_list = [] + # if no file specified, attempt to restore the current focused tab, works if current focused tab is a session file in the specified session files directory. + if not check_f[0] or len(check_f[1]) < 2: + open_session_file = environ["QUTE_URL"] + if session_path in open_session_file: + files_list = [open_session_file.split("/")[-1].split(".")[0]] + else: + inform_error( + "you must specify sessions to restore or have a session file open in browser and in focus!" + ) + else: + files_list = check_f[1][1:] + close_tabs = check_flags(args, "-c")[0] + new_window = check_flags(args, "-n")[0] + if close_tabs == True: + run_command("tab-only") + run_command( + "message-info '-c found, closing all open tabs before restoring...'" + ) + for file in files_list: + tab_list = parse_html(read_html(session_path + file)) + for tab in tab_list: + if close_tabs == True: + run_command("open " + tab[0]) + close_tabs = False + else: + run_command("open -t " + tab[0]) + run_command("message-info 'successfully restored " + file + "'") + + +# removes the specified session files +def delete_sessions(session_path, args): + check_f = check_flags(args, "-f") + if not check_f[0] or len(check_f[1]) < 2: + inform_error("you must specify sessions to delete!") + for file in check_f[1][1:]: + if file == "index": + inform_error("cannot modify index!") + file_path = session_path + file + remove(file_path + ".html") + run_command("message-info 'session " + file + " successfully deleted.'") + update_index_file(session_path) + + +# merge specified sessions, or unspecified sessions in dir, based on -i or -a flag respectively +def merge_sessions(session_path, args): + sessions_to_merge = [] + final_session_tabs = [] + check_f = check_flags(args, "-f") + if not check_f[0] or len(check_f[1]) < 2: + inform_error("missing -f or no output session name specified!") + file_path = session_path + check_f[1][1] + check_i = check_flags(args, "-i") + check_a = check_flags(args, "-a") + if "index" in check_i[1]: + inform_error("cannot modify index!") + if "index" in check_f[1]: + inform_error("cannot modify index!") + if check_i[0] and check_a[0]: + inform_error("cannot use -i and -a at the same time!") + elif not check_i[0]: + if not check_a[0]: + inform_error("must use -a or -i flag in merge command!") + else: + if len(check_a[1]) < 2: + inform_error("-a found but no files specified") + for item in listdir(session_path): + if ( + item.split(".")[1] == "html" + and item != "index.html" + and item.split(".")[0] not in check_a[1][1:] + ): + sessions_to_merge.append(item.split(".")[0]) + else: + if len(check_i[1]) < 2: + inform_error("-i found but no files specified!") + for item in check_i[1][1:]: + sessions_to_merge.append(item) + for item in sessions_to_merge: + for tab in parse_html(read_html(session_path + item)): + final_session_tabs.append(tab) + # if -k flag not found, delete merged sessions + if not check_flags(args, "-k")[0]: + for item in sessions_to_merge: + remove(session_path + item + ".html") + run_command("message-info '" + item + " deleted.'") + else: + run_command("message-info '-k found, input sessions not deleted.'") + final_session_tabs = check_for_duplicate_tabs(final_session_tabs) + write_html(file_path, build_html(file_path, final_session_tabs)) + run_command("message-info 'specified sessions merged to " + check_f[1][1] + "'") + update_index_file(session_path) + + +# export specified qutebrowser session file into html session file +def export_session(session_path, args): + check_f = check_flags(args, "-f") + check_p = check_flags(args, "-p") + if not check_f[0] or len(check_f[1]) < 2: + file_path = session_path + check_p[1][1].split("/")[-1].split(".")[0] + else: + file_path = session_path + check_f[1][1] + if "index" == file_path.split("/")[-1]: + inform_error("cannot modify index!") + if path.exists(file_path + ".html"): + inform_error( + "a file with the same name as the output file already exists. Please specify a different file name for export." + ) + if not check_p[0] or len(check_p[1]) < 2: + inform_error("missing -p or no qutebrowser session file specified!") + session_file = check_p[1][1] + session_tabs = [] + export_history = check_flags(args, "-h")[0] + all_windows = check_flags(args, "-w")[0] + session_tabs = get_qbsession_tabs( + read_qutebrowser_session_file(session_file), export_history, all_windows + ) + session_tabs = check_for_duplicate_tabs(session_tabs) + write_html(file_path, build_html(file_path, session_tabs)) + run_command( + "message-info 'specified qutebrowser session successfully exported to " + + file_path + + ".html'" + ) + if check_flags(args, "-r")[0] == True: + remove(session_file) + run_command("message-info '-r found, deleting specified qutebrowser session'") + update_index_file(session_path) + run_command("open -t file:///" + file_path + ".html") + + +# remove a tab from a session file by it's index +def remove_tab(session_path, args): + check_f = check_flags(args, "-f") + check_t = check_flags(args, "-t") + if not check_t[0]: + inform_error( + "-t missing or no index specified! You must specify one or more links to remove from the session!" + ) + if not check_f[0] or len(check_f[1]) < 2: + open_tab = environ["QUTE_URL"] + if session_path in open_tab: + file_path = open_tab.split(".")[0].split("//")[1] + else: + inform_error( + "you must specify a session to modify or have a session file open in browser and in focus!" + ) + else: + file_path = session_path + check_f[1][1] + if "index" == file_path.split("/")[-1]: + inform_error("cannot modify index!") + tab_list = parse_html(read_html(file_path)) + indexes_list = check_t[1][1:] + indexes_int = [] + for ind in indexes_list: + indexes_int.append(int(ind)) + indexes_int.sort(reverse=True) + for ind in indexes_int: + tab_list.pop(ind - 1) + write_html(file_path, build_html(file_path, tab_list)) + update_index_file(session_path) + if not check_f[0] or len(check_f[1]) < 2: + run_command("reload") + # TODO check if session is now empty, if so load index file and delete the session + + +# changes the title in the file, changes the name of the file, updates index +def rename_session(session_path, args): + check_f = check_flags(args, "-f") + if not check_f[0] or len(check_f[1]) < 2: + open_tab = environ["QUTE_URL"] + if session_path in open_tab: + file_path = open_tab.split(".")[0].split("//")[1] + old_filename = file_path.split("/")[-1] + else: + inform_error( + "you must specify a session to modify or have a session file open in browser and in focus!" + ) + else: + old_filename = check_f[1][1] + file_path = session_path + old_filename + if "index" == old_filename: + inform_error("cannot modify index!") + check_n = check_flags(args, "-n") + if not check_n[0] or len(check_n[1]) < 2: + inform_error( + "missing -n or no output session specified! what do you want to change the name to?" + ) + new_filename = check_n[1][1] + html_doc = read_html(file_path).replace( + "<title>" + old_filename, "<title>" + new_filename + ) + write_html(session_path + new_filename, html_doc) + remove(file_path + ".html") + update_index_file(session_path) + run_command( + "message-info '" + + old_filename + + " successfully renamed to " + + new_filename + + "'" + ) + if not check_f[0] or len(check_f[1]) < 2: + run_command("open " + session_path + new_filename + ".html") + + +def run(): + sessions_path = argv[1] + if len(argv) < 3: + inform_error("no command given!") + command = argv[2] + if len(argv) > 3: + args = split_out_args(argv[3:]) + else: + args = [] + if command == "save": + save(sessions_path, args) + elif command == "save-all": + save_all(sessions_path, args) + elif command == "open": + open_session_files(sessions_path, args) + elif command == "restore": + restore_sessions(sessions_path, args) + elif command == "merge": + merge_sessions(sessions_path, args) + elif command == "delete": + delete_sessions(sessions_path, args) + elif command == "export": + export_session(sessions_path, args) + elif command == "remove": + remove_tab(sessions_path, args) + elif command == "rename": + rename_session(sessions_path, args) + elif command == "update-index": + update_index_file(sessions_path) + run_command("message-info 'index updated.'") + elif command == "help": + inform_error("everybody needs a little help sometimes.") + else: + inform_error("invalid command!") + + +script_path = argv[0] +wait_time = 0.3 +print(argv) +run() diff --git a/mac/.config/qutebrowser/userscripts/translate b/mac/.config/qutebrowser/userscripts/translate new file mode 100755 index 0000000..25c66dd --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/translate @@ -0,0 +1,116 @@ +#! /usr/bin/env python3 + +import argparse +import json +import os +import sys +import urllib.parse + +import requests + + +def js(message): + return f""" + (function() {{ + var box = document.createElement('div'); + box.style.position = 'fixed'; + box.style.bottom = '10px'; + box.style.right = '10px'; + box.style.backgroundColor = 'white'; + box.style.color = 'black'; + box.style.borderRadius = '8px'; + box.style.padding = '10px'; + box.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; + box.style.zIndex = '10000'; + box.innerText = decodeURIComponent("{message}"); + document.body.appendChild(box); + + function removeBox(event) {{ + if (!box.contains(event.target)) {{ + box.remove(); + document.removeEventListener('click', removeBox); + }} + }} + document.addEventListener('click', removeBox); + }})(); + """ + + +def translate_google(text, target_lang): + encoded_text = urllib.parse.quote(text) + response = requests.get( + f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={target_lang}&dt=t&q={encoded_text}" + ) + response_json = json.loads(response.text) + translated_text = "" + for i in response_json[0]: + translated_text += i[0] + return translated_text + + +def translate_libretranslate(text, url, key, target_lang): + response = requests.post( + f"{url}/translate", + data={"q": text, "source": "auto", "target": target_lang, "api_key": key}, + ) + return response.json()["translatedText"] + + +def main(): + parser = argparse.ArgumentParser( + description="Translate text using different providers." + ) + parser.add_argument( + "--provider", + choices=["google", "libretranslate"], + required=False, + default="google", + help="Translation provider to use", + ) + parser.add_argument( + "--libretranslate_url", + required=False, + default="https://libretranslate.com", + help="URL for LibreTranslate API", + ) + parser.add_argument( + "--libretranslate_key", + required=False, + default="", + help="API key for LibreTranslate", + ) + parser.add_argument( + "--target_lang", + required=False, + default="en", + help="Target language for translation", + ) + args = parser.parse_args() + + qute_fifo = os.getenv("QUTE_FIFO") + if not qute_fifo: + sys.stderr.write( + f"Error: {sys.argv[0]} can not be run as a standalone script.\n" + ) + sys.stderr.write( + "It is a qutebrowser userscript. In order to use it, call it using 'spawn --userscript' as described in qute://help/userscripts.html\n" + ) + sys.exit(1) + + text = os.getenv("QUTE_SELECTED_TEXT", "") + + if args.provider == "google": + translated_text = translate_google(text, args.target_lang) + elif args.provider == "libretranslate": + translated_text = translate_libretranslate( + text, args.libretranslate_url, args.libretranslate_key, args.target_lang + ) + + js_code = js(urllib.parse.quote(translated_text)).replace("\n", " ") + + with open(qute_fifo, "a") as fifo: + fifo.write(f"jseval -q {js_code}\n") + + +if __name__ == "__main__": + main() diff --git a/mac/.config/qutebrowser/userscripts/usage.txt b/mac/.config/qutebrowser/userscripts/usage.txt new file mode 100644 index 0000000..f2d0ff7 --- /dev/null +++ b/mac/.config/qutebrowser/userscripts/usage.txt @@ -0,0 +1,99 @@ +If your browser just opened this by itself, you used tab-manager incorrectly or ran the "help" command. +The most common mistakes are misspelling a session name when attempting to restore or open, forgetting a required flag or required arguments. +You should have gotten a toast message from qutebrowser telling you what you did wrong. +If you did everything correctly and this document still pops up by itself, feel free to open an issue. Include what command you ran, what flags you passed and what toast message you got. +See the usage information below for proper usage instructions. + +to run tab-manager, +``` +:spawn path/to/tab-manager /abspath/to/sessions/dir <command> -flag -flag <?argument(s)> +``` +in the qutebrowser command line. + +I have included some useful keybind examples further below to make it easier for you to set up. + +The following are valid commands, and their flags and operations. +Optional arguments are listed as such, otherwise they must be present. + +save - appends current tab to specified session unless it is already there, creates session if it doesn't exist + -f <session> - specifies the session to write to. Takes only one session +remove - removes a tab link from a session file, by index + -t <index> - specifies the index of the tab to remove, must be an integer + optional: + -f <session> - specifies the session file to modify. Takes only one session + when used with no session specified, it attempts to remove the index from an open session HTML. + if run this way with any other page in focus other than a session file you will get an error +save-all - appends all open tabs to specified session, creates it if it doesn't exist, does not save duplicate tabs + -f <session> - specifies the session to write to. Takes only one session. + optional: + -c - close all tabs after save-all and open session as HTML in browser + -o - overwrites existing session instead of appending non-duplicate tabs +open - opens the specified session files as HTML with links and title + optional: + -f <session1> <session2>... - specifies the session(s) to open, can handle multiple sessions + when run with no session specified, it opens an index with links to all sessions in the directory + "index" can also be specified when specifying multiple files to open, if you want to open the index as well as a specific session +restore - opens all links in a session or multiple sessions, like session restore, restores them with existing open tabs by default + optional: + -f <session1> <session2>... - specifies the session(s) to restore, can take multiple sessions + -c - closes all tabs before restoring. default behavior is to add tabs to existing session + when run with no sessions specified or no "-f" flag, it restores the session in the current focused tab. + if run this way with any other page in the focused tab you will get an error +merge - merges arbitrary number of session files, removes duplicates + -f <session> - specifies the output session, Takes only one session + -i <session1> <session2>... - specifies sessions to merge, takes one or more existing sessions as input. + -a <session1> <session2>... - merges all session files in the directory except specified sessions. basically allows you to specify which sessions not to merge rather than which ones to merge + the "-a" and "-i" flags are mutually exclusive and one of the two must be used, but never both. + optional: + -k - keep pre merged sessions after creating the merged session; default behavior is to delete input sessions after merge +delete - deletes specified session(s) + -f <session1> <session2>... - specifies the session(s) to delete, can take multiple sessions +rename - renames a session and updates the index + -f <session> - session to rename + -n <new_session_name> - new name +export - DOES NOT WORK ON HTML! use this to export a qutebrowser session file to html so as to open the links in a different browser. Always removes duplicate tabs + -f <output_file> - the HTML file to write to, will be written to specified sessions directory + -p <abspath/to/qutebrowser/session/file> - the qutebrowser session file to read, absolute path + optional: + -w - export all windows in qutebrowser session, for multiwindow sessions + -h - export history of each tab as well, when used with "-w" it will export history for all windows in the session + -r - remove original qutebrowser session file after export +update-index - just updates the index, useful if you manually copied a session file from one directory to another +help - opens this file in the browser + +Some Useful Keybinds: + +- general bind, to manually enter commands, flags and arguments +``` +:bind zg set-cmd-text -s :spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ +``` +- open one or more sessions as HTML, or open index +``` +:bind zo set-cmd-text -s :spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ open -f +``` +- restore specified sessions, or the current open HTML file if it is a valid session +``` +:bind zr set-cmd-text -s :spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ restore -f +``` +- restore, same as above but close all open tabs first +``` +:bind zR set-cmd-text -s :spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ restore -c -f +``` +- save all and overwrite specified session (update session, don't append): +``` +:bind za set-cmd-text -s :spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ save-all -o -f +``` +- append current focused tab to specified session +``` +:bind zs set-cmd-text -s :spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ save -f +``` +- delete session +``` +:bind zD set-cmd-text -s :spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ delete -f +``` +- open this file +``` +:bind zh spawn --userscript path/to/tab-manager /abspath/to/sessions/dir/ help +``` + +In the above examples. everything is bound under "z". Change the keys to your requirements, change the directories, add binds with flags as you need them, remove binds you won't use, etc. |
