;; Copyright (C) 2020 Koushk Roy <kroy@twilio.com>
;; Copyright (C) 2020 Vee <vee@vnsf.xyz>
;; Copyright (C) 2020 Simon South <simon@simonsouth.net>
-;; Copyright (C) 2019-2020 Tim Vaughan <plugd@thelambdalab.xyz>
+;; Copyright (C) 2019-2021 Tim Vaughan <plugd@thelambdalab.xyz>
;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
;; Created: 11 April 2019
-;; Version: 2.11.0
+;; Version: 3.0.0
;; Keywords: comm gopher
-;; Homepage: https://alexschroeder.ch/cgit/elpher
+;; Homepage: https://thelambdalab.xyz/elpher
;; Package-Requires: ((emacs "27.1"))
;; This file is not part of GNU Emacs.
;; - 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.
;; Elpher is under active development. Any suggestions for
;; improvements are welcome, and can be made on the official
-;; project page, https://alexschroeder.ch/cgit/elpher.
+;; project page, gopher://thelambdalab.xyz/elpher, or via the
+;; project mailing list at https://lists.sr.ht/~michel-slm/elpher.
;;; Code:
(defalias 'elpher-color-filter-apply
(if (fboundp 'xterm-color-filter)
(lambda (s)
- (let ((xterm-color-render nil))
+ (let ((_xterm-color-render nil))
(xterm-color-filter s)))
'ansi-color-filter-apply)
"A function to filter out ANSI escape sequences.")
+
(defalias 'elpher-color-apply
(if (fboundp 'xterm-color-filter)
'xterm-color-filter
;;; Global constants
;;
-(defconst elpher-version "2.11.0"
+(defconst elpher-version "3.0.0"
"Current version of elpher.")
(defconst elpher-margin-width 6
(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))
+ ((special start) elpher-get-start-page nil)
+ ((special history) elpher-get-history-page nil)
+ ((special history-all) elpher-get-history-all-page nil))
"Association list from types to getters, renderers, margin codes and index faces.")
(defvar elpher--gemini-page-headings nil
"List of headings on the page.")
-(defvar elpher--gemini-page-links nil
- "List of links on the page.")
-(defvar elpher--gemini-page-links-cache (make-hash-table :test 'equal)
- "Hash of addresses and page links.")
+;;; Declarations to avoid compiler warnings.
+;;
+
+(eval-when-compile
+ (defvar bookmark-make-record-function)
+ (declare-function bookmark-store "bookmark")
+ (declare-function org-link-store-props "ol")
+ (declare-function org-link-set-parameters "ol")
+ (defvar thing-at-point-uri-schemes)
+ (defvar mu4e~view-beginning-of-url-regexp))
+
;;; Customization group
;;
(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
Otherwise, the SOCKS proxy is only used for connections to onion services."
:type '(boolean))
-(defcustom elpher-gemini-number-links nil
- "If non-nil, number links in gemini pages when rendering.
-Links can be accessed by pressing `v' ('visit') followed by the link number."
+(defcustom elpher-number-links nil
+ "If non-nil, number links in pages when rendering.
+Links can be followed numerically by pressing `M' and entering by the link number."
:type '(boolean))
;; Face customizations
(let ((url (url-generic-parse-url url-string)))
(unless (and (not (url-fullness url)) (url-type url))
(setf (url-fullness url) t)
- (setf (url-filename url)
- (url-unhex-string (url-filename url)))
(unless (url-type url)
(setf (url-type url) elpher-default-url-type))
(unless (url-host url)
"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)
For gopher addresses this is a combination of the selector type and selector."
(if (symbolp address)
nil
- (url-filename address)))
+ (url-unhex-string (url-filename address))))
(defun elpher-address-host (address)
"Retrieve host from ADDRESS object."
(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)
"Return non-nill if ADDRESS object is a gopher address."
(and (not (elpher-address-special-p address))
- (member (elpher-address-protocol address) '("gopher gophers"))))
+ (member (elpher-address-protocol address) '("gopher" "gophers"))))
(defun elpher-gopher-address-selector (address)
"Retrieve gopher selector from ADDRESS object."
"Set the address corresponding to PAGE to NEW-ADDRESS."
(setcar (cdr page) new-address))
-(defvar elpher-current-page nil) ; buffer local
-(defvar elpher-history nil) ; buffer local
+(defvar elpher-current-page nil
+ "The current page for this Elpher buffer.")
+
+(defvar elpher-history nil
+ "The local history for this Elpher buffer.
+This variable is used by `elpher-back' and
+`elpher-show-history'.")
+
+(defvar elpher-history-all nil
+ "The global history for all Elpher buffers.
+This variable is used by `elpher-show-history-all'.")
(defun elpher-visit-page (page &optional renderer no-history)
"Visit PAGE using its own renderer or RENDERER, if non-nil.
unless NO-HISTORY is non-nil."
(elpher-save-pos)
(elpher-process-cleanup)
+ (elpher-reset-link-number-counter)
(unless (or no-history
(equal (elpher-page-address elpher-current-page)
(elpher-page-address page)))
- (push elpher-current-page elpher-history))
+ (push elpher-current-page elpher-history)
+ (push elpher-current-page elpher-history-all))
(setq-local elpher-current-page page)
(let* ((address (elpher-page-address page))
(type (elpher-address-type address))
- (type-record (cdr (assoc type elpher-type-map)))
- (page-links nil))
+ (type-record (cdr (assoc type elpher-type-map))))
(if type-record
- (progn
- (funcall (car type-record)
- (if renderer
- renderer
- (cadr type-record)))
- (setq page-links (gethash address elpher--gemini-page-links-cache))
- (if page-links
- (setq elpher--gemini-page-links page-links)))
+ (funcall (car type-record)
+ (if renderer
+ renderer
+ (cadr type-record)))
(elpher-visit-previous-page)
(pcase type
(`(gopher ,type-char)
If non-nil, FORCE-IPV4 causes the network connection to be made over
ipv4 only. (The default behaviour when this is not set depends on
-the host operating system and the local network capabilities."
+the host operating system and the local network capabilities.)"
(if (and use-tls (not (gnutls-available-p)))
(error "Use of TLS requires Emacs to be compiled with GNU TLS support")
(unless (< (elpher-address-port address) 65536)
address
(elpher-address-to-url address))))))))
+(defvar elpher--link-number-counter 0
+ "Used to number links on elpher pages.")
+(defun elpher-reset-link-number-counter ()
+ "Reset the link number counter."
+ (setq-local elpher--link-number-counter 0))
+
+(defun elpher--insert-text-button (label &rest properties)
+ "Insert a potentially-numbered button into the current buffer.
+The text for the button is provided by LABEL, while the button
+properties in PROPERTIES are as per `insert-text-button'."
+
+ (if elpher-number-links
+ (setq-local elpher--link-number-counter (+ elpher--link-number-counter 1)))
+ (let ((pref (if elpher-number-links
+ (concat "[" (number-to-string elpher--link-number-counter) "] ")
+ "")))
+ (apply #'insert-text-button (cons (concat pref label) properties))))
+
(defun elpher-insert-index-record (display-string &optional address)
"Function to insert an index record into the current buffer.
The contents of the record are dictated by DISPLAY-STRING and ADDRESS.
(filtered-display-string (elpher-color-filter-apply display-string))
(page (elpher-make-page filtered-display-string address)))
(elpher-insert-margin margin-code)
- (insert-text-button filtered-display-string
- 'face face
- 'elpher-page page
- 'action #'elpher-click-link
- 'follow-link t
- 'help-echo #'elpher--page-button-help))
+ (elpher--insert-text-button filtered-display-string
+ 'face face
+ 'elpher-page page
+ 'action #'elpher-click-link
+ 'follow-link t
+ 'help-echo #'elpher--page-button-help))
(pcase type
('nil ;; Information
(elpher-insert-margin)
(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--read-answer-polyfill (question answers)
+ "Polyfill for `read-answer' in Emacs 26.1.
+QUESTION is a string containing a question, and ANSWERS
+is a list of possible answers."
+ (completing-read question (mapcar 'identity answers)))
+
+(if (fboundp 'read-answer)
+ (defalias 'elpher-read-answer 'read-answer)
+ (defalias 'elpher-read-answer 'elpher--read-answer-polyfill))
(defun elpher-choose-client-certificate ()
"Prompt for a client certificate to use to establish a TLS connection."
(address (elpher-address-from-gemini-url url))
(type (if address (elpher-address-type address) nil))
(type-map-entry (cdr (assoc type elpher-type-map))))
- (setq elpher--gemini-page-links (append elpher--gemini-page-links `(,address)))
(when display-string
(insert elpher-gemini-link-string)
(if type-map-entry
(let* ((face (elt type-map-entry 3))
(filtered-display-string (elpher-color-filter-apply display-string))
(page (elpher-make-page filtered-display-string address)))
- (insert-text-button filtered-display-string
- 'face face
- 'elpher-page page
- 'action #'elpher-click-link
- 'follow-link t
- 'help-echo #'elpher--page-button-help))
+ (elpher--insert-text-button filtered-display-string
+ 'face face
+ 'elpher-page page
+ 'action #'elpher-click-link
+ 'follow-link t
+ 'help-echo #'elpher--page-button-help))
(insert (propertize display-string 'face 'elpher-unknown)))
(insert "\n"))))
(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)))
"Render DATA as a gemini map file, PARAMETERS is currently unused."
(elpher-with-clean-buffer
(setq elpher--gemini-page-headings nil)
- (let ((preformatted nil)
- (link-counter 1))
+ (let ((preformatted nil))
(auto-fill-mode 1)
(setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
- (setq elpher--gemini-page-links '())
(dolist (line (split-string data "\n"))
(cond
((string-prefix-p "```" line) (setq preformatted (not preformatted)))
(propertize line 'face 'elpher-gemini-preformatted))
"\n"))
((string-prefix-p "=>" line)
- (progn
- (if elpher-gemini-number-links
- (insert
- (concat
- "["
- (number-to-string link-counter)
- "] ")))
- (setq link-counter (1+ link-counter))
- (elpher-gemini-insert-link line)))
+ (elpher-gemini-insert-link line))
((string-prefix-p "#" line) (elpher-gemini-insert-header line))
(t (elpher-gemini-insert-text line)))))
(setq elpher--gemini-page-headings (nreverse elpher--gemini-page-headings))
(elpher-cache-content
(elpher-page-address elpher-current-page)
- (buffer-string))
- (puthash
- (elpher-page-address elpher-current-page)
- elpher--gemini-page-links
- elpher--gemini-page-links-cache)))
+ (buffer-string))))
(defun elpher-render-gemini-plain-text (data _parameters)
"Render DATA as plain text file. PARAMETERS is currently unused."
"\n"
" - TAB/Shift-TAB: next/prev item on current page\n"
" - RET/mouse-1: open item under cursor\n"
- " - m: select an item on current page by name (autocompletes)\n"
+ " - m/M: select an item on current page by name (autocompletes) or number\n"
" - u/mouse-3/U: return to previous page or to the start page\n"
- " - o/O: visit different selector or the root menu of the current server\n"
" - g: go to a particular address (gopher, gemini, finger)\n"
- " - v: visit a numbered link on a gemini page\n"
+ " - o/O: open a different address selector or the root menu of the current server\n"
" - d/D: download item under cursor or current page\n"
" - 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"
+ " - s/S: show history of current buffer or for all buffers\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"
+ " - !: set character coding system for gopher (default is to autodetect)\n"
" - T: toggle TLS gopher mode\n"
" - F: forget/discard current TLS client certificate\n"
" - .: display the raw server response for the current page\n"
(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)
+ "Your bookmarks are stored in your ")
+ (let ((help-string "RET,mouse-1: Open Emacs bookmark list"))
+ (elpher--insert-text-button "Emacs bookmark list"
+ 'face 'link
+ 'action (lambda (_)
+ (interactive)
+ (call-interactively #'bookmark-bmenu-list))
+ 'follow-link t
+ 'help-echo help-string))
+ (insert ".\n")
+ (insert (propertize
+ "(Bookmarks from legacy elpher-bookmarks files will be automatically imported.)\n"
+ 'face 'shadow))
(insert "\n"
"For Elpher release news or to leave feedback, visit:\n")
(elpher-insert-index-record "The Elpher Project Page"
(insert "\n"
"** Refer to the ")
(let ((help-string "RET,mouse-1: Open Elpher info manual (if available)"))
- (insert-text-button "Elpher info manual"
- 'face 'link
- 'action (lambda (_)
- (interactive)
- (info "(elpher)"))
- 'follow-link t
- 'help-echo help-string))
+ (elpher--insert-text-button "Elpher info manual"
+ 'face 'link
+ 'action (lambda (_)
+ (interactive)
+ (info "(elpher)"))
+ 'follow-link t
+ 'help-echo help-string))
(insert " for the full documentation. **\n")
(insert (propertize
(concat " (This should be available if you have installed Elpher using\n"
'face 'shadow))
(elpher-restore-pos)))
-;; Bookmarks page page retrieval
+;; History page retrieval
+
+(defun elpher-history ()
+ "Show the history of pages leading to the current page in this buffer.
+Use \\[elpher-history-all] to see the entire history.
+This is rendered using `elpher-get-history-page' via `elpher-type-map'."
+ (interactive)
+ (elpher-visit-page
+ (elpher-make-page "Elpher History Page"
+ (elpher-make-special-address 'history))))
+
+(defun elpher-history-all ()
+ "Show the all the pages you've visited using Elpher.
+Use \\[elpher-history] to see just the history for the current buffer.
+This is rendered using `elpher-get-history-all-page' via `elpher-type-map'."
+ (interactive)
+ (elpher-visit-page
+ (elpher-make-page "Elpher History Of All Seen Pages"
+ (elpher-make-special-address 'history-all))))
+
+(defun elpher-get-history-page (renderer)
+ "Getter which displays the history page (RENDERER must be nil)."
+ (when renderer
+ (elpher-visit-previous-page)
+ (error "Command not supported for history page"))
+ (elpher-show-history elpher-history))
-(defun elpher-get-bookmarks-page (renderer)
- "Getter to load and display the current bookmark list (RENDERER must be nil)."
+(defun elpher-get-history-all-page (renderer)
+ "Getter which displays the history page (RENDERER must be nil)."
(when renderer
(elpher-visit-previous-page)
- (error "Command not supported for bookmarks page"))
+ (error "Command not supported for history page"))
+ (elpher-show-history elpher-history-all))
+
+(defun elpher-show-history (pages)
+ "Show all PAGES in the Elpher buffer."
(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)))
+ (insert "---- Visited link history ----\n\n")
+ (if pages
+ (dolist (page pages)
+ (when page
+ (let ((display-string (elpher-page-display-string page))
+ (address (elpher-page-address page)))
+ (elpher-insert-index-record display-string address))))
+ (insert "No history items found.\n"))
+ (insert "\n----------------------------")))
;;; Bookmarks
-;;
-(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)))))
+;; 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)))))
+
+;;;###autoload
+(defun elpher-bookmark-jump (bookmark)
+ "Go to a particular BOOKMARK."
+ (let* ((url (cdr (assq 'location bookmark))))
+ (elpher-go url)))
+
+(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 file FILE 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
+
(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
+(defun elpher-org-mode-integration ()
+ "Set up `elpher' integration for `org-mode'."
(org-link-set-parameters
"elpher"
:store #'elpher-org-store-link
(elpher-org-export-link link description format "finger"))
:follow (lambda (link _arg) (elpher-org-follow-link link "finger"))))
+(add-hook 'org-mode-hook #'elpher-org-mode-integration)
+
+;;; Browse URL
+
;;;###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://"))
-(with-eval-after-load 'mu4e-view
- ;; Make mu4e aware of the gemini world
- (setq mu4e~view-beginning-of-url-regexp
- "\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:"))
+;;; Mu4e:
+
+;; Make mu4e aware of the gemini world
+(setq mu4e~view-beginning-of-url-regexp
+ "\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:")
;;; Interactive procedures
;;
(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 ()
(elpher-address-to-url address)))))
(elpher-visit-page (elpher-make-page url (elpher-address-from-url url))))))
-(defun elpher-visit-gemini-numbered-link (n)
- "Visit link designated by a number N."
- (interactive "nLink number: ")
- (if (or (> n (length elpher--gemini-page-links))
- (< n 1))
- (user-error "Invalid link number"))
- (let ((address (nth (1- n) elpher--gemini-page-links)))
- (elpher-go (url-recreate-url address))))
-
(defun elpher-redraw ()
"Redraw current page."
(interactive)
(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."
(goto-char (button-start b))
(button-activate b)))))))
+(defun elpher-jump-to-number (n)
+ "Jump to directory entry number N."
+ (interactive "nDirectory item/link number: ")
+ (let* ((link-map (reverse (elpher-build-link-map))))
+ (if link-map
+ (if (<= 1 n (length link-map))
+ (let ((b (cdr (elt link-map (- n 1)))))
+ (goto-char (button-start b))
+ (button-activate b))
+ (error "No link with that number")))))
+
(defun elpher-root-dir ()
"Visit root of current server."
(interactive)
(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 "<backtab>") 'elpher-prev-link)
(define-key map (kbd "C-M-i") 'elpher-prev-link)
(define-key map (kbd "u") 'elpher-back)
- (define-key map (kbd "U") 'elpher-back-to-start)
+ (define-key map (kbd "-") 'elpher-back)
+ (define-key map (kbd "^") 'elpher-back)
(define-key map [mouse-3] 'elpher-back)
- (define-key map (kbd "O") 'elpher-root-dir)
+ (define-key map (kbd "U") 'elpher-back-to-start)
(define-key map (kbd "g") 'elpher-go)
(define-key map (kbd "o") 'elpher-go-current)
+ (define-key map (kbd "O") 'elpher-root-dir)
+ (define-key map (kbd "s") 'elpher-history)
+ (define-key map (kbd "S") 'elpher-history-all)
(define-key map (kbd "r") 'elpher-redraw)
(define-key map (kbd "R") 'elpher-reload)
(define-key map (kbd "T") 'elpher-toggle-tls)
(define-key map (kbd "d") 'elpher-download)
(define-key map (kbd "D") 'elpher-download-current)
(define-key map (kbd "m") 'elpher-jump)
+ (define-key map (kbd "M") 'elpher-jump-to-number)
(define-key map (kbd "i") 'elpher-info-link)
(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 "S") 'elpher-set-gopher-coding-system)
+ (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 "!") 'elpher-set-gopher-coding-system)
(define-key map (kbd "F") 'elpher-forget-current-certificate)
- (define-key map (kbd "v") 'elpher-visit-gemini-numbered-link)
(when (fboundp 'evil-define-key*)
(evil-define-key*
'motion map
(kbd "u") 'elpher-back
(kbd "-") 'elpher-back
(kbd "^") 'elpher-back
- (kbd "U") 'elpher-back-to-start
[mouse-3] 'elpher-back
- (kbd "o") 'elpher-go
- (kbd "O") 'elpher-go-current
+ (kbd "U") 'elpher-back-to-start
+ (kbd "g") 'elpher-go
+ (kbd "o") 'elpher-go-current
+ (kbd "O") 'elpher-root-dir
(kbd "r") 'elpher-redraw
(kbd "R") 'elpher-reload
(kbd "T") 'elpher-toggle-tls
(kbd ".") 'elpher-view-raw
(kbd "d") 'elpher-download
(kbd "D") 'elpher-download-current
- (kbd "J") 'elpher-jump
+ (kbd "m") 'elpher-jump
+ (kbd "M") 'elpher-jump-to-number
(kbd "i") 'elpher-info-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 "S") 'elpher-set-gopher-coding-system
- (kbd "F") 'elpher-forget-current-certificate
- (kbd "v") 'elpher-visit-gemini-numbered-link))
+ (kbd "a") 'elpher-set-bookmark-no-overwrite
+ (kbd "A") 'bookmark-set-no-overwrite
+ (kbd "B") 'bookmark-bmenu-list
+ (kbd "!") 'elpher-set-gopher-coding-system
+ (kbd "F") 'elpher-forget-current-certificate))
map)
"Keymap for gopher client.")
"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 (lambda () 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