;;;; main.lisp -- oneliners.cli entrypoint (defpackage oneliners.cli (:use :cl) (:local-nicknames (#:api #:oneliners.api-client) (#:a #:alexandria))) (in-package :oneliners.cli) ;;; CONFIG AND RESULTS FILE LOCATIONS (defvar *config* nil "A configuration plist") (defun make-config (&key host api-token) (append (when host (list :host host)) (when api-token (list :api-token api-token)))) (defun valid-config-p (config) (and (listp config) (evenp (length config)) (stringp (getf config :host)) t)) (defun write-default-config-to-disk () (let ((conf-file (config-file))) (ensure-directories-exist conf-file) (with-open-file (out conf-file :direction :output) (print (make-config :host "http://localhost:8888") out)))) (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 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)) (write-default-config-to-disk)) (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 config-file () (merge-pathnames ".config/oneliners.config" (user-homedir-pathname))) (defun last-search-file () (merge-pathnames ".last_oneliners_search" (user-homedir-pathname))) (defun fetch-nth-oneliner (n) "Returns nil if there is no nth oneliner from the search history." (when (uiop:file-exists-p (last-search-file)) (nth n (uiop:read-file-form (last-search-file))))) (defun command-on-system (cmd) ) (defun tags-from-oneliner (oneliner) (loop for cmd? in (ppcre:split " +" oneliner) when (command-on-system cmd?) collect cmd?)) (defun prompt (prompt &key (out-stream *standard-output*) (in-stream *standard-input*)) (princ prompt out-stream) (read-line in-stream)) ;;; API REQUEST FUNCTIONS ;; (defun flag-item (item-number) ;; (ensure-config) ;; (api:request-with ;; (:host (host)) ;; (a:if-let ((token (api-token)) ;; (oneliner (history item-number))) ;; (api:patch--flag-oneliner (getf oneliner :id) :token token)))) (defun add-new-oneliner () (ensure-config) (assert (api-token) () "Cannot add a oneliner without an api token.") (let* ((oneliner (prompt "Oneliner: ")) (init-tags (tags-from-oneliner oneliner)) (brief (prompt "Brief Description: ")) (tags (when (y-or-n-p "Add tags in addition to: ~{~a ~}?" init-tags) (ppcre:split " +" (prompt "(e.g. foo bar goo): ")))) (explaination (when (y-or-n-p "Provide an explaination?") (prompt "Go head: ")))) (format t "Adding a new oneliner.~%") ;; (api:request-with ;; (:host (host) ;; :body (jonathan:to-json ;; (list :oneliners oneliner ;; :brief brief ;; :tags ))) ;; (api:post--add-oneliner :token (api-token))) (format t "Added~%"))) (defun request-invite-code () (ensure-config) (api:request-with (:host (host)) (format t "Invite Code: ~a~%" (getf (jonathan:parse (api:post--make-invite :token (api-token))) :code)))) (defun login (user pass) (ensure-config) (a:when-let (response (jonathan:parse (api:request-with (:host (host)) (api:post--token-contributor user :password pass)))) (setf (api-token) (getf response :token)) (write-config-to-disk) (format t "Access token written to ~a~%" (config-file)))) (defun redeem-invite (token name pass) (ensure-config ) (api:request-with (:host (host)) (api:post--redeem-invite token :username name :password1 pass :password2 pass))) (defun search-for-oneliners (terms limit not-flagged-p) (assert (loop for term in terms never (find #\, term) )) (ensure-config) (print (api:request-with (:host (host)) (api:get--search :tags (str:join "," terms) :limit limit :notflagged (if not-flagged-p "true" "false"))))) ;;; RUNNING COMMANDS (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)))) (defun run-with-shell (command &key (shell-name (parent-process-name)) (await-output-p 0.8) (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) (when 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))))))