Ceramic » Tutorial

Installation

Until Ceramic is available from Quicklisp, you have to clone it to your Quicklisp local-projects directory. Assuming your Quicklisp directory is ~/quicklisp:

git clone https://github.com/ceramic/ceramic.git ~/quicklisp/local-projects

Then, either restart Lisp or run (ql:register-local-projects).

Getting Started

First, we need to load Ceramic. We do this with Quicklisp:

CL-USER> (ql:quickload :ceramic)

Ceramic needs to download some things to run, so let's do that:

CL-USER> (ceramic:setup)

Now we're all set up. Let's start creating some browser windows. Run the following code:

;; Start the underlying Electron process
(ceramic:start)

;; Create a browser window
(defvar window (ceramic:make-window :url "https://www.google.com/"
                                    :width 800
                                    :height 600))

;; Show it
(ceramic:show window)

An 800-by-600 pixel browser window pointed at Google should pop up.

Building a Desktop Web App

With Ceramic, we can write a web application – using Clack– and create windows that use it.

We'll be using Lucerne, a web framework built on Clack to write the web app, and Ceramic to use it as a desktop app.

First, load Lucerne:

CL-USER> (ql:quickload :lucerne)

Then, start the Electron process,

CL-USER> (ceramic:start)

Now, let's create a basic application. The source code and system definition of this app is in the examples/hello-world/ directory of the Ceramic source code, so you can play around with it.

(in-package :cl-user)
(defpackage ceramic-hello-world
  (:use :cl :lucerne)
  (:export :run))
(in-package :ceramic-hello-world)
(annot:enable-annot-syntax)

;; Define an application
(defapp app)

;; Route requests to "/" to this function
@route app "/"
(defview hello ()
  (respond "Hello, world!"))

(defvar *window* nil)

(defvar *port* 8000)

(defun run ()
  (setf *window*
        (ceramic:make-window :url (format nil "http://localhost:~D/" *port*)))
  (ceramic:show *window*)
  (start app :port *port*))

(ceramic:define-entry-point :ceramic-hello-world ()
  (run))

Don't worry about that define-entry-point macro, we'll get to that later.

Calling run, a browser window with the text "Hello, world!" should pop up.

Shipping

Ceramic can compile an application -- both produce an executable of the server and compile all the required resources -- into a bundle, which is basically an archive file. All you have to do is call the bundle function:

(ceramic:bundle :ceramic-hello-world)

And that's it. Head over to the directory of the hello world example, examples/hello-world/, you'll see a .tar file with the app's executable. Extract it and run the app. A window should pop up and you should see the 'Hello, World!' text.

You can also tell it where to store the bundle through an optional argument:

(ceramic:bundle :ceramic-hello-world :bundle-pathname #p"build/bundle.tar")

More Complex Apps

Of course, a web application isn't just routes, you also have to bundle all sorts of assets. Ceramic takes care of that for you.

For this example, we'll use MarkEdit: This is a simple Markdown editor and previewer. On startup, you get a window with two panes: On the left pane, you type Markdown code, and on the left, you get the rendered HTML. You can see it in action below:

To test the app locally, just run (markedit:start-app). To deploy the app, we run the bundler command:

(ceramic:bundle :markedit)

Extract the app and run it: You'll see the window pop up and you can start editing. Close it and the app shuts down.

The way Ceramic handles resources is by associating tags (such as web-assets or data-files) to certain pathnames relative to the system's source directory. In development, you reference a resource tag and get the pathname to that source directory. When bundling the app, resource directories are copied, and in production, when you reference a tag, you get the pathname to the copied directory inside the app's bundle.

If you look at the source of the MarkEdit application, you'll see resources are defined like this:

(define-resources :markedit ()
  (assets #p"assets/")
  (templates #p"templates/"))

What this means is the tag assets is associated to the assets/ directory in the directory of the :markedit system, and the templates tag is associated to the templates/ directory. You can associate tags to directories at any depth.

We the resource-directory function to get the directory associated to a tag. In MarkEdit we use this to set the directory where templates are stored so Djula can access them:

(djula:add-template-directory
 (resource-directory 'templates))

(defparameter +index+
  (djula:compile-template* "index.html"))

A useful function – to prevent you from calling merge-pathnames all the time – is the resource function: This takes a tag and a pathname, and is the equivalent of adding that pathname to the end of the directory associated to that tag:

(resource 'assets #p"css/style.css")

;; is the same as
(merge-pathnames #p"css/style.css"
                 (resource-directory 'assets))