X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?p=elpher.git;a=blobdiff_plain;f=elpher.el;h=be78a0aeb8caad731a48f5dcc5f5d50f271a89f6;hp=48aaf140b51b70a3fb309019d77da79666fdcb6c;hb=00aa89d8ad9f321e4eb288499a404897eb9188c4;hpb=258d54f685ffdec64da6b28236972cf0e872fa56 diff --git a/elpher.el b/elpher.el index 48aaf14..be78a0a 100644 --- a/elpher.el +++ b/elpher.el @@ -2,9 +2,9 @@ ;; Copyright (C) 2019 Tim Vaughan -;; Author: Tim Vaughan +;; Author: Tim Vaughan ;; Created: 11 April 2019 -;; Version: 2.4.4 +;; Version: 2.6.1 ;; Keywords: comm gopher ;; Homepage: http://thelambdalab.xyz/elpher ;; Package-Requires: ((emacs "26")) @@ -37,7 +37,8 @@ ;; - direct visualisation of image files, ;; - a simple bookmark management system, ;; - connections using TLS encryption, -;; - support for 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 @@ -62,12 +63,13 @@ (require 'url-util) (require 'subr-x) (require 'dns) +(require 'ansi-color) ;;; Global constants ;; -(defconst elpher-version "2.4.4" +(defconst elpher-version "2.6.1" "Current version of elpher.") (defconst elpher-margin-width 6 @@ -88,6 +90,7 @@ ((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) @@ -163,10 +166,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 +180,11 @@ 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 ;; @@ -268,6 +272,8 @@ address refers to, via the table `elpher-type-map'." 'gemini) ((equal protocol "telnet") 'telnet) + ((equal protocol "finger") + 'finger) (t 'other-url))))) (defun elpher-address-protocol (address) @@ -287,6 +293,10 @@ For gopher addresses this is a combination of the selector type and selector." "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.)" @@ -335,12 +345,15 @@ If no address is defined, returns 0. (This is for compatibility with the URL li ;; 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) @@ -614,9 +627,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 @@ -627,9 +641,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 @@ -654,7 +666,7 @@ If ADDRESS is not supplied or nil the record is rendered as an (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.") + "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." @@ -673,12 +685,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))))) @@ -789,7 +811,7 @@ to ADDRESS." (let* ((kill-buffer-query-functions nil) (port (elpher-address-port address)) (host (elpher-address-host address)) - (response-string "") + (response-string-parts nil) (proc (open-network-stream "elpher-process" nil (if force-ipv4 (dns-query host) host) @@ -807,9 +829,11 @@ to ADDRESS." (set-process-coding-system proc 'binary) (set-process-filter proc (lambda (_proc string) - (cancel-timer timer) - (setq response-string - (concat response-string string)))) + (when timer + (cancel-timer timer) + (setq timer nil)) + (setq response-string-parts + (cons string response-string-parts)))) (set-process-sentinel proc (lambda (proc event) (condition-case the-error @@ -821,7 +845,7 @@ to ADDRESS." (concat (elpher-address-to-url address) "\r\n")))) ((string-prefix-p "deleted" event)) ; do nothing - ((and (string-empty-p response-string) + ((and (not response-string-parts) (not force-ipv4)) ; Try again with IPv4 (message "Connection failed. Retrying with IPv4.") @@ -829,7 +853,7 @@ to ADDRESS." (elpher-get-gemini-response address renderer t)) (t (funcall #'elpher-process-gemini-response - response-string + (apply #'concat (reverse response-string-parts)) renderer) (elpher-restore-pos))) (error @@ -1009,11 +1033,73 @@ 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)))) +;; 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) @@ -1309,8 +1395,7 @@ When run interactively HOST-OR-URL is read from the minibuffer." (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))) @@ -1348,9 +1433,7 @@ When run interactively HOST-OR-URL is read from the minibuffer." (let ((address-copy (elpher-address-from-url (elpher-address-to-url address)))) (setf (url-filename address-copy) "") - (elpher-visit-page - (elpher-make-page (elpher-address-to-url address-copy) - address-copy)))) + (elpher-go (elpher-address-to-url address-copy)))) (error "Command invalid for %s" (elpher-page-display-string elpher-current-page))))) (defun elpher-bookmarks-current-p ()