aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/app.lisp (renamed from src/app.lisp)5
-rw-r--r--app/package.lisp5
-rw-r--r--clpmfile.lock7
-rw-r--r--lib/lib.lisp (renamed from src/lib.lisp)303
-rw-r--r--lib/oneliner.lisp59
-rw-r--r--lib/package.lisp20
-rw-r--r--lib/prompt.lisp (renamed from src/prompt.lisp)5
-rw-r--r--lib/running.lisp47
-rw-r--r--lib/state.lisp62
-rw-r--r--lib/term.lisp15
-rw-r--r--lib/util.lisp37
-rw-r--r--oneliners.cli.asd13
12 files changed, 321 insertions, 257 deletions
diff --git a/src/app.lisp b/app/app.lisp
index c3b4c0a..44a5f97 100644
--- a/src/app.lisp
+++ b/app/app.lisp
@@ -15,10 +15,7 @@
;; 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/>.
-(defpackage #:oneliners.cli.app
- (:use #:cl #:net.didierverna.clon)
- (:local-nicknames (#:a #:alexandria)
- (#:cli #:oneliners.cli)))
+
(in-package :oneliners.cli.app)
diff --git a/app/package.lisp b/app/package.lisp
new file mode 100644
index 0000000..7e4c234
--- /dev/null
+++ b/app/package.lisp
@@ -0,0 +1,5 @@
+
+(defpackage #:oneliners.cli.app
+ (:use #:cl #:net.didierverna.clon)
+ (:local-nicknames (#:a #:alexandria)
+ (#:cli #:oneliners.cli)))
diff --git a/clpmfile.lock b/clpmfile.lock
index e852e8c..76e1373 100644
--- a/clpmfile.lock
+++ b/clpmfile.lock
@@ -76,7 +76,6 @@
("oneliners.api-client"))
("oneliners.cli.asd" :version :newest :source :implicit-file :systems
("oneliners.cli"))
-("osicat" :version "2022-02-20" :source "quicklisp" :systems ("osicat"))
("proc-parse" :version "2019-08-13" :source "quicklisp" :systems ("proc-parse"))
("quri" :version "2021-06-30" :source "quicklisp" :systems ("quri"))
("smart-buffer" :version "2021-10-21" :source "quicklisp" :systems
@@ -109,7 +108,6 @@
("alexandria" ((:system :name "static-vectors") (:system :name "alexandria"))
((:system :name "quri") (:system :name "alexandria"))
((:system :name "proc-parse") (:system :name "alexandria"))
- ((:system :name "osicat") (:system :name "alexandria"))
((:system :name "fast-io") (:system :name "alexandria"))
((:system :name "fast-http") (:system :name "alexandria"))
((:system :name "dexador") (:system :name "alexandria"))
@@ -135,8 +133,6 @@
("cffi" ((:system :name "static-vectors") (:system :name "cffi"))
((:system :name "static-vectors") (:system :name "cffi-grovel"))
- ((:system :name "osicat") (:system :name "cffi"))
- ((:system :name "osicat") (:system :name "cffi-grovel"))
((:system :name "net.didierverna.clon.termio") (:system :name "cffi"))
((:system :name "cl-readline") (:system :name "cffi"))
((:system :name "cl+ssl") (:system :name "cffi"))
@@ -231,8 +227,6 @@
("oneliners.cli.asd" (t (:asd-file :name "oneliners.cli.asd")))
-("osicat" ((:system :name "oneliners.cli") (:system :name "osicat")))
-
("proc-parse" ((:system :name "jonathan") (:system :name "proc-parse"))
((:system :name "fast-http") (:system :name "proc-parse"))
((:system :name "cl-cookie") (:system :name "proc-parse")))
@@ -251,7 +245,6 @@
((:system :name "oneliners.cli") (:system :name "trivial-clipboard")))
("trivial-features"
- ((:system :name "osicat") (:system :name "trivial-features"))
((:system :name "dexador") (:system :name "trivial-features"))
((:system :name "cl+ssl") (:system :name "trivial-features"))
((:system :name "cffi") (:system :name "trivial-features"))
diff --git a/src/lib.lisp b/lib/lib.lisp
index 4e1630e..f49201a 100644
--- a/src/lib.lisp
+++ b/lib/lib.lisp
@@ -14,44 +14,53 @@
;; 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/>.
-
-
-(defpackage #:oneliners.cli
- (:use :cl)
- (:import-from #:oneliners.cli.prompt #:prompt)
- (:local-nicknames (#:api #:oneliners.api-client)
- (#:a #:alexandria)))
-
(in-package :oneliners.cli)
;;; CONFIG AND RESULTS FILE LOCATIONS
-(defvar *config* nil
- "A configuration plist")
-
(defvar *ol-output-timeout* 1)
-(defun ensure-local-state ()
- (ensure-config)
- (setf api::*host* (getf *config* :host)))
-
+(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 valid-config-p (config)
- (and (listp config)
- (evenp (length config))
- (stringp (getf config :host))
- t))
+(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 write-config-to-disk ()
- (let ((conf-file (config-file)))
- (ensure-directories-exist conf-file)
- (with-open-file (out conf-file :direction :output :if-exists :supersede)
- (print *config* out))))
+(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-config (&key host api-token editor (shell "bash"))
- (append (when host (list :host host))
- (when api-token (list :api-token api-token))
- (list :shell shell)))
(defun make-fresh-config ()
(format t "No configuration file has been found. Running Setup~%~%")
@@ -77,81 +86,19 @@
(make-fresh-config))
(fetch-config-from-disk))
-(defun host () (getf *config* :host))
-(defun api-token () (getf *config* :api-token))
-(defun (setf api-token) (newval)
- (setf (getf *config* :api-token) newval))
-(defun get-shell ()
- (getf *config* :shell))
-(defun contributor-handle () (getf *config* :handle))
-(defun (setf contributor-handle) (newval)
- (setf (getf *config* :handle) newval))
-
-(defun config-file ()
- (merge-pathnames ".config/oneliners.config" (user-homedir-pathname)))
-
-(defun cached-oneliners-file ()
- (merge-pathnames ".cache/cached_oneliners" (user-homedir-pathname)))
-
-(defun wipe-cache ()
- (uiop:delete-file-if-exists (cached-oneliners-file)))
-
-(defun merge-into-cache (new-ols)
- (let* ((cached
- (when (uiop:file-exists-p (cached-oneliners-file))
- (with-open-file (input (cached-oneliners-file)) (read input))))
- (updated
- (append
- new-ols
- (loop for old in cached
- for id = (getf old :id)
- unless (find id new-ols :key (lambda (x) (getf x :id)))
- collect old))))
- (ensure-directories-exist (cached-oneliners-file))
- (with-open-file (output (cached-oneliners-file) :direction :output :if-exists :supersede)
- (print updated output))))
;;; UTILITIES
-(defun make-temp-file-name ()
- (namestring
- (merge-pathnames (format nil "~a" (gensym "oneliners")) (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 determins 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 tags-from-oneliner (oneliner)
- (remove-duplicates
- (remove-if-not #'executable-on-system-p (ppcre:split " +" oneliner))
- :test #'equal))
-
-
-
-
-
-(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))))))
+
+
+;; (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))
@@ -161,19 +108,9 @@ the directories that appear in the value of that variable."
(progn ,@body)
(format t "Could not find the oneliner specified by ~a~%" ,nvar)))))
-(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))))
+
+
+
;;; API REQUEST FUNCTIONS
@@ -219,73 +156,25 @@ and, failing that, try to fetch from configured server."
(:host (host))
(api:put--oneliner-oneliner-locked (getf ol :id) :token (api-token) :value "false"))))
-(defun collect-positional-arguments (oneliner)
- (remove-duplicates
- (sort
- (ppcre:all-matches-as-strings "#[1-9][0-9]*" oneliner)
- #'string<)
- :test #'equal))
-
-(defun collect-named-arguments (oneliner)
- (remove-duplicates
- (ppcre:all-matches-as-strings "#[A-Za-z][A-Za-z0-9_]*" oneliner)
- :test #'equal))
-
-(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 bind-vars-and-run-oneliner (ol args &optional force-clip)
- (let* ((oneliner (getf ol :oneliner))
- (runstyle (getf ol :runstyle))
- (pos-args (collect-positional-arguments oneliner))
- (named-args (collect-named-arguments oneliner)))
-
- (when (or (not (getf ol :isflagged))
- (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 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 valid-oneliner-string-p (string)
- (and (not (find #\newline string))
- (tags-from-oneliner string)))
+;;; PRINTING ONELINERS
-(defun valid-brief-description-p (string)
- (<= (length string) 72))
-(defun valid-runstyle-p (string)
- (member string '("auto" "manual") :test 'equalp))
+(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 valid-oneliner-name-p (string)
- (or (equal string "")
- (and (< 2 (length string))
- (ppcre:scan "^[a-zA-Z][a-zA-Z0-9_\-]+$" string))))
(defun add-new-oneliner ()
(ensure-config)
@@ -491,16 +380,6 @@ and, failing that, try to fetch from configured server."
(defun update-cached-item (item)
(merge-into-cache (list item)))
-(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)))
-
(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
@@ -570,53 +449,3 @@ and, failing that, try to fetch from configured server."
;;; RUNNING THINGS IN THE SHELL.
-(defun parent-process-name ()
- "Prints the name of the parent process of the current process."
- (let ((ppidfile (format nil "/proc/~a/status" (osicat-posix:getppid))))
- (first (last
- (ppcre:split "\\s"
- (with-open-file (input ppidfile)
- (read-line input)))))))
-
-(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 (parent-process-name))
- (await-output-p *ol-output-timeout*)
- (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/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/src/prompt.lisp b/lib/prompt.lisp
index afe8604..6c847f1 100644
--- a/src/prompt.lisp
+++ b/lib/prompt.lisp
@@ -1,9 +1,6 @@
;;;; prompt.lisp -- a function using readlline to collect text from the user
-(defpackage #:oneliners.cli.prompt
- (:use #:cl)
- (:local-nicknames (#:rl #:cl-readline))
- (:export #:prompt))
+
(in-package :oneliners.cli.prompt)
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)))
diff --git a/oneliners.cli.asd b/oneliners.cli.asd
index eee30ba..5b0f2e3 100644
--- a/oneliners.cli.asd
+++ b/oneliners.cli.asd
@@ -6,15 +6,18 @@
"str"
"jonathan"
"dexador"
- "osicat"
"net.didierverna.clon"
"cl-readline"
- "magic-ed"
+ "magic-ed"
"oneliners.api-client")
- :components ((:module "src"
+ :components ((:module "lib"
:components
- ((:file "prompt")
- (:file "lib")
+ ((:file "package")
+ (:file "prompt")
+ (:file "cli")))
+ (:module "app"
+ :components
+ ((:file "package")
(:file "app"))))
:description "")