;; Author: Tim Vaughan <tgvaughan@gmail.com>
;; Created: 11 April 2019
-;; Version: 1.0.0
+;; Version: 1.1.0
;; Keywords: comm gopher
;; Homepage: https://github.com/tgvaughan/elpher
;; Package-Requires: ((emacs "25"))
;; - (m)enu key support, similar to Emacs' info browser,
;; - clickable web and gopher links in plain text.
-;; The caching mechanism works by maintaining a hierarchy of visited
-;; pages rather than a linear history, meaning that it is quick and
-;; easy to navigate this history.
+;; Visited pages are stored as a hierarchy rather than a linear history,
+;; meaning that navigation between these pages is quick and easy.
;; To launch Elpher, simply use 'M-x elpher'. This will open a start
;; page containing information on key bindings and suggested starting
;;; Code:
(provide 'elpher)
+(require 'seq)
+(require 'pp)
;;; Global constants
;;
use as the start page."
:type '(list string string integer))
+
;;; Model
;;
;; Node
-(defvar elpher-seen-nodes (make-hash-table :test 'equal)
- "Table mapping addresses to existing (seen) node objects.")
-
(defun elpher-make-node (parent address getter &optional content pos)
"Create a node in the gopher page hierarchy.
page.
The optional arguments CONTENT and POS can be used to fill the cached
-content and cursor position fields of the node.
-
-If the hash table `elpher-seen-nodes' contains a key equal to ADDRESS,
-the node contained as its value will be returned instead."
- (let ((existing-node (gethash address elpher-seen-nodes)))
- (if existing-node
- existing-node
- (let ((new-node (list parent address getter content pos)))
- (puthash address new-node elpher-seen-nodes)
- new-node))))
+content and cursor position fields of the node."
+ (list parent address getter content pos))
(defun elpher-node-parent (node)
"Retrieve the parent node of NODE."
(goto-char pos)
(goto-char (point-min)))))
+
;;; Buffer preparation
;;
'(erase-buffer))
args)))
+
;;; Index rendering
;;
(display-string (elt fields 0))
(selector (elt fields 1))
(host (elt fields 2))
- (port (elt fields 3))
- (address (elpher-make-address selector host port))
- (type-map-entry (alist-get type elpher-type-map)))
+ (port (elt fields 3)))
+ (elpher-insert-index-record-helper type display-string selector host port)))
+
+(defun elpher-insert-index-record-helper (type display-string selector host port)
+ (let ((address (elpher-make-address selector host port))
+ (type-map-entry (alist-get type elpher-type-map)))
(if type-map-entry
(let ((getter (car type-map-entry))
(margin-code (cadr type-map-entry))
'elpher-node (elpher-make-node elpher-current-node
address
getter)
+ 'elpher-node-type type
'action #'elpher-click-link
'follow-link t
'help-echo (format "mouse-1, RET: open '%s' on %s port %s"
'face 'elpher-unknown-face)))))
(insert "\n")))
+(defun elpher-click-link (button)
+ "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
+ (let ((node (button-get button 'elpher-node)))
+ (elpher-visit-node node)))
+
+(defun elpher-click-url (button)
+ "Function called when the url link BUTTON is activated (via mouse or keypress)."
+ (let ((url (button-get button 'elpher-url)))
+ (if elpher-open-urls-with-eww
+ (browse-web url)
+ (browse-url url))))
+
;;; Selector retrieval (all kinds)
;;
(protocol (downcase (match-string 1))))
(if (string= protocol "gopher")
(let* ((host (match-string 2))
- (port 70)
+ (port (if (> (length (match-string 3)) 1)
+ (string-to-number (substring (match-string 3) 1))
+ 70))
(type-and-selector (match-string 4))
(type (if (> (length type-and-selector) 1)
(elt type-and-selector 1)
(message (format "Download complate, saved to file %s."
elpher-download-filename)))))))))
+;;; Bookmarks
+;;
-;;; Navigation procedures
+(defun elpher-make-bookmark (type display-string address)
+ (list type display-string address))
+
+(defun elpher-bookmark-type (bookmark)
+ (elt bookmark 0))
+
+(defun elpher-bookmark-display-string (bookmark)
+ (elt bookmark 1))
+
+(defun elpher-bookmark-address (bookmark)
+ (elt bookmark 2))
+
+(defun elpher-save-bookmarks (bookmarks)
+ (with-temp-file (locate-user-emacs-file "elpher-bookmarks")
+ (erase-buffer)
+ (pp bookmarks (current-buffer))))
+
+(defun elpher-load-bookmarks ()
+ (with-temp-buffer
+ (ignore-errors
+ (insert-file-contents (locate-user-emacs-file "elpher-bookmarks"))
+ (goto-char (point-min))
+ (read (current-buffer)))))
+
+(defun elpher-add-bookmark (bookmark)
+ (let ((bookmarks (elpher-load-bookmarks)))
+ (add-to-list 'bookmarks bookmark)
+ (elpher-save-bookmarks bookmarks)))
+
+(defun elpher-remove-bookmark (bookmark)
+ (elpher-save-bookmarks
+ (seq-filter (lambda (this-bookmark)
+ (not (equal bookmark this-bookmark)))
+ (elpher-load-bookmarks))))
+
+(defun elpher-display-bookmarks ()
+ (interactive)
+ (elpher-with-clean-buffer
+ (insert
+ "Use 'u' to return to the previous page.\n\n"
+ "---- Bookmark list ----\n\n")
+ (let ((bookmarks (elpher-load-bookmarks)))
+ (if bookmarks
+ (dolist (bookmark (elpher-load-bookmarks))
+ (let ((type (elpher-bookmark-type bookmark))
+ (display-string (elpher-bookmark-display-string bookmark))
+ (address (elpher-bookmark-address bookmark)))
+ (elpher-insert-index-record-helper type display-string
+ (elpher-address-selector address)
+ (elpher-address-host address)
+ (elpher-address-port address))))
+ (insert "No bookmarks found.\n")))
+ (insert "\n-----------------------")
+ (goto-char (point-min))
+ (elpher-next-link)))
+
+(defun elpher-bookmark-link ()
+ "Bookmark the link at point."
+ (interactive)
+ (let ((button (button-at (point))))
+ (if button
+ (let ((node (button-get button 'elpher-node))
+ (type (button-get button 'elpher-node-type))
+ (label (button-label button)))
+ (if node
+ (progn
+ (elpher-add-bookmark
+ (elpher-make-bookmark type
+ label
+ (elpher-node-address node)))
+ (message "Bookmarked \"%s\"" label))
+ (error "Can only bookmark gopher links, not general URLs.")))
+ (error "No link selected."))))
+
+(defun elpher-unbookmark-link ()
+ "Remove bookmark for the link at point."
+ (interactive)
+ (let ((button (button-at (point))))
+ (if button
+ (let ((node (button-get button 'elpher-node))
+ (type (button-get button 'elpher-node-type)))
+ (if node
+ (elpher-remove-bookmark
+ (elpher-make-bookmark type
+ (button-label button)
+ (elpher-node-address node)))
+ (error "Can only bookmark gopher links, not general URLs.")))
+ (error "No link selected."))))
+
+;;; Interactive navigation procedures
;;
(defun elpher-next-link ()
(interactive)
(backward-button 1))
-(defun elpher-click-link (button)
- "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
- (let ((node (button-get button 'elpher-node)))
- (elpher-visit-node node)))
-
-(defun elpher-click-url (button)
- "Function called when the url link BUTTON is activated (via mouse or keypress)."
- (let ((url (button-get button 'elpher-url)))
- (if elpher-open-urls-with-eww
- (browse-web url)
- (browse-url url))))
-
(defun elpher-follow-current-link ()
"Open the link or url at point."
(interactive)
(interactive)
(if (elpher-node-parent elpher-current-node)
(elpher-visit-parent-node)
- (message "No previous site.")))
+ (error "No previous site.")))
(defun elpher-download ()
"Download the link at point."
(if node
(elpher-visit-node (button-get button 'elpher-node)
#'elpher-get-node-download)
- (message "Can only download gopher links, not general URLs.")))
- (message "No link selected."))))
+ (error "Can only download gopher links, not general URLs.")))
+ (error "No link selected."))))
(defun elpher-build-link-map ()
"Build alist mapping link names to destination nodes in current buffer."
(elpher-visit-node (elpher-make-node elpher-current-node
root-address
#'elpher-get-index-node)))
- (message "Already at root directory of current server.")))
- (message "Command invalid for Elpher start page."))))
+ (error "Already at root directory of current server.")))
+ (error "Command invalid for Elpher start page."))))
+
;;; Mode and keymap
;;
(define-key map (kbd "d") 'elpher-download)
(define-key map (kbd "m") 'elpher-menu)
(when (fboundp 'evil-define-key)
- (evil-define-key 'normal map
+ (add-to-list 'evil-motion-state-modes 'elpher-mode)
+ (evil-define-key 'motion map
(kbd "TAB") 'elpher-next-link
(kbd "C-]") 'elpher-follow-current-link
(kbd "C-t") 'elpher-back
(kbd "R") 'elpher-reload
(kbd "w") 'elpher-view-raw
(kbd "d") 'elpher-download
- (kbd "m") 'elpher-menu))
+ (kbd "m") 'elpher-menu
+ (kbd "a") 'elpher-bookmark-link
+ (kbd "x") 'elpher-unbookmark-link
+ (kbd "B") 'elpher-display-bookmarks))
map)
"Keymap for gopher client.")