OOPify GeminiItems - makes interface code a bit tidier and concentrates all URL wrangling in one place. I *think* this makes things clearer.

This commit is contained in:
Solderpunk 2019-08-13 19:56:15 +03:00
parent 642117f553
commit d8d59f51e4
1 changed files with 83 additions and 77 deletions

160
av98.py
View File

@ -74,28 +74,10 @@ _ANSI_COLORS = {
"black": "\x1b[0;30m", "black": "\x1b[0;30m",
} }
CRLF = '\r\n'
# Lightweight representation of an item in Geminispace
GeminiItem = collections.namedtuple("GeminiItem",
("scheme", "host", "port", "path", "name"))
def url_to_geminiitem(url, name=None):
# urllibparse.urlparse can handle IPv6 addresses, but only if they
# are formatted very carefully, in a way that users almost
# certainly won't expect. So, catch them early and try to fix
# them...
if url.count(":") > 2: # Best way to detect them?
url = fix_ipv6_url(url)
# Prepend a gemini schema if none given
if "://" not in url:
url = "gemini://" + url
u = urllib.parse.urlparse(url)
# https://tools.ietf.org/html/rfc4266#section-2.1
path = u.path
return GeminiItem(u.scheme, u.hostname, u.port or 1965, path, name)
def fix_ipv6_url(url): def fix_ipv6_url(url):
if not url.count(":") > 2: # Best way to detect them?
return url
# If there's a pair of []s in there, it's probably fine as is. # If there's a pair of []s in there, it's probably fine as is.
if "[" in url and "]" in url: if "[" in url and "]" in url:
return url return url
@ -115,40 +97,80 @@ def fix_ipv6_url(url):
return schema + "://" + schemaless return schema + "://" + schemaless
return schemaless return schemaless
def geminiitem_to_url(gi): standard_ports = {
if gi and gi.host: "gemini": 1965,
return ("%s://%s%s%s" % ( "gopher": 70,
gi.scheme, "http": 80,
gi.host, "https": 443,
"" if gi.port == 1965 else ":%d" % gi.port, }
gi.path if gi.path.startswith("/") else "/"+gi.path,
))
elif gi:
return gi.path
else:
return ""
def geminiitem_from_line(line, menu_gi): class GeminiItem():
assert line.startswith("=>")
assert line[2:].strip()
bits = line[2:].strip().split(maxsplit=1)
link = bits[0]
name = bits[1] if len(bits) == 2 else link
if "://" in link:
return url_to_geminiitem(link, name)
else:
return GeminiItem("gemini", menu_gi.host, menu_gi.port, link, name)
def geminiitem_to_line(gi, name=""): def __init__(self, url, name=""):
name = ((name or gi.name) or geminiitem_to_url(gi)) if "://" not in url:
path = gi.path url = "gemini://" + url
return "=> %s %s" % (geminiitem_to_url(gi), name) self.url = fix_ipv6_url(url)
self.name = name
parsed = urllib.parse.urlparse(self.url)
self.scheme = parsed.scheme
self.host = parsed.hostname
self.port = parsed.port or standard_ports[self.scheme]
self.path = parsed.path
def root_url(self):
return urllib.parse.urlunparse((self.scheme,
# Don't include redundant ports in the netloc
self.host if self.port == standard_ports[self.scheme] else self.host + ":" + str(self.port),
"/", "", "", ""))
def up_url(self):
pathbits = list(os.path.split(self.path))
# Get rid of empty string from trailing /
while not pathbits[-1]:
pathbits.pop()
# Don't try to go higher than root
if len(pathbits) == 1:
return self.url
# Get rid of bottom component
pathbits.pop()
new_path = os.path.join(*pathbits)
return urllib.parse.urlunparse((self.scheme,
self.host if self.port == standard_ports[self.scheme] else self.host + ":" + str(self.port),
new_path, "", "", ""))
def absolutise_url(self, relative_url):
"""
Convert a relative URL to an absolute URL by using the URL of this
GeminiItem as a base.
"""
# Absolutise URL, which annoyingly needs a valid scheme...
base_url = self.url.replace("gemini://", "https://")
absolute = urllib.parse.urljoin(base_url, relative_url)
absolute = absolute.replace("https://", "gemini://")
return absolute
def to_map_line(self, name=None):
if name or self.name:
return "=> {} {}".format(self.url, name or self.name)
else:
return "=> {}".format(self.url)
@classmethod
def from_map_line(cls, line, origin_gi):
print(line)
assert line.startswith("=>")
assert line[2:].strip()
bits = line[2:].strip().split(maxsplit=1)
bits[0] = origin_gi.absolutise_url(bits[0])
return cls(*bits)
CRLF = '\r\n'
# Cheap and cheerful URL detector # Cheap and cheerful URL detector
def looks_like_url(word): def looks_like_url(word):
return "." in word and word.startswith("gemini://") return "." in word and word.startswith("gemini://")
# Decorators # GeminiClient Decorators
def needs_gi(inner): def needs_gi(inner):
def outer(self, *args, **kwargs): def outer(self, *args, **kwargs):
if not self.gi: if not self.gi:
@ -256,8 +278,8 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
print("User input not supported.") print("User input not supported.")
# Redirects # Redirects
elif status.startswith("3"): elif status.startswith("3"):
self._debug("Following redirect to %s." % mime) new_gi = GeminiItem(gi.absolutise_url(mime))
new_gi = GeminiItem(gi.host, gi.port, mime, None) self._debug("Following redirect to %s." % new_gi.url)
self._go_to_gi(new_gi) self._go_to_gi(new_gi)
return return
# Errors # Errors
@ -362,9 +384,8 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
# knowledge of earlier failures. # knowledge of earlier failures.
raise err raise err
# Send request and wrap response in a file descriptor # Send request and wrap response in a file descriptor
url = geminiitem_to_url(gi) self._debug("Sending %s<CRLF>" % gi.url)
self._debug("Sending %s<CRLF>" % url) s.sendall((gi.url + CRLF).encode("UTF-8"))
s.sendall((url + CRLF).encode("UTF-8"))
return address, s.makefile(mode = "rb") return address, s.makefile(mode = "rb")
def _get_addresses(self, host, port): def _get_addresses(self, host, port):
@ -414,7 +435,7 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
for line in body.splitlines(): for line in body.splitlines():
if line.startswith("=>"): if line.startswith("=>"):
try: try:
gi = geminiitem_from_line(line, menu_gi) gi = GeminiItem.from_map_line(line, menu_gi)
self.index.append(gi) self.index.append(gi)
tmpf.write(self._format_geminiitem(len(self.index), gi) + "\n") tmpf.write(self._format_geminiitem(len(self.index), gi) + "\n")
except: except:
@ -431,16 +452,9 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
subprocess.call(shlex.split(cmd_str % self.idx_filename)) subprocess.call(shlex.split(cmd_str % self.idx_filename))
def _format_geminiitem(self, index, gi, url=False): def _format_geminiitem(self, index, gi, url=False):
line = "[%d] " % index line = "[%d] %s" % (index, gi.name or gi.url)
# Add item name
if gi.name:
line += gi.name
# Use URL in place of name if we didn't get here from a menu
else:
line += geminiitem_to_url(gi)
# Add URL if requested
if gi.name and url: if gi.name and url:
line += " (%s)" % geminiitem_to_url(gi) line += " (%s)" % gi.url
return line return line
def _show_lookup(self, offset=0, end=None, url=False): def _show_lookup(self, offset=0, end=None, url=False):
@ -590,9 +604,7 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
self._go_to_gi(gi) self._go_to_gi(gi)
# If this isn't a mark, treat it as a URL # If this isn't a mark, treat it as a URL
else: else:
url = line self._go_to_gi(GeminiItem(line))
gi = url_to_geminiitem(url)
self._go_to_gi(gi)
@needs_gi @needs_gi
def do_reload(self, *args): def do_reload(self, *args):
@ -602,11 +614,7 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
@needs_gi @needs_gi
def do_up(self, *args): def do_up(self, *args):
"""Go up one directory in the path.""" """Go up one directory in the path."""
gi = self.gi self._go_to_gi(GeminiItem(self.gi.up_url()))
pathbits = os.path.split(self.gi.path)
new_path = os.path.join(*pathbits[0:-1])
new_gi = GeminiItem(gi.host, gi.port, new_path, "1", gi.name)
self._go_to_gi(new_gi)
def do_back(self, *args): def do_back(self, *args):
"""Go back to the previous gemini item.""" """Go back to the previous gemini item."""
@ -636,9 +644,7 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
@needs_gi @needs_gi
def do_root(self, *args): def do_root(self, *args):
"""Go to root selector of the server hosting current item.""" """Go to root selector of the server hosting current item."""
gi = GeminiItem(self.gi.scheme, self.gi.host, self.gi.port, "", self._go_to_gi(GeminiItem(self.gi.root_url()))
"Root of %s" % self.gi.host)
self._go_to_gi(gi)
def do_tour(self, line): def do_tour(self, line):
"""Add index items as waypoints on a tour, which is basically a FIFO """Add index items as waypoints on a tour, which is basically a FIFO
@ -665,7 +671,7 @@ Current tour can be listed with `tour ls` and scrubbed with `tour clear`."""
elif line == "*": elif line == "*":
self.waypoints.extend(self.lookup) self.waypoints.extend(self.lookup)
elif looks_like_url(line): elif looks_like_url(line):
self.waypoints.append(url_to_geminiitem(line)) self.waypoints.append(GeminiItem(line))
else: else:
for index in line.split(): for index in line.split():
try: try:
@ -696,7 +702,7 @@ Think of it like marks in vi: 'mark a'='ma' and 'go a'=''a'."""
line = line.strip() line = line.strip()
if not line: if not line:
for mark, gi in self.marks.items(): for mark, gi in self.marks.items():
print("[%s] %s (%s)" % (mark, gi.name, geminiitem_to_url(gi))) print("[%s] %s (%s)" % (mark, gi.name, gi.url))
elif line.isalpha() and len(line) == 1: elif line.isalpha() and len(line) == 1:
self.marks[line] = self.gi self.marks[line] = self.gi
else: else:
@ -838,7 +844,7 @@ Use 'ls -l' to see URLs."""
@needs_gi @needs_gi
def do_url(self, *args): def do_url(self, *args):
"""Print URL of most recently visited item.""" """Print URL of most recently visited item."""
print(geminiitem_to_url(self.gi)) print(self.gi.url)
### Bookmarking stuff ### Bookmarking stuff
@needs_gi @needs_gi
@ -847,7 +853,7 @@ Use 'ls -l' to see URLs."""
Bookmarks are stored in the ~/.av98-bookmarks.txt file. Bookmarks are stored in the ~/.av98-bookmarks.txt file.
Optionally, specify the new name for the bookmark.""" Optionally, specify the new name for the bookmark."""
with open(os.path.expanduser("~/.av98-bookmarks.txt"), "a") as fp: with open(os.path.expanduser("~/.av98-bookmarks.txt"), "a") as fp:
fp.write(geminiitem_to_line(self.gi, name=line)) fp.write(self.gi.to_map_line(line))
def do_bookmarks(self, *args): def do_bookmarks(self, *args):
"""Show the current bookmarks menu. """Show the current bookmarks menu.