78db43898b
This removes the ASDF system definition for Gemma and switches the code over to buildLisp. The program builds (including some terrifying hacks to get the frontend to work), but there are some bizarre runtime issues that I need to debug.
189 lines
5.9 KiB
Common Lisp
189 lines
5.9 KiB
Common Lisp
;; Copyright (C) 2016-2020 Vincent Ambo <mail@tazj.in>
|
|
;;
|
|
;; This file is part of Gemma.
|
|
;;
|
|
;; Gemma is free software: you can redistribute it and/or modify it
|
|
;; under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
(defpackage gemma
|
|
(:use :cl
|
|
:local-time
|
|
:cl-json)
|
|
(:import-from :sb-posix :getenv)
|
|
(:shadowing-import-from :sb-posix :getcwd)
|
|
(:export :start-gemma :config :main))
|
|
(in-package :gemma)
|
|
|
|
;; TODO: Store an average of how many days it was between task
|
|
;; completions. Some of the current numbers are just guesses
|
|
;; anyways.
|
|
|
|
(defmacro in-case-of (x &body body)
|
|
"Evaluate BODY if X is non-nil, binding the value of X to IT."
|
|
`(let ((it ,x))
|
|
(when it ,@body)))
|
|
|
|
;; Set default configuration parameters
|
|
(defvar *gemma-port* 4242
|
|
"Port on which the Gemma web server listens.")
|
|
|
|
(defvar *static-file-location* "frontend/"
|
|
"Folder from which to serve static assets. If built inside of Nix,
|
|
the path is injected during the build.")
|
|
|
|
(defvar *p-tasks* nil
|
|
"All tasks registered in this Gemma instance.")
|
|
|
|
(defun initialise-persistence (data-dir)
|
|
(setq *p-tasks* (cl-prevalence:make-prevalence-system data-dir))
|
|
|
|
;; Initialise database ID counter
|
|
(or (> (length (cl-prevalence:find-all-objects *p-tasks* 'task)) 0)
|
|
(cl-prevalence:tx-create-id-counter *p-tasks*)))
|
|
|
|
(defun config (&key port data-dir)
|
|
"Configuration function for use in the Gemma configuration file."
|
|
|
|
(in-package :gemma)
|
|
(in-case-of port (defparameter *gemma-port* it))
|
|
(initialise-persistence (or data-dir "data/")))
|
|
|
|
;;
|
|
;; Define task management system
|
|
;;
|
|
|
|
(defclass task ()
|
|
((id :reader id
|
|
:initarg :id)
|
|
|
|
;; (Unique) name of the task
|
|
(name :type symbol
|
|
:initarg :name
|
|
:accessor name-of)
|
|
|
|
;; Maximum completion interval
|
|
(days :type integer
|
|
:initarg :days
|
|
:accessor days-of)
|
|
|
|
;; Optional description
|
|
(description :type string
|
|
:initarg :description
|
|
:accessor description-of)
|
|
|
|
;; Last completion time
|
|
(done-at :type timestamp
|
|
:initarg :done-at
|
|
:accessor last-done-at)))
|
|
|
|
(defmacro deftask (task-name days &optional description)
|
|
(unless (get-task task-name)
|
|
`(progn (cl-prevalence:tx-create-object
|
|
*p-tasks*
|
|
'task
|
|
(quote ((name ,task-name)
|
|
(days ,days)
|
|
(description ,(or description ""))
|
|
(done-at ,(now)))))
|
|
(cl-prevalence:snapshot *p-tasks*))))
|
|
|
|
(defun get-task (name)
|
|
(cl-prevalence:find-object-with-slot *p-tasks* 'task 'name name))
|
|
|
|
(defun list-tasks ()
|
|
(cl-prevalence:find-all-objects *p-tasks* 'task))
|
|
|
|
(defun days-remaining (task)
|
|
"Returns the number of days remaining before the supplied TASK reaches its
|
|
maximum interval."
|
|
(let* ((expires-at (timestamp+ (last-done-at task)
|
|
(days-of task) :day))
|
|
(secs-until-expiry (timestamp-difference expires-at (now))))
|
|
(round (/ secs-until-expiry 60 60 24))))
|
|
|
|
(defun sort-tasks (tasks)
|
|
"Sorts TASKS in descending order by number of days remaining."
|
|
(sort (copy-list tasks)
|
|
(lambda (t1 t2) (< (days-remaining t1)
|
|
(days-remaining t2)))))
|
|
|
|
(defun complete-task (name &optional at)
|
|
"Mark the task with NAME as completed, either now or AT specified time."
|
|
(cl-prevalence:tx-change-object-slots *p-tasks* 'task
|
|
(id (get-task name))
|
|
`((done-at ,(or at (now)))))
|
|
(cl-prevalence:snapshot *p-tasks*))
|
|
|
|
;;
|
|
;; Define web API
|
|
;;
|
|
|
|
(defun response-for (task)
|
|
"Create a response object to be JSON encoded for TASK."
|
|
`((:name . ,(name-of task))
|
|
(:description . ,(description-of task))
|
|
(:remaining . ,(days-remaining task))))
|
|
|
|
(defun start-gemma ()
|
|
(in-package :gemma)
|
|
|
|
;; Load configuration
|
|
(load (pathname (or (getenv "GEMMA_CONFIG")
|
|
"/etc/gemma/config.lisp")))
|
|
|
|
;; Set up web server
|
|
(hunchentoot:start
|
|
(make-instance 'hunchentoot:easy-acceptor
|
|
:port *gemma-port*
|
|
:document-root *static-file-location*))
|
|
|
|
;; Task listing handler
|
|
(hunchentoot:define-easy-handler
|
|
(get-tasks :uri "/tasks") ()
|
|
|
|
(setf (hunchentoot:content-type*) "application/json")
|
|
(setf (hunchentoot:header-out "Access-Control-Allow-Origin") "*")
|
|
(encode-json-to-string
|
|
;; Construct a frontend-friendly representation of the tasks.
|
|
(mapcar #'response-for (sort-tasks (list-tasks)))))
|
|
|
|
;; Task completion handler
|
|
(hunchentoot:define-easy-handler
|
|
(complete-task-handler :uri "/complete") (task)
|
|
(setf (hunchentoot:content-type*) "application/json")
|
|
(let* ((key (find-symbol (camel-case-to-lisp task) "GEMMA")))
|
|
(format t "Marking task ~A as completed" key)
|
|
(complete-task key)
|
|
(encode-json-to-string (response-for (get-task key))))))
|
|
|
|
(defun main ()
|
|
"This function serves as the entrypoint for ASDF-built executables.
|
|
It joins the Hunchentoot server thread to keep the process running
|
|
for as long as the server is alive."
|
|
|
|
(start-gemma)
|
|
(sb-thread:join-thread
|
|
(find-if (lambda (th)
|
|
(string= (sb-thread:thread-name th)
|
|
(format nil "hunchentoot-listener-*:~A" *gemma-port*)))
|
|
(sb-thread:list-all-threads))))
|
|
|
|
;; Experimentation / testing stuff
|
|
|
|
(defun randomise-completion-times ()
|
|
"Set some random completion timestamps for all tasks"
|
|
(mapcar
|
|
(lambda (task)
|
|
(complete-task (name-of task)
|
|
(timestamp- (now)
|
|
(random 14)
|
|
:day)))
|
|
(cl-prevalence:find-all-objects *p-tasks* 'task)))
|
|
|
|
(defun clear-all-tasks ()
|
|
(mapcar (lambda (task) (cl-prevalence:tx-delete-object *p-tasks* 'task (id task)))
|
|
(cl-prevalence:find-all-objects *p-tasks* 'task)))
|
|
|
|
;; (randomise-completion-times)
|