;;;; state.lisp -- functions for dealing with client state ;; Copyright (C) 2022 Colin Okay ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU Affero General Public License as ;; published by the Free Software Foundation, either version 3 of the ;; License, or (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU Affero General Public License for more details. ;; You should have received a copy of the GNU Affero General Public License ;; along with this program. If not, see . (in-package :oneliners.cli) ;;; Config Struct (defplist config (handle "") (api-token "") (host "") (shell "bash")) ;;; CONFIG VAR AND OPERATIONS (defvar *config* nil "Holds a config struct instance.") (defun api-token () (let ((token (config-api-token *config*))) (unless (and token (plusp (length token))) (error () "No API TOKEN")) token)) (defun (setf api-token) (newvalue) (setf (config-api-token *config*) newvalue)) (defun assert-logged-in () "throws an error if no plausbile api token is part of the current config." (api-token)) (defun handle () (config-handle *config*)) (defun (setf handle) (newvalue) (setf (config-handle *config*) newvalue)) (defun host () (config-host *config*)) (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 () "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 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))) (defun write-config-to-disk () (print-to-file *config* (config-file))) (defun write-cache-to-disk () (print-to-file *cache* (cached-oneliners-file))) (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))) (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")) (format t "A new config file has been created, it will be saved to ~a~%." (config-file)) (format t "Be sure to set your EDITOR environment variable if you plan to contribute to the wiki.~%")) (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." (if (uiop:file-exists-p (config-file)) (read-config-file) (make-fresh-config))) ;;; HOSTS (defvar *host* nil) ;;; STATE LOADING MACRO (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* (ensure-config)) (*cache* (read-cache-file)) (*drafts* (read-drafts-file)) (*host* (config-host *config*)) (errorsp nil)) (unwind-protect (handler-case (progn (assert *host* () "ol must be configured with a server host.") (set-term-width) ,@body) (dexador.error:http-request-failed (e) (setf errorsp t) (format *error-output* "Operation failed. The server at ~a returned with ~a~%" *host* (dexador.error:response-status e))) (error (e) (setf errorsp t) (format *error-output* "Unknown Error: ~a~%" e))) (unless errorsp (write-drafts-to-disk) (write-cache-to-disk) (write-config-to-disk)))))