From f7350756d0dbe759915a454d2ef30244fd3fa5f2 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 22 Apr 2021 11:07:49 +0200 Subject: [PATCH] qute: Update bookmarklets and config structure qute: Add gemini integration Added simple integration for gemini. When following a link (`f` or `F`) to a page which begins with the `gemini://` protocol, it will automatically convert the page to html and display it instead. qute: Update configuration structure Moved larger setting blocks (cmd aliaes, content settings, key mappings, url settings) into their own files. qute: Add readability, code_select userscripts Added userscript to invoke (python) readability mode which will render the page in a much more nicely to read display. Can be invoked either through `:spawn --userscript readability` or the key combination `r`. Added userscript to copy code snippets from websites, using the `code` html tag. Invoked through `;c` to fit into the other extended hinting options qutebrowser provides. qute: Add open downloads, default download location Added ability to open last downloads with `gD`, replaces the previous open last download -- this one lets you select with dmenu where the old option only opened the very last download automatically. Set the download directory to default to XDG directory, with fallback to `~/downloads` if the env var is not set. qute: Set vifm filepicker Set vifm to be the filepicker for qute. Can be used to select single or multiple files. Simply select the intended files in vifm and they will be passed through to qutebrowser (and thus whatever website). --- qutebrowser/.config/qutebrowser/alias.py | 33 ++ qutebrowser/.config/qutebrowser/config.py | 202 ++-------- qutebrowser/.config/qutebrowser/content.py | 44 +++ qutebrowser/.config/qutebrowser/keys.py | 73 ++++ qutebrowser/.config/qutebrowser/url.py | 26 ++ .../qutebrowser/userscripts/code_select.py | 53 +++ .../share/qutebrowser/userscripts/qute-gemini | 357 ++++++++++++++++++ .../qutebrowser/userscripts/qute-gemini-tab | 1 + 8 files changed, 623 insertions(+), 166 deletions(-) create mode 100644 qutebrowser/.config/qutebrowser/alias.py create mode 100644 qutebrowser/.config/qutebrowser/content.py create mode 100644 qutebrowser/.config/qutebrowser/keys.py create mode 100644 qutebrowser/.config/qutebrowser/url.py create mode 100755 qutebrowser/.local/share/qutebrowser/userscripts/code_select.py create mode 100755 qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini create mode 120000 qutebrowser/.local/share/qutebrowser/userscripts/qute-gemini-tab 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