X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?p=elpher.git;a=blobdiff_plain;f=elpher.el;h=39e32a2d4a3e06a9443b5a6634411338892ef15c;hp=ef20de034e71f891beccc6fea904db443244f512;hb=6d80df84e2c13ffad263ec444ac724e7ec17735c;hpb=3d2817e533e03d2d0eb9e0dd5d573083127bc055 diff --git a/elpher.el b/elpher.el index ef20de0..39e32a2 100644 --- a/elpher.el +++ b/elpher.el @@ -5,6 +5,7 @@ ;; Copyright (C) 2021 Christopher Brannon ;; Copyright (C) 2021 Omar Polo ;; Copyright (C) 2021 Noodles! +;; Copyright (C) 2021 Abhiseck Paira ;; Copyright (C) 2020-2021 Alex Schroeder ;; Copyright (C) 2020 Zhiwei Chen ;; Copyright (C) 2020 condy0919 @@ -19,7 +20,7 @@ ;; Author: Tim Vaughan ;; 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")) @@ -73,19 +74,18 @@ ;; (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 @@ -109,7 +109,9 @@ (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)) "Association list from types to getters, renderers, margin codes and index faces.") @@ -127,7 +129,9 @@ (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 @@ -215,6 +219,17 @@ some servers which do not support IPv6 can take a long time to time-out." Otherwise, the SOCKS proxy is only used for connections to onion services." :type '(boolean)) +(defcustom elpher-use-emacs-bookmark-menu nil + "If non-nil, elpher will only use the native Emacs bookmark menu. +Otherwise, \\[elpher-show-bookmarks] will visit a special elpher bookmark +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 @@ -371,38 +386,38 @@ requiring gopher-over-TLS." (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." - (if (elpher-address-special-p address) - nil - (url-encode-url (url-recreate-url address)))) + (url-encode-url (url-recreate-url address))) (defun elpher-address-type (address) "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." + (pcase (elpher-address-type address) (`(special ,subtype) t))) (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))) @@ -424,18 +439,13 @@ For gopher addresses this is a combination of the selector type and selector." (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." @@ -473,9 +483,9 @@ If no address is defined, returns 0. (This is for compatibility with the URL li (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." @@ -580,6 +590,7 @@ previously-visited pages,unless NO-HISTORY is non-nil." (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 @@ -714,7 +725,7 @@ ERROR can be either an error object or a string." (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." @@ -1121,13 +1132,16 @@ If ADDRESS is not supplied or nil the record is rendered as an (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 @@ -1261,8 +1275,7 @@ that the response was malformed." (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) @@ -1441,7 +1454,7 @@ treatment that a separate function is warranted." (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)) @@ -1610,21 +1623,50 @@ The result is rendered using RENDERER." (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)))) +;; File page -;; Start page retrieval - -(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" @@ -1665,12 +1707,12 @@ The result is rendered using RENDERER." (elpher-address-from-url "gemini://geminispace.info/search")) (insert "\n" "Your bookmarks are stored in your ") - (let ((help-string "RET,mouse-1: Open Emacs bookmark list")) - (insert-text-button "Emacs bookmark list" + (let ((help-string "RET,mouse-1: Open bookmark list")) + (insert-text-button "bookmark list" 'face 'link 'action (lambda (_) (interactive) - (call-interactively #'elpher-open-bookmarks)) + (call-interactively #'elpher-show-bookmarks)) 'follow-link t 'help-echo help-string)) (insert ".\n") @@ -1705,8 +1747,8 @@ The result is rendered using RENDERER." '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))) @@ -1750,7 +1792,7 @@ This is rendered using `elpher-get-visited-pages-page' via `elpher-type-map'." (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") @@ -1761,8 +1803,8 @@ This is rendered using `elpher-get-visited-pages-page' via `elpher-type-map'." (address (elpher-page-address page))) (elpher-insert-index-record display-string address)))) (insert "No history items found.\n")) - (insert "\n" footer-line "\n" - "Select and entry or press 'u' to return to the previous page.") + (insert "\n " footer-line "\n" + "Select an entry or press 'u' to return to the previous page.") (elpher-restore-pos)))) @@ -1837,7 +1879,6 @@ To bookmark the link at point use \\[elpher-bookmark-link]." (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)))) @@ -1849,10 +1890,38 @@ To bookmark the link at point use \\[elpher-bookmark-link]." (bookmark-store display-string (cdr record) t))) (bookmark-save)) -(defun elpher-open-bookmarks () +(defun elpher-get-bookmarks-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")) + (elpher-with-clean-buffer + (insert " ---- Elpher Bookmarks ---- \n\n") + (bookmark-maybe-load-default-file) + (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 ") + (insert-text-button "Emacs bookmark menu" + 'action (lambda (_) + (interactive) + (call-interactively #'bookmark-bmenu-list)) + 'follow-link t + 'help-echo "RET,mouse-1: open Emacs bookmark menu") + (insert (substitute-command-keys + ",\nwhich can also be opened from anywhere using '\\[bookmark-bmenu-list]'.")) + (elpher-restore-pos))) + +(defun elpher-show-bookmarks () "Display the current list of elpher bookmarks. -This is just a call to `bookmark-bmenu-list', but we also check for a legacy -bookmark file and offer to import it." +This will also check for a legacy bookmark file and offer to import it." (interactive) (let ((old-bookmarks-file (or (and (boundp 'elpher-bookmarks-file) elpher-bookmarks-file) @@ -1863,7 +1932,11 @@ bookmark file and offer to import it." "\" found. Import now?"))) (elpher-bookmark-import old-bookmarks-file) (rename-file old-bookmarks-file (concat old-bookmarks-file "-legacy")))) - (call-interactively #'bookmark-bmenu-list)) + (if elpher-use-emacs-bookmark-menu + (call-interactively #'bookmark-bmenu-list) + (elpher-visit-page + (elpher-make-page "Elpher Bookmarks" + (elpher-make-special-address 'bookmarks))))) ;;; Integrations @@ -1978,6 +2051,12 @@ supports the old protocol elpher, where the link is self-contained." (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 ;; @@ -2001,21 +2080,23 @@ supports the old protocol elpher, where the link is self-contained." "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 () @@ -2065,11 +2146,12 @@ When run interactively HOST-OR-URL is read from the minibuffer." (let ((button (button-at (point)))) (if button (let ((page (button-get button 'elpher-page))) - (if (elpher-address-special-p (elpher-page-address page)) - (error "Cannot download %s" - (elpher-page-display-string page)) - (elpher-visit-page (button-get button 'elpher-page) - #'elpher-render-download))) + (unless page + (error "Not an elpher page")) + (when (elpher-address-special-p (elpher-page-address page)) + (error "Cannot download %s" (elpher-page-display-string page))) + (elpher-visit-page (button-get button 'elpher-page) + #'elpher-render-download)) (error "No link selected")))) (defun elpher-download-current () @@ -2125,17 +2207,18 @@ When run interactively HOST-OR-URL is read from the minibuffer." "Display information on PAGE." (let ((display-string (elpher-page-display-string page)) (address (elpher-page-address page))) - (if (elpher-address-special-p address) - (message "Special page: %s" display-string) - (message "%s" (elpher-address-to-url address))))) + (message "%s" (elpher-address-to-url address)))) (defun elpher-info-link () "Display information on page corresponding to link at point." (interactive) (let ((button (button-at (point)))) - (if button - (elpher-info-page (button-get button 'elpher-page)) - (error "No item selected")))) + (unless button + (error "No item selected")) + (let ((page (button-get button 'elpher-page))) + (unless page + (error "Not an elpher page")) + (elpher-info-page page)))) (defun elpher-info-current () "Display information on current page." @@ -2144,20 +2227,21 @@ When run interactively HOST-OR-URL is read from the minibuffer." (defun elpher-copy-page-url (page) "Copy URL representation of address of PAGE to `kill-ring'." - (let ((address (elpher-page-address page))) - (if (elpher-address-special-p address) - (error (format "Cannot represent %s as URL" (elpher-page-display-string page))) - (let ((url (elpher-address-to-url address))) - (message "Copied \"%s\" to kill-ring/clipboard." url) - (kill-new url))))) + (let* ((address (elpher-page-address page)) + (url (elpher-address-to-url address))) + (message "Copied \"%s\" to kill-ring/clipboard." url) + (kill-new url))) (defun elpher-copy-link-url () "Copy URL of item at point to `kill-ring'." (interactive) (let ((button (button-at (point)))) - (if button - (elpher-copy-page-url (button-get button 'elpher-page)) - (error "No item selected")))) + (unless button + (error "No item selected")) + (let ((page (button-get button 'elpher-page))) + (unless page + (error "Not an elpher page")) + (elpher-copy-page-url page)))) (defun elpher-copy-current-url () "Copy URL of current page to `kill-ring'." @@ -2205,7 +2289,7 @@ When run interactively HOST-OR-URL is read from the minibuffer." (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 "B") 'elpher-open-bookmarks) + (define-key map (kbd "B") 'elpher-show-bookmarks) (define-key map (kbd "!") 'elpher-set-gopher-coding-system) (define-key map (kbd "F") 'elpher-forget-current-certificate) (when (fboundp 'evil-define-key*) @@ -2237,7 +2321,7 @@ When run interactively HOST-OR-URL is read from the minibuffer." (kbd "C") 'elpher-copy-current-url (kbd "a") 'elpher-bookmark-link (kbd "A") 'elpher-bookmark-current - (kbd "B") 'elpher-open-bookmarks + (kbd "B") 'elpher-show-bookmarks (kbd "!") 'elpher-set-gopher-coding-system (kbd "F") 'elpher-forget-current-certificate)) map)