Updated link to gemini gopher page in README.
[elpher.git] / elpher.el
index 3a11487..474eee1 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -1,10 +1,10 @@
-;;; elpher.el --- A friendly gopher client.  -*- lexical-binding:t -*-
+;;; elpher.el --- A friendly gopher client  -*- lexical-binding:t -*-
 
 ;; Copyright (C) 2019 Tim Vaughan
 
-;; Author: Tim Vaughan <tgvaughan@gmail.com>
+;; Author: Tim Vaughan <timv@ughan.xyz>
 ;; Created: 11 April 2019
-;; Version: 2.4.4
+;; Version: 2.5.2
 ;; Keywords: comm gopher
 ;; Homepage: http://thelambdalab.xyz/elpher
 ;; Package-Requires: ((emacs "26"))
@@ -37,7 +37,7 @@
 ;; - direct visualisation of image files,
 ;; - a simple bookmark management system,
 ;; - connections using TLS encryption,
-;; - support for the fledgling Gemini protocol.
+;; - the fledgling Gemini protocol.
 
 ;; To launch Elpher, simply use 'M-x elpher'.  This will open a start
 ;; page containing information on key bindings and suggested starting
 (require 'url-util)
 (require 'subr-x)
 (require 'dns)
+(require 'ansi-color)
 
 
 ;;; Global constants
 ;;
 
-(defconst elpher-version "2.4.4"
+(defconst elpher-version "2.5.2"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
 Otherwise, use the system browser via the BROWSE-URL function."
   :type '(boolean))
 
-(defcustom elpher-buttonify-urls-in-directories t
-  "If non-nil, turns URLs matched in directories into clickable buttons."
-  :type '(boolean))
-
 (defcustom elpher-use-header t
   "If non-nil, display current page information in buffer header."
   :type '(boolean))
@@ -181,6 +178,12 @@ allows switching from an encrypted channel back to plain text without user input
   "Specifies the number of seconds to wait for a network connection to time out."
   :type '(integer))
 
+(defcustom elpher-filter-ansi-from-text nil
+  "If non-nil, filter ANSI escape sequences from text.
+The default behaviour is to use the ansi-color package to interpret these
+sequences."
+  :type '(boolean))
+
 ;;; Model
 ;;
 
@@ -308,8 +311,6 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
       ""
     (substring (url-filename address) 2)))
 
-;; Page
-
 
 ;; Cache
 
@@ -332,18 +333,21 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
   "Set the cursor position cache for ADDRESS to POS."
   (puthash address pos elpher-pos-cache))
 
+
 ;; Page
 
-(defun elpher-make-page (address display-string)
-  (list address display-string))
+(defun elpher-make-page (display-string address)
+  "Create a page with DISPLAY-STRING and ADDRESS."
+  (list display-string address))
 
-(defun elpher-page-address (page)
+(defun elpher-page-display-string (page)
+  "Retrieve the display string corresponding to PAGE."
   (elt page 0))
 
-(defun elpher-page-display-string (page)
+(defun elpher-page-address (page)
+  "Retrieve the address corresponding to PAGE."
   (elt page 1))
 
-
 (defvar elpher-current-page nil)
 (defvar elpher-history nil)
 
@@ -353,8 +357,10 @@ Additionally, push PAGE onto the stack of previously-visited pages,
 unless NO-HISTORY is non-nil."
   (elpher-save-pos)
   (elpher-process-cleanup)
-  (unless no-history
-    (push page elpher-history))
+  (unless (or no-history
+              (equal (elpher-page-address elpher-current-page)
+                     (elpher-page-address page)))
+    (push elpher-current-page elpher-history))
   (setq elpher-current-page page)
   (let* ((address (elpher-page-address page))
          (type (elpher-address-type address))
@@ -376,8 +382,9 @@ unless NO-HISTORY is non-nil."
 (defun elpher-visit-previous-page ()
   "Visit the previous page in the history."
   (let ((previous-page (pop elpher-history)))
-    (when previous-page
-      (elpher-visit-page previous-page nil t))))
+    (if previous-page
+        (elpher-visit-page previous-page nil t)
+      (error "No previous page."))))
       
 (defun elpher-reload-current-page ()
   "Reload the current page, discarding any existing cached content."
@@ -612,9 +619,10 @@ If ADDRESS is not supplied or nil the record is rendered as an
     (if type-map-entry
         (let* ((margin-code (elt type-map-entry 2))
                (face (elt type-map-entry 3))
-               (page (elpher-make-page display-string address)))
+               (filtered-display-string (ansi-color-filter-apply display-string))
+               (page (elpher-make-page filtered-display-string address)))
           (elpher-insert-margin margin-code)
-          (insert-text-button display-string
+          (insert-text-button filtered-display-string
                               'face face
                               'elpher-page page
                               'action #'elpher-click-link
@@ -625,9 +633,7 @@ If ADDRESS is not supplied or nil the record is rendered as an
          (elpher-insert-margin)
          (let ((propertized-display-string
                 (propertize display-string 'face 'elpher-info)))
-           (insert (if elpher-buttonify-urls-in-directories
-                       (elpher-buttonify-urls propertized-display-string)
-                     propertized-display-string))))
+           (insert (elpher-process-text-for-display propertized-display-string))))
         (`(gopher ,selector-type) ;; Unknown
          (elpher-insert-margin (concat (char-to-string selector-type) "?"))
          (insert (propertize display-string
@@ -651,8 +657,8 @@ If ADDRESS is not supplied or nil the record is rendered as an
 ;; Text rendering
 
 (defconst elpher-url-regex
-  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]*[a-zA-Z0-9\-]\\|\[[a-zA-Z0-9:]+\]\\)\\(:[0-9]+\\)?\\(/\\([0-9a-zA-Z\-_~?/@|:.%#]*[0-9a-zA-Z\-_~?/@|#]\\)?\\)?"
-  "Regexp used to locate and buttniofy URLs in text files loaded by elpher.")
+  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]*[a-zA-Z0-9\-]\\|\[[a-zA-Z0-9:]+\]\\)\\(:[0-9]+\\)?\\(/\\([0-9a-zA-Z\-_~?/@|:.%#=&]*[0-9a-zA-Z\-_~?/@|#]\\)?\\)?"
+  "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
 
 (defun elpher-buttonify-urls (string)
   "Turn substrings which look like urls in STRING into clickable buttons."
@@ -671,12 +677,22 @@ If ADDRESS is not supplied or nil the record is rendered as an
                             'face 'button)))
     (buffer-string)))
 
+(defconst elpher-ansi-regex "\x1b\\[[^m]*m"
+  "Wildly incomplete regexp used to strip out some troublesome ANSI escape sequences.")
+
+(defun elpher-process-text-for-display (string)
+  "Perform any desired processing of STRING prior to display as text.
+Currently includes buttonifying URLs and processing ANSI escape codes."
+  (elpher-buttonify-urls (if elpher-filter-ansi-from-text
+                             (ansi-color-filter-apply string)
+                           (ansi-color-apply string))))
+
 (defun elpher-render-text (data &optional _mime-type-string)
   "Render DATA as text.  MIME-TYPE-STRING is unused."
   (elpher-with-clean-buffer
    (if (not data)
        t
-     (insert (elpher-buttonify-urls (elpher-preprocess-text-response data)))
+     (insert (elpher-process-text-for-display (elpher-preprocess-text-response data)))
      (elpher-cache-content
       (elpher-page-address elpher-current-page)
       (buffer-string)))))
@@ -805,7 +821,9 @@ to ADDRESS."
           (set-process-coding-system proc 'binary)
           (set-process-filter proc
                               (lambda (_proc string)
-                                (cancel-timer timer)
+                                (when timer
+                                  (cancel-timer timer)
+                                  (setq timer nil))
                                 (setq response-string
                                       (concat response-string string))))
           (set-process-sentinel proc
@@ -1007,7 +1025,7 @@ For instance, the filename /a/b/../c/./d will reduce to /a/c/d"
 (defun elpher-render-gemini-plain-text (data _parameters)
   "Render DATA as plain text file.  PARAMETERS is currently unused."
   (elpher-with-clean-buffer
-   (insert (elpher-buttonify-urls data))
+   (insert (elpher-process-text-for-display data))
    (elpher-cache-content
     (elpher-page-address elpher-current-page)
     (buffer-string))))
@@ -1233,15 +1251,15 @@ If ADDRESS is already bookmarked, update the label only."
   (interactive)
   (push-button))
 
-(defun elpher-go ()
-  "Go to a particular gopher site read from the minibuffer."
-  (interactive)
-  (let ((page
-         (let ((host-or-url (read-string "Gopher or Gemini URL: ")))
-           (elpher-make-page host-or-url
-                             (elpher-address-from-url host-or-url)))))
+(defun elpher-go (host-or-url)
+  "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 ((page (elpher-make-page host-or-url
+                                (elpher-address-from-url host-or-url))))
     (switch-to-buffer "*elpher*")
-    (elpher-visit-page page)))
+    (elpher-visit-page page)
+    '()))
 
 (defun elpher-go-current ()
   "Go to a particular site read from the minibuffer, initialized with the current URL."
@@ -1255,16 +1273,12 @@ If ADDRESS is already bookmarked, update the label only."
 (defun elpher-redraw ()
   "Redraw current page."
   (interactive)
-  (if elpher-current-page
-      (elpher-visit-page elpher-current-page)
-    (message "No current site.")))
+  (elpher-visit-page elpher-current-page))
 
 (defun elpher-reload ()
   "Reload current page."
   (interactive)
-  (if elpher-current-page
-      (elpher-reload-current-page)
-    (message "No current site.")))
+  (elpher-reload-current-page))
 
 (defun elpher-toggle-tls ()
   "Toggle TLS encryption mode for gopher."
@@ -1280,19 +1294,15 @@ If ADDRESS is already bookmarked, update the label only."
 (defun elpher-view-raw ()
   "View raw server response for current page."
   (interactive)
-  (if elpher-current-page
-      (if (elpher-address-special-p (elpher-page-address elpher-current-page))
-          (error "This page was not generated by a server")
-        (elpher-visit-page elpher-current-page
-                           #'elpher-render-raw))
-    (message "No current site.")))
+  (if (elpher-address-special-p (elpher-page-address elpher-current-page))
+      (error "This page was not generated by a server")
+    (elpher-visit-page elpher-current-page
+                       #'elpher-render-raw)))
 
 (defun elpher-back ()
   "Go to previous site."
   (interactive)
-  (if elpher-history
-      (error "No previous site")
-    (elpher-visit-previous-page)))
+  (elpher-visit-previous-page))
 
 (defun elpher-download ()
   "Download the link at point."
@@ -1315,8 +1325,7 @@ If ADDRESS is already bookmarked, update the label only."
              (elpher-page-display-string elpher-current-page))
     (elpher-visit-page (elpher-make-page
                         (elpher-page-display-string elpher-current-page)
-                        (elpher-page-address elpher-current-page)
-                        elpher-current-page)
+                        (elpher-page-address elpher-current-page))
                        #'elpher-render-download
                        t)))