aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorColin Okay <okay@toyful.space>2022-03-11 08:35:55 -0600
committerColin Okay <okay@toyful.space>2022-03-11 08:35:55 -0600
commit08ba2769abb7a36817a725d30d64cfd36f5bcf32 (patch)
tree3068a5d9e1a4afd06a5f8b236658d4094e30dfef /lib
parent62afeb3f21d3d8e8db045a001271e686e944a049 (diff)
separated app and lib modules, -osicat dep, +packages.lisp
Diffstat (limited to 'lib')
-rw-r--r--lib/lib.lisp451
-rw-r--r--lib/oneliner.lisp59
-rw-r--r--lib/package.lisp20
-rw-r--r--lib/prompt.lisp30
-rw-r--r--lib/running.lisp47
-rw-r--r--lib/state.lisp62
-rw-r--r--lib/term.lisp15
-rw-r--r--lib/util.lisp37
8 files changed, 721 insertions, 0 deletions
diff --git a/lib/lib.lisp b/lib/lib.lisp
new file mode 100644
index 0000000..f49201a
--- /dev/null
+++ b/lib/lib.lisp
@@ -0,0 +1,451 @@
+;;;; main.lisp -- oneliners.cli entrypoint
+
+;; Copyright (C) 2022 Colin Okay
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+(in-package :oneliners.cli)
+
+;;; CONFIG AND RESULTS FILE LOCATIONS
+
+(defvar *ol-output-timeout* 1)
+
+(defun run-item (ident args &key force-clip (timeout nil timeout-p))
+ (with-oneliner (ol ident)
+ (let ((*ol-output-timeout* (if timeout-p timeout *ol-output-timeout*)))
+ (bind-vars-and-run-oneliner ol args force-clip))))
+
+(defun bind-vars-and-run-oneliner (ol args &optional force-clip)
+ (let* ((oneliner (oneliner-oneliner ol))
+ (runstyle (oneliner-runstyle ol))
+ (pos-args (collect-positional-arguments oneliner))
+ (named-args (collect-named-arguments oneliner)))
+
+ (when (or (not (oneliner-isflagged ol))
+ (y-or-n-p "This oneliner is flagged. Are you sure you want to run it?"))
+ ;; substitute positional args
+ (loop for param in pos-args
+ for arg in args
+ do (setf oneliner (str:replace-all param arg oneliner)))
+ ;; substitute named args
+ (setf args
+ (mapcar (lambda (s) (str:split "=" s))
+ (nthcdr (length pos-args) args)))
+ (loop for var in named-args
+ for bound = (assoc (subseq var 1) args :test #'equal)
+ when bound
+ do (setf oneliner
+ (str:replace-all var (second bound) oneliner)))
+
+ (handle-run-oneliner oneliner (or force-clip (equalp runstyle "manual"))))))
+
+(defun handle-run-oneliner (ol &optional clip)
+ (if clip
+ (progn (trivial-clipboard:text ol)
+ (format t "Copied oneliner to clipboard~%"))
+ (progn
+ (ensure-config)
+ (format t "Attempting to run:~%")
+ (princ ol)
+ (princ #\newline)
+ (princ #\newline)
+ (run-with-shell ol :shell-name (or (get-shell) "bash")))))
+
+
+(defun make-fresh-config ()
+ (format t "No configuration file has been found. Running Setup~%~%")
+ (setf *config*
+ (make-config
+ :host (prompt "Oneliner Instance Host: "
+ :prefill "https://api.oneliners.wiki")
+ :shell (prompt "With which shell should commands be run: "
+ :prefill "bash")))
+ (write-config-to-disk)
+ (format t "Configuration has been written to ~a~%. Edit this at any time.~%~%"
+ (config-file)))
+
+(defun fetch-config-from-disk ()
+ (let ((conf
+ (uiop:with-safe-io-syntax ()
+ (uiop:read-file-form (config-file)))))
+ (assert (valid-config-p conf) () "Invalid configuration file")
+ (setf *config* conf)))
+
+(defun ensure-config ()
+ (unless (uiop:file-exists-p (config-file))
+ (make-fresh-config))
+ (fetch-config-from-disk))
+
+
+;;; UTILITIES
+
+
+;; (defun cached-result (n)
+;; (when (uiop:file-exists-p (cached-oneliners-file))
+;; (let ((contents (with-open-file (input (cached-oneliners-file)) (read input))))
+;; (etypecase n
+;; (integer
+;; (find n contents :key (lambda (x) (getf x :id))))
+;; (string
+;; (find n contents :key (lambda (x) (getf x :name)) :test #'equal))))))
+
+
+(defmacro with-oneliner ((var name-or-id) &body body)
+ (assert (symbolp var))
+ (let ((nvar (gensym)))
+ `(let ((,nvar ,name-or-id))
+ (a:if-let (,var (the-oneliner ,nvar))
+ (progn ,@body)
+ (format t "Could not find the oneliner specified by ~a~%" ,nvar)))))
+
+
+
+
+
+;;; API REQUEST FUNCTIONS
+
+(defun the-oneliner (name-or-id)
+ "Get the oneliner with name-or-id. Try to fetch from local cache,
+and, failing that, try to fetch from configured server."
+ (a:if-let ((ol (cached-result name-or-id)))
+ ol
+ (progn
+ (ensure-config)
+ (a:when-let (ol
+ (api:request-with (:host (host))
+ (jonathan:parse
+ (api:get--oneliner-entry name-or-id))))
+ (merge-into-cache (list ol))
+ ol))))
+
+(defun flag-item (ident)
+ (with-oneliner (ol ident)
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (api:put--oneliner-entry-flag (getf ol :id) :token (api-token) :value "true"))))
+
+(defun unflag-item (item-number)
+ (with-oneliner (ol item-number)
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (api:put--oneliner-entry-flag (getf ol :id) :token (api-token) :value "false"))))
+
+(defun lock-item (item-number)
+ (with-oneliner (ol item-number)
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (api:put--oneliner-oneliner-locked (getf ol :id) :token (api-token) :value "true"))))
+
+(defun unlock-item (item-number)
+ (with-oneliner (ol item-number)
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (api:put--oneliner-oneliner-locked (getf ol :id) :token (api-token) :value "false"))))
+
+
+
+;;; PRINTING ONELINERS
+
+
+(defun print-item-explanation (name-or-number)
+ (with-oneliner (ol name-or-number)
+ (set-term-width)
+ (print-oneliner-result-for-user ol)
+ (a:when-let (explanation (getf ol :explanation))
+ (format t "EXPLANATION:~%~%")
+ (princ
+ (string-trim
+ '(#\newline #\space #\tab)
+ (if (str:starts-with? (getf ol :oneliner) explanation)
+ (subseq explanation (length (getf ol :oneliner)))
+ explanation)))
+ (terpri))))
+
+
+(defun add-new-oneliner ()
+ (ensure-config)
+ (assert (api-token) () "Cannot add a oneliner without an api token.")
+ (let* ((oneliner
+ (prompt "Oneliner: "
+ :expect 'valid-oneliner-string-p
+ :retry-text "Oneliners must contain at least one command: "))
+ (name
+ (string-trim
+ '(#\space #\newline #\tab #\linefeed)
+ (prompt "Name (leave blank for none): "
+ :expect 'valid-oneliner-name-p
+ :retry-text "Must begin with a letter contain only letters, numbers, - and _.")))
+ (init-tags
+ (tags-from-oneliner oneliner))
+ (brief
+ (prompt "Brief Description: "
+ :expect 'valid-brief-description-p
+ :retry-text "Too long. Must be <= 72 characters: "))
+ (tags
+ (progn
+ (format t "Tags include: ~{~a ~}~%" init-tags)
+ (append init-tags
+ (ppcre:split " +"
+ (prompt "More tags here, or Enter to skip: ")))))
+ (runstyle
+ (string-upcase
+ (prompt "Runstyle (auto or manual): "
+ :expect 'valid-runstyle-p
+ :retry-text "Must be (auto or manual): "
+ :prefill "auto")))
+ (explanation
+ (when (y-or-n-p "Provide an explanation?")
+ (string-from-editor
+ (format nil "~a~%~%" oneliner)))))
+ (api:request-with
+ (:host (host)
+ :body (jonathan:to-json
+ (list :oneliner oneliner
+ :name (if (plusp (length name)) name :null)
+ :tags tags
+ :brief brief
+ :explanation explanation
+ :runstyle runstyle))
+ :content-type "application/json")
+ (api:post--oneliner :token (api-token))
+ (format t "Added~%"))))
+
+(defun edit-item (ident)
+ (with-oneliner (ol ident)
+ (ensure-config)
+ (assert (api-token) () "Cannot edit a oneliner without an api token.")
+ (let* ((oneliner
+ (prompt "Oneliner: "
+ :expect 'valid-oneliner-string-p
+ :retry-text "Oneliners must contain at least one command: "
+ :prefill (getf ol :oneliner)))
+ (name
+ (string-trim
+ '(#\space #\newline #\tab #\linefeed)
+ (prompt "Name (leave blank for none): "
+ :expect 'valid-oneliner-name-p
+ :retry-text "Must begin with a letter contain only letters, numbers, - and _."
+ :prefill (if (getf ol :name) (getf ol :name) ""))))
+ (brief
+ (prompt "Brief Description: "
+ :expect 'valid-brief-description-p
+ :retry-text "Too long. Must be <= 72 characters: "
+ :prefill (getf ol :brief)))
+ (init-tags
+ (tags-from-oneliner oneliner))
+ (tags
+ (progn
+ (format t "Tags include: ~{~a ~}~%" init-tags)
+ (append init-tags
+ (ppcre:split " +"
+ (prompt "More tags here, or Enter to skip: "
+ :prefill (str:join " "
+ (set-difference
+ (getf ol :tags)
+ init-tags
+ :test 'equal)))))))
+ (runstyle
+ (string-upcase
+ (prompt "Runstyle (auto or manual): "
+ :expect 'valid-runstyle-p
+ :retry-text "Must be (auto or manual): "
+ :prefill (getf ol :runstyle))))
+ (explanation
+ (when (y-or-n-p "Provide an explanation?")
+ (string-from-editor (getf ol :explanation)))))
+ (let ((new-item
+ (list :oneliner oneliner
+ :tags tags
+ :brief brief
+ :name (if (plusp (length name)) name :null)
+ :explanation explanation
+ :runstyle runstyle)))
+ (api:request-with
+ (:host (host)
+ :body (jonathan:to-json
+ new-item)
+ :content-type "application/json")
+ (api:patch--oneliner-entry-edit (getf ol :id) :token (api-token))
+ (update-cached-item new-item)
+ (format t "OK~%"))))))
+
+(defun request-invite-code ()
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (let ((invite (jonathan:parse (api:post--invite :token (api-token)))))
+ (format t "Invite Code: ~a~%Expires: ~a~%"
+ (getf invite :code)
+ (getf invite :expires)))))
+
+(defun login (user pass)
+ (ensure-config)
+ (a:when-let (response (jonathan:parse
+ (api:request-with
+ (:host (host)
+ :body (jonathan:to-json (list :password pass :handle user))
+ :content-type "application/json")
+ (api:post--access))))
+ (setf (api-token) (getf response :token)
+ (contributor-handle) user)
+ (write-config-to-disk)
+ (format t "Access token written to ~a~%You may now make contributions to the wiki!.~%"
+ (config-file))))
+
+(defun change-pw (current new repeated)
+ (unless (equal new repeated)
+ (error "The new password doesn't match the repeated value. Double check."))
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (api:put--contributor-who-password (contributor-handle)
+ :token (api-token)
+ :value new
+ :repeated new
+ :current current)))
+
+(defparameter +agree-to-the-unlicense+
+ "By creating this contributor account, I agree that my contributions
+ be released into the public domain, for the benefit of the public at
+ large, and to the detriment of my heirs and successors. I intend
+ this dedication to be an overt act of relinquishment in perpetuity
+ of all present and future rights to my contributions under software
+ copyright law copyright law. More specifically, I agree to release all of my
+ contributions using The Unlicense. (see https://unlicense.org/)")
+
+
+(defun prompt-for-signature ()
+ (if (y-or-n-p "Provide a contributor signature about yourself? ")
+ (prompt "Go ahead: ")
+ ""))
+
+(defun change-signature ()
+ (let ((new-sig
+ (prompt-for-signature)))
+ (ensure-config)
+ (api:request-with
+ (:host (host)
+ :body (jonathan:to-json (list :signature new-sig))
+ :content-type "application/json")
+ (api:put--contributor-who-signature (contributor-handle) :token (api-token))
+ (format t "Your signature was changed.~%"))))
+
+(defun print-contributor (contributor)
+ (format t "~20a ~@[-- ~a~]~%"
+ (getf contributor :handle)
+ (getf contributor :signature)))
+
+(defun show-contributor (name)
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (a:when-let (contributor
+ (api:get--contributor-who name))
+ (print-contributor (jonathan:parse contributor)))))
+
+(defun redeem-invite (token name pass)
+ (ensure-config )
+ (when (yes-or-no-p +agree-to-the-unlicense+)
+ (api:request-with
+ (:host (host)
+ :body (jonathan:to-json (list :handle name
+ :password1 pass :password2 pass
+ :signature (prompt-for-signature)))
+ :content-type "application/json")
+ (api:post--invite-redeem-code token)
+ (format t "Account made for ~a. You may log in now~%" name))))
+
+;;TODO: check this .. shouldnt access be a username???
+(defun revoke-access ()
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (api:delete--access-access (api-token) :token (api-token))
+ (format t "You were logged out~%")))
+
+(defun update-cached-item (item)
+ (merge-into-cache (list item)))
+
+(defun print-oneliner-result-for-user (oneliner)
+ (unless *term-width* (set-term-width)) ; setting here as a fallback, can set it elswere if desired.
+ (let* ((title-line-format-str
+ (concatenate 'string "~" (prin1-to-string *term-width*) "<[~a]~;~a~;~a~>~%"))
+ (tags-line-format-string
+ (concatenate 'string "~" (prin1-to-string *term-width*) "<~a~;by ~a~>~%")))
+ (loop repeat *term-width* do (princ #\_))
+ (terpri)
+ (format t title-line-format-str
+ (getf oneliner :id)
+ (or (getf oneliner :name) " ")
+ (format nil "~:[ ~;⚠~]~:[ ~;🔒~]~:[ ~;📋~]"
+ (getf oneliner :isflagged)
+ (getf oneliner :islocked)
+ (equalp "manual" (getf oneliner :runstyle))))
+ (format t tags-line-format-string
+ (format nil "tags: ~{~a~^ ~}"
+ (getf oneliner :tags))
+ (getf oneliner :createdby))
+ (loop
+ with brief = (getf oneliner :brief)
+ for x from 0 to (length brief) by *term-width*
+ do (format t "~a~%"
+ (string-trim '(#\space)
+ (alexandria-2:subseq* brief x (+ x *term-width*)))))
+ (format t "~%~a~%~%" (getf oneliner :oneliner))))
+
+(defun cache-and-print-search-response (response)
+ (merge-into-cache
+ (loop for oneliner in (getf (jonathan:parse response) :oneliners)
+ collect oneliner
+ do (print-oneliner-result-for-user oneliner))))
+
+(defun newest-oneliners (&optional limit)
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (let ((response
+ (if limit
+ (api:get--oneliners-newest :limit limit)
+ (api:get--oneliners-newest))))
+ (cache-and-print-search-response response))))
+
+(defun all-flagged-oneliners (&optional limit)
+ (ensure-config)
+ (api:request-with
+ (:host (host))
+ (let ((response
+ (if limit
+ (api:get--oneliners-all-flagged :limit limit)
+ (api:get--oneliners-all-flagged))))
+ (cache-and-print-search-response response))))
+
+(defun search-for-oneliners (terms limit not-flagged-p all-flagged-p newestp)
+ (assert (loop for term in terms never (find #\, term) ))
+ (set-term-width)
+ (ensure-config)
+ (let ((response
+ (api:request-with
+ (:host (host))
+ (api:get--oneliners :tags (str:join "," terms)
+ :limit limit
+ :notflagged (if not-flagged-p "true" "false")
+ :newest (if newestp "true" "false")
+ :onlyflagged (if all-flagged-p "true" "false")))))
+ (cache-and-print-search-response response)))
+
+;;; RUNNING THINGS IN THE SHELL.
+
diff --git a/lib/oneliner.lisp b/lib/oneliner.lisp
new file mode 100644
index 0000000..b7b404b
--- /dev/null
+++ b/lib/oneliner.lisp
@@ -0,0 +1,59 @@
+;;;; oneliner.lisp -- holds a local representation of onelienrs.
+
+(in-package :oneliners.cli)
+
+(defstruct oneliner
+ id
+ name
+ oneliner
+ tags
+ brief
+ explanation
+ runstyle
+ createdat
+ editedat
+ createdby
+ isflagged
+ islocked)
+
+
+(defun collect-positional-arguments (ol)
+ "Collects the names of all positional arguments in the oneliner, prefix included."
+ (remove-duplicates
+ (sort
+ (ppcre:all-matches-as-strings "#[1-9][0-9]*" (oneliner-oneliner ol))
+ #'string<)
+ :test #'equal))
+
+(defun collect-named-arguments (ol)
+ "Collects the names of all named arguments in the oneliner, prefix included"
+ (remove-duplicates
+ (ppcre:all-matches-as-strings "#[A-Za-z][A-Za-z0-9_]*" (oneliner-oneliner ol))
+ :test #'equal))
+
+(defun tags-from-oneliner (string)
+ "Splits a string using consequitive whitespace as a separator,
+returning a set of tags"
+ (remove-duplicates
+ (remove-if-not #'executable-on-system-p (ppcre:split " +" string))
+ :test #'equal))
+
+
+
+
+;;; VALIDATION OF ONELINER SLOT VALUES
+
+(defun valid-oneliner-string-p (string)
+ (and (not (find #\newline string))
+ (tags-from-oneliner string)))
+
+(defun valid-brief-description-p (string)
+ (<= (length string) 72))
+
+(defun valid-runstyle-p (string)
+ (member string '("auto" "manual") :test 'equalp))
+
+(defun valid-oneliner-name-p (string)
+ (or (equal string "")
+ (and (< 2 (length string))
+ (ppcre:scan "^[a-zA-Z][a-zA-Z0-9_\-]+$" string))))
diff --git a/lib/package.lisp b/lib/package.lisp
new file mode 100644
index 0000000..dd336dc
--- /dev/null
+++ b/lib/package.lisp
@@ -0,0 +1,20 @@
+
+
+(defpackage #:oneliners.cli.running
+ (:use #:cl)
+ (:export #:run-with-shell))
+
+(defpackage #:oneliners.cli.term
+ (:use #:cl)
+ (:export #:*term-width*))
+
+(defpackage #:oneliners.cli.prompt
+ (:use #:cl)
+ (:local-nicknames (#:rl #:cl-readline))
+ (:export #:prompt))
+
+(defpackage #:oneliners.cli
+ (:use :cl)
+ (:import-from #:oneliners.cli.prompt #:prompt)
+ (:local-nicknames (#:api #:oneliners.api-client)
+ (#:a #:alexandria)))
diff --git a/lib/prompt.lisp b/lib/prompt.lisp
new file mode 100644
index 0000000..6c847f1
--- /dev/null
+++ b/lib/prompt.lisp
@@ -0,0 +1,30 @@
+;;;; prompt.lisp -- a function using readlline to collect text from the user
+
+
+
+(in-package :oneliners.cli.prompt)
+
+(defun prompt (prompt
+ &key
+ (expect (constantly t))
+ retry-text
+ (prefill ""))
+ ;; register a prefill hook
+ (rl:register-hook
+ :pre-input
+ (lambda ()
+ (rl:insert-text prefill)
+ (rl:redisplay)))
+ (unwind-protect
+ (loop
+ with prompt-text = prompt
+ with should-retry-p = t
+ while should-retry-p
+ for line = (rl:readline :prompt prompt-text)
+ when (funcall expect line)
+ do (setf should-retry-p nil)
+ when retry-text
+ do (setf prompt-text retry-text)
+ finally (return line))
+ ;; unregisters the hook.
+ (rl:register-hook :pre-input nil)))
diff --git a/lib/running.lisp b/lib/running.lisp
new file mode 100644
index 0000000..5f417e4
--- /dev/null
+++ b/lib/running.lisp
@@ -0,0 +1,47 @@
+;;;; running.lisp -- functions for running oneliners
+
+
+
+
+(in-package :oneliners.cli.running)
+
+(defmacro wait-until ((&key (timeout 1) (poll-every 0.01)) &body check)
+ "Run CHECK every POLL-EVERY seconds until either TIMEOUT seconds
+have passed or CHECK returns non-nil."
+ (let ((clockvar (gensym))
+ (var (gensym)))
+ `(loop
+ for ,clockvar from 0 by ,poll-every to ,timeout
+ for ,var = (progn ,@check)
+ when ,var
+ return ,var
+ do (sleep ,poll-every)
+ finally (return nil))))
+
+(defun run-with-shell
+ (command
+ &key
+ shell-name
+ await-output-p
+ (output-stream *standard-output*))
+ "run COMMAND, a string, in a fresh shell environment, initialized
+with SHELL-NAME. The output from the command read line by line and is
+printed to OUTPUT-STREAM. "
+ (let ((shell
+ (uiop:launch-program shell-name :input :stream :output :stream)))
+ (symbol-macrolet ((shell-input (uiop:process-info-input shell))
+ (shell-output (uiop:process-info-output shell)))
+ (write-line command shell-input)
+ (finish-output shell-input)
+ (if (and await-output-p
+ (plusp await-output-p)
+ (wait-until (:timeout await-output-p :poll-every 0.005)
+ (listen shell-output)))
+ (loop while (listen shell-output)
+ do (princ (read-line shell-output) output-stream)
+ (terpri output-stream)
+ (sleep 0.005))))))
+
+
+
+
diff --git a/lib/state.lisp b/lib/state.lisp
new file mode 100644
index 0000000..b98f5ba
--- /dev/null
+++ b/lib/state.lisp
@@ -0,0 +1,62 @@
+;;;; state.lisp -- functions for dealing with client state
+
+(in-package :oneliners.cli)
+
+;;; Config Struct
+
+(defstruct config
+ handle
+ api-token
+ host
+ shell)
+
+(defvar *config* nil
+ "Holds a config struct instance.")
+
+(defvar *cache* nil
+ "Holds cached oneliners as a list.")
+
+;;; GETTING AND SETTING STATE, DYNAMICALLY BOUND
+
+(defun merge-oneliners (new)
+ "Modifies *CACHE*. Merge updated oneliners into the *cache*, ensuring to remove old versions."
+ (setf *cache*
+ (nconc
+ new
+ (delete-if
+ (lambda (old-oneliner)
+ (find (oneliner-id old-oneliner)
+ new
+ :key #'oneliner-id
+ :test #'equal))
+ *cache*))))
+
+(defun get-cached (id-or-name)
+ "Looks up a oneliner instance by ID-OR-NAME using the current binding of *cache*. "
+ (find id-or-name
+ *cache*
+ :key (etypecase id-or-name
+ (integer #'oneliner-id)
+ (string #'oneliner-name))
+ :test #'equal))
+
+;;; LOADING AND SAVING STATE
+
+(defun config-file ()
+ "Returns the pahtname holding the location of the config file."
+ (merge-pathnames ".config/oneliners.config" (user-homedir-pathname)))
+
+(defun cached-oneliners-file ()
+ "Returns the pathname holding the location of the cache."
+ (merge-pathnames ".cache/oneliners.cache" (user-homedir-pathname)))
+
+(defun wipe-cache ()
+ "Deletes the cache, if present."
+ (uiop:delete-file-if-exists (cached-oneliners-file)))
+
+(defun write-config-to-disk ()
+ (print-to-file *config* (config-file)))
+
+(defun write-cache-to-disk ()
+ (print-to-file *cache* (cached-oneliners-file)))
+
diff --git a/lib/term.lisp b/lib/term.lisp
new file mode 100644
index 0000000..c5b472a
--- /dev/null
+++ b/lib/term.lisp
@@ -0,0 +1,15 @@
+;;;; term.lisp -- functions for dealing with the terminal
+
+
+
+(in-package :oneliners.cli.term)
+
+(defvar *term-width* nil)
+
+(defun set-term-width ()
+ ;; tput cols b/c getenv COLUMNS wasnt working on some terminals
+ (setf *term-width*
+ (-
+ (or (parse-integer (uiop:run-program '("tput" "cols") :output :string) :junk-allowed t)
+ 80)
+ 4)))
diff --git a/lib/util.lisp b/lib/util.lisp
new file mode 100644
index 0000000..290d541
--- /dev/null
+++ b/lib/util.lisp
@@ -0,0 +1,37 @@
+;;;; util.lisp
+
+(in-package :oneliners.cli)
+
+(defun make-temp-file-name ()
+ "Simply makes a file name for a temp file. Uses
+UIOP:TEMPORARY-DIRECTORY for the directory."
+ (namestring
+ (merge-pathnames (format nil "~a~a" (gensym "oneliners") (get-universal-time))
+ (uiop:temporary-directory))))
+
+
+(defun string-from-editor (&optional contents)
+ (let ((filename (make-temp-file-name)))
+ (when contents (a:write-string-into-file contents filename :if-exists :supersede))
+ (unwind-protect
+ (magic-ed:magic-ed filename :eval nil :output :string)
+ (uiop:delete-file-if-exists filename))))
+
+
+(defun executable-on-system-p (name)
+ "A hack that heuristically determines whether or not an executable
+with the provided name is on the system. It is not perfect. It
+consults the environment PATH, and looks for the command in any of
+the directories that appear in the value of that variable."
+ #+unix
+ (loop for path in (str:split ":" (uiop:getenv "PATH"))
+ for directory = (cons :absolute
+ (cdr (str:split "/" path)))
+ thereis (uiop:file-exists-p
+ (make-pathname :name name :directory directory))))
+
+(defun print-to-file (printable-object pathname &optional (if-exists :supersede))
+ "Prints an object to a file, ensuring that the containing directory exists first."
+ (ensure-directories-exist pathname)
+ (with-open-file (out pathname :direction :output :if-exists if-exists)
+ (print printable-object out)))