diff --git a/qutebrowser/.config/qutebrowser/alias.py b/qutebrowser/.config/qutebrowser/alias.py new file mode 100644 index 0000000..60c6a18 --- /dev/null +++ b/qutebrowser/.config/qutebrowser/alias.py @@ -0,0 +1,33 @@ +c.aliases["gem"] = "hint links userscript qute-gemini" + +# Use q for quitting a tab (mimicks vim buffer) - qa is used for exiting +c.aliases["q"] = "tab-close" +# if we save sessions with w, load sessions with e (again, mimicks vim) +c.aliases["e"] = "session-load" + +# wallabag add current page, either with walla command, or bw +c.aliases["add-wallabag"] = "spawn --userscript wallabag_add.sh" + +# add to (my) shaarli instance +c.aliases["add-shaarli"] = "spawn --userscript shaarli_add.sh" + +# re-opens the current page on the web archive overview page +c.aliases["send-to-archive"] = "open https://web.archive.org/web/{url}" + +# sends current page to outline and thus through readability mode +c.aliases["send-to-outline"] = "open https://outline.com/{url}" + +# save current page to pdf file +c.aliases["save_to_pdf"] = "spawn --userscript pagetopdf.sh" + +# translate current page / selection with google translate +c.aliases["translate-page-google"] = "spawn --userscript translate_google.sh" +c.aliases[ + "translate-selection-google" +] = "spawn --userscript translate_google.sh --text" + +# print picture of current url as qr code +c.aliases["show-qr"] = "spawn --userscript qr" + +# add a task of current page to taskwarrior +c.aliases["taskadd"] = "spawn --userscript taskadd" diff --git a/qutebrowser/.config/qutebrowser/config.py b/qutebrowser/.config/qutebrowser/config.py index 1078f1f..5faabad 100644 --- a/qutebrowser/.config/qutebrowser/config.py +++ b/qutebrowser/.config/qutebrowser/config.py @@ -11,20 +11,39 @@ from qutebrowser.config.configfiles import ConfigAPI # noqa: F401 # load additional settings configured via autoconfig.yml config.load_autoconfig() -## CHANGING DEFAULTS -# rebind moving tabs to free for download -config.bind("gG", "tab-give") -# switch binds for scroll-marks and quick-/book-marks -config.bind("m", "mode-enter set_mark") -config.bind("`", "quickmark-save") -config.bind("~", "bookmark-add") - c.completion.web_history.max_items = 1000 c.hints.uppercase = True -c.editor.command = ["alacritty", "-e", "nvim", "-f", "{file}"] +c.editor.command = [ + "alacritty", + "-e", + "nvim", + "-f", + "{file}", + "-c", + "normal {line}G{column0}l", +] -# LOOK -# ---- +# change filepicker +c.fileselect.handler = "external" +picker = [ + "alacritty", + "--class", + "floating,floating", + "-e", + "vifm", + "--choose-files", + "{}", +] +c.fileselect.single_file.command = picker +c.fileselect.multiple_files.command = picker + +c.downloads.location.directory = os.getenv("XDG_DOWNLOAD_DIR", "~/downloads") +c.downloads.location.prompt = False + +config.source("alias.py") +config.source("keys.py") +config.source("content.py") +config.source("url.py") # Tab-Bar # have tab bar on the right, not on the top @@ -35,166 +54,17 @@ c.tabs.width = "15%" c.colors.webpage.bg = "#555555" -# FUNCTION -# -------- - # Prevents *all* tabs from being loaded on restore, only loads on activating them c.session.lazy_restore = True -# Binds -# 'Leader' key binding -leader = "" -# toggles ('cycles') between tabs always showing, or only when switching between them -config.bind( - leader + "tt", - "config-cycle -t tabs.show always switching ;; config-cycle -t statusbar.show in-mode always", -) -config.bind(leader + "th", "set tabs.position bottom") -config.bind(leader + "tH", "set tabs.position top") -config.bind(leader + "tv", "set tabs.position right") -config.bind(leader + "tV", "set tabs.position left") - -# [M]edia shortcuts - watch, queue, download media -# bind mpv to play the current page/links, using a single instance which queues the next link passed -config.bind(leader + "M", "spawn umpv {url}") -config.bind(leader + "m", "hint links spawn umpv {hint-url}") -# bind youtube-dl to download the current page/links -config.bind(leader + "dM", "spawn vidl {url}") -config.bind( - leader + "dm", - "hint --rapid links spawn vidl {hint-url}", -) - -# save current page to pdf file -c.aliases["save_to_pdf"] = "spawn --userscript pagetopdf.sh" -# set to gp, to mirror gd (download) just as go-Pdfdownload -config.bind(leader + "dp", "save_to_pdf", mode="normal") - -# open last download -config.bind("gD", "download-open") - -# Use q for quitting a tab (mimicks vim buffer) - qa is used for exiting -c.aliases["q"] = "tab-close" -# if we save sessions with w, load sessions with e (again, mimicks vim) -c.aliases["e"] = "session-load" - -# bookmarklet aliases: -# Bookmarklets get assigned to ", mirroring the opening of marks, but not interfering -# with scroll-marks (which are really useful!) - -# wallabag add current page, either with walla command, or bw -c.aliases["add-wallabag"] = "spawn --userscript wallabag_add.sh" -config.bind('"w', "add-wallabag", mode="normal") - -# add to (my) shaarli instance -c.aliases["add-shaarli"] = "spawn --userscript shaarli_add.sh" -config.bind('"s', "add-shaarli", mode="normal") - -# re-opens the current page on the web archive overview page -c.aliases["send-to-archive"] = "open https://web.archive.org/web/{url}" -config.bind('"a', "send-to-archive", mode="normal") - -# sends current page to outline and thus through readability mode -c.aliases["send-to-outline"] = "open https://outline.com/{url}" -config.bind('"o', "send-to-outline", mode="normal") - -# translate current page / selection with google translate -c.aliases["translate-page-google"] = "spawn --userscript translate_google.sh" -c.aliases[ - "translate-selection-google" -] = "spawn --userscript translate_google.sh --text" -config.bind('"g', "translate-page-google", mode="normal") -config.bind('"G', "translate-selection-google", mode="normal") - -# print picture of current url as qr code -c.aliases["show-qr"] = "spawn --userscript qr" -config.bind('"q', "show-qr") - -# set stylesheets for the browser to use -# leader - ss to remove all applied stylesheets -config.bind( - leader + "s", - "config-cycle content.user_stylesheets " + 'stylesheets/stylesheet.css ""', -) - -# Enable and disable javascript -config.bind(leader + "js", "config-cycle content.javascript.enabled true false") - -# Reload this config -config.bind(leader + "VV", "config-source") - -c.url.searchengines = { - "DEFAULT": "https://search.martyoeh.me/?q={}", - "l": "https://links.martyoeh.me/?searchterm={}&searchtags=", - "ddg": "https://duckduckgo.com/?q={}", - "d": "https://www.dict.cc/?s={}", - "t": "https://www.thesaurus.com/browse/{}", - "gt": "https://translate.google.com/#auto/de/{}", - "dt": "https://www.deepl.com/translator#en/de/{}", - "g": "https://www.google.com/search?q={}", - "r": "https://old.reddit.com/r/{}", - "w": "https://en.wikipedia.org/w/index.php?search={}", - "gh": "https://github.com/search?q={}", - "al": "https://wiki.archlinux.org/index.php/{}", - "aur": "https://aur.archlinux.org/packages/?K={}", - "yt": "https://www.youtube.com/results?search_query={}", - "maps": "https://www.google.fr/maps?q={}", - "gol": "https://golang.org/pkg/{}/", - "man": "https://manned.org/browse/search?q={}", - "lib": "http://libgen.fun/search.php?req={}", - "sci": "https://sci-hub.do/{}", - "alt": "https://alternativeto.net/software/{}/?license=opensource", - "hn": "https://hn.algolia.com/?q={}", - "kb": "https://soeg.kb.dk/discovery/search?query=any,contains,{}&tab=Everything&search_scope=MyInst_and_CI&vid=45KBDK_KGL:KGL&offset=0&lang=en", -} - -c.content.blocking.enabled = True -c.content.blocking.method = "both" -c.content.blocking.adblock.lists = [ - "http://www.malwaredomainlist.com/hostslist/hosts.txt", - "http://someonewhocares.org/hosts/hosts", - "http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext", - "https://secure.fanboy.co.nz/fanboy-cookiemonster.txt", +# for code_select.py userscript +# Allows copying code sections to clipboard easily +c.hints.selectors["code"] = [ + # Selects all code tags whose direct parent is not a pre tag + ":not(pre) > code", + "pre", ] -c.content.blocking.hosts.lists = [ - "http://winhelp2002.mvps.org/hosts.zip", - "http://malwaredomains.lehigh.edu/files/justdomains.zip", -] -c.content.blocking.whitelist = ["piwik.org"] - -c.content.autoplay = False -c.content.pdfjs = False -c.content.javascript.enabled = False -js_whitelist = [ - "*://*.youtube.com/*", - "*://127.0.0.1/*", - "*://asciinema.org/*", - "*://calendar.google.com/*", - "*://clockify.me/tracker*", - "*://darksky.net/*", - "*://deepl.com/*", - "*://duckduckgo.com/*", - "*://fosstodon.org/*", - "*://github.com/*", - "*://gitlab.com/*", - "*://localhost/*", - "*://mail.google.com/*", - "*://maps.google.com/*", - "*://*.martyoeh.me/*", - "*://news.ycombinator.com/*", - "*://old.reddit.com/*", - "*://todoist.com/*", - "*://toggl.com/*", - "*://translate.google.com/*", - "chrome://*/*", - "file://*", - "qute://*/*", -] -for page in js_whitelist: - with config.pattern(page) as p: - p.content.javascript.enabled = True # give the browser nice theme colors - config.source("colorscheme.py") diff --git a/qutebrowser/.config/qutebrowser/content.py b/qutebrowser/.config/qutebrowser/content.py new file mode 100644 index 0000000..0a204c9 --- /dev/null +++ b/qutebrowser/.config/qutebrowser/content.py @@ -0,0 +1,44 @@ +c.content.blocking.enabled = True +c.content.blocking.method = "both" +c.content.blocking.adblock.lists = [ + "http://www.malwaredomainlist.com/hostslist/hosts.txt", + "http://someonewhocares.org/hosts/hosts", + "http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext", + "https://secure.fanboy.co.nz/fanboy-cookiemonster.txt", +] +c.content.blocking.hosts.lists = [ + "http://winhelp2002.mvps.org/hosts.zip", + "http://malwaredomains.lehigh.edu/files/justdomains.zip", +] +c.content.blocking.whitelist = ["piwik.org"] +c.content.autoplay = False +c.content.pdfjs = False +c.content.javascript.enabled = False +js_whitelist = [ + "*://*.youtube.com/*", + "*://127.0.0.1/*", + "*://asciinema.org/*", + "*://calendar.google.com/*", + "*://clockify.me/tracker*", + "*://darksky.net/*", + "*://deepl.com/*", + "*://duckduckgo.com/*", + "*://fosstodon.org/*", + "*://github.com/*", + "*://gitlab.com/*", + "*://localhost/*", + "*://mail.google.com/*", + "*://maps.google.com/*", + "*://*.martyoeh.me/*", + "*://news.ycombinator.com/*", + "*://old.reddit.com/*", + "*://todoist.com/*", + "*://toggl.com/*", + "*://translate.google.com/*", + "chrome://*/*", + "file://*", + "qute://*/*", +] +for page in js_whitelist: + with config.pattern(page) as p: + p.content.javascript.enabled = True diff --git a/qutebrowser/.config/qutebrowser/keys.py b/qutebrowser/.config/qutebrowser/keys.py new file mode 100644 index 0000000..71e8758 --- /dev/null +++ b/qutebrowser/.config/qutebrowser/keys.py @@ -0,0 +1,73 @@ +# Key mappings +# 'Leader' key binding +leader = "" + +## CHANGED DEFAULTS + +# rebind moving tabs to free for download +config.bind("gG", "tab-give") +# switch binds for scroll-marks and quick-/book-marks +config.bind("m", "mode-enter set_mark") +config.bind("`", "quickmark-save") +config.bind("~", "bookmark-add") + +## ADDED +# toggles ('cycles') between tabs always showing, or only when switching between them +config.bind( + leader + "tt", + "config-cycle -t tabs.show always switching ;; config-cycle -t statusbar.show in-mode always", +) +config.bind(leader + "th", "set tabs.position bottom") +config.bind(leader + "tH", "set tabs.position top") +config.bind(leader + "tv", "set tabs.position right") +config.bind(leader + "tV", "set tabs.position left") + +# [M]edia shortcuts - watch, queue, download media +# bind mpv to play the current page/links, using a single instance which queues the next link passed +config.bind(leader + "M", "spawn umpv {url}") +config.bind(leader + "m", "hint links spawn umpv {hint-url}") +# bind youtube-dl to download the current page/links +config.bind(leader + "dM", "spawn vidl {url}") +config.bind( + leader + "dm", + "hint --rapid links spawn vidl {hint-url}", +) + +# Download shortcuts +config.bind(leader + "dd", "download", mode="normal") +config.bind(leader + "dp", "save_to_pdf", mode="normal") +# open last download +config.bind("gD", "spawn --userscript open_download") + +config.bind('"w', "add-wallabag", mode="normal") +config.bind('"s', "add-shaarli", mode="normal") + +config.bind('"a', "send-to-archive", mode="normal") +config.bind('"o', "send-to-outline", mode="normal") + +config.bind('"g', "translate-page-google", mode="normal") +config.bind('"G', "translate-selection-google", mode="normal") + +config.bind('"q', "show-qr") + +config.bind(leader + "r", "spawn --userscript readability") + +# set stylesheets for the browser to use +config.bind( + leader + "s", + "config-cycle content.user_stylesheets " + 'stylesheets/stylesheet.css ""', +) + +config.bind(leader + "a", "set-cmd-text -s :taskadd") + +# Enable and disable javascript +config.bind(leader + "js", "config-cycle content.javascript.enabled true false") + +# Reload configuration +config.bind(leader + "VV", "config-source") + +# Enable code snippet hinting mode +config.bind(";c", "hint code userscript code_select.py") +# first looks if it's a gemini link and then fall back to http +config.bind(";g", "hint links userscript qute-gemini") +config.bind(";G", "hint links userscript qute-gemini-tab") diff --git a/qutebrowser/.config/qutebrowser/url.py b/qutebrowser/.config/qutebrowser/url.py new file mode 100644 index 0000000..bcaf601 --- /dev/null +++ b/qutebrowser/.config/qutebrowser/url.py @@ -0,0 +1,26 @@ +c.url.searchengines = { + "DEFAULT": "https://search.martyoeh.me/?q={}", + "l": "https://links.martyoeh.me/?searchterm={}&searchtags=", + "ddg": "https://duckduckgo.com/?q={}", + "d": "https://www.dict.cc/?s={}", + "t": "https://www.thesaurus.com/browse/{}", + "gt": "https://translate.google.com/#auto/de/{}", + "dt": "https://www.deepl.com/translator#en/de/{}", + "g": "https://www.google.com/search?q={}", + "r": "https://old.reddit.com/r/{}", + "w": "https://en.wikipedia.org/w/index.php?search={}", + "gh": "https://github.com/search?q={}", + "al": "https://wiki.archlinux.org/index.php/{}", + "aur": "https://aur.archlinux.org/packages/?K={}", + "yt": "https://www.youtube.com/results?search_query={}", + "maps": "https://www.google.fr/maps?q={}", + "gol": "https://golang.org/pkg/{}/", + "man": "https://manned.org/browse/search?q={}", + "lib": "http://libgen.fun/search.php?req={}", + "#sci": "https://sci-hub.do/{}", + "alt": "https://alternativeto.net/software/{}/?license=opensource", + "hn": "https://hn.algolia.com/?q={}", + "kb": "https://soeg.kb.dk/discovery/search?query=any,contains,{}&tab=Everything&search_scope=MyInst_and_CI&vid=45KBDK_KGL:KGL&offset=0&lang=en", +} +c.url.default_page = "https://start.duckduckgo.com" +c.url.start_pages = ["https://start.duckduckgo.com"] diff --git a/qutebrowser/.local/share/qutebrowser/userscripts/code_select.py b/qutebrowser/.local/share/qutebrowser/userscripts/code_select.py new file mode 100755 index 0000000..185f7f4 --- /dev/null +++ b/qutebrowser/.local/share/qutebrowser/userscripts/code_select.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import os +import html +import re +import sys +import xml.etree.ElementTree as ET + +try: + import pyperclip +except ImportError: + PYPERCLIP = False +else: + PYPERCLIP = True + + +def parse_text_content(element): + root = ET.fromstring(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) + if PYPERCLIP: + pyperclip.copy(code_text) + send_command_to_qute( + "message-info 'copied to clipboard: {info}{suffix}'".format( + info=code_text.splitlines()[0], + 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/qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini b/qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini new file mode 100755 index 0000000..3e3110e --- /dev/null +++ b/qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +# qute-gemini - Open Gemini links in qutebrowser and render them as HTML +# +# SPDX-FileCopyrightText: 2019-2020 solderpunk +# SPDX-FileCopyrightText: 2020 Aaron Janse +# SPDX-FileCopyrightText: 2020 petedussin +# SPDX-FileCopyrightText: 2020-2021 Sotiris Papatheodorou +# SPDX-License-Identifier: GPL-3.0-or-later + +import cgi +import html +import os +import socket +import ssl +import sys +import tempfile +import urllib.parse + +from typing import Tuple + +_version = "1.0.0" + +_max_redirects = 5 + +_error_page_template = """ + + +Error opening page: URL + + + +

qute-gemini error

+

Error while opening:
URL_TEXT

+

DESCRIPTION

+ + +""" + +_status_code_desc = { + "1": "Gemini status code 1 Input. This is not implemented in qute-gemini.", + "10": "Gemini status code 10 Input. This is not implemented in qute-gemini.", + "11": "Gemini status code 11 Sensitive Input. This is not implemented in qute-gemini.", + "3": "Gemini status code 3 Redirect. Stopped after " + + str(_max_redirects) + + " redirects.", + "30": "Gemini status code 30 Temporary Redirect. Stopped after " + + str(_max_redirects) + + " redirects.", + "31": "Gemini status code 31 Permanent Redirect. Stopped after " + + str(_max_redirects) + + " redirects.", + "4": "Gemini status code 4 Temporary Failure. Server message: META", + "40": "Gemini status code 40 Temporary Failure. Server message: META", + "41": "Gemini status code 41 Server Unavailable. The server is unavailable due to overload or maintenance. Server message: META", + "42": "Gemini status code 42 CGI Error. A CGI process, or similar system for generating dynamic content, died unexpectedly or timed out. Server message: META", + "43": "Gemini status code 43 Proxy Error. A proxy request failed because the server was unable to successfully complete a transaction with the remote host. Server message: META", + "44": "Gemini status code 44 Slow Down. Rate limiting is in effect. Please wait META seconds before making another request to this server.", + "5": "Gemini status code 5 Permanent Failure. Server message: META", + "50": "Gemini status code 50 Permanent Failure. Server message: META", + "51": "Gemini status code 51 Not Found. he requested resource could not be found but may be available in the future. Server message: META", + "52": "Gemini status code 52 Gone. The resource requested is no longer available and will not be available again. Server message: META", + "53": "Gemini status code 53 Proxy Request Refused. The request was for a resource at a domain not served by the server and the server does not accept proxy requests. Server message: META", + "59": "Gemini status code 59 Bad Request. The server was unable to parse the client's request, presumably due to a malformed request. Server message: META", + "6": "Gemini status code 6 Client Certificate Required. This is not implemented in qute-gemini.", +} + + +def qute_url() -> str: + """Get the URL passed to the script by qutebrowser.""" + return os.environ["QUTE_URL"] + + +def qute_fifo() -> str: + """Get the FIFO or file to write qutebrowser commands to.""" + return os.environ["QUTE_FIFO"] + + +def html_href(url: str, description: str) -> str: + return "".join(['', description, ""]) + + +def qute_gemini_css_path() -> str: + """Return the path where the custom CSS file is expected to be.""" + try: + base_dir = os.environ["XDG_DATA_HOME"] + except KeyError: + base_dir = os.path.join(os.environ["HOME"], ".local/share") + return os.path.join(base_dir, "qutebrowser/userscripts/qute-gemini.css") + + +def gemini_absolutise_url(base_url: str, relative_url: str) -> str: + """Absolutise relative gemini URLs. + + Adapted from gcat: https://github.com/aaronjanse/gcat + """ + if "://" not in relative_url: + # Python's URL tools somehow only work with known schemes? + base_url = base_url.replace("gemini://", "http://") + relative_url = urllib.parse.urljoin(base_url, relative_url) + relative_url = relative_url.replace("http://", "gemini://") + return relative_url + + +def gemini_fetch_url(url: str) -> Tuple[str, str, str, str, str]: + """Fetch a Gemini URL and return the content as a string. + + url: URL with gemini:// or no scheme. + Returns 4 strings: the content, the URL the content was fetched from, the + Gemini status code, the value of the meta field and an error message. + + Adapted from gcat: https://github.com/aaronjanse/gcat + """ + # Parse the URL to get the hostname and port + parsed_url = urllib.parse.urlparse(url) + if not parsed_url.scheme: + url = "gemini://" + url + parsed_url = urllib.parse.urlparse(url) + if parsed_url.scheme != "gemini": + return "", "Received non-gemini:// URL: " + url + if parsed_url.port is not None: + useport = parsed_url.port + else: + useport = 1965 + # Do the Gemini transaction, looping for redirects + redirects = 0 + while True: + # Send the request + s = socket.create_connection((parsed_url.hostname, useport)) + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + s = context.wrap_socket(s, server_hostname=parsed_url.netloc) + s.sendall((url + "\r\n").encode("UTF-8")) + # Get the status code and meta + fp = s.makefile("rb") + header = fp.readline().decode("UTF-8").strip() + status, meta = header.split()[:2] + # Follow up to 5 redirects + if status.startswith("3"): + url = gemini_absolutise_url(url, meta) + parsed_url = urllib.parse.urlparse(url) + redirects += 1 + if redirects > _max_redirects: + # Too many redirects + break + # Otherwise we're done + else: + break + # Process the response + content = "" + error_msg = "" + # 2x Success + if status.startswith("2"): + media_type, media_type_opts = cgi.parse_header(meta) + # Decode according to declared charset defaulting to UTF-8 + if meta.startswith("text/gemini"): + charset = media_type_opts.get("charset", "UTF-8") + content = fp.read().decode(charset) + else: + error_msg = "Expected media type text/gemini but received " + media_type + # Handle errors + else: + # Try matching a 2-digit and then a 1-digit status code + try: + error_msg = _status_code_desc[status[0:2]] + except KeyError: + try: + error_msg = _status_code_desc[status[0]] + except KeyError: + error_msg = "The server sent back something weird." + # Substitute the contents of meta into the error message if needed + error_msg = error_msg.replace("META", meta) + return content, url, status, meta, error_msg + + +def gemtext_to_html( + gemtext: str, url: str, original_url: str, status: str, meta: str +) -> str: + """Convert gemtext to HTML. + + title: Used as the document title. + url: The URL the gemtext was received from. Used to resolve + relative URLs in the gemtext content. + original_url: The URL the original request was made at. + status: The Gemini status code returned by the server. + meta: The meta returned by the server. + Returns the HTML representation as a string. + """ + # Accumulate converted gemtext lines + lines = [ + '', + '', + "\t", + "\t\t" + html.escape(url) + "", + "\t\t", + "\t", + "\t", + "\t
", + ] + in_pre = False + in_list = False + # Add an extra newline to ensure list tags are closed properly + for line in (gemtext + "\n").splitlines(): + # Add the list closing tag + if not line.startswith("*") and in_list: + lines.append("\t\t") + in_list = False + # Blank line, ignore + if not line: + pass + # Link + elif line.startswith("=>"): + l = line[2:].split(None, 1) + # Use the URL itself as the description if there is none + if len(l) == 1: + l.append(l[0]) + # Encode the link description + l[1] = html.escape(l[1]) + # Resolve relative URLs + l[0] = gemini_absolutise_url(url, l[0]) + lines.append("\t\t

" + html_href(l[0], l[1]) + "

") + # Preformated toggle + elif line.startswith("```"): + if in_pre: + lines.append("\t\t") + else: + lines.append("\t\t
")
+            in_pre = not in_pre
+        # Preformated
+        elif in_pre:
+            lines.append(line)
+        # Header
+        elif line.startswith("###"):
+            lines.append("\t\t

" + html.escape(line[3:].strip()) + "

") + elif line.startswith("##"): + lines.append("\t\t

" + html.escape(line[2:].strip()) + "

") + elif line.startswith("#"): + lines.append("\t\t

" + html.escape(line[1:].strip()) + "

") + # List + elif line.startswith("*"): + if not in_list: + lines.append("\t\t
    ") + in_list = True + lines.append("\t\t\t
  • " + html.escape(line[1:].strip()) + "
  • ") + # Quote + elif line.startswith(">"): + lines.extend( + [ + "\t\t
    ", + "\t\t\t

    " + line[1:].strip() + "

    ", + "\t\t
    ", + ] + ) + # Normal text + else: + lines.append("\t\t

    " + html.escape(line.strip()) + "

    ") + url_html = html_href(url, html.escape(url)) + original_url_html = html_href(original_url, html.escape(original_url)) + lines.extend( + [ + "", + "\t
", + "\t
", + "\t\t", + "\t\t\tContent from " + url_html, + "\t\t", + "\t\t
", + "\t\t\t
Original URL
", + "\t\t\t
" + original_url_html + "
", + "\t\t\t
Status
", + "\t\t\t
" + status + "
", + "\t\t\t
Meta
", + "\t\t\t
" + meta + "
", + "\t\t\t
Fetched by
", + '\t\t\t
qute-gemini ' + + str(_version) + + "
", + "\t\t
", + "\t
", + "\t", + "", + ] + ) + return "\n".join(lines) + + +def get_css() -> str: + # Search for qute-gemini.css in the directory this script is located in + css_file = qute_gemini_css_path() + if os.path.isfile(css_file): + # Return the file contents + with open(css_file, "r") as f: + return f.read().strip() + else: + # Use no CSS + return "" + + +def qute_error_page(url: str, description: str) -> str: + """Return a data URI error page like qutebrowser does. + + url: The URL of the page that failed to load. + description: A description of the error. + Returns a data URI containing the error page. + """ + # Generate the HTML error page + html_page = _error_page_template.replace("URL", url) + html_page = html_page.replace("URL_TEXT", html.escape(url)) + html_page = html_page.replace("DESCRIPTION", html.escape(description)) + html_page = html_page.replace("CSS", get_css()) + # URL encode and return as a data URI + return "data:text/html;charset=UTF-8," + urllib.parse.quote(html_page) + + +def open_gemini(url: str, open_args: str) -> None: + """Open Gemini URL in qutebrowser.""" + # Get the Gemini content + content, content_url, status, meta, error_msg = gemini_fetch_url(url) + if error_msg: + # Generate an error page in a data URI + open_url = qute_error_page(url, error_msg) + else: + # Success, convert to HTML in a temporary file + tmpf = tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) + tmp_filename = tmpf.name + tmpf.close() + with open(tmp_filename, "w") as f: + f.write(gemtext_to_html(content, content_url, url, status, meta)) + open_url = " file://" + tmp_filename + # Open the HTML file in qutebrowser + with open(qute_fifo(), "w") as qfifo: + qfifo.write("open " + open_args + open_url) + + +def open_other(url: str, open_args: str) -> None: + """Open non-Gemini URL in qutebrowser.""" + with open(qute_fifo(), "w") as qfifo: + qfifo.write("open " + open_args + " " + url) + + +if __name__ == "__main__": + # Open in the current or a new tab depending on the script name + if sys.argv[0].endswith("-tab"): + open_args = "-t" + else: + open_args = "" + # Select how to open the URL depending on its scheme + url = qute_url() + parsed_url = urllib.parse.urlparse(url) + if parsed_url.scheme == "gemini": + open_gemini(url, open_args) + else: + open_other(url, open_args) diff --git a/qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini-tab b/qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini-tab new file mode 120000 index 0000000..bfcb515 --- /dev/null +++ b/qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini-tab @@ -0,0 +1 @@ +qute-gemini \ No newline at end of file