;; Copyright (C) 2019 Tim Vaughan
-;; Author: Tim Vaughan <tgvaughan@gmail.com>
+;; Author: Tim Vaughan <timv@ughan.xyz>
;; Created: 11 April 2019
-;; Version: 2.5.0
+;; Version: 2.6.0
;; Keywords: comm gopher
;; Homepage: http://thelambdalab.xyz/elpher
;; Package-Requires: ((emacs "26"))
;; - direct visualisation of image files,
;; - a simple bookmark management system,
;; - connections using TLS encryption,
-;; - the fledgling Gemini protocol.
+;; - the fledgling Gemini protocol,
+;; - the greybeard Finger 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.5.0"
+(defconst elpher-version "2.6.0"
"Current version of elpher.")
(defconst elpher-margin-width 6
((gopher ?s) elpher-get-gopher-page elpher-render-download "snd" elpher-binary)
((gopher ?h) elpher-get-gopher-page elpher-render-html "htm" elpher-html)
(gemini elpher-get-gemini-page elpher-render-gemini "gem" elpher-gemini)
+ (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)
"Specifies the number of seconds to wait for a network connection to time out."
:type '(integer))
-(defcustom elpher-strip-ansi-from-text t
- "If non-nil, strip ANSI escape sequences from gopher menus and text/gemini files.
-This is occasionally desirable, as these sequences are not understood natively by
-Emacs, and tend to result in a garbled display."
+(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
'gemini)
((equal protocol "telnet")
'telnet)
+ ((equal protocol "finger")
+ 'finger)
(t 'other-url)))))
(defun elpher-address-protocol (address)
"Retrieve host from ADDRESS object."
(url-host address))
+(defun elpher-address-user (address)
+ "Retrieve user from ADDRESS object."
+ (url-user 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.)"
;; Page
(defun elpher-make-page (display-string address)
+ "Create a page with DISPLAY-STRING and ADDRESS."
(list display-string address))
(defun elpher-page-display-string (page)
+ "Retrieve the display string corresponding to PAGE."
(elt page 0))
(defun elpher-page-address (page)
+ "Retrieve the address corresponding to PAGE."
(elt page 1))
(defvar elpher-current-page nil)
(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
(defconst elpher-ansi-regex "\x1b\\[[^m]*m"
"Wildly incomplete regexp used to strip out some troublesome ANSI escape sequences.")
-(defun elpher-strip-ansi (string)
- "Strip ANSI escape codes from STRING."
- (with-temp-buffer
- (insert string)
- (goto-char (point-min))
- (while (re-search-forward elpher-ansi-regex nil t)
- (delete-region (match-beginning 0) (match-end 0)))
- (buffer-string)))
-
-
(defun elpher-process-text-for-display (string)
- "Perform any desired processing of text prior to display.
-Currently includes buttonifying URLs and optionally stripping ANSI escape codes."
- (elpher-buttonify-urls (if elpher-strip-ansi-from-text
- (elpher-strip-ansi string)
- 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."
(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
(elpher-page-address elpher-current-page)
(buffer-string))))
+;; Finger page connection
+
+(defun elpher-get-finger-page (renderer &optional force-ipv4)
+ "Opens a finger connection to the current page address and renders it using RENDERER."
+ (let* ((address (elpher-page-address elpher-current-page))
+ (content (elpher-get-cached-content address)))
+ (if (and content (funcall renderer nil))
+ (elpher-with-clean-buffer
+ (insert content)
+ (elpher-restore-pos))
+ (elpher-with-clean-buffer
+ (insert "LOADING... (use 'u' to cancel)"))
+ (condition-case the-error
+ (let* ((kill-buffer-query-functions nil)
+ (user (let ((filename (elpher-address-filename address)))
+ (if (> (length filename) 1)
+ (substring filename 1)
+ (elpher-address-user address))))
+ (port (let ((given-port (elpher-address-port address)))
+ (if (> given-port 0) given-port 79)))
+ (host (elpher-address-host address))
+ (selector-string "")
+ (proc (open-network-stream "elpher-process"
+ nil
+ (if force-ipv4 (dns-query host) host)
+ port
+ :type 'plain
+ :nowait t))
+ (timer (run-at-time elpher-connection-timeout
+ nil
+ (lambda ()
+ (pcase (process-status proc)
+ ('connect
+ (elpher-process-cleanup)
+ (unless force-ipv4
+ (message "Connection timed out. Retrying with IPv4 address.")
+ (elpher-get-finger-page renderer t))))))))
+ (setq elpher-network-timer timer)
+ (set-process-coding-system proc 'binary)
+ (set-process-filter proc
+ (lambda (_proc string)
+ (cancel-timer timer)
+ (setq selector-string
+ (concat selector-string string))))
+ (set-process-sentinel proc
+ (lambda (_proc event)
+ (condition-case the-error
+ (cond
+ ((string-prefix-p "deleted" event))
+ ((string-prefix-p "open" event)
+ (let ((inhibit-eol-conversion t))
+ (process-send-string
+ proc
+ (concat user "\r\n"))))
+ (t
+ (cancel-timer timer)
+ (funcall renderer selector-string)
+ (elpher-restore-pos)))))))
+ (error
+ (elpher-network-error address the-error))))))
+
+
;; Other URL page opening
(defun elpher-get-other-url-page (renderer)
(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)))