diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/app.lisp | 447 |
1 files changed, 272 insertions, 175 deletions
diff --git a/app/app.lisp b/app/app.lisp index 32f3ad2..c9372c8 100644 --- a/app/app.lisp +++ b/app/app.lisp @@ -19,22 +19,9 @@ (in-package :oneliners.cli.app) ;;; VERSION -(defparameter +ol-version+ "0.6.0") +(defparameter +ol-version+ "0.7.0") ;;; HELP TEXTS -(defparameter +invite-help-text+ - " -New contributor accounts are added to the your oneliners server by -redeeming invite tokens. - -When the --redeem option is passed, the ARGS section is expected to be -three items long, and is interpreted as TOKEN USERNAME PASSWORD. E.g.: - - ol --redeem PHONEYTOKEN c00lhacker my1337pw - -Would attempt to make a new user named c00lhacker with password -my1337pw.") - (defparameter +oneliners-variables-help-text+ " Oneliners may contain variables. There are positional variables and @@ -48,7 +35,7 @@ number, which must 1 or greater. For example: The #1 and #2 are a positional variables. You might call the above like - ol --run 8 Doofus Tuesday + ol run 8 Doofus Tuesday Assuming that the above oneliner has ID 8, then \"Hello Doofus, Happy Tuesday\" would print to the console. @@ -61,7 +48,7 @@ letters, numbers, or the symbol _. For example: The #name and #thing are named variables. You might call the above like so: - ol --run 3 name=Goober thing='sock in the nose' + ol run 3 name=Goober thing='sock in the nose' Which should print \"Hello Goober you get a sock in the nose\". @@ -81,94 +68,180 @@ export EDITOR=/usr/bin/zile ") +(defparameter +icons-in-printout-docs+ + "In the printout of oneliners, several unicode \"icons\" appear. +Their meaning is as follows: + +- ⚠ : Indicates that the oneliner is flagged. +- 🔒 : Indicates that the oneliner is locked. +- 📋 : Indicates that the oneliner will be copied to the clipboard. + +") + ;;; CLON SYNOPSIS DEFINITION -(defsynopsis (:postfix "[ARGUMENTS ...]") - (group (:header "SEARCH OPTIONS") - (text :contents "By default, ARGUMENTS are interpeted as search terms for oneliners. For example:") - (text :contents "$ ol grep awk # search for oneliners involving both grep and awk") +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun group-by (n xs &optional default) + (loop for l on xs by (lambda (l) (nthcdr n l)) + when (<= n (length l)) + collect (subseq l 0 n) + else + collect (append l (loop repeat (- n (length l)) collect default)))) + + (defun tabulate-strings (line-width columns strings) + (let ((row-format + (apply 'concatenate 'string + "~" (prin1-to-string line-width) "<" + (loop for i from 0 below columns + collect "~a" + when (< i (1- columns)) + collect "~;" + else + collect "~>")))) + (loop for group in (group-by columns strings " ") + collect (apply 'format nil row-format group))))) + +(defsynopsis (:postfix "COMMAND [ARGS...]") + (group (:header "SEARCHING FOR ONELINERS" :hidden t) + (text :contents "Usage: ol [OPTIONS] search [TERMS...]") (text :contents " ") - - (lispobj :long-name "limit" + (text :contents "Search for oneliners that have been tagged with all of TERMS.") + (text :contents "E.g. `ol search grep awk`") + (text :contents " ") + (text :contents "Options:") + (lispobj :long-name "limit" :argument-type :optional :argument-name "NUMBER" :default-value 10 :description "The maximum number of results to return." :typespec 'integer) (flag :long-name "all-flagged" - :description "Request that only flagged oneliners are returned. Without any ARGUMENTS, simply returns all flagged oneliners.") + :description "Request that only flagged oneliners are returned. Without any TERMS, simply returns all flagged oneliners.") (flag :long-name "not-flagged" - :description "Request that no flagged oneliners are returned with the search results. Does nothing without ARGUMENTS") + :description "Request that no flagged oneliners are returned with the search results. Does nothing without TERMS.") (flag :long-name "newest" - :description "Return newest oneliners that match. Without any ARGUMENTS, simply returns the newest oneliners.")) - (text :contents " ") - (group (:header "EXECUTION OPTIONS") - (text :contents "Several options override the default interpretation of ARGUMENTS.") - (text :contents "Execution options interpret the first argument as the identifier of a oneliner: ") - (text :contents "$ ol <EXECUTION OPTION> <NAME or ID> [MORE ARGUMENTS...]") + :description "Return newest oneliners that match. Without any TERMS, simply returns the newest oneliners.")) + (group (:header "RUNNING ONELINERS" :hidden t) + (text :contents "Usage: ol [OPTIONS] run <IDENTIFIER> [ARGS...]") + (text :contents " ") + (text :contents "Run the oneliner identified by IDENTIFIER, if it exists, with any required ARGS.") + (text :contents "IDENTIFIER should either be the name or the unique numeric ID of a oneliner.") + (text :contents "E.g. `ol run demo foo bar` # run \"demo\" with args \"foo\" and \"bar\"") (text :contents " ") - (flag :long-name "run" - :description "Executes a oneliner by NAME or ID. See also help topic 'variables'.") - (flag :long-name "clip" - :description "Like --run, but puts the oneliner into the clipboard.")) - (text :contents " ") - (group (:header "OTHER OPTIONS") - (flag :long-name "show" - :description "View all information for the oneliner given by NAME or ID") + (text :contents "Options:") (lispobj :long-name "timeout" :argument-type :optional :argument-name "SECONDS" - :default-value 2 + :default-value 1 :typespec 'integer - :description "How long to wait for standard output before giving up.")) - (text :contents " ") - (group (:header "HELP OPTIONS") - (flag :long-name "whois" - :description "View information about a contributor. The first argument is a contributor handle.") - (flag :long-name "help" - :description "Print help for a topic. Topics: wiki, account, invites, variables, editor") - (flag :long-name "version" - :description "Print the client's version")) - (group (:header "Advanced Options" :hidden t) - (flag :long-name "clear-cache" - :description "Clears all cached search results from your system.")) - (group (:header "Editor" :hidden t) + :description "How many seconds to wait for standard output before giving up.") + (flag :long-name "verbose" + :short-name "v" + :description "Prints a message indicating the oneliner text that is about to be run prior to execution.") + (flag :long-name "confirm" + :short-name "c" + :description "Prompts the user for confirmation before running. Implies --verbose.") + (flag :long-name "draft" + :description "Indicates that you wish to run a draft of a oneliner identified by IDENTIFIER.")) + (group (:header "CLIPPING ONELINERS" :hidden t) + (text :contents "Usage: ol clip <IDENTIFIER> [ARGS...]") + (text :contents " ") + (text :contents "Instead of running a oneliner, copy it to your system's clipboard") + (text :contents "ol clip demo-1 foo extra=bar")) + (group (:header "SHOWING INFORMATION ABOUT ONELINERS" :hidden t) + (text :contents "Usage: ol show <IDENTIFIER>") + (text :contents " ") + (text :contents "Print information about a oneliner to the screen.")) + (group (:header "NEW ONELINERS" :hidden t) + (text :contents "Usage: ol new") + (text :contents " ") + (text :contents "Interactively create a new oneliner and upload it to the server.")) + (group (:header "EDITING ONELINERS" :hidden t) + (text :contents "Usage: ol edit <IDENTIFIER>") + (text :contents " ") + (text :contents "Interactively alter a oneliner and uplaod it to the server.") + (text :contents " ") + (text :contents "Options:") + (flag :long-name "redraft" + :description "Indicates that you wish to edit a draft instead of a published oneliner.")) + (group (:header "PUBLISHING ONELINER DRAFTS" :hidden t) + (text :contents "Usage: ol publish <DRAFT>") + (text :contents " ") + (text :contents "Submits a draft oneliner to the wiki server, and, when successful, deletes the draft.")) + (group (:header "DRAFTS LISTING" :hidden t) + (text :contents "Usage: ol drafts") + (text :contents " ") + (text :contents "Prints a listing of current drafts of oneliners yet to be published.")) + (group (:header "TRASH DRAFT" :hidden t) + (text :contents "Usage: ol trash <DRAFT>") + (text :contents " ") + (text :contents "Trashes a draft.")) + (group (:header "FLAGGING AND UNFLAGGING ONELINERS" :hidden t) + (text :contents "Usage: ol <flag | unflag> <IDENTIFIER>") + (text :contents " ") + (text :contents + "Flag or unflag a oneliner. A flagged oneliner is marked as potentially hazardous and will prompt users before exectuion.") + (text :contents + "Flagged oneliners may also be specifically factor into search using the --all-flagged or --not-flagged search options.")) + (group (:header "LOCKING AND UNLOCKING ONELINERS" :hidden t) + (text :contents "Usage: ol <lock | unlock> <IDENTIFIER>") + (text :contents " ") + (text :contents "(ADMINS ONLY) Lock or unlock a oneliner. A locked oneliner may not be altered or edited.")) + (group (:header "REDEEMING INVITE TOKENS" :hidden t) + (text :contents "Usage: ol redeem <INVITE> <HANDLE> <PASSWORD>") + (text :contents " ") + (text :contents "Redeem an invite token, INVITE, and reate a new contributor account on the wiki server with user handle and password.")) + (group (:header "INVITE TOKENS" :hidden t) + (text :contents "Usage: ol invite") + (text :contents " ") + (text :contents "Generate a new invite token if you are allowed to do so.")) + (group (:header "LOGIN AND LOGOUT" :hidden t) + (text :contents "Usage: ol <login | logout> [HANDLE PASWORD]") + (text :contents " ") + (text :contents "Login or logout. If logging in, provide a handle and password.") + (text :contents "Once logged in to your configured server, an API access token will be written to your config file, allowing you to make contributions to the wiki.")) + (group (:header "PASSWORD CHANGES" :hidden t) + (text :contents "Usage: ol password <OLD> <NEW> <REPEATED>") + (text :contents " ") + (text :contents "Change your password on the configured server.")) + (group (:header "SIGNATURE CHANGES" :hidden t) + (text :contents "Usage: ol signature") + (text :contents " ") + (text :contents "Interactively update your contributor signature.")) + (group (:header "WHOIS CONTRIBUTOR" :hidden t) + (text :contents "Usage: ol whois <HANDLE>") + (text :contents " ") + (text :contents "Print information about a contributor.")) + (group (:header "EDITOR CONFIGURATION" :hidden t) (text :contents +configure-your-edtior+)) - (group (:header "Variables" :hidden t) + (group (:header "VARIABLES IN ONELINERS") (text :contents +oneliners-variables-help-text+)) - (group (:header "Wiki" :hidden t) - (text :contents "Options For Managing Oneliners") - (flag :long-name "add" - :description "Intaractively add a oneliner and update the wiki.") - (flag :long-name "edit" - :description "Interactively edit a oneliner and update the wiki.") - (flag :long-name "flag" - :description "Flag a oneliner for review.") - (flag :long-name "unflag" - :description "If you have admin priviliges, unflag a oneliner.") - (flag :long-name "lock" - :description "If you have admin priviliges, lock a oneliner from being edited.") - (flag :long-name "unlock" - :description "If you have admin priviliges, unlock a oneliner.")) - (group (:header "Account" :hidden t) - (text :contents "Options for Managing Your Contributor Account") - (flag :long-name "login" - :description "Attempt to login to your contributor account. ARGS are interpreted as USERNAME PASSWORD.") - (flag :long-name "logout" - :description "Revoke your own access token.") - (flag :long-name "change-password" - :description "Change your password. ARGS are interpreted as CURRENTPW NEWPW NEWPWAGAIN") - (flag :long-name "change-signature" - :description "Change your contributor signature. You will be prompted.")) - (group (:header "Invites" :hidden t) + (group (:header "ICONS IN PRINTOUT") + (text :contents +icons-in-printout-docs+)) + (group (:header "HELP MENU") + (text :contents "Usage: ol help [SECTION]") + (text :contents " ") + (text :contents "Print a help menu. With no arguments, prints this help.") (text :contents " ") - (text :contents "Options For Making Invites and Redeeming Tokens") - (flag :long-name "invite" - :description "Request an invite token to send to a friend.") - (flag :long-name "redeem" - :description "Redeem an invite token.") - (text :contents +invite-help-text+))) + (text :contents "Command sections include:") + (text :contents + (str:join + #\newline + (tabulate-strings + 40 5 + '("search" "run" "clip" "show" "new" "edit" + "delete" "drafts" "trash" "publish" "flag" + "lock" "redeem" "invite" "login" "whois" + "password" "signature")))) + (text :contents " ") + (text :contents "Additional topics include:") +(text :contents + (str:join + #\newline + (tabulate-strings + 40 5 + '("variables" "editor" "icons")))))) ;;; HELPERS @@ -179,100 +252,124 @@ you to go to sleep with it at night is written for the author more than the users." (loop for item in (net.didierverna.clon::items *synopsis*) when (and (typep item 'net.didierverna.clon::group) - (string-equal header (net.didierverna.clon::header item))) + (string-equal header (net.didierverna.clon::header item) + :end2 (length header))) return item)) -(defun prepare-oneliner-arguments (arguments) - "Takes a list of arguments, as gathered by (REMAINDER), and returns - a list that looks like (ID-OR-NAME . ARGS) where ID-OR-NAME is - either an integer or a string." - (a:if-let (id (parse-integer (first arguments) :junk-allowed t)) - (cons id (rest arguments)) - arguments)) - ;;; MAIN ENTRY POINT (defun main () - "Entry point for our standalone application." - (make-context) - (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))) - ((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))))) + (macrolet ((help-and-quit-unless (topic check) + `(unless ,check + (help-topic ,topic) + (uiop:quit)))) + (make-context) + (handler-case + (a:if-let (arguments (remainder)) + (destructuring-bind (command . args) arguments + (let ((id-or-name + (when args + (or (parse-integer (first args) :junk-allowed t) + (first args))))) + (cli:with-local-state + (ecase (a:make-keyword (string-upcase command)) + (:help + (princ #\newline) + (help-topic (or (first args) "help"))) + (:search + (cond + ;; if there are args, use them as search terms + (args + (cli:search-for-oneliners + args + (getopt :long-name "limit") + (getopt :long-name "not-flagged") + (getopt :long-name "all-flagged") + (getopt :long-name "newest"))) + ;; no args, but a --newest flag, just return newest + ((getopt :long-name "newest") + (cli::newest-oneliners (getopt :long-name "limit"))) + ;; no args, but a --all-falgged + ((getopt :long-name "all-flagged") + (cli::all-flagged-oneliners (getopt :long-name "limit"))) + ;; otherwise, print help for search + (t + (help-topic "search") + (uiop:quit)))) + + (:run + (help-and-quit-unless "run" id-or-name) + (cli:run-item id-or-name (rest args) + :verbose (getopt :long-name "verbose") + :confirm (getopt :long-name "confirm") + :timeout (getopt :long-name "timeout") + :draftp (getopt :long-name "draft"))) + (:clip + (help-and-quit-unless "clip" id-or-name) + (cli:run-item id-or-name (rest args) + :force-clip t + :draftp (getopt :long-name "draft"))) + (:show + (help-and-quit-unless "show" id-or-name) + (cli:print-item-explanation id-or-name)) + (:new + (cli:add-new-oneliner)) + (:edit + (help-and-quit-unless "edit" id-or-name) + (cli:edit-item id-or-name (getopt :long-name "redraft"))) + (:delete + (help-and-quit-unless "delete" id-or-name) + (cli::delete-item id-or-name)) + (:publish + (help-and-quit-unless "publish" id-or-name) + (cli::publish-draft id-or-name)) + (:trash + (help-and-quit-unless "trash" id-or-name) + (cli::drop-draft id-or-name)) + (:drafts + (cli::print-drafts)) + (:flag + (help-and-quit-unless "flag" id-or-name) + (cli:flag-item id-or-name)) + (:unflag + (help-and-quit-unless "flag" id-or-name) + (cli:unflag-item id-or-name)) + (:lock + (help-and-quit-unless "lock" id-or-name) + (cli:lock-item id-or-name)) + (:unlock + (help-and-quit-unless "lock" id-or-name) + (cli:unlock-item id-or-name)) + (:redeem + (help-and-quit-unless "redeem" (= 3 (length args))) + (apply 'cli:redeem-invite args)) + (:invite + (cli:request-invite-code)) + (:login + (help-and-quit-unless "login" (= 3 (length args))) + (apply 'cli:login args)) + (:logout + (cli:revoke-access)) + (:password + (help-and-quit-unless "password" (= 3 (length args))) + (apply 'cli:change-pw args)) + (:signature + (cli:change-signature)) + (:whois + (help-and-quit-unless "whois" args) + (cli:show-contributor (first args))))))) + (help-topic "help")) + (#+sbcl sb-sys:interactive-interrupt + #+ccl ccl:interrupt-signal-condition + () + (format t "Aborted by User Interrupt.~%") + (uiop:quit)))) (uiop:quit)) + +(defun help-topic (topic) + (a:if-let (group (find-group-with-header topic)) + (help :item group) + (help))) + + + |