1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
;;;; 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 <http://www.gnu.org/licenses/>.
(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 ()
(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*))
;;; 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")))
(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)))
;;; 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))
(api:*host* (config-host *config*)))
(handler-case
(progn
(assert api:*host* () "ol must be configured with a server host.")
(set-term-width)
,@body
;; only if there is no error do we save the local state.
(write-drafts-to-disk)
(write-cache-to-disk)
(write-config-to-disk))
(dexador.error:http-request-failed (e)
(format *error-output* "Operation failed. The server at ~a returned with ~a~%"
api:*host*
(dexador.error:response-status e)))
(error (e)
(format *error-output* "~a~%" e)))))
|