qutebrowser: Make use of dotter for dir structure

Since we now use dotter we can simplify the dir structure for
qutebrowser a lot. Everything dot-filed earlier can now reside
in simple directories called config (for ~/.config/qutebrowser),
data (for ~/.local/share/qutebrowser), and scripts
(for ~/.local/bin) files.
This commit is contained in:
Marty Oehme 2023-10-03 13:43:00 +02:00
parent dcde027a67
commit 8681d34946
Signed by: Marty
GPG key ID: EDBF2ED917B2EF6A
49 changed files with 9 additions and 5 deletions

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#consent-page > div > div > div > form > div.wizard-body > div.actions.couple > a"/>
<wait/>
<click element="#select-legit-all-purpose > span.toggleAll.toggle-accpet-all"/>
<click element="#consent-page > div > form > div.page-footer > div > div > button"/>
</block>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#yDmH0d > c-wiz > div > div > div.NIoIEf > div.G4njw > div.qqtRac > div > div > div > a"/>
<wait/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > div:nth-child(3) > div.uScs5d > div > div.uScs5d > div:nth-child(1) > div > button"/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > div:nth-child(4) > div.uScs5d > div > div.uScs5d > div:nth-child(1) > div > button"/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > div:nth-child(5) > div.IgeUeb > div.uScs5d > div > div.uScs5d > div:nth-child(1) > div > button"/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > form > div > button"/>
</block>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<!-- TODO: You cannot block cookies on ebay -->
<click element="#gdpr-banner > div.gdpr-banner__wrapper > div > a"/>
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#onetrust-reject-all-handler"/>
</block>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#gdpr-single-choice-overlay > div > div.wt-overlay__footer.wt-pt-xs-3 > div.wt-overlay__footer__action.wt-display-flex-md.wt-flex-grow-md-1.wt-order-xs-1.wt-order-md-neg1.wt-justify-content-flex-end > button"/>
<wait/>
<click element="#gdpr-privacy-settings > div > div.wt-overlay__footer.wt-align-items-center > div.wt-overlay__footer__action > div > div:nth-child(3) > button"/>
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<!-- TODO: Facebook changes IDs of buttons -->
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#wrapper > div.notification-banner > div > a"/>
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<remove element=".hide-consent"/>
</block>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#EDu5ze > div"/>
<wait/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > div:nth-child(4) > div.uScs5d > div > div.uScs5d > div:nth-child(1) > div > button"/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > div:nth-child(5) > div.uScs5d > div > div.uScs5d > div:nth-child(1) > div > button"/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > div:nth-child(6) > div.IgeUeb > div.uScs5d > div > div.uScs5d > div:nth-child(1) > div > button"/>
<click element="#yDmH0d > c-wiz > div > div > div > div.VP4fnf > form > div > button"/>
</block>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<!-- WARNING: The cookie-free version of this websites looks very different-->
<click element="#modal-host > div > div > div > div > div > div > div.css-zndjj2 > a"/>
<wait time="1000"/>
<click element="#__next > div.css-qbih8b > div > button.css-1glzvq1"/>
</block>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#onetrust-pc-btn-handler"/>
<click element="#cookie-preferences > div.save-preference-btn-container > button"/>
</block>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<!-- TODO: You cannot block cookies, only accept? -->
<click element="body > div.RnEpo.Yx5HN > div > div > div > div.mt3GC > button.aOOlW.HoLwm"/>
</block>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#artdeco-global-alert-container > div.artdeco-global-alert.artdeco-global-alert--NOTICE.artdeco-global-alert--COOKIE_CONSENT > section > div > div.artdeco-global-alert-action__wrapper > button:nth-child(1)"/>
<wait time="1500"/>
<click element="body > header > a:nth-child(1) > li-icon"/>
</block>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#modal-host > div > div > div > div > div > div > div.css-zndjj2 > a"/>
<wait time="1000"/>
<click element="#__next > div.css-1m0hjq0 > div > button.css-1mzn52w"/>
</block>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#modal-host > div > div > div > div > div > div > div.css-zndjj2 > a"/>
<wait/>
<click element="#__next > div.css-1m0hjq0 > div > button.css-1mzn52w"/>
</block>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#wcpConsentBannerCtrl > div._2j0fmugLb1FgYz6KPuB91w > button:nth-child(2)"/>
<wait/>
<click element="#_1dp8Vp5m3HwAqGx8qBmFV2_c1_reject"/>
<click element="#_1dp8Vp5m3HwAqGx8qBmFV2_c2_reject"/>
<click element="#_1dp8Vp5m3HwAqGx8qBmFV2_c3_reject"/>
<click element="#wcpCookiePreferenceCtrl > div.AFsJE948muYyzCMktdzuk > div > div.nohp3sIG12ZBhzcMnPala > button._3tOu1FJ59c_xz_PmI1lKV5._1zNQOqxpBFSokeCLGi_hGr"/>
</block>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<!-- TODO: Clicking on the button does not work -->
<!-- <click element="#cookie-disclosure > div.cta-btn-container > button:nth-child(2)"/> -->
<!-- TODO: Workaround: Close -->
<click element="#cookie-disclosure > div.btn-container > button > span.icon-close"/>
</block>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<!-- CANNOT OPT OUT OF EVERYTHING -->
<!-- TODO: Does not work? -->
<click element="#site-content > div.gdpr.shown.expanded.expanded-dock.css-17nqy7q.e1x0szx60 > div.css-183a15u.ejw0p350 > div > div.css-1s2i4vk > a"/>
<debug/>
<wait/>
<debug msg="After"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(1) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(2) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(3) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(4) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(5) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(6) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(7) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(8) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(9) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(10) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(11) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(12) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-2 > tbody > tr:nth-child(14) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(1) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(2) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(3) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(4) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(5) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(6) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(7) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(8) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(9) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(10) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(11) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(12) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(13) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(14) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(15) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(16) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(17) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(18) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-3 > tbody > tr:nth-child(19) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-4 > tbody > tr:nth-child(1) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-4 > tbody > tr:nth-child(2) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-4 > tbody > tr:nth-child(3) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-4 > tbody > tr:nth-child(4) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-4 > tbody > tr:nth-child(7) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#gdpr-faq-cookie-table-4 > tbody > tr:nth-child(8) > td:nth-child(5) > a:nth-child(2)"/>
<click element="#opt-out-of-new-york-times-nonessential-trackers"/>
<!-- TODO: Close the new tab -->
<!-- TODO: Close the banner -->
</block>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#manageCookiesLink"/>
<wait time="1000"/>
<click element="#performance"/>
<click element="#functional"/>
<click element="#submitCookiesBtn"/>
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#SHORTCUT_FOCUSABLE_DIV > div:nth-child(6) > div._3q-XSJ2vokDQrvdG6mR__k > section > div > section > section > form:nth-child(1) > button"/>
</block>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#cookie-link-manage"/>
<!-- TODO: Not sure clicking the right thing -->
<click element="#cookie-btn-allow"/>
</block>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="body > div.ff-sans.ps-fixed.z-nav-fixed.ws4.sm\\:w-auto.p32.bg-black-750.fc-white.bar-lg.b16.l16.r16.js-consent-banner > div > button.grid--cell.s-btn.s-btn__filled.js-cookie-settings"/>
<wait time="1000"/>
<click element="#onetrust-pc-sdk > div > div.s-modal--footer.mt0.grid.gs8.gsx > button.grid--cell.s-btn.s-btn__primary.save-preference-btn-handler.onetrust-close-btn-handler.js-consent-banner-hide.js-consent-save"/>
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#rejectAllButton"/>
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#react-root > div > div > div.css-1dbjc4n.r-13qz1uu.r-417010 > main > div > div > div.css-1dbjc4n.r-1awozwy.r-1m3jxhj.r-1upvrn0.r-18u37iz.r-1d7fvdj.r-d9fdf6.r-tvv088.r-13qz1uu > div.css-18t94o4.css-1dbjc4n.r-1niwhzg.r-11mg6pl.r-sdzlij.r-1phboty.r-rs99b7.r-18kxxzh.r-1q142lx.r-1w2pmg.r-19u6a5r.r-1mnahxq.r-ero68b.r-1gg2371.r-1ny4l3l.r-1fneopy.r-o7ynqc.r-6416eg.r-lrvibr > div > span"/>
</block>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<remove element=".oaDry"/>
</block>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<block>
<click element="#consent-page > div > div > div > form > div.wizard-body > div.actions.couple > a"/>
<wait time="2000"/>
<!-- This is not my spelling mistake :) vvvvvv -->
<click element="#select-legit-all-purpose > span.toggleAll.toggle-accpet-all"/>
<click element="#consent-page > div > form > div.page-footer > div > div > button"/>
</block>

View file

@ -0,0 +1,54 @@
#!/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) -> str:
root = ET.fromstring(element)
text = ET.tostring(root, encoding="unicode", method="text")
text = html.unescape(text)
return text
def send_command_to_qute(command) -> None:
if fifo := os.environ.get("QUTE_FIFO"):
with open(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 and 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()

View file

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Goes to the sci-hub page for the current article, based on DOI.
Based on the work in
https://github.com/cadadr/configuration/blob/4b6a241d04d113f322b960890a0d0a0ab783a7b3/dotfiles/qutebrowser/userscripts/doi
with much gratitude.
The program can be invoked with DOI on a page selected, through the hinting mode when selecting a DOI link or on a publisher page (any page where doi meta-tags are set) - works on ScienceDirect, Taylor&Francis, Springer, etc.
That means you can give it a doi through a link or on the current page, for example with the following mappings:
```python
config.bind('"p', "spawn --userscript doi2scihub")
config.bind(';p', "hint links userscript doi2scihub")
```
You can also pass the doi as the (only) argument to the userscript:
```
:spawn --userscript doi2scihub https://doi.org/10.37394/23207.2021.18.68
`
Updates its sci-hub link based on the one listed on sci-hub wiki page.
"""
import os
import re
import sys
import html.parser
import requests
mode = os.getenv("QUTE_MODE")
text = None
class DoiTagParser(html.parser.HTMLParser):
doi = None
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if self.doi == None and tag == "meta":
if (
("name", "citation_doi") in attrs
or ("name", "dc.identifier") in attrs
or ("scheme", "doi") in attrs
):
for att in attrs:
if att[0] == "content":
self.doi = att[1]
break
class SciHubLinkParser(html.parser.HTMLParser):
current = None
link_patt = re.compile(r"^(?P<url>https?://sci-hub\..+)/about$")
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if self.current == None and tag == "a":
for att in attrs:
if att[0] == "href" and self.link_patt.match(att[1] or ""):
match = self.link_patt.match(att[1] or "")
self.current = match["url"] if match and match["url"] else None
def get_scihub_url(wiki_page: str = "https://wikiless.org/wiki/Sci-Hub"):
resp = requests.get(wiki_page)
parser = SciHubLinkParser()
parser.feed(resp.text)
return parser.current or "https://sci-hub.ru"
# use doi argument if we got one
if len(sys.argv) > 1:
text = sys.argv[1]
# use the hinted url
elif mode == "hints":
text = os.getenv("QUTE_URL", "").strip()
# use the current selection
elif mode == "command" and os.getenv("QUTE_SELECTED_TEXT"):
text = os.getenv("QUTE_SELECTED_TEXT", "").strip()
# just try to find a doi on current page
elif os.getenv("QUTE_HTML"):
with open(os.getenv("QUTE_HTML", ""), "r") as source:
parser = DoiTagParser()
parser.feed(source.read())
text = parser.doi
with open(os.getenv("QUTE_FIFO", ""), "w") as fifo:
if not text:
fifo.write(f'message-warning "Could not find a valid DOI"')
sys.exit()
# DOI syntax: https://www.doi.org/doi_handbook/2_Numbering.html#2.2.
#
# Note that this probably matches a subset of possible DOIs, as it
# seems that theres no practical limitation on neither the length nor
# the contents of the DOI. But IMHO this is a healthy subset.
doi_re = re.compile(
# match possible URI prefix
r"(?P<blah>((https?)?://)?doi\.org/)?"
# match actual DOI
r"(?P<meat>[a-zA-Z0-9\./\-_]+)"
)
match = doi_re.match(text)
if match is None or match["meat"] is None:
fifo.write(
f"message-warning \"'{text}' is probably not a DOI, or update regexp\""
)
else:
url = get_scihub_url()
doi = match["meat"]
fifo.write(f"open -t {url}/{doi}")

View file

@ -0,0 +1,10 @@
#! /usr/bin/bash
# Download current page as pdf file
# use title of current page / selected text as filename
filename=${QUTE_SELECTED_TEXT:-$QUTE_TITLE}
# revert to default name if nothing else is set; remove special chars
filename=$(echo "${filename:-downloaded.pdf}" | sed 's/[<>,/]/_/g')
# print to pdf
echo "print --pdf '$QUTE_DOWNLOAD_DIR/${1:-$filename}.pdf'" >>"$QUTE_FIFO"

Binary file not shown.

View file

@ -0,0 +1,398 @@
#!/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
# 2022-2023 Marty Oehme (added stand-alone script capability)
# Use it as a qutebrowser userscript to open gemini pages:
# Put this file in qutebrowser userscript folder and call command
# `:spawn --userscript qute-gemini "gemini://my-gemini-url.org"`
# or
# `:hint links userscript qute-gemini` to open from selected link
# Rename file to `qute-gemini-tab` (or create symlink) to open
# any gemini url as a new tab.
# Since the script also opens normal URLs you can even replace your
# normal link hint mapping with it (usually f or F for tabbed) and
# continue surfing like normal, only that you can now also access
# any gemini pages as if they were part of the normal http protocol.
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 = """<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Error opening page: URL</title>
<style>
CSS
</style>
</head>
<body>
<h1>qute-gemini error</h1>
<p>Error while opening:<br/><a href="URL">URL_TEXT</a></p>
<p>DESCRIPTION</p>
</body>
</html>
"""
_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. The 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.get("QUTE_URL", "")
def qute_fifo() -> str:
"""Get the FIFO or file to write qutebrowser commands to."""
return os.environ.get("QUTE_FIFO", "")
def html_href(url: str, description: str) -> str:
return "".join(['<a href="', url, '">', description, "</a>"])
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, "59", "", "Non-gemini 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 = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">',
"\t<head>",
"\t\t<title>" + html.escape(url) + "</title>",
"\t\t<style>",
get_css(),
"\t\t</style>",
"\t</head>",
"\t<body>",
"\t<article>",
]
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</ul>")
in_list = False
# Blank line, ignore
if not line:
pass
# Link
elif line.startswith("=>"):
ln = line[2:].split(None, 1)
# Use the URL itself as the description if there is none
if len(ln) == 1:
ln.append(ln[0])
# Encode the link description
ln[1] = html.escape(ln[1])
# Resolve relative URLs
ln[0] = gemini_absolutise_url(url, ln[0])
lines.append("\t\t<p>" + html_href(ln[0], ln[1]) + "</p>")
# Preformated toggle
elif line.startswith("```"):
if in_pre:
lines.append("\t\t</pre>")
else:
lines.append("\t\t<pre>")
in_pre = not in_pre
# Preformated
elif in_pre:
lines.append(line)
# Header
elif line.startswith("###"):
lines.append("\t\t<h3>" + html.escape(line[3:].strip()) + "</h3>")
elif line.startswith("##"):
lines.append("\t\t<h2>" + html.escape(line[2:].strip()) + "</h2>")
elif line.startswith("#"):
lines.append("\t\t<h1>" + html.escape(line[1:].strip()) + "</h1>")
# List
elif line.startswith("*"):
if not in_list:
lines.append("\t\t<ul>")
in_list = True
lines.append("\t\t\t<li>" + html.escape(line[1:].strip()) + "</li>")
# Quote
elif line.startswith(">"):
lines.extend(
[
"\t\t<blockquote>",
"\t\t\t<p>" + line[1:].strip() + "</p>",
"\t\t</blockquote>",
]
)
# Normal text
else:
lines.append("\t\t<p>" + html.escape(line.strip()) + "</p>")
url_html = html_href(url, html.escape(url))
original_url_html = html_href(original_url, html.escape(original_url))
lines.extend(
[
"",
"\t</article>",
"\t<details>",
"\t\t<summary>",
"\t\t\tContent from " + url_html,
"\t\t</summary>",
"\t\t<dl>",
"\t\t\t<dt>Original URL</dt>",
"\t\t\t<dd>" + original_url_html + "</dd>",
"\t\t\t<dt>Status</dt>",
"\t\t\t<dd>" + status + "</dd>",
"\t\t\t<dt>Meta</dt>",
"\t\t\t<dd>" + meta + "</dd>",
"\t\t\t<dt>Fetched by</dt>",
'\t\t\t<dd><a href="https://git.sr.ht/~sotirisp/qute-gemini">qute-gemini '
+ str(_version)
+ "</a></dd>",
"\t\t</dl>",
"\t</details>",
"\t</body>",
"</html>",
]
)
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) -> str:
"""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()
if not tmp_filename:
return ""
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
return open_url
def open_url(url: str, open_args: str) -> None:
parsed_url = urllib.parse.urlparse(url)
if parsed_url.scheme == "gemini":
to_open = open_gemini(url)
else:
to_open = url
if not to_open:
return
fifo = qute_fifo()
if fifo and fifo != "":
with open(fifo, "w") as qfifo:
qfifo.write(f"open {open_args} {to_open}")
return
os.system(f"xdg-open {to_open}")
if __name__ == "__main__":
# Open in the current or a new tab depending on the script name
if sys.argv[0].endswith("-tab"):
open_args = "-b -r"
else:
open_args = ""
# Take url to open as argument or from qutebrowser url
if len(sys.argv) > 1:
url = sys.argv[1]
else:
url = qute_url()
# Select how to open the URL depending on its scheme
open_url(url, open_args)

View file

@ -0,0 +1 @@
qute-gemini

View file

@ -0,0 +1 @@
../../scripts/qutedmenu

View file

@ -0,0 +1 @@
../../scripts/recently-downloaded

View file

@ -0,0 +1,27 @@
#! /usr/bin/env bash
#
# Send current page/link to a shaarli instance.
#
# Can be used for sending the current page via:
# :spawn --userscript shaarli_add.sh
# for sending an arbitrary page passed as argument:
# :spawn --userscript shaarli_add.sh https://myinterestingpage.com
# or for sending a hinted link:
# :hint links userscript shaarli_add.sh
#
# Configure your shaarli instance with this:
SHAARLI_INSTANCE="https://links.martyoeh.me"
# send page to shaarli instance and open the 'post' page to edit it
if [ "$#" -gt 0 ]; then
BM="$SHAARLI_INSTANCE/?post=$*"
else
BM="$SHAARLI_INSTANCE/?post=$QUTE_URL"
fi
if [ -n "$QUTE_FIFO" ]; then
echo "open -t -r $BM" >>"$QUTE_FIFO"
else
xdg-open "$BM"
fi

View file

@ -0,0 +1,51 @@
#!/usr/bin/env sh
#
# Translate the page, or if `--text` argument is given the current selection, with google translate.
#
# Adapted code from https://github.com/AckslD/Qute-Translate, with much gratitude.
PAGE="https://lingva.garudalinux.org/SOURCELANGUAGE/TARGETLANGUAGE/TRANSLATETEXT"
while [ $# -gt 0 ]; do
case $1 in
-s | --source)
QUTE_TRANS_SOURCE=$2
shift
shift
;;
-t | --target)
QUTE_TRANS_TARGET=$2
shift
shift
;;
--url)
QUTE_TRANS_URL="true"
shift
;;
--text)
QUTE_TRANS_URL="false"
shift
;;
esac
done
if [ -z "$QUTE_TRANS_SOURCE" ]; then
# Default use automatic language for source
QUTE_TRANS_SOURCE="auto"
fi
if [ -z "$QUTE_TRANS_TARGET" ]; then
# Default use English for target
QUTE_TRANS_TARGET="en"
fi
if [ "$QUTE_TRANS_URL" = "false" ]; then
# Translate selected text
PAGE=$(echo "$PAGE" | sed -e "s/SOURCELANGUAGE/$QUTE_TRANS_SOURCE/" -e "s/TARGETLANGUAGE/$QUTE_TRANS_TARGET/" -e "s/TRANSLATETEXT/$QUTE_SELECTED_TEXT/")
echo "open -t ${PAGE}" >>"$QUTE_FIFO"
else
# Default translate URL
PAGE="https://translate.google.com/translate?"
CONT_KEY="u"
CONTENT="$QUTE_URL"
echo "open -t ${PAGE}sl=${QUTE_TRANS_SOURCE}&tl=${QUTE_TRANS_TARGET}&${CONT_KEY}=\"${CONTENT}\"" >>"$QUTE_FIFO"
fi

View file

@ -0,0 +1,26 @@
#! /usr/bin/env bash
#
# Send current page/link to a wallabag instance.
#
# Can be used for sending the current page via:
# :spawn --userscript wallabag_add.sh
# for sending an arbitrary page passed as argument:
# :spawn --userscript wallabag_add.sh https://myinterestingtext.com
# or for sending a hinted link:
# :hint links userscript wallabag_add.sh
#
# Configure your wallabag instance with this:
WALLABAG_INSTANCE="https://read.martyoeh.me"
# only works for wallabag v2.*
if [ "$#" -gt 0 ]; then
BM="$WALLABAG_INSTANCE/bookmarklet?url=$*"
else
BM="$WALLABAG_INSTANCE/bookmarklet?url=$QUTE_URL"
fi
if [ -n "$QUTE_FIFO" ]; then
echo "open -b -r $BM" >>"$QUTE_FIFO"
else
xdg-open "$BM"
fi