summaryrefslogtreecommitdiff
path: root/browser.py
diff options
context:
space:
mode:
Diffstat (limited to 'browser.py')
-rw-r--r--browser.py220
1 files changed, 220 insertions, 0 deletions
diff --git a/browser.py b/browser.py
new file mode 100644
index 0000000..9d1d421
--- /dev/null
+++ b/browser.py
@@ -0,0 +1,220 @@
+import sys
+import socket
+import ssl
+
+from PySide2 import QtCore, QtWidgets
+
+def htmlescape(text):
+ return text.replace('<', '&lt;').replace('>', '&gt;')
+
+def gem2html(gem):
+ html = []
+ state = 'text'
+ blanklines = 0
+ for line in gem.split('\n'):
+ if line.startswith('```'):
+ if state == 'pre':
+ newstate = 'text'
+ blanklines = 0
+ else:
+ newstate = 'pre'
+ elif state == 'pre':
+ newstate = 'pre'
+ elif line.startswith('=>'):
+ newstate = 'links'
+ elif line.startswith('* '):
+ newstate = 'list'
+ else:
+ newstate = 'text'
+
+ if state != 'pre':
+ if len(line.strip()) == 0:
+ blanklines += 1
+ if blanklines > 1:
+ html.append('<br/>')
+ continue
+ blanklines = 0
+
+ if state != newstate:
+ if state in ('links', 'list'):
+ html.append('</ul>')
+ elif state == 'pre':
+ html.append('</pre>')
+
+ if newstate in ('links', 'list'):
+ html.append('<ul>')
+ elif newstate == 'pre':
+ html.append('<pre>')
+ state = newstate
+
+ if line.startswith('```'):
+ pass
+ elif state == 'links':
+ tokens = line.split(None, 2)
+ if len(tokens) == 3:
+ _, url, text = tokens
+ html.append('<li><a href="{url}">{text}</a></li>'.format(url=url, text=text))
+ else:
+ _, url = tokens
+ html.append('<li><a href="{url}">{url}</a></li>'.format(url=url))
+ elif state == 'list':
+ html.append('<li>{}</li>'.format(line[2:]))
+ elif state == 'pre':
+ html.append(line)
+ else:
+ if line.startswith('###'):
+ html.append('<h3>{}</h3>'.format(line[3:].lstrip()))
+ elif line.startswith('##'):
+ html.append('<h2>{}</h2>'.format(line[2:].lstrip()))
+ elif line.startswith('#'):
+ html.append('<h1>{}</h1>'.format(line[1:].lstrip()))
+ else:
+ html.append('<p>{}</p>'.format(htmlescape(line)))
+ return '\n'.join(html)
+
+def absolute_url(base, url):
+ """
+ modifies `url` in place
+ """
+ if not url.scheme():
+ url.setScheme(base.scheme())
+ if not url.host():
+ url.setHost(base.host())
+ if not url.path().startswith('/'):
+ url.setPath(base.path().rsplit('/', 1)[0] + '/' + url.path())
+ if url.port() == -1:
+ url.setPort(1965)
+ return url
+
+def gem_get(url):
+ if len(url.path()) == 0:
+ url.setPath('/')
+ return {
+ 'status': '32',
+ 'meta': url.toDisplayString(),
+ }
+ context = ssl.create_default_context()
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ with socket.create_connection((url.host(), url.port())) as sock:
+ with context.wrap_socket(sock, server_hostname=url.host()) as ssock:
+ ssock.sendall('gemini://{}{}\r\n'.format(url.host(), url.path()).encode('utf8'))
+ fp = ssock.makefile(mode='rb')
+ header = fp.readline(1027)
+ status, meta = header.decode('utf8').split(None, 1)
+ if status[0] != '2':
+ return {
+ 'status': status,
+ 'meta': meta.strip(),
+ }
+ body = fp.read()
+ return {
+ 'status': status,
+ 'meta': meta.strip(),
+ 'body': body.decode('utf8'),
+}
+
+class GViewport(QtWidgets.QTextBrowser):
+
+ hoverUrlChanged = QtCore.Signal(str)
+
+ def __init__(self, address_bar):
+ self._current_url = None
+ self._last_redirect = (QtCore.QUrl(), {})
+ self.address_bar = address_bar
+ self._hover_url = None
+ QtWidgets.QTextBrowser.__init__(self)
+
+ def mouseMoveEvent(self, event):
+ cur = self.cursorForPosition(event.localPos().toPoint())
+ hover_url = cur.charFormat().anchorHref()
+ hover_url = absolute_url(self._current_url, QtCore.QUrl(hover_url))
+ if hover_url != self._hover_url:
+ print(hover_url)
+ self._hover_url = hover_url
+ self.hoverUrlChanged.emit(self._hover_url.toString())
+ return super().mouseMoveEvent(event)
+
+ def loadResource(self, type_, url):
+ if self._last_redirect[0].toString() == url.toString():
+ gem = self._last_redirect[1]
+ else:
+ if not url.scheme():
+ url.setScheme(self._current_url.scheme())
+ if not url.host():
+ url.setHost(self._current_url.host())
+ if url.port() == -1:
+ url.setPort(1965)
+ if not url.path().startswith('/'):
+ url.setPath(self._current_url.path().rsplit('/', 1)[0] + '/' + url.path())
+ gem = gem_get(absolute_url(self._current_url, url))
+ if 'body' in gem:
+ html = gem2html(gem['body'])
+ else:
+ html = '<h1>{} {}</h1>'.format(gem['status'], gem['meta'])
+ self._current_url = url
+ return html
+
+ def setSource(self, url):
+ if url.scheme() != 'gemini':
+ return
+ gem = gem_get(absolute_url(self._current_url, url))
+ while gem['status'][0] == '3':
+ url = QtCore.QUrl(gem['meta'])
+ if url.port() == 1965:
+ url.setPort(-1)
+ print('redirect: {}'.format(url))
+ gem = gem_get(absolute_url(self._current_url, url))
+ self._last_redirect = (url, gem)
+ if url.port() == 1965:
+ url.setPort(-1)
+ print('setSource: {}'.format(url))
+ return super().setSource(url)
+
+ def setRawSource(self):
+ return self.setSource(QtCore.QUrl(self.address_bar.text()))
+
+class GUrlBar(QtWidgets.QLineEdit):
+
+ def __init__(self):
+ QtWidgets.QLineEdit.__init__(self)
+
+ def setUrl(self, url):
+ return self.setText(url.toDisplayString())
+
+class GBrowser(QtWidgets.QMainWindow):
+
+ def __init__(self):
+ super(GBrowser, self).__init__()
+ self.initUI()
+
+ def initUI(self):
+ self.statusBar().showMessage('Ready')
+
+ back = QtWidgets.QPushButton("back")
+ forward = QtWidgets.QPushButton("forward")
+ address = GUrlBar()
+ toolbar = QtWidgets.QToolBar()
+ toolbar.addWidget(back)
+ toolbar.addWidget(forward)
+ toolbar.addWidget(address)
+ browser = GViewport(address)
+
+ back.clicked.connect(browser.backward)
+ forward.clicked.connect(browser.forward)
+ browser.sourceChanged.connect(address.setUrl)
+ address.returnPressed.connect(browser.setRawSource)
+ browser.hoverUrlChanged.connect(self.statusBar().showMessage)
+
+ self.addToolBar(toolbar)
+ self.setCentralWidget(browser)
+
+ browser.setSource(QtCore.QUrl('gemini://gemini.circumlunar.space/'))
+
+ self.setGeometry(10, 10, 1024, 750)
+ self.setWindowTitle('Gemini Browser')
+ self.show()
+
+app = QtWidgets.QApplication(sys.argv)
+ex = GBrowser()
+sys.exit(app.exec_())