;; - caching of visited sites,
;; - pleasant and configurable colouring of Gopher directories,
;; - direct visualisation of image files,
-;; - a simple bookmark management system,
;; - gopher connections using TLS encryption,
;; - the fledgling Gemini protocol,
;; - the greybeard Finger protocol.
(finger elpher-get-finger-page elpher-render-text "txt" elpher-text)
(telnet elpher-get-telnet-page nil "tel" elpher-telnet)
(other-url elpher-get-other-url-page nil "url" elpher-other-url)
- ((special bookmarks) elpher-get-bookmarks-page nil "/" elpher-index)
((special start) elpher-get-start-page nil))
"Association list from types to getters, renderers, margin codes and index faces.")
(defcustom elpher-open-urls-with-eww nil
"If non-nil, open URL selectors using eww.
-Otherwise, use the system browser via the BROWSE-URL function."
+Otherwise, use the system browser via the `browse-url' function."
:type '(boolean))
(defcustom elpher-use-header t
"Specify the string used for bullets when rendering gemini maps."
:type '(string))
-(defcustom elpher-bookmarks-file (locate-user-emacs-file "elpher-bookmarks")
- "Specify the name of the file where elpher bookmarks will be saved."
- :type '(file))
-
(defcustom elpher-ipv4-always nil
"If non-nil, elpher will always use IPv4 to establish network connections.
This can be useful when browsing from a computer that supports IPv6, because
"Create an ADDRESS object corresponding to the given special address symbol TYPE."
type)
+(defun elpher-make-start-page ()
+ "Create the start page."
+ (elpher-make-page "Elpher Start Page"
+ (elpher-make-special-address 'start)))
+
(defun elpher-address-to-url (address)
"Get string representation of ADDRESS, or nil if ADDRESS is special."
(if (elpher-address-special-p address)
(url-port address)))
(defun elpher-address-special-p (address)
- "Return non-nil if ADDRESS object is special (e.g. start page, bookmarks page)."
+ "Return non-nil if ADDRESS object is special (e.g. start page page)."
(symbolp address))
(defun elpher-address-gopher-p (address)
(error "Gemini server response unknown: %s %s"
response-code response-meta))))))
+(unless (fboundp 'read-answer)
+ (defun read-answer (question answers)
+ "Backfill for the new read-answer code."
+ (completing-read question (mapcar 'identity answers))))
+
(defun elpher-choose-client-certificate ()
"Prompt for a client certificate to use to establish a TLS connection."
(let* ((read-answer-short t))
(propertize text-line 'face 'elpher-gemini-quoted))
(t text-line))
text-line))
- (adaptive-fill-mode t))
+ (adaptive-fill-mode t)
+ ;; fill-prefix is important for adaptive-fill-mode: without
+ ;; it, multi-line list items are not indented correct
+ (fill-prefix (if (match-string 2 text-line)
+ (replace-regexp-in-string "[>\*]" " " (match-string 0 text-line))
+ nil)))
(insert (elpher-process-text-for-display processed-text-line))
(newline)))
" - i/I: info on item under cursor or current page\n"
" - c/C: copy URL representation of item under cursor or current page\n"
" - a/A: bookmark the item under cursor or current page\n"
- " - x/X: remove bookmark for item under cursor or current page\n"
- " - B: visit the bookmarks page\n"
+ " - B: list all bookmarks\n"
" - r: redraw current page (using cached contents if available)\n"
" - R: reload current page (regenerates cache)\n"
" - S: set character coding system for gopher (default is to autodetect)\n"
(elpher-make-gopher-address ?7 "/v2/vs" "gopher.floodgap.com" 70))
(elpher-insert-index-record "Gemini Search Engine (geminispace.info)"
(elpher-address-from-url "gemini://geminispace.info/search"))
- (insert "\n"
- "This page contains your bookmarked sites (also visit with B):\n")
- (elpher-insert-index-record "Your Bookmarks" 'bookmarks)
(insert "\n"
"For Elpher release news or to leave feedback, visit:\n")
(elpher-insert-index-record "The Elpher Project Page"
'face 'shadow))
(elpher-restore-pos)))
-;; Bookmarks page page retrieval
-
-(defun elpher-get-bookmarks-page (renderer)
- "Getter to load and display the current bookmark list (RENDERER must be nil)."
- (when renderer
- (elpher-visit-previous-page)
- (error "Command not supported for bookmarks page"))
- (elpher-with-clean-buffer
- (insert "---- Bookmark list ----\n\n")
- (let ((bookmarks (elpher-load-bookmarks)))
- (if bookmarks
- (dolist (bookmark bookmarks)
- (let ((display-string (elpher-bookmark-display-string bookmark))
- (address (elpher-address-from-url (elpher-bookmark-url bookmark))))
- (elpher-insert-index-record display-string address)))
- (insert "No bookmarks found.\n")))
- (insert "\n-----------------------\n"
- "\n"
- "- u: return to previous page\n"
- "- x: delete selected bookmark\n"
- "- a: rename selected bookmark\n"
- "\n"
- "Bookmarks are stored in the file ")
- (let ((filename elpher-bookmarks-file)
- (help-string "RET,mouse-1: Open bookmarks file in new buffer for editing."))
- (insert-text-button filename
- 'face 'link
- 'action (lambda (_)
- (interactive)
- (find-file filename))
- 'follow-link t
- 'help-echo help-string))
- (insert "\n")
- (elpher-restore-pos)))
+;;; Bookmarks
+;; This code allows Elpher to use the standard Emacs bookmarks: `C-x r
+;; m' to add a bookmark, `C-x r l' to list bookmarks (which is where
+;; you can anotate bookmarks!), `C-x r b' to jump to a bookmark, and
+;; so on. See the Bookmarks section in the Emacs info manual for more.
+
+(defvar elpher-bookmark-link nil
+ "Prefer bookmarking a link or the current page.
+Bind this variable dynamically, or set it to t.
+If you set it to t, the commands \\[bookmark-set-no-overwrite]
+and \\[elpher-set-bookmark-no-overwrite] do the same thing.")
+
+(defun elpher-bookmark-make-record ()
+ "Return a bookmark record.
+If `elpher-bookmark-link' is non-nil and point is on a link button,
+return a bookmark record for that link. Otherwise, return a bookmark
+record for the current elpher page."
+ (let* ((button (and elpher-bookmark-link (button-at (point))))
+ (page (if button
+ (button-get button 'elpher-page)
+ elpher-current-page))
+ (address (elpher-page-address page))
+ (url (elpher-address-to-url address))
+ (display-string (elpher-page-display-string page))
+ (pos (if button nil (point))))
+ (if (elpher-address-special-p address)
+ (error "Cannot bookmark %s" display-string)
+ `(,display-string
+ (defaults . (,display-string))
+ (position . ,pos)
+ (location . ,url)
+ (handler . elpher-bookmark-jump)))))
-;;; Bookmarks
-;;
+;;;###autoload
+(defun elpher-bookmark-jump (bookmark)
+ "Go to a particular BOOKMARK."
+ (let* ((url (cdr (assq 'location bookmark))))
+ (elpher-go url)))
-(defun elpher-make-bookmark (display-string url)
- "Make an elpher bookmark.
-DISPLAY-STRING determines how the bookmark will appear in the
-bookmark list, while URL is the url of the entry."
- (list display-string url))
-
-(defun elpher-bookmark-display-string (bookmark)
- "Get the display string of BOOKMARK."
- (elt bookmark 0))
-
-(defun elpher-set-bookmark-display-string (bookmark display-string)
- "Set the display string of BOOKMARK to DISPLAY-STRING."
- (setcar bookmark display-string))
-
-(defun elpher-bookmark-url (bookmark)
- "Get the address for BOOKMARK."
- (elt bookmark 1))
-
-(defun elpher-save-bookmarks (bookmarks)
- "Record the bookmark list BOOKMARKS to the user's bookmark file.
-Beware that this completely replaces the existing contents of the file."
- (let ((bookmark-dir (file-name-directory elpher-bookmarks-file)))
- (unless (file-directory-p bookmark-dir)
- (make-directory bookmark-dir)))
- (with-temp-file elpher-bookmarks-file
- (erase-buffer)
- (insert "; Elpher bookmarks file\n\n"
- "; Bookmarks are stored as a list of (label URL) items.\n"
- "; Feel free to edit by hand, but take care to ensure\n"
- "; the list structure remains intact.\n\n")
- (pp bookmarks (current-buffer))))
-
-(defun elpher-load-bookmarks ()
- "Get the list of bookmarks from the users's bookmark file."
- (let ((bookmarks
- (with-temp-buffer
- (ignore-errors
- (insert-file-contents elpher-bookmarks-file)
- (goto-char (point-min))
- (read (current-buffer))))))
- (if (and bookmarks (listp (cadar bookmarks)))
- (progn
- (message "Reading old bookmark file. (Will be updated on write.)")
- (mapcar (lambda (old-bm)
- (list (car old-bm)
- (elpher-address-to-url (apply #'elpher-make-gopher-address
- (cadr old-bm)))))
- bookmarks))
- bookmarks)))
-
-(defun elpher-add-address-bookmark (address display-string)
- "Save a bookmark for ADDRESS with label DISPLAY-STRING.)))
-If ADDRESS is already bookmarked, update the label only."
- (let ((bookmarks (elpher-load-bookmarks))
- (url (elpher-address-to-url address)))
- (let ((existing-bookmark (rassoc (list url) bookmarks)))
- (if existing-bookmark
- (elpher-set-bookmark-display-string existing-bookmark display-string)
- (push (elpher-make-bookmark display-string url) bookmarks)))
- (elpher-save-bookmarks bookmarks)))
-
-(defun elpher-remove-address-bookmark (address)
- "Remove any bookmark to ADDRESS."
- (let ((url (elpher-address-to-url address)))
- (elpher-save-bookmarks
- (seq-filter (lambda (bookmark)
- (not (equal (elpher-bookmark-url bookmark) url)))
- (elpher-load-bookmarks)))))
+(defun elpher-set-bookmark-no-overwrite ()
+ "Bookmark the link at point.
+To bookmark the current page, use \\[bookmark-set-no-overwrite]."
+ (interactive)
+ (let ((elpher-bookmark-link t))
+ (bookmark-set-no-overwrite)))
+
+(defun elpher-bookmark-import (file)
+ "Import Elpher bookmarks into Emacs bookmarks."
+ (interactive (list (if (and (boundp 'elpher-bookmarks-file)
+ (file-readable-p elpher-bookmarks-file))
+ elpher-bookmarks-file
+ (read-file-name "Old Elpher bookmarks: "
+ user-emacs-directory nil t
+ "elpher-bookmarks"))))
+ (require 'bookmark)
+ (dolist (bookmark (with-temp-buffer
+ (insert-file-contents file)
+ (read (current-buffer))))
+ (let* ((display-string (car bookmark))
+ (url (cadr bookmark))
+ (record `(,display-string
+ (location . ,url)
+ (handler . elpher-bookmark-jump))))
+ (bookmark-store display-string (cdr record) t)))
+ (bookmark-save))
;;; Integrations
;;
+;;; Org
+
+;; Avoid byte compilation warnings.
+(eval-when-compile
+ (declare-function org-link-store-props "ol")
+ (declare-function org-link-set-parameters "ol"))
+
(defun elpher-org-export-link (link description format protocol)
"Export a LINK with DESCRIPTION for the given PROTOCOL and FORMAT.
url
(format "%s (%s)" desc url))))))
-;; Avoid byte compilation warnings.
-(declare-function org-link-store-props "ol")
(defun elpher-org-store-link ()
"Store link to an `elpher' page in Org."
(when (eq major-mode 'elpher-mode)
(defun elpher-org-follow-link (link protocol)
"Visit a LINK for the given PROTOCOL.
-PROTOCOL may be one of gemini, gopher or finger. This method also support old
-paramter elpher, where link is self-contained."
+PROTOCOL may be one of gemini, gopher or finger. This method also
+supports the old protocol elpher, where the link is self-contained."
(let ((url (if (equal protocol "elpher")
(string-remove-prefix "elpher:" link)
(format "%s:%s" protocol link))))
(elpher-go url)))
-;; Avoid byte compilation warnings.
-(declare-function org-link-set-parameters "ol")
(with-eval-after-load 'org
(org-link-set-parameters
"elpher"
(elpher-org-export-link link description format "finger"))
:follow (lambda (link _arg) (elpher-org-follow-link link "finger"))))
+;;; Browse URL
+
+;; Avoid byte compilation warnings.
+(eval-when-compile
+ (defvar thing-at-point-uri-schemes))
+
;;;###autoload
(defun elpher-browse-url-elpher (url &rest _args)
"Browse URL using Elpher. This function is used by `browse-url'."
(elpher-go url))
;; Use elpher to open gopher, finger and gemini links
-(with-eval-after-load 'browse-url
- ;; For recent version of `browse-url' package
- (if (boundp 'browse-url-default-handlers)
- (add-to-list
- 'browse-url-default-handlers
- '("^\\(gopher\\|finger\\|gemini\\)://" . elpher-browse-url-elpher))
- ;; Patch browse-url-default-browser for older ones
- (advice-add 'browse-url-default-browser :before-while
- (lambda (url &rest _args)
- (let ((scheme (downcase (car (split-string url ":" t)))))
- (if (member scheme '("gemini" "gopher" "finger"))
- ;; `elpher-go' always returns nil, which will stop the
- ;; advice chain here in a before-while
- (elpher-go url)
- ;; chain must continue, then return t.
- t))))))
-
-;; Avoid byte compilation warnings.
-(eval-when-compile
- (defvar thing-at-point-uri-schemes)
- (defvar mu4e~view-beginning-of-url-regexp))
+;; For recent version of `browse-url' package
+(if (boundp 'browse-url-default-handlers)
+ (add-to-list
+ 'browse-url-default-handlers
+ '("^\\(gopher\\|finger\\|gemini\\)://" . elpher-browse-url-elpher))
+ ;; Patch `browse-url-browser-function' for older ones. The value of
+ ;; that variable is `browse-url-default-browser' by default, so
+ ;; that's the function that gets advised.
+ (advice-add browse-url-browser-function :before-while
+ (lambda (url &rest _args)
+ "Handle gemini, gopher, and finger schemes using Elpher."
+ (let ((scheme (downcase (car (split-string url ":" t)))))
+ (if (member scheme '("gemini" "gopher" "finger"))
+ ;; `elpher-go' always returns nil, which will stop the
+ ;; advice chain here in a before-while
+ (elpher-go url)
+ ;; chain must continue, then return t.
+ t)))))
;; Register "gemini://" as a URI scheme so `browse-url' does the right thing
(with-eval-after-load 'thingatpt
(add-to-list 'thing-at-point-uri-schemes "gemini://"))
+;;; Mu4e:
+
+(eval-when-compile
+ (defvar mu4e~view-beginning-of-url-regexp))
+
(with-eval-after-load 'mu4e-view
;; Make mu4e aware of the gemini world
(setq mu4e~view-beginning-of-url-regexp
(address (elpher-address-from-url cleaned-host-or-url))
(page (elpher-make-page cleaned-host-or-url address)))
(switch-to-buffer elpher-buffer-name)
- (elpher-visit-page page)
+ (elpher-with-clean-buffer
+ (elpher-visit-page page))
nil))
(defun elpher-go-current ()
(interactive)
(setq-local elpher-current-page nil)
(setq-local elpher-history nil)
- (let ((start-page (elpher-make-page "Elpher Start Page"
- (elpher-make-special-address 'start))))
- (elpher-visit-page start-page)))
+ (elpher-visit-page (elpher-make-start-page)))
(defun elpher-download ()
"Download the link at point."
(elpher-go (elpher-address-to-url address-copy))))
(error "Command invalid for %s" (elpher-page-display-string elpher-current-page)))))
-(defun elpher-bookmarks-current-p ()
- "Return non-nil if current page is a bookmarks page."
- (equal (elpher-address-type (elpher-page-address elpher-current-page))
- '(special bookmarks)))
-
-(defun elpher-reload-bookmarks ()
- "Reload bookmarks if current page is a bookmarks page."
- (if (elpher-bookmarks-current-p)
- (elpher-reload-current-page)))
-
-(defun elpher-bookmark-current ()
- "Bookmark the current page."
- (interactive)
- (let ((address (elpher-page-address elpher-current-page))
- (display-string (elpher-page-display-string elpher-current-page)))
- (if (not (elpher-address-special-p address))
- (let ((bookmark-display-string (read-string "Bookmark display string: "
- display-string)))
- (elpher-add-address-bookmark address bookmark-display-string)
- (message "Bookmark added."))
- (error "Cannot bookmark %s" display-string))))
-
-(defun elpher-bookmark-link ()
- "Bookmark the link at point."
- (interactive)
- (let ((button (button-at (point))))
- (if button
- (let* ((page (button-get button 'elpher-page))
- (address (elpher-page-address page))
- (display-string (elpher-page-display-string page)))
- (if (not (elpher-address-special-p address))
- (let ((bookmark-display-string (read-string "Bookmark display string: "
- display-string)))
- (elpher-add-address-bookmark address bookmark-display-string)
- (elpher-reload-bookmarks)
- (message "Bookmark added."))
- (error "Cannot bookmark %s" display-string)))
- (error "No link selected"))))
-
-(defun elpher-unbookmark-current ()
- "Remove bookmark for the current page."
- (interactive)
- (let ((address (elpher-page-address elpher-current-page)))
- (when (and (not (elpher-address-special-p address))
- (y-or-n-p "Really remove bookmark for the current page? "))
- (elpher-remove-address-bookmark address)
- (message "Bookmark removed."))))
-
-(defun elpher-unbookmark-link ()
- "Remove bookmark for the link at point."
- (interactive)
- (let ((button (button-at (point))))
- (if button
- (when (y-or-n-p "Really remove bookmark for this link? ")
- (let ((page (button-get button 'elpher-page)))
- (elpher-remove-address-bookmark (elpher-page-address page))
- (elpher-reload-bookmarks)
- (message "Bookmark removed.")))
- (error "No link selected"))))
-
-;;;###autoload
-(defun elpher-bookmarks ()
- "Visit bookmarks page."
- (interactive)
- (switch-to-buffer elpher-buffer-name)
- (elpher-visit-page
- (elpher-make-page "Bookmarks Page" (elpher-make-special-address 'bookmarks))))
-
(defun elpher-info-page (page)
"Display information on PAGE."
(let ((display-string (elpher-page-display-string page))
(define-key map (kbd "I") 'elpher-info-current)
(define-key map (kbd "c") 'elpher-copy-link-url)
(define-key map (kbd "C") 'elpher-copy-current-url)
- (define-key map (kbd "a") 'elpher-bookmark-link)
- (define-key map (kbd "A") 'elpher-bookmark-current)
- (define-key map (kbd "x") 'elpher-unbookmark-link)
- (define-key map (kbd "X") 'elpher-unbookmark-current)
- (define-key map (kbd "B") 'elpher-bookmarks)
+ (define-key map (kbd "a") 'elpher-set-bookmark-no-overwrite)
+ (define-key map (kbd "A") 'bookmark-set-no-overwrite)
+ (define-key map (kbd "B") 'bookmark-bmenu-list)
(define-key map (kbd "S") 'elpher-set-gopher-coding-system)
(define-key map (kbd "F") 'elpher-forget-current-certificate)
(define-key map (kbd "v") 'elpher-visit-gemini-numbered-link)
(kbd "I") 'elpher-info-current
(kbd "c") 'elpher-copy-link-url
(kbd "C") 'elpher-copy-current-url
- (kbd "a") 'elpher-bookmark-link
- (kbd "A") 'elpher-bookmark-current
- (kbd "x") 'elpher-unbookmark-link
- (kbd "X") 'elpher-unbookmark-current
- (kbd "B") 'elpher-bookmarks
+ (kbd "a") 'elpher-set-bookmark-no-overwrite
+ (kbd "A") 'bookmark-set-no-overwrite
+ (kbd "B") 'bookmark-bmenu-list
(kbd "S") 'elpher-set-gopher-coding-system
(kbd "F") 'elpher-forget-current-certificate
(kbd "v") 'elpher-visit-gemini-numbered-link))
"Major mode for elpher, an elisp gopher client.
This mode is automatically enabled by the interactive
-functions which initialize the gopher client, namely
-`elpher', `elpher-go' and `elpher-bookmarks'."
+functions which initialize the client, namely
+`elpher', and `elpher-go'."
(setq-local elpher--gemini-page-headings nil)
(setq-local elpher-current-page nil)
(setq-local elpher-history nil)
(setq-local elpher-buffer-name (buffer-name))
-
- (setq-local imenu-create-index-function
- (lambda ()
- elpher--gemini-page-headings)))
+ (setq-local bookmark-make-record-function #'elpher-bookmark-make-record)
+ (setq-local imenu-create-index-function #'elpher--gemini-page-headings))
(when (fboundp 'evil-set-initial-state)
(evil-set-initial-state 'elpher-mode 'motion))
(pop-to-buffer-same-window buf)
(unless (buffer-modified-p)
(elpher-mode)
- (let ((start-page (elpher-make-page
- "Elpher Start Page"
- (elpher-make-special-address 'start))))
- (elpher-visit-page start-page))
+ (elpher-visit-page (elpher-make-start-page))
"Started Elpher."))); Otherwise (elpher) evaluates to start page string.
;;; elpher.el ends here