diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/client.lisp | 255 | ||||
-rw-r--r-- | lib/oneliner.lisp | 15 | ||||
-rw-r--r-- | lib/package.lisp | 17 | ||||
-rw-r--r-- | lib/state.lisp | 90 |
4 files changed, 269 insertions, 108 deletions
diff --git a/lib/client.lisp b/lib/client.lisp index fe29932..61c3130 100644 --- a/lib/client.lisp +++ b/lib/client.lisp @@ -16,12 +16,16 @@ ;; along with this program. If not, see <http://www.gnu.org/licenses/>. (in-package :oneliners.cli) -;; NOTE WHILE HACKING. Each of the functions below that make HTTP -;; requests is meant to be called within the body of a -;; WITH-LOCAL-STATE form. If you are hacking in the REPL, make sure -;; to wrap function calls appropriately. +;;;; >>>>>NOTE WHILE HACKING<<<<<. +;;;; ---------------------------------------------------------------- +;;;; Each of the functions below that make HTTP requests are meant to +;;;; be called within the body of a WITH-LOCAL-STATE form. If you are +;;;; hacking in the REPL, make sure to wrap function calls +;;;; appropriately. +;;;; ---------------------------------------------------------------- -;;; GETTING ONELINERS & Displaying oneliners + +;;; GETTING ONELINERS & DISPLAYING ONELINERS (defun search-for-oneliners (terms limit &optional not-flagged-p all-flagged-p newestp) (assert (loop for term in terms never (find #\, term)) () "Search terms may not contain commas.") @@ -45,12 +49,18 @@ not in the local cache, try to fetch from configured server." (defmacro when-oneliner ((var name-or-id) &body body) "Finds the oneliner with name-or-id and binds it to var before running the body. If such a oneliner can be found." - (assert (symbolp var)) (let ((nvar (gensym))) `(let ((,nvar ,name-or-id)) (a:when-let (,var (the-oneliner ,nvar)) ,@body)))) +(defmacro when-draft ((var name) &body body) + "Like when-oneliner but restricts itself to local drafts." + (let ((nvar (gensym))) + `(let ((,nvar ,name)) + (a:when-let (,var (fetch-draft ,nvar)) + ,@body)))) + (defun newest-oneliners (&optional limit) (let ((response (if limit @@ -72,52 +82,62 @@ running the body. If such a oneliner can be found." (princ #\newline) (princ (oneliner-explanation ol))))) +(defun print-drafts () + (when *drafts* + (format t (concatenate 'string "~%~" (prin1-to-string *term-width*) "< ~;DRAFTS~; ~>~%")) + (dolist (draft *drafts*) + (print-oneliner-result-for-user (cdr draft))))) + ;;; RUNNING ONELINERS (defvar *ol-output-timeout* 1) -(defun run-item (ident args &key force-clip (timeout nil timeout-p)) - (when-oneliner (ol ident) - (let ((*ol-output-timeout* (if timeout-p timeout *ol-output-timeout*))) - (bind-vars-and-run-oneliner ol args force-clip)))) +(defun run-item (ident args &key force-clip (timeout nil timeout-p) draftp verbose confirm) + "Runs a oneliner identified by IDENT (if available) with arguments ARGS." + (let ((ol (if draftp (fetch-draft ident) (the-oneliner ident)))) + (when ol + (let ((*ol-output-timeout* (if timeout-p timeout *ol-output-timeout*))) + (bind-vars-and-run-oneliner ol args force-clip verbose confirm))))) -(defun bind-vars-and-run-oneliner (ol args &optional force-clip) +(defun bind-vars-and-run-oneliner (ol args &optional force-clip verbose confirm) (let* ((oneliner (oneliner-oneliner ol)) (runstyle (oneliner-runstyle ol)) (pos-args (get-positional-arguments ol)) (named-args (get-named-arguments ol))) + + (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))) + (when (or (not (oneliner-isflagged ol)) (y-or-n-p "This oneliner is flagged. Are you sure you want to run it?")) + (when (or verbose confirm) + (format t "Attempting to run:~%") + (princ oneliner) + (princ #\newline)) ;; 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")))))) + (when (or (not confirm) + (y-or-n-p "Proceed?")) + (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 - (format t "Attempting to run:~%") - (princ ol) - (princ #\newline) - (princ #\newline) - (run-with-shell ol :shell-name (or (shell) "bash") :await-output-p *ol-output-timeout*)))) + (run-with-shell ol :shell-name (or (shell) "bash") :await-output-p *ol-output-timeout*))) ;;; ADDING ONELINERS - (defun add-new-oneliner () (api-token) ;; fails with error if not set. ;; read each field required to make a onelienr in from a prompt. @@ -131,8 +151,18 @@ running the body. If such a oneliner can be found." (prompt "Name (leave blank for none): " :expect 'valid-oneliner-name-p :retry-text "Must begin with a letter contain only letters, numbers, - and _."))) + + (draft-name + (unless (y-or-n-p "Upload immediately instead of keeping a draft?") + (if (plusp (length name)) + name + (prompt "No name was provided, name this draft: " + :expect 'valid-oneliner-name-p + :retry-text "Must begin with a letter contain only letters, numbers, - and _.")))) + (init-tags (parse-oneliner-tags oneliner-string)) + (brief (prompt "Brief Description: " :expect 'valid-brief-description-p @@ -153,25 +183,34 @@ running the body. If such a oneliner can be found." (when (y-or-n-p "Provide an explanation?") (string-from-editor (format nil "~a~%~%" oneliner-string))))) - (api:request-with - (:body (jonathan:to-json - (list :oneliner oneliner-string - :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)) ;TODO: update api to return the instance created. - (format t "Added~%")))) + (let ((local + (make-oneliner + :oneliner oneliner-string + :name name + :tags tags + :brief brief + :explanation explanation + :runstyle runstyle))) + (if draft-name + ;; if this is a draft, save it to disk. + (progn + (put-draft draft-name local) + (format t "Saved draft ~a~%Do `ol --draft run ~a` to test~%" + draft-name + draft-name)) + ;; otherwise, format the oneliner as json and make a request + ;; to create a new oneliner in the wiki + (api:request-with + (:body (oneliner-to-json-body local) + :content-type "application/json") + (api:post--oneliner :token (api-token)) ;TODO: update api to return the instance created. + (format t "Added Oneliner~%")))))) ;;; EDITING ONELINERS -(defun edit-item (ident) +(defun edit-item (ident &optional draftp) (api-token) ;; fails with error if not set. - (when-oneliner (ol ident) - ;; Like adding a oneliner, read each field in from a prompt. - ;; Here, prefil the field with its current value. + (let ((ol (if draftp (fetch-draft ident) (the-oneliner ident)))) (let* ((oneliner-string (prompt "Oneliner: " :expect 'valid-oneliner-string-p @@ -184,13 +223,23 @@ running the body. If such a oneliner can be found." :expect 'valid-oneliner-name-p :retry-text "Must begin with a letter contain only letters, numbers, - and _." :prefill (or (oneliner-name ol) "")))) + + (draft-name + (unless (y-or-n-p "Upload edits immediately instead of keeping a draft?") + (if (plusp (length name)) + name + (prompt "No name was provided, name this draft: " + :expect 'valid-oneliner-name-p + :retry-text "Must begin with a letter contain only letters, numbers, - and _.")))) (brief (prompt "Brief Description: " :expect 'valid-brief-description-p :retry-text "Too long. Must be <= 72 characters: " :prefill (oneliner-brief ol))) + (init-tags (parse-oneliner-tags oneliner-string)) + (tags (progn (format t "Tags include: ~{~a ~}~%" init-tags) @@ -208,49 +257,93 @@ running the body. If such a oneliner can be found." :expect 'valid-runstyle-p :retry-text "Must be (auto or manual): " :prefill (oneliner-runstyle ol)))) + (explanation - (when (y-or-n-p "Provide an explanation?") + (when (y-or-n-p "Alter the explanation?") (string-from-editor (oneliner-explanation ol))))) - (let ((new-item - (list :oneliner oneliner-string - :tags tags - :brief brief - :name (if (plusp (length name)) name :null) - :explanation explanation - :runstyle runstyle))) - (api:request-with - (:body (jonathan:to-json new-item) - :content-type "application/json") + + (let ((local + (make-oneliner + :id (oneliner-id ol) + :oneliner oneliner-string + :name (if (plusp (length name)) name :null) + :tags tags + :brief brief + :explanation explanation + :runstyle runstyle))) + (if draft-name + (progn + (put-draft draft-name local) + (format t "Saved draft ~a~%Do `ol --draft run ~a` to test~%" + draft-name + draft-name)) + (api:request-with + (:body (oneliner-to-json-body local) + :content-type "application/json") + (api:patch--oneliner-entry-edit (oneliner-id ol) :token (api-token)) + ;(merge-oneliners (list new-item)) ;;TODO: this is broken, wait for API update. + (format t "Edits accepted~%"))))))) + +;;TODO: need to sync cache here. +(defun publish-draft (name) + (when-draft (ol name) + (api:request-with + (:body (oneliner-to-json-body ol) + :content-type "application/json") + (if (oneliner-id ol) (api:patch--oneliner-entry-edit (oneliner-id ol) :token (api-token)) - (merge-oneliners (list new-item)) ;;TODO: this is broken, wait for API update. - (format t "OK~%")))))) + (api:post--oneliner :token (api-token))) + ;; if that worked, no http error occured, so this next part will run + (drop-draft name) + (format t "Draft ~a published and removed from drafts.~%" name)))) ;;; ADMIN OF ONELINER ENTRIES +(defun delete-item (ident) + (when-oneliner (ol ident) + (api:delete--oneliner-oneliner + ident + :token (api-token)) + ;; if we've made it this far no http error has been returned, + ;; hence we can delete it from the cache + (remove-from-cache ident))) + (defun flag-item (ident) + (when-oneliner (ol ident) + (api:put--oneliner-entry-flag + ident + :token (api-token) + :value "true") + ;; no http error, so we flag the cached version, ol. + (setf (oneliner-isflagged ol) t))) + +(defun unflag-item (ident) + (when-oneliner (ol ident) + (api:put--oneliner-entry-flag + ident + :token (api-token) + :value "false") + ;; no http error, so we can unflag the cached version, ol + (setf (oneliner-isflagged ol) nil))) + +(defun lock-item (ident) + (when-oneliner (ol ident) + (api:put--oneliner-oneliner-locked + ident + :token (api-token) + :value "true") + ;; no http error, so we can lock the cached version, ol + (setf (oneliner-islocked ol) t))) + +(defun unlock-item (ident) (when-oneliner (ol ident) - (api:put--oneliner-entry-flag (oneliner-id ol) - :token (api-token) - :value "true"))) - -(defun unflag-item (item-number) - (when-oneliner (ol item-number) - (api:put--oneliner-entry-flag (oneliner-id ol) - :token (api-token) - :value "false"))) - -(defun lock-item (item-number) - (when-oneliner (ol item-number) - (api:put--oneliner-oneliner-locked (oneliner-id ol) - :token (api-token) - :value "true"))) - -(defun unlock-item (item-number) - (when-oneliner (ol item-number) - (api:put--oneliner-oneliner-locked (oneliner-id ol) - :token (api-token) - :value "false"))) + (api:put--oneliner-oneliner-locked + ident + :token (api-token) + :value "false") + ;; no http error, so we can unlock the cached version, ol + (setf (oneliner-islocked ol) nil))) ;;; ACCOUNT AND INVITE STUFF diff --git a/lib/oneliner.lisp b/lib/oneliner.lisp index 4828b2d..668a82c 100644 --- a/lib/oneliner.lisp +++ b/lib/oneliner.lisp @@ -94,3 +94,18 @@ (string-trim '(#\space) (alexandria-2:subseq* (oneliner-brief ol) x (+ x *term-width*))))) (format t "~%~a~%~%" (oneliner-oneliner ol)))) + +;;;; json serialization + +(defun oneliner-to-json-body (ol) + "Takes a oneliner structure and produces some json suitable for +sending to the server. ID and some other fields are omitted." + (jonathan:to-json + (list :oneliner (oneliner-oneliner ol) + :tags (oneliner-tags ol) + :brief (oneliner-brief ol) + :name (if (plusp (length (oneliner-name ol))) + (oneliner-name ol) + :null) + :explanation (oneliner-explanation ol) + :runstyle (oneliner-runstyle ol)))) diff --git a/lib/package.lisp b/lib/package.lisp index cf1fbcb..6359b12 100644 --- a/lib/package.lisp +++ b/lib/package.lisp @@ -36,4 +36,19 @@ (#:a #:alexandria)) (:export #:with-local-state #:search-for-oneliners - #:the-oneliner)) + #:the-oneliner + #:run-item + #:print-item-explanation + #:add-new-oneliner + #:edit-item + #:flag-item + #:unflag-item + #:lock-item + #:unlock-item + #:redeem-invite + #:request-invite-code + #:change-pw + #:revoke-access + #:change-signature + #:show-contributor + #:login)) diff --git a/lib/state.lisp b/lib/state.lisp index 0f69ff3..5ca5a7b 100644 --- a/lib/state.lisp +++ b/lib/state.lisp @@ -25,7 +25,7 @@ (host "") (shell "bash")) -;;; DYNAMIC VARS FOR CONFIG AND CACHE, AND SOME GETTERS +;;; CONFIG VAR AND OPERATIONS (defvar *config* nil "Holds a config struct instance.") @@ -50,9 +50,58 @@ (defun shell () (config-shell *config*)) + +;;; CACHE VAR AND OPERATIONS + (defvar *cache* nil "Holds cached oneliners as a list.") +(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)) + +(defun remove-from-cache (id-or-name) + "Removes an item from the contents of *cache*." + (a:when-let (found (get-cached id-or-name)) + (setf *cache* (delete found *cache*)))) + +;;; DRAFTS VAR AND OPERATIONS + +(defvar *drafts* nil + "Holds a list of oneliner drafts yet to be sent to the server.") + +(defun fetch-draft (name) + "Fetch a draft by name form the *DRAFTS* association list." + (cdr (assoc name *drafts* :test #'string-equal))) + +(defun drop-draft (name) + "Drop a draft by NAME from the *DFRAFTS* association list." + (setf *DRAFTS* (delete (assoc name *DRAFTS* :test #'string-equal) *DRAFTS*))) + +(defun put-draft (name draft) + "Modifies *DRAFTS*, adding a new DRAFT associated with NAME. If NAME +is already associated, that old association is deleted." + (drop-draft name) + (push (cons name draft) *drafts*)) + ;;; LOADING AND SAVING STATE (defun config-file () @@ -63,6 +112,10 @@ "Returns the pathname holding the location of the cache." (merge-pathnames ".cache/oneliners.cache" (user-homedir-pathname))) +(defun drafts-file () + "Returns the pathame holding the location of the oneliner drafts file." + (merge-pathnames ".cache/oneliners.drafts" (user-homedir-pathname))) + (defun wipe-cache () "Deletes the cache, if present." (uiop:delete-file-if-exists (cached-oneliners-file))) @@ -94,6 +147,12 @@ CACHED-ONELINERS-FILE. NIL if there is no such file." :shell (prompt "With which shell should oneliners be run? " :prefill "bash"))) +(defun read-drafts-file () + (read-from-file (drafts-file))) + +(defun write-drafts-to-disk () + (print-to-file *drafts* (drafts-file))) + (defun ensure-config () "Ensures that a configuration file exists on disk, prompting the user for some input if it does not." @@ -101,29 +160,6 @@ user for some input if it does not." (read-config-file) (make-fresh-config))) -;;; 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)) ;;; STATE LOADING MACRO @@ -132,13 +168,15 @@ user for some input if it does not." sets the api's *host* variable. If BODY produces no errors, the " `(let* ((*config* (ensure-config)) (*cache* (read-cache-file)) + (*drafts* (read-drafts-file)) (api:*host* (config-host *config*))) - (assert api:*host* () "ol must be configured with a server host.") - (set-term-width) (handler-case (progn + (assert api:*host* () "ol must be configured with a server host.") + (set-term-width) ,@body ;; only if there is no error do we save the local state. + (write-drafts-to-disk) (write-cache-to-disk) (write-config-to-disk)) (error (e) |