aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Okay <okay@toyful.space>2022-03-12 14:17:17 -0600
committerColin Okay <okay@toyful.space>2022-03-12 14:17:17 -0600
commit8fba7071223fb6744407789c1b1ae5a4549779e5 (patch)
tree4ec5fd7d2e07877ce6020e268499d0119d2ae735
parent6d706967f1de63de80c83766053e896ab4792420 (diff)
built and ran refactored client app
-rw-r--r--app/app.lisp173
-rw-r--r--clpmfile.lock7
-rw-r--r--lib/client.lisp656
-rw-r--r--lib/oneliner.lisp4
-rw-r--r--lib/package.lisp7
-rw-r--r--lib/state.lisp57
-rw-r--r--lib/util.lisp9
7 files changed, 427 insertions, 486 deletions
diff --git a/app/app.lisp b/app/app.lisp
index fea4aa3..28f5465 100644
--- a/app/app.lisp
+++ b/app/app.lisp
@@ -16,7 +16,6 @@
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
-
(in-package :oneliners.cli.app)
;;; VERSION
@@ -196,95 +195,85 @@ than the users."
(defun main ()
"Entry point for our standalone application."
(make-context)
- (handler-case
- (let ((arguments (remainder)))
- (cond
- ((getopt :long-name "version")
- (format t "Oneliner CLI Version: ~a~%" +ol-version+))
- ((getopt :long-name "help")
- (if (and arguments (find-group-with-header (first arguments)))
- (help :item (find-group-with-header (first arguments)))
- (help))
- (uiop:quit))
- ((getopt :long-name "whois")
- (assert (first arguments) () "--whois requires an argument, a user handle.")
- (cli::show-contributor (first arguments)))
-
- ((getopt :long-name "redeem")
- (assert (= 3 (length arguments)) () "--redeem requires exatly three arguments.")
- (destructuring-bind (token name pass) arguments
- (cli::redeem-invite token name pass)))
-
- ((getopt :long-name "login")
- (assert (= 2 (length arguments)) () "--login requires exactly two arguments.")
- (destructuring-bind (user pass) arguments
- (cli::login user pass)))
-
- ((getopt :long-name "change-password")
- (assert (= 3 (length arguments)) () "--change-password requires exactly three arguments." )
- (destructuring-bind (current new repeated) arguments
- (cli::change-pw current new repeated)))
-
- ((getopt :long-name "change-signature")
- (cli::change-signature))
-
- ((getopt :long-name "invite")
- (cli::request-invite-code))
-
- ((getopt :long-name "logout")
- (cli::revoke-access))
-
- ((getopt :long-name "add")
- (cli::add-new-oneliner))
-
- ((and (null arguments) (getopt :long-name "all-flagged"))
- (cli::all-flagged-oneliners (getopt :long-name "limit")))
-
- ((and (null arguments) (getopt :long-name "newest"))
- (cli::newest-oneliners (getopt :long-name "limit")))
-
- ((getopt :long-name "clear-cache")
- (cli::wipe-cache))
-
- (arguments
- (destructuring-bind (id-or-name . args) (prepare-oneliner-arguments arguments)
- (cond
- ((getopt :long-name "flag")
- (cli::flag-item id-or-name ))
- ((getopt :long-name "unflag")
- (cli::unflag-item id-or-name))
- ((getopt :long-name "lock")
- (cli::lock-item id-or-name ))
- ((getopt :long-name "unlock")
- (cli::unlock-item id-or-name ))
- ((getopt :long-name "edit")
- (cli::edit-item id-or-name ))
- ((getopt :long-name "show")
- (cli::print-item-explanation id-or-name))
- ((getopt :long-name "clip")
- (cli::run-item id-or-name args
- :force-clip t
- :timeout (getopt :long-name "timeout")))
- ((getopt :long-name "run")
- (cli::run-item id-or-name args
- :timeout (getopt :long-name "timeout")))
-
- (t ; arguments but no overriding flags, search wiki
- (cli::search-for-oneliners arguments
- (getopt :long-name "limit")
- (getopt :long-name "not-flagged")
- (getopt :long-name "all-flagged")
- (getopt :long-name "newest")))))
- (uiop:quit))
-
- (t ; no arguments and no options, print help
- (help)))
- (uiop:quit))
- (error (e)
- (format *error-output* "~%ERROR: ~a~%" e)
- (uiop:quit))
- (#+sbcl sb-sys:interactive-interrupt
- #+ccl ccl:interrupt-signal-condition
- ()
- (format t "Aborted by User Interrupt.~%")
+ (cli:with-local-state
+ (let ((arguments (remainder)))
+ (cond
+ ((getopt :long-name "version")
+ (format t "Oneliner CLI Version: ~a~%" +ol-version+))
+ ((getopt :long-name "help")
+ (if (and arguments (find-group-with-header (first arguments)))
+ (help :item (find-group-with-header (first arguments)))
+ (help))
+ (uiop:quit))
+ ((getopt :long-name "whois")
+ (assert (first arguments) () "--whois requires an argument, a user handle.")
+ (cli::show-contributor (first arguments)))
+
+ ((getopt :long-name "redeem")
+ (assert (= 3 (length arguments)) () "--redeem requires exatly three arguments.")
+ (destructuring-bind (token name pass) arguments
+ (cli::redeem-invite token name pass)))
+
+ ((getopt :long-name "login")
+ (assert (= 2 (length arguments)) () "--login requires exactly two arguments.")
+ (destructuring-bind (user pass) arguments
+ (cli::login user pass)))
+
+ ((getopt :long-name "change-password")
+ (assert (= 3 (length arguments)) () "--change-password requires exactly three arguments." )
+ (destructuring-bind (current new repeated) arguments
+ (cli::change-pw current new repeated)))
+
+ ((getopt :long-name "change-signature")
+ (cli::change-signature))
+
+ ((getopt :long-name "invite")
+ (cli::request-invite-code))
+
+ ((getopt :long-name "logout")
+ (cli::revoke-access))
+
+ ((getopt :long-name "add")
+ (cli::add-new-oneliner))
+
+ ((and (null arguments) (getopt :long-name "all-flagged"))
+ (cli::all-flagged-oneliners (getopt :long-name "limit")))
+
+ ((and (null arguments) (getopt :long-name "newest"))
+ (cli::newest-oneliners (getopt :long-name "limit")))
+
+ ((getopt :long-name "clear-cache")
+ (cli::wipe-cache))
+
+ (arguments
+ (destructuring-bind (id-or-name . args) (prepare-oneliner-arguments arguments)
+ (cond
+ ((getopt :long-name "flag")
+ (cli::flag-item id-or-name ))
+ ((getopt :long-name "unflag")
+ (cli::unflag-item id-or-name))
+ ((getopt :long-name "lock")
+ (cli::lock-item id-or-name ))
+ ((getopt :long-name "unlock")
+ (cli::unlock-item id-or-name ))
+ ((getopt :long-name "edit")
+ (cli::edit-item id-or-name ))
+ ((getopt :long-name "show")
+ (cli::print-item-explanation id-or-name))
+ ((getopt :long-name "clip")
+ (cli::run-item id-or-name args
+ :force-clip t
+ :timeout (getopt :long-name "timeout")))
+ ((getopt :long-name "run")
+ (cli::run-item id-or-name args
+ :timeout (getopt :long-name "timeout")))
+
+ (t ; arguments but no overriding flags, search wiki
+ (cli::search-for-oneliners arguments
+ (getopt :long-name "limit")
+ (getopt :long-name "not-flagged")
+ (getopt :long-name "all-flagged")
+ (getopt :long-name "newest"))))))
+ (t ; no arguments and no options, print help
+ (help)))
(uiop:quit))))
diff --git a/clpmfile.lock b/clpmfile.lock
index 76e1373..6e77eea 100644
--- a/clpmfile.lock
+++ b/clpmfile.lock
@@ -75,7 +75,7 @@
("oneliners.api-client.asd" :version :newest :source :implicit-file :systems
("oneliners.api-client"))
("oneliners.cli.asd" :version :newest :source :implicit-file :systems
- ("oneliners.cli"))
+ ("oneliners.cli" "oneliners.cli/app"))
("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
@@ -154,6 +154,7 @@
("cl-change-case" ((:system :name "str") (:system :name "cl-change-case")))
("cl-clon"
+ ((:system :name "oneliners.cli/app") (:system :name "net.didierverna.clon"))
((:system :name "oneliners.cli") (:system :name "net.didierverna.clon"))
((:system :name "net.didierverna.clon.termio")
(:system :name "net.didierverna.clon.core"))
@@ -225,7 +226,9 @@
((:system :name "oneliners.cli") (:system :name "oneliners.api-client"))
(t (:asd-file :name "oneliners.api-client.asd")))
-("oneliners.cli.asd" (t (:asd-file :name "oneliners.cli.asd")))
+("oneliners.cli.asd"
+ ((:system :name "oneliners.cli/app") (:system :name "oneliners.cli"))
+ (t (:asd-file :name "oneliners.cli.asd")))
("proc-parse" ((:system :name "jonathan") (:system :name "proc-parse"))
((:system :name "fast-http") (:system :name "proc-parse"))
diff --git a/lib/client.lisp b/lib/client.lisp
index 22d75b8..2753329 100644
--- a/lib/client.lisp
+++ b/lib/client.lisp
@@ -16,17 +16,31 @@
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(in-package :oneliners.cli)
-;;; UTILITIES
+;; 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.
-(defun cache-and-print-search-response (json)
- "Takes a json string and parses it. Using the parse results, create
-ONELINER instances. Print those using PRINT-ONELINER-RESULT-FOR-USER
-and then ensure the cache is updated."
- (merge-oneliners
- (loop for oneliner-plist in (getf (jonathan:parse json) :oneliners)
- for oneliner = (apply #'make-oneliner oneliner-plist)
- collect oneliner
- do (print-oneliner-result-for-user oneliner))))
+;;; GETTING 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.")
+ (let ((json
+ (api:get--oneliners :tags (str:join "," terms)
+ :limit limit
+ :notflagged (true-or-false not-flagged-p)
+ :newest (true-or-false newestp)
+ :onlyflagged (true-or-false all-flagged-p))))
+ (cache-and-print-search-response json)))
+
+(defun the-oneliner (name-or-id)
+ "Get the oneliner with name-or-id. First look in the local cache. If
+not in the local cache, try to fetch from configured server."
+ (a:if-let ((ol (get-cached name-or-id)))
+ ol
+ (let ((ol (jonathan:parse (api:get--oneliner-entry name-or-id))))
+ (merge-oneliners (list ol))
+ ol)))
(defmacro when-oneliner ((var name-or-id) &body body)
"Finds the oneliner with name-or-id and binds it to var before
@@ -37,40 +51,28 @@ running the body. If such a oneliner can be found."
(a:when-let (,var (the-oneliner ,nvar))
,@body))))
+(defun newest-oneliners (&optional limit)
+ (let ((response
+ (if limit
+ (api:get--oneliners-newest :limit limit)
+ (api:get--oneliners-newest))))
+ (cache-and-print-search-response response)))
-;;; SEARCHING FOR 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.")
- (with-local-state
- (let ((json
- (api:get--oneliners :tags (str:join "," terms)
- :limit limit
- :notflagged (true-or-false not-flagged-p)
- :newest (true-or-false newestp)
- :onlyflagged (true-or-false all-flagged-p))))
- (cache-and-print-search-response json))))
-
-(defun the-oneliner (name-or-id)
- "Get the oneliner with name-or-id. First look in the local cache. If
-not in the local cache, try to fetch from configured server."
- (with-local-state
- (a:if-let ((ol (get-cached name-or-id)))
- ol
- (let ((ol (jonathan:parse (api:get--oneliner-entry name-or-id))))
- (merge-oneliners (list ol))
- ol))))
-
+(defun all-flagged-oneliners (&optional limit)
+ (let ((response
+ (if limit
+ (api:get--oneliners-all-flagged :limit limit)
+ (api:get--oneliners-all-flagged))))
+ (cache-and-print-search-response response)))
;;; RUNNING ONELINERS
(defvar *ol-output-timeout* 1)
(defun run-item (ident args &key force-clip (timeout nil timeout-p))
- (with-local-state
- (when-oneliner (ol ident)
- (let ((*ol-output-timeout* (if timeout-p timeout *ol-output-timeout*)))
- (bind-vars-and-run-oneliner ol args force-clip)))))
+ (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 bind-vars-and-run-oneliner (ol args &optional force-clip)
(let* ((oneliner (oneliner-oneliner ol))
@@ -104,351 +106,241 @@ not in the local cache, try to fetch from configured server."
(princ ol)
(princ #\newline)
(princ #\newline)
- (run-with-shell ol :shell-name (or (config-shell *config*) "bash")))))
+ (run-with-shell ol :shell-name (or (shell) "bash")))))
+
+;;; 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.
+ (let* ((oneliner-string
+ (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
+ (parse-oneliner-tags oneliner-string))
+ (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-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~%"))))
+
+;;; EDITING ONELINERS
+
+(defun edit-item (ident)
+ (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* ((oneliner-string
+ (prompt "Oneliner: "
+ :expect 'valid-oneliner-string-p
+ :retry-text "Oneliners must contain at least one command: "
+ :prefill (oneliner-oneliner ol)))
+ (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 (or (oneliner-name ol) ""))))
+ (brief
+ (prompt "Brief Description: "
+ :expect 'valid-brief-description-p
+ :retry-text "Too long. Must be <= 72 characters: "
+ :prefill (oneliner-brief ol)))
+ (init-tags
+ (tags-from-oneliner oneliner-string))
+ (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
+ (oneliner-tags ol)
+ init-tags
+ :test 'equal)))))))
+ (runstyle
+ (string-upcase
+ (prompt "Runstyle (auto or manual): "
+ :expect 'valid-runstyle-p
+ :retry-text "Must be (auto or manual): "
+ :prefill (oneliner-runstyle ol))))
+ (explanation
+ (when (y-or-n-p "Provide an explanation?")
+ (string-from-editor (oneliner-explanation ol)))))
+ (let ((new-item
+ (list :oneliner oneliner
+ :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")
+ (api:patch--oneliner-entry-edit (oneliner-id ol) :token (api-token))
+ (update-cached-item new-item)
+ (format t "OK~%"))))))
+
+
+;;; ADMIN OF ONELINER ENTRIES
+
+(defun flag-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")))
+
+
+;;; ACCOUNT AND INVITE STUFF
+
+(defun request-invite-code ()
+ (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)
+ (let ((response
+ (jonathan:parse
+ (api:request-with
+ (:body (jonathan:to-json (list :password pass :handle user))
+ :content-type "application/json")
+ (api:post--access)))))
+ (setf (api-token) (getf response :token)
+ (handle) user)
+ (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."))
+ (api:put--contributor-who-password (handle)
+ :token (api-token)
+ :value new
+ :repeated new
+ :current current))
+
+(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 show-contributor (name)
+ (let ((contributor (api:get--contributor-who name)))
+ (print-contributor (jonathan:parse contributor))))
+
+
+(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 redeem-invite (token name pass)
+ (when (yes-or-no-p +agree-to-the-unlicense+)
+ (api:request-with
+ (: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 ()
+ (api:delete--access-access (api-token) :token (api-token))
+ (format t "You were logged out~%"))
+
+
+;;; UTILITIES
+
+(defun cache-and-print-search-response (json)
+ "Takes a json string and parses it. Using the parse results, create
+ONELINER instances. Print those using PRINT-ONELINER-RESULT-FOR-USER
+and then ensure the cache is updated."
+ (merge-oneliners
+ (loop for oneliner-plist in (getf (jonathan:parse json) :oneliners)
+ for oneliner = (apply #'make-oneliner oneliner-plist)
+ collect oneliner
+ do (print-oneliner-result-for-user oneliner))))
+
+
+(defun prompt-for-signature ()
+ "Just prompt the user for confirmation about whether or not to
+change their signature."
+ (if (y-or-n-p "Provide a contributor signature about yourself? ")
+ (prompt "Go ahead: ")
+ ""))
+
-;; (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))))))
-
-
-
-
-
-
-
-;; ;;; 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 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))
-
-
-;; (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 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))))
-
-
-
-;; ;;; RUNNING THINGS IN THE SHELL.
+(defun print-contributor (contributor)
+ (format t "~20a ~@[-- ~a~]~%"
+ (getf contributor :handle)
+ (getf contributor :signature)))
diff --git a/lib/oneliner.lisp b/lib/oneliner.lisp
index ccfd3dc..39aad0d 100644
--- a/lib/oneliner.lisp
+++ b/lib/oneliner.lisp
@@ -48,11 +48,13 @@
(ppcre:all-matches-as-strings "#[A-Za-z][A-Za-z0-9_]*" (oneliner-oneliner ol))
:test #'equal))
+
+
;;; VALIDATION OF ONELINER SLOT VALUES
(defun valid-oneliner-string-p (string)
(and (not (find #\newline string))
- (tags-from-oneliner string)))
+ (parse-oneliner-tags string)))
(defun valid-brief-description-p (string)
(<= (length string) 72))
diff --git a/lib/package.lisp b/lib/package.lisp
index 0ed47b0..cf1fbcb 100644
--- a/lib/package.lisp
+++ b/lib/package.lisp
@@ -20,7 +20,7 @@
(defpackage #:oneliners.cli.term
(:use #:cl)
- (:export #:*term-width*))
+ (:export #:*term-width* #:set-term-width))
(defpackage #:oneliners.cli.prompt
(:use #:cl)
@@ -33,4 +33,7 @@
(:import-from #:oneliners.cli.term #:*term-width* #:set-term-width)
(:import-from #:oneliners.cli.running #:run-with-shell)
(:local-nicknames (#:api #:oneliners.api-client)
- (#:a #:alexandria)))
+ (#:a #:alexandria))
+ (:export #:with-local-state
+ #:search-for-oneliners
+ #:the-oneliner))
diff --git a/lib/state.lisp b/lib/state.lisp
index 0c01d4d..676ff10 100644
--- a/lib/state.lisp
+++ b/lib/state.lisp
@@ -20,14 +20,36 @@
;;; Config Struct
(defstruct config
- handle
- api-token
- host
- shell)
+ (handle "")
+ (api-token "")
+ (host "")
+ (shell "bash"))
+
+;;; DYNAMIC VARS FOR CONFIG AND CACHE, AND SOME GETTERS
(defvar *config* nil
"Holds a config struct instance.")
+(defun api-token ()
+ (a:if-let (token (config-api-token *config*))
+ token
+ (error () "No API TOKEN")))
+
+(defun (setf api-token) (newvalue)
+ (setf (config-api-token *config*) newvalue))
+
+(defun handle ()
+ (config-handle *config*))
+
+(defun (setf handle) (newvalue)
+ (setf (config-handle *config*) newvalue))
+
+(defun host ()
+ (config-host *config*))
+
+(defun shell ()
+ (config-shell *config*))
+
(defvar *cache* nil
"Holds cached oneliners as a list.")
@@ -46,7 +68,10 @@
(uiop:delete-file-if-exists (cached-oneliners-file)))
(defun write-config-to-disk ()
- (print-to-file *config* (config-file)))
+ (print-to-file
+ (with-slots (handle api-token host shell) *config*
+ (list :handle handle :api-token api-token :host host :shell shell))
+ (config-file)))
(defun write-cache-to-disk ()
(print-to-file *cache* (cached-oneliners-file)))
@@ -54,13 +79,31 @@
(defun read-config-file ()
"Read a configuration from the location returned by CONFIG-FILE. NIL
if there is no such file"
- (read-from-file (config-file)))
+ (a:when-let ((conf
+ (read-from-file (config-file))))
+ (apply 'make-config conf)))
(defun read-cache-file ()
"Read the cache from the location returned by
CACHED-ONELINERS-FILE. NIL if there is no such file."
(read-from-file (cached-oneliners-file)))
+(defun make-fresh-config ()
+ "Prompts the user to supply some values for a config file."
+ (format t "It seems you are calling `ol` for the first time. Running Setup~%~%")
+ (make-config
+ :host (prompt "Oneliner Server Host: "
+ :prefill "https://api.oneliners.wiki")
+ :shell (prompt "With which shell should oneliners be run? "
+ :prefill "bash")))
+
+(defun ensure-config ()
+ "Ensures that a configuration file exists on disk, prompting the
+user for some input if it does not."
+ (if (uiop:file-exists-p (config-file))
+ (read-config-file)
+ (make-fresh-config)))
+
;;; GETTING AND SETTING STATE, DYNAMICALLY BOUND
(defun merge-oneliners (new)
@@ -90,7 +133,7 @@ CACHED-ONELINERS-FILE. NIL if there is no such file."
(defmacro with-local-state (&body body)
"Binds the *config* and *cache* dynamic variables from disk, and
sets the api's *host* variable. If BODY produces no errors, the "
- `(let* ((*config* (read-config-file))
+ `(let* ((*config* (ensure-config))
(*cache* (read-cache-file))
(api:*host* (config-host *config*)))
(assert api:*host* () "ol must be configured with a server host.")
diff --git a/lib/util.lisp b/lib/util.lisp
index 2a6d456..27f389e 100644
--- a/lib/util.lisp
+++ b/lib/util.lisp
@@ -46,6 +46,14 @@ the directories that appear in the value of that variable."
thereis (uiop:file-exists-p
(make-pathname :name name :directory directory))))
+(defun parse-oneliner-tags (string)
+ "Splits a string using consequtive whitespace as a separator, and
+returns a set of strings that name executable system commands, as
+determined by EXECUTABLE-ON-SYSTEM-P."
+ (remove-duplicates
+ (remove-if-not #'executable-on-system-p (ppcre:split " +" string))
+ :test #'equal))
+
(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)
@@ -61,3 +69,4 @@ the directories that appear in the value of that variable."
(defun true-or-false (what)
"Returns the strings \"true\" or \"false\" depending on whehter or not WHAT is null"
(if what "true" "false"))
+