src/main.lisp @ f592e6093609
default tip
Merge.
author |
Steve Losh <steve@stevelosh.com> |
date |
Mon, 13 Apr 2020 21:29:44 -0400 |
parents |
1d2776f7fa4a da80130c3bbf |
children |
(none) |
(in-package :brows)
;;;; State --------------------------------------------------------------------
(defparameter *regex*
(ppcre:create-scanner
;; https://gist.github.com/gruber/249502
"(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))"
:case-insensitive-mode t))
(defparameter *version* (asdf:component-version (asdf:find-system :brows)))
(defparameter *urls* nil)
(defparameter *pos* 0)
(defparameter *actions* (make-hash-table))
;;;; Utils --------------------------------------------------------------------
(defun read-standard-input-into-string ()
(with-output-to-string (result)
(let* ((buffer-size 4096)
(buffer (make-array buffer-size :element-type 'character)))
(iterate
(for bytes-read = (read-sequence buffer *standard-input*))
(write-sequence buffer result :start 0 :end bytes-read)
(while (= bytes-read buffer-size))))))
(defun clamp (lo v hi)
(max lo (min hi v)))
(defun incf-pos (delta)
(setf *pos* (clamp 0 (+ *pos* delta) (1- (length *urls*)))))
;;;; Actions ------------------------------------------------------------------
(defclass action ()
((tty :initarg :tty :accessor tty)
(keys :initarg :keys :accessor keys)
(thunk :initarg :thunk :accessor thunk)))
(defun create-action (thunk keys tty)
(let ((action (make-instance 'action :thunk thunk :keys keys :tty tty)))
(dolist (key (alexandria:ensure-list keys))
(setf (gethash key *actions*) action))))
(defmacro define-action (keys program &key
(url 'url)
(arguments `(list ,url))
exit
tty)
`(create-action
(lambda (,url)
(external-program:run ,program ,arguments
,@(if tty
'(:output t :input t)
'()))
(when ,exit (throw 'done nil)))
,keys
,tty))
(defun perform-action (action)
(funcall (thunk action) (aref *urls* *pos*))
(boots:redraw :full (tty action)))
;;;; Input --------------------------------------------------------------------
(defun find-urls (string)
(let ((matches (ppcre:all-matches-as-strings
*regex* string
;; ccl can't take non-simple-strings as external program
;; args, because fuck me
:sharedp nil)))
(coerce (remove-duplicates matches :test #'string-equal) 'vector)))
(defun read-input (path)
(if (equal "-" path)
(read-standard-input-into-string)
(alexandria:read-file-into-string path)))
(defun process-input (input)
(find-urls input))
;;;; UI -----------------------------------------------------------------------
(defun draw (pad)
(boots:draw pad 0 0 (format nil "brows v~A" *version*))
(iterate
(for y :from 2 :below (boots:height pad))
(for url :in-vector *urls* :with-index i)
(for selected = (= i *pos*))
(when selected
(boots:draw pad 0 y "-> " (boots:attr :bold t)))
(boots:draw pad 3 y url (boots:attr :bold selected))))
(defun init ()
(let ((*package* (find-package :brows)))
(load "~/.browsrc" :if-does-not-exist nil))
(setf *urls* (process-input (read-input "-"))))
(defun main ()
(iterate
(boots:redraw)
(for event = (boots:read-event))
(for action = (gethash event *actions*))
(if action
(perform-action action)
(case event
((#\Q #\q) (return-from main))
((#\k :up) (incf-pos -1))
((#\j :down) (incf-pos 1))
(t nil)))))
(defun run ()
(catch 'done
(with-open-file (input "/dev/tty" :direction :input)
(with-open-file (output "/dev/tty" :direction :output :if-exists :append)
(boots/terminals/ansi:with-ansi-terminal (terminal :input-stream input :output-stream output)
(boots:with-screen (screen terminal :root (boots:make-canvas :draw #'draw))
(init)
(main)))))))
;;;; CLI ----------------------------------------------------------------------
(adopt:define-string *documentation*
"Brows is a utility for finding links in a chunk of text and presenting a ~
nice text-based UI for opening them. It's written (and customizable) in ~
Common Lisp.")
(defparameter *ui*
(adopt:make-interface
:name "brows"
:usage "[OPTIONS]"
:summary "Find links and present a menu for opening them in a browser."
:help *documentation*
:examples '(("Present a menu for opening some links:" .
"curl http://stevelosh.com/ | brows"))
:contents (list (adopt:make-option 'help
:help "Display help and exit."
:long "help"
:short #\h
:reduce (constantly t)))))
(defun toplevel ()
(handler-case
(multiple-value-bind (arguments options) (adopt:parse-options *ui*)
(when (gethash 'help options)
(adopt:print-help-and-exit *ui*))
(when arguments
(error "Unrecognized command line arguments: ~S" arguments))
(run))
(error (c) (adopt:print-error-and-exit c))))