clic.lisp - clic - Clic is an command line interactive client for gopher written in Common LISP
 (HTM) git clone git://bitreich.org/clic/ git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/clic/
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Tags
 (DIR) README
 (DIR) LICENSE
       ---
       clic.lisp (26994B)
       ---
            1 (in-package :cl-user)
            2 
            3 ;;;; C binding to get terminal informations
            4 #+ecl
            5 (progn
            6   (ffi:clines "
            7     #include <sys/ioctl.h>
            8     #include <limits.h>
            9     #include <unistd.h>
           10 
           11     #ifdef __OpenBSD__
           12     void gotoPledge() {
           13        pledge(\"dns inet stdio rpath tty wpath cpath proc exec\",NULL);
           14     }
           15 
           16     void kioskPledge() {
           17        pledge(\"dns inet stdio tty rpath\",NULL);
           18     }
           19     #endif
           20 
           21     int ttyPredicate() {
           22       return isatty(fileno(stdout)); }
           23     unsigned int getTerminalHeight()  {
           24       struct winsize w;
           25       return ioctl(1,TIOCGWINSZ,&w)<0?UINT_MAX:w.ws_row;}")
           26   #+openbsd
           27   (progn
           28     (ffi:def-function
           29      ("kioskPledge" c-kiosk-pledge)
           30      () :returning :void)
           31     (ffi:def-function
           32      ("gotoPledge" c-pledge)
           33      () :returning :void))
           34   (ffi:def-function
           35       ("getTerminalHeight" c-termsize)
           36       () :returning :unsigned-int)
           37   (ffi:def-function
           38       ("ttyPredicate" c-ttyp)
           39       () :returning :int))
           40 ;;;; END C binding
           41 
           42 ;; structure to store links
           43 (defstruct location host port type uri tls text
           44            :predicate)
           45 
           46 ;;;; kiosk mode 
           47 (defparameter *kiosk-mode* nil)
           48 
           49 ;;;; no split mode
           50 (defparameter *no-split* nil)
           51 
           52 (defmacro kiosk-mode(&body code)
           53   "prevent code if kiosk mode is enabled"
           54   `(progn
           55      (when (not *kiosk-mode*)
           56        ,@code)))
           57 
           58 ;;;; BEGIN GLOBAL VARIABLES
           59 
           60 ;;; array of lines in buffer
           61 (defparameter *buffer* nil)
           62 ;;; array of lines of last menu
           63 (defparameter *previous-buffer* nil)
           64 
           65 ;;; bandwidth usage counter
           66 (defparameter *total-bandwidth-in* 0)
           67 (defparameter *last-bandwidth-in* 0)
           68 
           69 ;;; a list containing the last viewed pages
           70 (defparameter *history*   '())
           71 
           72 ;;; contain duration of the last request
           73 (defparameter *duration* 0)
           74 
           75 ;;; when clic loads a type 1 page, we store location structures here
           76 (defparameter *links*     (make-hash-table))
           77 
           78 ;;; Colors for use in the code
           79 (defparameter *colors*    (make-hash-table))
           80 
           81 ;;; List of allowed item types
           82 (defparameter *allowed-selectors*
           83   (list "0" "1" "2" "3" "4" "5" "6" "i"
           84         "h" "7" "8" "9" "+" "T" "g" "I"))
           85 
           86 ;;;; END GLOBAL VARIABLES
           87 
           88 ;;;; BEGIN ANSI colors
           89 (defun add-color(name type hue)
           90   "Storing a ANSI color string into *colors*"
           91   (setf (gethash name *colors*)
           92         (format nil "~a[~a;~am" #\Escape type hue)))
           93 
           94 (defun get-color(name) (gethash name *colors*))
           95 (add-color 'red        1 31)
           96 (add-color 'reset      0 70)
           97 (add-color 'bg-black   0 40)
           98 (add-color 'folder     4 34)
           99 (add-color 'green      1 32)
          100 (add-color 'file       0 35)
          101 (add-color 'cyan       0 46)
          102 (add-color 'http       0 33)
          103 ;;;; END ANSI colors
          104 
          105 (defun clear()
          106   "Clear the screen"
          107   (format t "~A[H~@*~A[J" #\escape))
          108 
          109 ;;;; is the output interactive or a pipe ?
          110 (defun ttyp()
          111   "return t if the output is a terminal"
          112   ;; we use this variable in case we don't want to be interactive
          113   ;; like when we use a cmd arg to get an image
          114   #+ecl
          115   (if (= 1 (c-ttyp))
          116       t
          117       nil))
          118 
          119 (defun copy-array(from)
          120   "return a new array containing the same elements as the parameter"
          121   (let ((dest (make-array (length from)
          122                           :fill-pointer 0
          123                           :initial-element nil
          124                           :adjustable t)))
          125     (loop for element across from
          126        do
          127          (vector-push element dest))
          128     dest))
          129 
          130 (defun print-with-color(text &optional (color 'reset) (line-number nil))
          131   "Used to display a line with a color"
          132   (format t "~3A| ~a~a~a~%" (if line-number line-number "") (get-color color) text (get-color 'reset)))
          133 
          134 (defmacro foreach-buffer(&body code)
          135   `(progn
          136      (loop for line across *buffer* do ,@code)))          
          137 
          138 (defmacro easy-socket(&body code)
          139   "avoid duplicated code used for sockets"
          140   `(progn
          141 
          142      ;; try tls connection
          143      (usocket:with-client-socket (socket sock host port)
          144        (handler-case
          145            (let ((stream
          146                   (cl+ssl:make-ssl-client-stream
          147                    sock
          148                    :external-format '(:utf-8 :eol-style :lf)
          149                    :unwrap-stream-p t
          150                    ;;:verify nil
          151                    :hostname host)))
          152                  ;; store in metadata that we are using TLS
          153              (setf (location-tls (car *history*)) t)
          154              ,@code)
          155 
          156          ;; fallback to regular plaintext connection if tls fails
          157          (t (c)
          158            (usocket:with-client-socket (socket stream host port)
          159              ,@code))))))
          160 
          161 (defmacro check(identifier &body code)
          162   "Macro to define a new syntax to make 'when' easier for formatted-output function"
          163   `(progn (when (string= ,identifier line-type) ,@code)))
          164 
          165 (defun split(text separator)
          166   "this function split a string with separator and return a list"
          167   (let ((text (concatenate 'string text (string separator))))
          168     (loop for char across text
          169        counting char into count
          170        when (char= char separator)
          171        collect
          172        ;; we look at the position of the left separator from right to left
          173          (let ((left-separator-position (position separator text :from-end t :end (- count 1))))
          174            (subseq text
          175                    ;; if we can't find a separator at the left of the current, then it's the start of
          176                    ;; the string
          177                    (if left-separator-position (+ 1 left-separator-position) 0)
          178                    (- count 1))))))
          179 
          180 (defun formatted-output(line)
          181   "Used to display gopher response with color one line at a time"
          182   
          183   ;; we check that the line is longer than 1 char and that it has tabs
          184   (when (and
          185          (< 1 (length line))
          186          (position #\Tab line))
          187     (let ((line-type (subseq line 0 1))
          188           (field      (split (subseq line 1) #\Tab)))
          189 
          190       ;; if split worked
          191       (when (>= (length field) 4)
          192         (let ((line-number (+ 1 (hash-table-count *links*)))
          193               (text (car field))
          194               (uri  (cadr field))
          195               (host (caddr field))
          196               (port (parse-integer (cadddr field))))
          197 
          198           ;; see RFC 1436
          199           ;; section 3.8
          200           (if (member line-type *allowed-selectors* :test #'equal)
          201               (progn
          202 
          203                 ;; RFC, page 4
          204                 (check "i"
          205                        (print-with-color text))
          206 
          207                 ;; 0 text file
          208                 (check "0"
          209                        (setf (gethash line-number *links*)
          210                              (make-location :host host :port port :uri uri :type line-type :text text))
          211                        (print-with-color text 'file line-number))
          212 
          213                 ;; 1 directory
          214                 (check "1"
          215                        (setf (gethash line-number *links*)
          216                              (make-location :host host :port port :uri uri :type line-type :text text))
          217                        (print-with-color text 'folder line-number))
          218 
          219                 ;; 2 CSO phone-book
          220                 ;; WE SKIP
          221                 (check "2")
          222 
          223                 ;; 3 Error
          224                 (check "3"
          225                        (print-with-color "error" 'red line-number))
          226 
          227                 ;; 4 BinHexed Mac file
          228                 (check "4"
          229                        (print-with-color text))
          230 
          231                 ;; 5 DOS Binary archive
          232                 (check "5"
          233                        (print-with-color "selector 5 not implemented" 'red))
          234 
          235                 ;; 6 Unix uuencoded file
          236                 (check "6"
          237                        (print-with-color "selector 6 not implemented" 'red))
          238 
          239                 ;; 7 Index search server
          240                 (check "7"
          241                        (setf (gethash line-number *links*)
          242                              (make-location :host host :port port :uri uri :type line-type :text text))
          243                        (print-with-color text 'red line-number))
          244 
          245                 ;; 8 Telnet session
          246                 (check "8"
          247                        (print-with-color "selector 8 not implemented" 'red))
          248 
          249                 ;; 9 Binary
          250                 (check "9"
          251                        (setf (gethash line-number *links*)
          252                              (make-location :host host :port port :uri uri :type line-type :text text))
          253                        (print-with-color text 'red line-number))
          254 
          255                 ;; + redundant server
          256                 (check "+"
          257                        (print-with-color "selector + not implemented" 'red))
          258 
          259                 ;; T text based tn3270 session
          260                 (check "T"
          261                        (print-with-color "selector T not implemented" 'red))
          262 
          263                 ;; g GIF file
          264                 (check "g"
          265                        (setf (gethash line-number *links*)
          266                              (make-location :host host :port port :uri uri :type line-type :text text))
          267                        (print-with-color text 'red line-number))
          268 
          269                 ;; I image
          270                 (check "I"
          271                        (setf (gethash line-number *links*)
          272                              (make-location :host host :port port :uri uri :type line-type :text text))
          273                        (print-with-color text 'red line-number))
          274 
          275                 ;; h http link
          276                 (check "h"
          277                        (setf (gethash line-number *links*) uri)
          278                        (print-with-color text 'http line-number))) ;;;; end of known types
          279 
          280               ;; unknown type
          281               (print-with-color (format nil
          282                                         "invalid type ~a : ~a" line-type text)
          283                                 'red)))))))
          284 
          285 (defun download-binary(host port uri)
          286   (easy-socket
          287    ;; sending the request to the server
          288    (format stream "~a~a~a" uri #\Return #\Newline)
          289    (force-output stream)
          290 
          291    ;; save into a file in /tmp
          292    (let* ((filename (subseq uri (1+ (position #\/ uri :from-end t))))
          293           (path (concatenate 'string "/tmp/" filename)))
          294      (with-open-file (output path
          295                              :element-type '(unsigned-byte 8)
          296                              :direction :output :if-exists :supersede)
          297        (let ((buf (make-array 4096 :element-type '(unsigned-byte 8))))
          298          (loop for pos = (read-sequence buf stream)
          299             while (plusp pos)
          300             do
          301               (format t ".")
          302               (force-output)
          303               (write-sequence buf output :end pos)))
          304        (format t "~%File downloaded into ~a (~a bytes)~%" path (file-length output))))))
          305 
          306 
          307 (defun getpage(host port uri &optional (search nil))
          308   "send a request and store the answer (in *buffer* if text or save a file if binary)"
          309 
          310   ;; we reset the buffer
          311   (setf *buffer*
          312         (make-array 200
          313                     :fill-pointer 0
          314                     :initial-element nil
          315                     :adjustable t))
          316   (setf *last-bandwidth-in* 0)
          317 
          318   (let ((real-time (get-internal-real-time)))
          319     ;; we prepare informations about the connection
          320     (easy-socket
          321      ;; sending the request to the server
          322      (if search
          323          (format stream "~a        ~a~a~a" uri search #\Return #\Newline)
          324          (format stream "~a~a~a"       uri #\Return #\Newline))
          325      (force-output stream)
          326 
          327      ;; not binary
          328      ;; for each line we receive we store it in *buffer*
          329      (loop for line = (read-line stream nil nil)
          330         count line into lines
          331         while line
          332         do
          333         ;; count bandwidth usage
          334         (incf *total-bandwidth-in* (length line))
          335         (incf *last-bandwidth-in* (length line))
          336         ;; increase array size if needed
          337           (when (= lines (- (array-total-size *buffer*) 1))
          338             (adjust-array *buffer* (+ 200 (array-total-size *buffer*))))
          339           (vector-push line *buffer*)))
          340 
          341 
          342     ;; we store the duration of the connection
          343     (setf *duration* (float (/ (- (get-internal-real-time) real-time)
          344                                internal-time-units-per-second)))))
          345 
          346 (defun g(key)
          347   "browse to the N-th link"
          348   (let ((destination (gethash key *links*)))
          349     (when destination
          350       (cond
          351         ;; visit a gopher link
          352         ((location-p destination)
          353          (visit destination))
          354         ;; visit http link
          355         ((search "URL:" destination)
          356          (kiosk-mode
          357           (uiop:run-program (list "xdg-open"
          358                                   (subseq destination 4)))))))))
          359 
          360 (defun filter-line(text)
          361   "display only lines containg text"
          362   (setf *previous-buffer* (copy-array *buffer*))
          363   (setf *buffer* (make-array 400
          364                              :fill-pointer 0
          365                              :initial-element nil
          366                              :adjustable t))
          367   ;; we create a new buffer from the current
          368   ;; with only lines matching the string (no regex)
          369   (loop for line across *previous-buffer*
          370      do
          371        (when (search text (car (split (subseq line 1) #\Tab)) :test #'char-equal)
          372          (vector-push line *buffer*)))
          373   (display-interactive-menu))
          374 
          375 (defun load-file-menu(path)
          376   "load a local file with a gophermap syntax and display it as a menu"
          377   ;; we set the buffer
          378   (setf *buffer*
          379         (make-array 200
          380                     :fill-pointer 0
          381                     :initial-element nil
          382                     :adjustable t))
          383 
          384   (with-open-file (stream path
          385                           :direction :input)
          386     (loop for line = (read-line stream nil nil)
          387        while line
          388        do
          389          (vector-push line *buffer*))))
          390 
          391 (defun p()
          392   "browse back to previous menu"
          393   (when (<= 2 (length *history*))
          394     (pop *history*)
          395     (visit (pop *history*))))
          396 
          397 (defun r()
          398   "reload the previous menu"
          399   (when (<= 1 (length *history*))
          400     (visit (pop *history*))))
          401 
          402 (defun s(number)
          403   "show url for the link $NUMBER"
          404   (let ((destination (gethash number *links*)))
          405     (if (not destination)
          406         (format t "No link ~a~%" number)
          407         (format t "gopher://~a~a/~a~a~%"
          408                 (location-host destination)
          409                 (let ((port (location-port destination)))
          410                   (if (= 70 port)
          411                       ""
          412                       (format nil ":~a" port)))
          413                 (location-type destination)
          414                 (location-uri destination)))))
          415 
          416 (defun help-shell()
          417   "show help for the shell"
          418   (format t "number            : go to link n~%")
          419   (format t "p or /            : go to previous page~%")
          420   (format t "h                 : display history~%")
          421   (format t "sNUMBER           : show url for link $NUMBER~%")
          422   (format t "r or *            : reload the page~%")
          423   (format t "help              : show this help~%")
          424   (format t "d                 : dump the raw reponse~%")
          425   (format t "/ text            : display online lines matching text~%")
          426   (format t "^D or x or q or . : quit clic~%"))
          427 
          428 (defun parse-url(url)
          429   "parse a gopher url and return a location"
          430   (cond ((or
          431           (string= "--help" url)
          432           (string= "-h"     url))
          433          (help-shell)
          434          (quit))
          435 
          436         ((string= "-k" url)
          437          #+openbsd
          438          (c-kiosk-pledge)
          439          (setf *kiosk-mode* t))
          440 
          441         ((string= "-t" url)
          442          (setf *no-split* t))
          443 
          444         ((= 0 (or (search "file://" url) 1))
          445          (load-file-menu (subseq url 7))
          446          (make-location :host 'local-file
          447                         :text url
          448                         :port nil
          449                         :type "1"
          450                         :uri url))
          451 
          452         (t
          453          (let ((url (if (search "gopher://" url)
          454                         (subseq url 9)
          455                         url)))
          456 
          457            ;; splitting with / to get host:port and uri
          458            ;; splitting host and port to get them
          459            (let* ((infos      (split url #\/))
          460                   (host-port (split (pop infos) #\:)))
          461 
          462              ;; create the location to visit
          463              (make-location  :host (pop host-port)
          464                              ;; default to port 70 if not supplied
          465                              :port (if host-port ;; <- empty if no port given
          466                                        (parse-integer (car host-port))
          467                                        70)
          468 
          469                              :text url
          470 
          471                              ;; if type is empty we default to "1"
          472                              :type (let ((type (pop infos)))
          473                                      (if (< 0 (length type)) type "1"))
          474                           
          475                              ;; glue remaining args between them
          476                              :uri (format nil "~{/~a~}" infos)))))))
          477 
          478 (defun get-argv()
          479   "Parse argv and return it"
          480   #+ecl
          481   (cdr (si::command-args)))
          482 
          483 (defun user-input(input)
          484   (cond
          485     ;; show help
          486     ((string= "help" input)
          487      (help-shell))
          488 
          489     ((search "s" input)
          490      (s (parse-integer (subseq input 1))))
          491 
          492     ((or
          493       (string= "*" input)
          494       (string= "ls" input)
          495       (string= "r" input))
          496      (r))
          497 
          498     ;; go to previous page
          499     ((or
          500       (string= "/" input)
          501       (string= "cd .." input)
          502       (string= "p" input))
          503      (p))
          504 
          505     ;; search a pattern in a menu
          506     ;; syntax /pattern
          507     ((and
          508       (search "/" input)
          509       (> (length input) 1))
          510      (filter-line (subseq input 1)))
          511 
          512     ;; same as previously
          513     ;; but with syntax / pattern
          514     ((= 0 (or (search "/ " input) 1))
          515      (filter-line (subseq input 2)))
          516 
          517     ;; dump raw informations
          518     ((string= "d" input)
          519      (foreach-buffer
          520       (format t "~a~%" line)))
          521 
          522     ;; exit
          523     ((or
          524       (eql nil input)
          525       (string= "NIL" input)
          526       (string= "." input)
          527       (string= "exit" input)
          528       (string= "x" input)
          529       (string= "q" input))
          530      'end)
          531 
          532     ;; show history
          533     ((string= "h" input)
          534       (setf *links* (make-hash-table))
          535         (loop for element in *history*
          536           do
          537             (formatted-output
          538               (format nil "~a~a        ~a        ~a        ~a~%"
          539                     (location-type element)
          540                     (location-text element)
          541                     (location-uri element)
          542                     (location-host element)
          543                     (location-port element)))))
          544 
          545 
          546     ;; follow a link
          547     (t
          548      ;; we ignore error in case of bad input
          549      ;; just do nothing
          550      (ignore-errors
          551        (g (parse-integer input))))))
          552 
          553 (defun display-interactive-binary-file()
          554   "call xdg-open on the binary file"
          555   (kiosk-mode
          556    (let* ((location (car *history*))
          557           (filename (subseq ;; get the text after last /
          558                      (location-uri location)
          559                      (1+ (position #\/
          560                                    (location-uri location)
          561                                    :from-end t))))
          562           (filepath (concatenate 'string "/tmp/" (or filename "index"))))
          563      (uiop:run-program (list "xdg-open" filepath)))))
          564   
          565 (defun display-text-stdout()
          566   "display the buffer to stdout"
          567   (foreach-buffer
          568    (format t "~a~%" line)))
          569 
          570 (defun display-with-pager()
          571   "display the buffer using $PAGER"
          572   (let* ((uri (location-uri (car *history*)))
          573          (filename (subseq uri (1+ (position #\/ uri :from-end t))))
          574          (path (concatenate 'string "/tmp/" (or filename "index"))))
          575     (with-open-file (output path
          576                             :direction :output
          577                             :if-does-not-exist :create
          578                             :if-exists :supersede)
          579       (foreach-buffer
          580        (format output "~a~%" line)))
          581     (uiop:run-program (nconc
          582                        (if (uiop:getenv "PAGER")
          583                            (split (uiop:getenv "PAGER") #\Space)
          584                            (list "less"))
          585                        (list path))
          586                       :input :interactive
          587                       :output :interactive)))
          588 
          589 ;; display a text file using the pager by piping
          590 ;; the data to out, no temp file
          591 (defun display-with-pager-kiosk()
          592   "display the buffer to stdout, we don't use system() in kiosk mode"
          593   (loop for line across *buffer*
          594         do
          595         (format t "~a~%" line)))
          596 
          597 (defun display-interactive-menu()
          598   "display a menu"
          599   ;; we store the user input outside of the loop
          600   ;; so if the user doesn't want to scroll
          601   ;; we break the loop and then execute the command
          602   (let ((input nil))
          603     (let ((rows (if *no-split*
          604                     -1
          605                     (* (- (c-termsize) 1))))) ; -1 for command bar
          606 
          607       (loop for line across *buffer*
          608          counting line into row
          609          do
          610            (formatted-output line)
          611 
          612 
          613          ;; split and ask to scroll or to type a command
          614            (when (= row rows)
          615              (setf row 0)
          616              (format t "~a   press enter or a shell command: "
          617                      (if *kiosk-mode* "KIOSK" ""))
          618              (force-output)
          619              (let ((first-input (read-char *standard-input* nil nil t)))
          620                (cond
          621                  ((not first-input)
          622                   (format t "~%") ;; display a newline
          623                   (setf input "x") ;; we exit
          624                   (loop-finish))
          625                  ((char= #\NewLine first-input)
          626                   ;; we hide previous line (prompt)
          627                   (format t "'~C[A~C[K~C" #\Escape #\Escape #\return))
          628                  (t
          629                   (unread-char first-input)
          630                   (let ((input-text (format nil "~a" (read-line nil nil))))
          631                     (setf input input-text)
          632                     (loop-finish)))))))
          633 
          634       ;; in case of shell command, do it
          635       (if input
          636           (user-input input)
          637           (when (< (length *buffer*) rows)
          638             (dotimes (i (- rows (length *buffer*)))
          639               (format t "~%")))))))
          640 
          641 (defun pipe-text(host port uri)
          642   "pipe text to stdout, with stdout not a TTY output"
          643   (getpage host port uri)
          644   (foreach-buffer
          645    (format t "~a~%" line)))
          646 
          647 (defun pipe-binary(host port uri)
          648   "pipe data to stdout, with stdout not a TTY output"
          649   (easy-socket
          650    (format stream "~a~a~a" uri #\Return #\Newline)
          651    (force-output stream)
          652 
          653    ;; write to the standard output
          654    (let ((buf (make-array 4096 :element-type '(unsigned-byte 8))))
          655      (loop for pos = (read-sequence buf stream)
          656         while (plusp pos)
          657         do
          658           (write-sequence buf *standard-output* :end pos)))))
          659 
          660 (defun pipe-to-stdout(destination)
          661   "fetch data and output to stdout without storing anything"
          662 
          663   (if (or
          664        (string= "0" (location-type destination))
          665        (string= "1" (location-type destination))
          666        (string= "7" (location-type destination)))
          667 
          668       (pipe-text (location-host destination)
          669                  (location-port destination)
          670                  (location-uri  destination))
          671 
          672       (pipe-binary (location-host destination)
          673                    (location-port destination)
          674                    (location-uri  destination))))
          675 
          676 (defun visit(destination)
          677   "fetch and display content interactively"
          678 
          679   ;; add it to the history !
          680   (push destination *history*)
          681 
          682   (let ((type
          683          (cond
          684 
          685            ;; fetch a menu
          686            ((string= "1" (location-type destination))
          687             (if (eql 'local-file (location-host destination))
          688                 'menu
          689                 (getpage (location-host destination)
          690                          (location-port destination)
          691                          (location-uri  destination)))
          692             'menu)
          693 
          694            ;; fetch a text file
          695            ((string= "0" (location-type destination))
          696             (getpage (location-host destination)
          697                      (location-port destination)
          698                      (location-uri  destination))
          699             'text)
          700 
          701            ;; fetch a menu after search
          702            ((string= "7" (location-type destination))
          703             (format t "Input : ")
          704             (let ((user-input (read-line nil nil)))
          705               (getpage (location-host destination)
          706                        (location-port destination)
          707                        (location-uri  destination)
          708                        user-input))
          709             'menu)
          710 
          711            ;; if not type 0 1 7 then it's binary
          712            (t
          713             (kiosk-mode
          714              (download-binary (location-host destination)
          715                               (location-port destination)
          716                               (location-uri destination)))
          717             'binary))))
          718 
          719     ;; we reset the links table ONLY if we have a new menu
          720     ;; we also keep the last menu buffer
          721     (when (eql type 'menu)
          722       (setf *previous-buffer* (copy-array *buffer*))
          723       (setf *links* (make-hash-table)))
          724 
          725 
          726     (if (eql type 'menu)
          727         (display-interactive-menu)
          728         (progn
          729           (if (eql type 'text)
          730               (if *kiosk-mode*
          731                   (display-with-pager-kiosk)
          732                   (display-with-pager))
          733             (kiosk-mode (display-interactive-binary-file)))
          734            ;; redraw last menu
          735            ;; we need to get previous buffer and reset links numbering
          736            (pop *history*)
          737            (when (and
          738                   *previous-buffer*
          739                   (not *kiosk-mode*))
          740              (setf *buffer* (copy-array *previous-buffer*))
          741              (setf *links* (make-hash-table))
          742              (display-interactive-menu))))))
          743 
          744 
          745 (defun display-prompt()
          746   "show the prompt and helper"
          747   (let ((last-page (car *history*)))
          748     (format t "~a~agopher://~a:~a/~a~a (~as, ~aKb) / (p)rev (r)edisplay (h)istory : "
          749             (if *kiosk-mode* "KIOSK " "")
          750             (if (location-tls last-page) "**TLS** " "UNSECURE ")
          751             (location-host last-page)
          752             (location-port last-page)
          753             (location-type last-page)
          754             (location-uri last-page)
          755             *duration*
          756             (floor (/ *last-bandwidth-in* 1024.0))))
          757   (force-output))
          758 
          759 (defun shell()
          760   "Shell for user interaction"
          761   (display-prompt)
          762 
          763   ;; we loop until X or Q is typed
          764   (loop for input = (format nil "~a" (read-line nil nil))
          765      while (not (or
          766                  (string= "NIL" input) ;; ^D
          767                  (string= "exit" input)
          768                  (string= "x" input)
          769                  (string= "q" input)))
          770      do
          771        (when (eq 'end (user-input input))
          772          (loop-finish))
          773        (display-prompt)))
          774 
          775 (defun main()
          776   "entry function of clic, we need to determine if the usage is one of
          777   the 3 following cases : interactive, not interactive or
          778   piped. Interactive is the state where the user will browse clic for
          779   multiple content. Not interactive is the case where clic is called
          780   with a parameter not of type 1, so it will fetch the content,
          781   display it and exit and finally, the redirected case where clic will
          782   print to stdout and exit."
          783 
          784   ;; pledge support on OpenBSD
          785   #+openbsd
          786   (c-pledge)
          787 
          788   ;; re-enable SIGINT (Ctrl+C) disabled for loading clic
          789   (ext:set-signal-handler ext:+sigint+ 'quit)
          790 
          791   (handler-case
          792     (let ((destination (car (last
          793                              (loop for element in (get-argv)
          794                                 collect (parse-url element))))))
          795 
          796       ;; if we didn't passed a url as parameter, use a default
          797       (if (not (location-p destination))
          798           (setf destination (make-location :host "gopherproject.org" :port 70 :uri "/" :type "1" :text "gopherproject")))
          799 
          800       ;; is there an output redirection ?
          801       (if (ttyp)
          802           (progn
          803             (clear)
          804             ;; if we don't ask a menu, not going interactive
          805             (if (not (string= "1" (location-type destination)))
          806                 ;; not interactive
          807                 (visit destination)
          808                 ;; if user want to drop from first page we need
          809                 ;; to look it here
          810                 (when (not (eq 'end (visit destination)))
          811                   ;; we continue to the shell if we are in a terminal
          812                   (shell)))
          813             (format t "~a kB in.~%" (floor (/ *total-bandwidth-in* 1024.0))))
          814           (pipe-to-stdout destination)))
          815     (t (error)
          816         (progn
          817           (format t "Something went wrong~%")
          818           (print error)))))
          819 
          820 ;; we allow ecl to use a new kind of argument
          821 ;; not sure how it works but that works
          822 #+ecl
          823 (defconstant +uri-rules+
          824   '(("*DEFAULT*" 1 "" :stop)))