;; Copyright (C) 2021 Christopher Brannon <chris@the-brannons.com>
;; Copyright (C) 2021 Omar Polo <op@omarpolo.com>
;; Copyright (C) 2021 Noodles! <nnoodle@chiru.no>
+;; Copyright (C) 2021 Abhiseck Paira <abhiseckpaira@disroot.org>
;; Copyright (C) 2020-2021 Alex Schroeder <alex@gnu.org>
;; Copyright (C) 2020 Zhiwei Chen <chenzhiwei03@kuaishou.com>
;; Copyright (C) 2020 condy0919 <condy0919@gmail.com>
;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
;; Created: 11 April 2019
-;; Version: 3.0.0
+;; Version: 3.1.0
;; Keywords: comm gopher
;; Homepage: https://thelambdalab.xyz/elpher
;; Package-Requires: ((emacs "27.1"))
;;
(require 'seq)
-(require 'pp)
(require 'shr)
(require 'url-util)
(require 'subr-x)
-(require 'dns)
(require 'nsm)
(require 'gnutls)
(require 'socks)
+(require 'bookmark)
;;; Global constants
;;
-(defconst elpher-version "3.0.0"
+(defconst elpher-version "3.1.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 start) elpher-get-start-page nil "E" elpher-index)
+ (file elpher-get-file-page nil "~" elpher-gemini)
+ ((special welcome) elpher-get-welcome-page nil "E" elpher-index)
((special bookmarks) elpher-get-bookmarks-page nil "E" elpher-index)
((special history) elpher-get-history-page nil "E" elpher-index)
((special visited-pages) elpher-get-visited-pages-page nil "E" elpher-index))
(defvar ansi-color-context)
(defvar bookmark-make-record-function)
(defvar mu4e~view-beginning-of-url-regexp)
- (defvar thing-at-point-uri-schemes))
+ (defvar eww-use-browse-url)
+ (defvar thing-at-point-uri-schemes)
+ (defvar xterm-color-preserve-properties))
;;; Customization group
page within which all of the standard elpher keybindings are active."
:type '(boolean))
+(defcustom elpher-start-page "about:welcome"
+ "Specify the page displayed initially by elpher.
+The default welcome screen \"about:welcome\", while the bookmarks list
+is \"about:bookmarks\". You can also specify local files via \"file:\".")
+
;; Face customizations
(defgroup elpher-faces nil
(defun elpher-make-special-address (type)
"Create an ADDRESS object corresponding to the given special address symbol TYPE."
- type)
+ (elpher-address-from-url (concat "about:" (symbol-name type))))
(defun elpher-address-to-url (address)
"Get string representation of ADDRESS, or nil if ADDRESS is special."
"Retrieve type of ADDRESS object.
This is used to determine how to retrieve and render the document the
address refers to, via the table `elpher-type-map'."
- (if (symbolp address)
- (list 'special address)
- (let ((protocol (url-type address)))
- (cond ((or (equal protocol "gopher")
- (equal protocol "gophers"))
- (list 'gopher
- (if (member (url-filename address) '("" "/"))
- ?1
- (string-to-char (substring (url-filename address) 1)))))
- ((equal protocol "gemini")
- 'gemini)
- ((equal protocol "telnet")
- 'telnet)
- ((equal protocol "finger")
- 'finger)
- (t 'other-url)))))
+ (let ((protocol (url-type address)))
+ (pcase (url-type address)
+ ("about"
+ (list 'special (intern (url-filename address))))
+ ((or "gopher" "gophers")
+ (list 'gopher
+ (if (member (url-filename address) '("" "/"))
+ ?1
+ (string-to-char (substring (url-filename address) 1)))))
+ ("gemini" 'gemini)
+ ("telnet" 'telnet)
+ ("finger" 'finger)
+ ("file" 'file)
+ (_ 'other-url))))
+
+(defun elpher-address-special-p (address)
+ "Return non-nil if ADDRESS is a special address."
+ (let ((type (url-type address)))
+ (and type
+ (listp type)
+ (eq (car type) 'special))))
(defun elpher-address-protocol (address)
"Retrieve the transport protocol for ADDRESS. This is nil for special addresses."
- (if (symbolp address)
+ (if (elpher-address-special-p address)
nil
(url-type address)))
(defun elpher-address-port (address)
"Retrieve port from ADDRESS object.
If no address is defined, returns 0. (This is for compatibility with the URL library.)"
- (if (symbolp address)
+ (if (elpher-address-special-p address)
0
(url-port address)))
-(defun elpher-address-special-p (address)
- "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"))))
+ (eq 'gopher (elpher-address-type address)))
(defun elpher-gopher-address-selector (address)
"Retrieve gopher selector from ADDRESS object."
(list display-string address))
(defun elpher-make-start-page ()
- "Create the start page."
- (elpher-make-page "Elpher Start Page"
- (elpher-make-special-address 'start)))
+ "Create the welcome page."
+ (elpher-make-page "Start Page"
+ (elpher-address-from-url elpher-start-page)))
(defun elpher-page-display-string (page)
"Retrieve the display string corresponding to PAGE."
(defmacro elpher-with-clean-buffer (&rest args)
"Evaluate ARGS with a clean *elpher* buffer as current."
+ (declare (debug (body))) ;; Allow edebug to step through body
`(with-current-buffer elpher-buffer-name
(unless (eq major-mode 'elpher-mode)
;; avoid resetting buffer-local variables
(cancel-timer elpher-network-timer)))
(defun elpher-make-network-timer (thunk)
- "Creates a timer to run the THUNK after `elpher-connection-timeout' seconds.
+ "Create a timer to run the THUNK after `elpher-connection-timeout' seconds.
This is just a wraper around `run-at-time' which additionally sets the
buffer-local variable `elpher-network-timer' to allow
`elpher-process-cleanup' to also clear the timer."
(if (not data)
nil
(if (display-images-p)
- (progn
- (let ((image (create-image
- data
- nil t)))
- (elpher-with-clean-buffer
- (insert-image image)
- (elpher-restore-pos))))
+ (let* ((image (create-image
+ data
+ nil t))
+ (window (get-buffer-window elpher-buffer-name)))
+ (when window
+ (setf (image-property image :max-width) (window-pixel-width window))
+ (setf (image-property image :max-height) (window-pixel-height window)))
+ (elpher-with-clean-buffer
+ (insert-image image)
+ (elpher-restore-pos)))
(elpher-render-download data))))
;; Search retrieval and rendering
(let ((redirect-address (elpher-address-from-gemini-url response-meta)))
(if (member redirect-address elpher-gemini-redirect-chain)
(error "Redirect loop detected"))
- (if (not (string= (elpher-address-protocol redirect-address)
- "gemini"))
+ (if (not (eq (elpher-address-type redirect-address) 'gemini))
(error "Server tried to automatically redirect to non-gemini URL: %s"
response-meta))
(elpher-page-set-address elpher-current-page redirect-address)
(if (string-empty-p (url-filename address))
(setf (url-filename address) "/")) ;ensure empty filename is marked as absolute
(setf (url-host address) (url-host current-address))
- (setf (url-port address) (url-port current-address))
+ (setf (url-portspec address) (url-portspec current-address)) ; (url-port) too slow!
(unless (string-prefix-p "/" (url-filename address)) ;deal with relative links
(setf (url-filename address)
(concat (file-name-directory (url-filename current-address))
(error "Command not supported for general URLs"))
(let* ((address (elpher-page-address elpher-current-page))
(url (elpher-address-to-url address)))
- (progn
- (elpher-visit-previous-page) ; Do first in case of non-local exits.
- (message "Opening URL...")
- (if elpher-open-urls-with-eww
- (browse-web url)
- (browse-url url)))))
-
+ (elpher-visit-previous-page) ; Do first in case of non-local exits.
+ (message "Opening URL...")
+ (if elpher-open-urls-with-eww
+ (browse-web url)
+ (browse-url url))))
-;; Start page retrieval
+;; File page
-(defun elpher-get-start-page (renderer)
- "Getter which displays the start page (RENDERER must be nil)."
+(defun elpher-get-file-page (renderer)
+ "Getter which retrieves the contents of a local file and renders it using RENDERER."
+ (let* ((address (elpher-page-address elpher-current-page))
+ (filename (elpher-address-filename address)))
+ (unless (file-exists-p filename)
+ (elpher-visit-previous-page)
+ (error "File not found"))
+ (unless (file-readable-p filename)
+ (elpher-visit-previous-page)
+ (error "Could not read from file"))
+ (funcall
+ (if renderer
+ renderer
+ (pcase (file-name-extension filename)
+ ((or "gmi" "gemini") #'elpher-render-gemini-map)
+ ((or "htm" "html") #'elpher-render-html)
+ ((or "jpg" "jpeg" "gif" "png" "bmp" "tif" "tiff")
+ #'elpher-render-image)
+ ((or "txt" "") #'elpher-render-text)
+ (t
+ #'elpher-render-download)))
+ (with-temp-buffer
+ (let ((coding-system-for-read 'binary)
+ (coding-system-for-write 'binary))
+ (insert-file-contents-literally filename)
+ (string-as-unibyte (buffer-string))))
+ nil)))
+
+
+;; Welcome page retrieval
+
+(defun elpher-get-welcome-page (renderer)
+ "Getter which displays the welcome page (RENDERER must be nil)."
(when renderer
(elpher-visit-previous-page)
- (error "Command not supported for start page"))
+ (error "Command not supported for welcome page"))
(elpher-with-clean-buffer
(insert " --------------------------------------------\n"
" Elpher Gopher and Gemini Client \n"
(insert "\n"
"Your bookmarks are stored in your ")
(let ((help-string "RET,mouse-1: Open bookmark list"))
- (insert-text-button "Emacs bookmark list"
+ (insert-text-button "bookmark list"
'face 'link
'action (lambda (_)
(interactive)
'help-echo help-string))
(insert "\n")
(insert (propertize
- (concat " (These documents should be available if you have installed Elpher \n"
- " using MELPA. Otherwise you may have to install the manual yourself.)\n")
+ (concat "(These documents should be available if you have installed Elpher \n"
+ " using MELPA. Otherwise you may have to install the manual yourself.)\n")
'face 'shadow))
(elpher-restore-pos)))
(defun elpher-display-history-links (pages title)
"Show all PAGES in an Elpher buffer with a given TITLE."
- (let* ((title-line (concat "---- " title " ----"))
+ (let* ((title-line (concat " ---- " title " ----"))
(footer-line (make-string (length title-line) ?-)))
(elpher-with-clean-buffer
(insert title-line "\n\n")
(address (elpher-page-address page)))
(elpher-insert-index-record display-string address))))
(insert "No history items found.\n"))
- (insert "\n" footer-line "\n"
+ (insert "\n " footer-line "\n"
"Select an entry or press 'u' to return to the previous page.")
(elpher-restore-pos))))
(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))))
(elpher-with-clean-buffer
(insert " ---- Elpher Bookmarks ---- \n\n")
(bookmark-maybe-load-default-file)
- (let ((bookmarks (bookmark-maybe-sort-alist)))
- (if bookmarks
- (dolist (bookmark bookmarks)
- (let* ((name (car bookmark))
- (url (alist-get 'location (cdr bookmark)))
- (address (elpher-address-from-url url)))
- (elpher-insert-index-record name address)))
- (insert "No bookmarked pages found.\n")))
+ (dolist (bookmark (bookmark-maybe-sort-alist))
+ (when (eq #'elpher-bookmark-jump (alist-get 'handler (cdr bookmark)))
+ (let* ((name (car bookmark))
+ (url (alist-get 'location (cdr bookmark)))
+ (address (elpher-address-from-url url)))
+ (elpher-insert-index-record name address))))
+ (when (<= (line-number-at-pos) 3)
+ (insert "No bookmarked pages found.\n"))
(insert "\n --------------------------\n\n"
"Select an entry or press 'u' to return to the previous page.\n\n"
"Bookmarks can be renamed or deleted via the ")
(setq mu4e~view-beginning-of-url-regexp
"\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:")
+;;; eww:
+
+;; Let elpher handle gemini, gopher links in eww buffer.
+(setq eww-use-browse-url
+ "\\`mailto:\\|\\(\\`gemini\\|\\`gopher\\|\\`finger\\)://")
+
;;; Interactive procedures
;;
"Go to a particular gopher site HOST-OR-URL.
When run interactively HOST-OR-URL is read from the minibuffer."
(interactive "sGopher or Gemini URL: ")
- (let* ((cleaned-host-or-url (string-trim host-or-url))
- (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-with-clean-buffer
- (elpher-visit-page page))
- nil))
+ (let ((trimmed-host-or-url (string-trim host-or-url)))
+ (unless (string-empty-p trimmed-host-or-url)
+ (let* ((address (elpher-address-from-url trimmed-host-or-url))
+ (page (elpher-make-page trimmed-host-or-url address)))
+ (switch-to-buffer elpher-buffer-name)
+ (elpher-with-clean-buffer
+ (elpher-visit-page page))
+ nil)))) ; non-nil value is displayed by eshell
(defun elpher-go-current ()
"Go to a particular site read from the minibuffer, initialized with the current URL."
(interactive)
- (let ((address (elpher-page-address elpher-current-page)))
- (let ((url (read-string "Gopher or Gemini URL: "
- (unless (elpher-address-special-p address)
- (elpher-address-to-url address)))))
+ (let* ((address (elpher-page-address elpher-current-page))
+ (url (read-string "Gopher or Gemini URL: "
+ (unless (elpher-address-special-p address)
+ (elpher-address-to-url address)))))
+ (unless (string-empty-p (string-trim url))
(elpher-visit-page (elpher-make-page url (elpher-address-from-url url))))))
(defun elpher-redraw ()