A GUI toolkit for Common Lisp

Content from 2016-08

Progress report #1

posted on 2016-08-30 09:00

Dear Supporters,

I'm publishing a progress report for month August with detailed timelog and brief description of undertakings performed each day. This file also contains report for the previous iteration sponsored by Professor Robert Strandh.


The most important achievement was securing funds for a few months of work with the Bountysource crowdfunding campaign. We've created some bounties to attract new developers. #clim channel on Freenode is active and we seem to regain the user base what results in interesting discussions, knowledge sharing and increased awareness about the project among non-clim users.

We have also gained valuable feedback about user expectations regarding the further development and issues which are the most inconvenient for them. We've added a new section on the website https://common-lisp.net/project/mcclim/involve which addresses some common questions and doubts and we have created a wiki on GitHub (not very useful yet https://github.com/robert-strandh/McCLIM/wiki).

During this time we are constantly working on identifying and fixing issues, cleaning up the code base and thinking about potential improvements. Curious reader may consult the git repository log, IRC log and read the logbook.

As a side note, I've exceeded time meant for this iteration by four hours, but I'm treating it as my free time. Additionally people may have noticed that I did some works on CLX not specifically related to McCLIM – this development was done on my own time as well.

Also, to address a few questions regarding our agenda – our roadmap is listed here: https://common-lisp.net/project/mcclim/involve. That means among other things, that we are concentrated on finishing and polishing the CLX backend and we are currently not working on any other backends.

If you have any questions, doubts or suggestions – please contact me either with email (daniel@turtleware.eu) or on IRC (my nick is jackdaniel).

Sincerely yours,
Daniel Kochmański

Fundraiser success

posted on 2016-08-16 11:00

Dear Lispers,

We are delighted and overwhelmed by the support that many of you have shown for our effort to crowdfund the maintenance and continued development of McCLIM. Never in our wildest dreams had we imagined that we would reach our highest monthly goal of 2000 USD in only a few days.

In addition to our regular maintenance and programmed improvements, at this level of funding, we will be able to post so-called "bounties", i.e., specific sums of cash for solving specific problems. Once posted, they will be available at this URL:


Like we mentioned in our initial appeal for contributions, this campaign has an important side-effect, beyond that of providing a budget for improvements, namely that of creating new excitement around McCLIM. This excitement will ultimately result in contributions in the form of code, thereby multiplying the importance of the direct monetary support.

In order to make our work on this project as transparent as possible, we will do our utmost to provide monthly reports on our progress, and we will provide details on how the cash was put to work.

We sincerely hope to make sufficient progress, sufficiently fast, that you will consider additional contributions, in the form of cash or code, in the future.


Robert Strandh and Daniel Kochmański

Crowdfunding McCLIM maintenance and development

posted on 2016-08-12 15:00

McCLIM is currently the only free native toolkit for developing GUI applications in Common Lisp. A while ago, I took over the maintenance, because I did not want to see McCLIM abandoned.

But since I have many other projects going, I decided to hire Daniel Kochmański part time (25%) to do some of the urgent work. This experience turned out so well that I would like for him to continue this work. Not only am I very pleased with the quality of Daniel's work, but I was also very surprised about the additional excitement that his work generated.

While I could continue funding Daniel myself for a while, I can not do so indefinitely. For that reason, I decided that we should try crowdfunding. Steady funding at the 25% level for a year or more would allow us to address some of the issues that often come up, such as the numerous remaining defects, the lack of modern visual appearance, and more.

If you are interested in seeing this happen, I strongly urge you to consider contributing to this project. Our target is modest. We would like to obtain at least 600 USD per month for at least one year. Even a small monthly contribution from a significant number of people can make this happen.

To contribute, please visit https://salt.bountysource.com/teams/mcclim and follow the instructions.


Robert Strandh

CLIM, the galaxy's most advanced graphics toolkit

posted on 2016-08-11 17:00

Setting up the basic environment

We assume that you already have configured the Common Lisp environment (including Quicklisp) and that you know the basics of the Common Lisp programming. The necessary systems to load are mcclim and clim-listener, you may load them with the Quicklisp. After that launch the listener in a separate thread:

(ql:quickload '(mcclim clim-listener))
(clim-listener:run-listener :new-process t)


Finding your way around CLIM

CLIM is the global minimum in graphics toolkits. Geometry, and means of abstracting it. Switch the listener into the CLIM-INTERNALS package to get started. Type (in-package climi) in the Listener REPL.


Evaluate the 4 following forms in Slime REPL, then call (cos-animation) in the Listener REPL to demonstrate CLIM's animation capabilities. You cannot evaluate (cos-animation) in the Slime REPL, as *standard-output* is bound to the its output stream which isn't a sheet, and thus cannot be drawn on.

(in-package climi)

(defparameter *scale-multiplier* 150
  "try modifying me while running!")

(defparameter *sleep-time* 0.0001
  "modify and eval to speed or slow the animation, set to `nil' to stop")

(defun cos-animation ()
  (let* ((range (loop for k from 0 to (* 2 pi) by 0.1 collect k)) ; length of 62
         (idx 0)
          (updating-output (*standard-output*)
            (loop for x from (nth idx range) to (+ (nth idx range) (* 2 pi)) by 0.01
               with y-offset = 150
               for x-offset = (- 10 (* *scale-multiplier* (nth idx range)))
               for y-value = (+ y-offset (* *scale-multiplier* (cos x)))
               for x-value = (+ x-offset (* *scale-multiplier* x))
               do (draw-point* *standard-output*
                               :ink +green+
                               :line-thickness 3)))))
    (loop while *sleep-time*
       do (progn (sleep *sleep-time*)
                 (if (= 61 idx) (setq idx 0) (incf idx))
                 (redisplay record *standard-output*)))))

If you want to stop the animation, issue in the Slime REPL:

(setf *sleep-time* nil)

If it wasn't already obvious, you can plot w/e.

 #'tanh :min-x (- 0 pi pi) :max-x pi :min-y -1.1 :max-y 1.5 :ink +blue+)

Drawning class hierarchy

Type ,clear output history in the Listener REPL and RET to clear the screen.

"," indicates that you are activating a command. Try typing comma, then C-/ to activate completion. C-c C-c to dismiss.

Children of the class CLIMI::SHEET can be drawn on using arbitrary geometry. Try (clim-listener::com-show-class-subclasses 'sheet) in the Listener REPL to view the subclasses of it.

Commands and presentations

The name COM-whatever indicates that the function in question is a clim command, which you can define in the Slime REPL like so,

(in-package clim-listener)

;;; Runme! We will need these in a moment.
(dolist (image-name '("mp-avatar.png"
  (uiop:run-program (format nil "curl https://common-lisp.net/project/mcclim/static/media/tutorial-1/~A --output /tmp/~A" 
                            image-name image-name)))

(define-listener-command (com-ls :name t)
  ((path 'string))
  (clim-listener::com-show-directory path))

try ,ls /tmp/ -- then ,display image <SPACE> and click on one of the displayed paths to supply it as an argument. At the core of CLIM is the notion of a presentation. Objects have presentation methods, ie, some arbitrary rendering geometry, and when PRESENT'd on the screen CLIM remembers the type. Thus one can supply objects of the appropriate types as arguments to a command simply by clicking on them. Read about CLIMI::DEFINE-COMMAND in the specification to learn more. Let's define our first presentation method.

Intermixing S-expressions with the presentation types

Evaluate the forms below in the Slime REPL:

(in-package climi)

(defvar lords '("mircea_popescu" "asciilifeform" "ben_vulpes"))

(defclass wot-identity ()
  ((name :accessor name :initarg :name :initform nil)
   (avatar :accessor avatar :initarg :avatar :initform nil)))

(defmethod lord? ((i wot-identity))
  (member (name i) lords  :test #'string=))

(define-presentation-type wot-identity ())

(defun make-identity (name avatar-pathname)
  (make-instance 'wot-identity
         :name name
         :avatar avatar-pathname))

(defparameter *identities*
  (mapcar (lambda (l) (apply #'make-identity l))
          '(("mircea_popescu" #P"/tmp/mp-avatar.png")
        ("ben_vulpes" #P"/tmp/vulpes-avatar.png")
        ("asciilifeform" #P"/tmp/stas-avatar.png")
        ("Suit" #P"/tmp/suit-avatar.png")
        ("RainbowDash" #P"/tmp/rainbow-dash-avatar.png")
        ("ChaosLord" #P"/tmp/chaos-lord-avatar.png"))))

(define-presentation-method present (object (type wot-identity)
                                            (view textual-view)
                                            &key acceptably)
  (declare (ignorable acceptably))
  (multiple-value-bind (x y)
      (stream-cursor-position stream)
    (with-slots (name avatar) object      
      (draw-pattern* stream
                     (climi::make-pattern-from-bitmap-file avatar :format :png)
                     (+ 150 x)
                     (+ 30 y))
      (draw-text* stream name (+ 153 x) (+ 167 y)
                  :ink +black+
                  :text-size 20)
      (draw-text* stream name (+ 152 x) (+ 166 y)
                  :ink (if (lord? object)
                  :text-size 20))
    (setf (stream-cursor-position stream)
          (values x (+ 200 y)))

(defun eval-and-then-call-me-in-the-listener ()
  (let* ((n 8)
         (sheet *standard-output*))
    (labels ((gen (i)
                (let* ((out-and-start '(f x)))
                   for k from 0 to i
                   do (setq out-and-start 
                            (apply #'append
                                    (lambda (s)
                                      (case s
                                        ;; (x '(y f + f f + + x)) 
                                        ;; (y '(y f + f f x - - f f x))
                                        (x '(+ y f + f f + y y +))
                                        (y '(f - y f f x f f))
                                        )) out-and-start))))
                  (remove-if (lambda (sym)
                               (member sym '(x y) :test 'eq))

      (let* ((x 300) (y 300) (new-x x) (new-y y) (a 1) (step 15))
         for r in (gen n)
         do (progn (cond ((= a 1) (setq new-y (+ step y)))
                         ((= a 2) (setq new-x (- x step)))
                         ((= a 3) (setq new-y (- y step)))
                         ((= a 4) (setq new-x (+ step x))))
                   (case r
                     (f (clim:draw-line* sheet x y new-x new-y
                                         :ink clim:+blue+
                                         :line-thickness 6
                                         :line-cap-shape :round)
                        (setq x new-x y new-y))
                     (- (setq a (if (= 1 a) 4 (1- a))))
                     (+ (setq a (if (= 4 a) 1 (1+ a))))
                     (t nil)))))
      (let* ((x 300) (y 300) (new-x x) (new-y y) (a 1) (step 15))
         for r in (gen n)
         do (progn (cond ((= a 1) (setq new-y (+ step y)))
                         ((= a 2) (setq new-x (- x step)))
                         ((= a 3) (setq new-y (- y step)))
                         ((= a 4) (setq new-x (+ step x))))
                   (case r
                     (f (clim:draw-line* sheet x y new-x new-y
                                         :ink clim:+white+
                                         :line-thickness 2
                                         :line-cap-shape :round)
                        (setq x new-x y new-y))
                     (- (setq a (if (= 1 a) 4 (1- a))))
                     (+ (setq a (if (= 4 a) 1 (1+ a))))
                     (t nil))))))))

Now type (dolist (i *identities*) (present i)) at the CLIM Listener.

Try typing (lord? at the listener and then clicking on one of the identities. Add a closing paren and RET. Notice how objects can be seamlessly intermixed with S-expressions. If this example fails for you it may be that you have not recent enough version of McCLIM.


Unripe fruits. The future isn't what it used to be - some assembly required.

  • (CLIM-DEMO::DEMODEMO) (available with system clim-examples)

  • The essential machinery of a 'live' GUI builder

  • Navigator (essentially an extended `apropos')


This blog covers history, tutorial