;;;; 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")) ;;; 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.") ;;; 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 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"))) (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) "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 (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)) (api:*host* (config-host *config*))) (assert api:*host* () "ol must be configured with a server host.") (set-term-width) (handler-case (progn ,@body ;; only if there is no error do we save the local state. (write-cache-to-disk) (write-config-to-disk)) (error (e) (format *error-output* "~a~%" e)))))