Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gemini support. #2024

Merged
merged 18 commits into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -537,4 +537,8 @@
[submodule "_build/cl-gopher"]
path = _build/cl-gopher
url = https://github.com/knusbaum/cl-gopher
shallow = true
[submodule "_build/phos"]
path = _build/phos
url = https://github.com/omar-polo/phos
shallow = true
1 change: 1 addition & 0 deletions _build/phos
Submodule phos added at d9b03c
1 change: 1 addition & 0 deletions build-scripts/nyxt.scm
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ WebKit browsing engine.")
("named-readtables" ,cl-named-readtables)
;; ("osicat" ,cl-osicat) ; Not needed for SBCL.
("parenscript" ,cl-parenscript)
("phos" ,cl-phos)
("plump" ,cl-plump)
("clss" ,cl-clss)
("quri" ,cl-quri)
Expand Down
3 changes: 2 additions & 1 deletion nyxt.asd
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ A naive benchmark on a 16 Mpbs bandwidth gives us
quri
serapeum
str
phos
plump
clss
spinneret
Expand Down Expand Up @@ -144,7 +145,7 @@ A naive benchmark on a 16 Mpbs bandwidth gives us
(:file "mode/list-history")
(:file "mode/bookmark-frequent-visits")
(:file "mode/web")
(:file "mode/gopher")
(:file "mode/small-web")
aartaka marked this conversation as resolved.
Show resolved Hide resolved
(:file "mode/reading-line")
(:file "mode/style")
(:file "mode/certificate-exception")
Expand Down
198 changes: 97 additions & 101 deletions source/mode/gopher.lisp → source/mode/small-web.lisp
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause

(uiop:define-package :nyxt/gopher-mode
(uiop:define-package :nyxt/small-web-mode
(:use :common-lisp :nyxt)
(:import-from #:class-star #:define-class)
(:import-from #:keymap #:define-key #:define-scheme)
(:import-from #:serapeum #:-> #:export-always)
(:documentation "Mode for Gopher page interaction."))
(in-package :nyxt/gopher-mode)
(:documentation "Mode for Gopher/Gemini page interaction."))
(in-package :nyxt/small-web-mode)
(use-nyxt-package-nicknames)

(defun update (mode)
(let ((url (url (buffer mode))))
(setf (line mode) (cl-gopher:parse-gopher-uri (render-url url))
(contents mode) (cl-gopher:get-line-contents (line mode)))))

(define-mode gopher-mode ()
"Gopher page interaction mode.
(run-thread "small-web-mode update thread"
(setf (url mode) url
(model mode) (str:string-case (quri:uri-scheme url)
("gopher" (cl-gopher:get-line-contents
(cl-gopher:parse-gopher-uri (render-url url))))
("gemini" (multiple-value-bind (status meta body)
(gemini:request url)
(if (and (eq :success status)
(str:starts-with-p "text/gemini" meta)
(stringp body))
(gemtext:parse-string body)
body))))))))

(define-mode small-web-mode ()
"Gopher/Gemini page interaction mode.

Renders gopher elements (provided by `cl-gopher') to human-readable HTML.

Expand All @@ -37,17 +47,17 @@ loading, you'd need to override `line->html' in the following way:

\(defmethod line->html ((line cl-gopher:image)) (image->link line))
\(defmethod line->html ((line cl-gopher:gif)) (image->html line))
\(defmethod line->html ((line cl-gopher:png)) (image->link line))"
\(defmethod line->html ((line cl-gopher:png)) (image->link line))

Gemini support is a bit more chaotic, but you can override `line->html' for
`phos/gemtext' elements too."
((rememberable-p nil)
(line :documentation "The line being opened.")
(contents :documentation "The contents of the current page.")
(search-engines
'()
:type list
:reader nil
:writer t
:documentation "A list of Gopher search-engines to use when doing `search-gopher'.
Create those with `make-gopher-search-engine'.")
(url :documentation "The URL being opened.")
(model :documentation "The contents of the current page.")
(redirections nil :documentation "The list of redirection Gemini URLs.")
(allowed-redirections-count
5
:documentation "The number of redirections that Gemini resources are allowed to make.")
(style (theme:themed-css (nyxt::theme *browser*)
(body
:background-color theme:background)
Expand All @@ -69,7 +79,7 @@ Create those with `make-gopher-search-engine'.")
(lambda (mode)
(hooks:remove-hook
(pre-request-hook (buffer mode))
'gopher-mode-disable)))
'small-web-mode-disable)))
(constructor
(lambda (mode)
(update mode)
Expand All @@ -78,81 +88,22 @@ Create those with `make-gopher-search-engine'.")
(make-handler-resource
(lambda (request-data)
(when (nyxt/auto-mode::new-page-request-p request-data)
(if (string= "gopher" (quri:uri-scheme (url request-data)))
(if (str:s-member '("gopher" "gemini")
(quri:uri-scheme (url request-data)))
(update mode)
(disable-modes '(gopher-mode) (buffer mode))))
(disable-modes '(small-web-mode) (buffer mode))))
request-data)
:name 'gopher-mode-disable))))))

(defmethod search-engines ((mode gopher-mode))
(mapcar (lambda (engine)
(typecase engine
(string (cl-gopher:parse-gopher-uri engine))
(cl-gopher:search-line engine)))
(slot-value mode 'search-engines)))

(defmethod prompter:object-attributes ((line cl-gopher:search-line))
`(("Terms" ,(or (cl-gopher:terms line) ""))
("Name" ,(cl-gopher:display-string line))))

(define-class gopher-search-source (prompter:source)
((prompter:name "Term Search")
(prompter:constructor (let ((mode (current-mode 'gopher)))
(union (search-engines mode)
(sera:filter (alex:rcurry #'typep 'cl-gopher:search-line)
(cl-gopher:lines (contents mode)))
:test #'string= :key #'cl-gopher:uri-for-gopher-line)))
(prompter:multi-selection-p t)
(prompter:filter-preprocessor
(lambda (suggestions source input)
(declare (ignore source))
(mapcar (lambda (suggestion)
(let ((value (cl-gopher:copy-gopher-line (prompter:value suggestion))))
(setf (cl-gopher:terms value) input)
(prompter:make-suggestion value)))
suggestions)))
(prompter:actions (list (make-command search-gopher* (lines)
(buffer-load (cl-gopher:uri-for-gopher-line (first lines)))
(dolist (line (rest lines))
(make-buffer
:url (cl-gopher:uri-for-gopher-line line)
:parent-buffer (current-buffer))))
(make-command search-gopher-new-buffer* (lines)
(dolist (line lines)
(make-buffer
:url (cl-gopher:uri-for-gopher-line line)
:parent-buffer (current-buffer))))
(make-command save-search-engine* (lines)
(nyxt::configure-slot
'search-engines 'gopher-mode
:value `(append %slot-default%
(list
,@(mapcar (lambda (line)
;; FIXME: Maybe save the query, actually?
(setf (cl-gopher:terms line) "")
`(make-gopher-search-engine
,(cl-gopher:uri-for-gopher-line line)
,(cl-gopher:display-string line)))
lines)))))))))

(export-always 'make-gopher-search-engine)
(defun make-gopher-search-engine (url name)
(let ((line (cl-gopher:parse-gopher-uri url)))
(setf (cl-gopher:display-string line) name)
line))

(define-command search-gopher ()
"Prompt for terms and search those in current page and saved search engines."
(prompt :prompt "Search Gopher for"
:sources (list (make-instance 'gopher-search-source))))
:name 'small-web-mode-disable))))))

;;; Gopher rendering.

(export-always 'line->html)
(defgeneric line->html (line)
(:documentation "Transform a gopher line to a reasonable HTML representation."))

(export-always 'render)
(defgeneric render (line &optional mode)
(:documentation "Produce a page content string/array given LINE.
(export-always 'gopher-render)
(defgeneric gopher-render (line &optional mode)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why prefixing with "gopher"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there's also gemini-render :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we have 2 methods specializations here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, no way -- gemini page body is a string (or a list of gemini objects if we parse it before rendering), while gopher rendering is line-oriented. Specializing against string doesn't feel right, so I'd better stay with two distinctly named functions.

Let me move the body of gemini-render into process-gemini-scheme in source/renderer/gtk.lisp. It will be a bit more coherent this way.

However, I'd leave gopher-render as it is, as it's Gopher-specific (like search-gopher) while everything else in small-web.lisp isn't.

(:documentation "Produce a Gopher page content string/array given LINE.
Second return value should be the MIME-type of the content."))

(defmethod line->html ((line cl-gopher:gopher-line))
Expand Down Expand Up @@ -196,9 +147,9 @@ Second return value should be the MIME-type of the content."))

(defmethod line->html ((line cl-gopher:search-line))
(spinneret:with-html-string
(:button :class "button search"
:onclick (ps:ps (nyxt/ps:lisp-eval `(nyxt/gopher-mode:search-gopher)))
(:b "[SEARCH] ") (cl-gopher:display-string line))))
(:a :class "button search"
:href (cl-gopher:uri-for-gopher-line line)
(:b "[SEARCH] ") (cl-gopher:display-string line))))

(defmethod line->html ((line cl-gopher:html-file))
(spinneret:with-html-string
Expand Down Expand Up @@ -229,20 +180,21 @@ Second return value should be the MIME-type of the content."))
(defmethod line->html ((line cl-gopher:uuencoded-file)) (file-link->html line))
(defmethod line->html ((line cl-gopher:unknown)) (file-link->html line))

(defmethod render ((line cl-gopher:gopher-line) &optional (mode (current-mode 'gopher)))
(let ((contents (cl-gopher:get-line-contents line)))
(defmethod gopher-render ((line cl-gopher:gopher-line) &optional (mode (current-mode 'small-web)))
(let ((contents (cl-gopher:get-line-contents line))
(spinneret:*html-style* :tree))
(spinneret:with-html-string
(:style (style (buffer mode)))
(:style (style mode))
(loop for line in (cl-gopher:lines contents)
collect (:raw (line->html line))))))

(defmethod render ((line cl-gopher:html-file) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:html-file) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(let ((contents (cl-gopher:get-line-contents line)))
(cl-gopher:content-string contents)))

(defmethod render ((line cl-gopher:text-file) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:text-file) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(let ((contents (cl-gopher:get-line-contents line)))
(values (str:join +newline+ (cl-gopher:lines contents)) "text/plain")))
Expand All @@ -254,30 +206,74 @@ Second return value should be the MIME-type of the content."))
(contents (cl-gopher:get-line-contents line)))
(values (cl-gopher:content-array contents) mime)))

(defmethod render ((line cl-gopher:image) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:image) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(render-binary-content line))

(defmethod render ((line cl-gopher:binary-file) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:binary-file) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(render-binary-content line))

(defmethod render ((line cl-gopher:binhex-file) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:binhex-file) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(render-binary-content line))

(defmethod render ((line cl-gopher:dos-file) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:dos-file) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(render-binary-content line))

(defmethod render ((line cl-gopher:uuencoded-file) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:uuencoded-file) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(render-binary-content line))

(defmethod render ((line cl-gopher:gif) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:gif) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(render-binary-content line "image/gif"))

(defmethod render ((line cl-gopher:png) &optional (mode (current-mode 'gopher)))
(defmethod gopher-render ((line cl-gopher:png) &optional (mode (current-mode 'small-web)))
(declare (ignore mode))
(render-binary-content line "image/png"))

;;; Gemini rendering.

(defmethod line->html ((element gemtext:element))
(spinneret:with-html-string
(:pre (gemtext:text element))))

(defmethod line->html ((element gemtext:paragraph))
(spinneret:with-html-string
(:p (gemtext:text element))))

(defmethod line->html ((element gemtext:title))
(spinneret:with-html-string
(case (gemtext:level element)
(1 (:h1 (gemtext:text element)))
(2 (:h2 (gemtext:text element)))
(3 (:h3 (gemtext:text element))))))

;; TODO: We used to build <ul>-lists out of those. Should we?
(defmethod line->html ((element gemtext:item))
(spinneret:with-html-string
(:li (gemtext:text element))))

(defmethod line->html ((element gemtext:link))
(spinneret:with-html-string
(let* ((url (render-url (gemtext:url element)))
(path (quri:uri-path (gemtext:url element)))
(mime (unless (uiop:emptyp path)
(mimes:mime-lookup path)))
(text (cond
((not (uiop:emptyp (gemtext:text element)))
(gemtext:text element))
((not (uiop:emptyp url))
url)
(t "[LINK]"))))
(cond
((str:starts-with-p "image/" mime)
(:a :href url (:img :src url :alt text)))
((str:starts-with-p "audio/" mime)
(:audio :src url :controls t text))
((str:starts-with-p "video/" mime)
(:video :src url :controls t))
(t (:a :class "button" :href url text))))
(:br)))