Monthly Archives: November 2014

The HP-67 emulator, rethinking the UI design

We have produced our basic interactive calculator with an ncurses interface, but it’s a bit unsatisfying.  The UI code has a somewhat more intimate knowledge of the internals of the engine than is really necessary, which means writing new user interfaces is difficult.  What we really want to do is to switch this around from a system where the UI does the polling to one where the event loop is located in the engine, and the UI knows only as much as it needs to display the calculator as seen by the specific interface.

What we want to produce is a system whereby the user interface sets itself up, paints its appearance, then invokes the engine.  The engine will then call back to the user interface to inform it of state changes and to tell it when and how to redraw itself.

We’ll put this in a new namespace, and create a base class from which real UIs will be derived.  The base class will contain many slots which describe those things that it might need in order to display correctly.  Generic functions will fill in these slots.  We’re defining the base class slots with readers, rather than accessors, to emphasize that the derived classes that define the user interfaces should not be modifying these fields, only reading them.  UIs that must take specialized action when one of these setter methods is called should generally use an :after method, unless there is a compelling reason to do something different.  The painting method will not have a base class definition, as every user interface will be different.

We will augment the list of generic functions as necessary.  For now, this list is a set of functions related to drawing of the user interface.  We have not yet written a definition for generic functions related to keypresses and input.

The current generic function definitions are found in a new file, ui.lisp.  They are reproduced here:
ui.lisp

;; The user interface base class and generic function definitions.

(defpackage :HP67-UI
  (:use :COMMON-LISP)
  (:export
   ))

(in-package :HP67-UI)

(defclass ui-base ()
  ((active-keys         :reader get-active-keys
                        :initform nil)
   (active-shift        :reader get-active-shift
                        :initform nil)
   (active-mode         :reader get-active-mode
                        :initform nil)
   (display-mode        :reader get-display-mode
                        :initform nil)
   (display-digits      :reader get-display-digits
                        :initform nil)
   (error-text          :reader get-error-text
                        :initform "")
   (stack-real-contents :reader get-stack-real-contents
                        :initform nil)
   (stack-imag-contents :reader get-stack-imag-contents
                        :initform nil)
   (memory-contents     :reader get-memory-contents
                        :initform nil)
   (program-contents    :reader get-program-contents
                        :initform nil)
   (program-counter     :reader get-program-counter
                        :initform nil)))



(defgeneric ui-set-active-keys (ui active-key-list)
  (:documentation "Used to inform the UI of what keys can be
pressed given the current state of the calculator.  The engine
will attempt not to call this function unless the active keys
list has changed."))

(defgeneric ui-set-active-shift (ui active-shift)
  (:documentation "Used to inform the UI that a shift key, (F, G,
or H) has been pressed."))

(defgeneric ui-set-active-mode (ui active-mode)
  (:documentation "Used to inform the UI of any change in
mode (interactive, program, running).  The engine will attempt
not to call this function unless the active mode has changed."))

(defgeneric ui-set-display-mode (ui display-mode display-digits)
  (:documentation "Used to inform the UI what the display mode
is.  The engine will attempt not to call this function unless the
display mode has changed."))

(defgeneric ui-set-complex-mode (ui how)
  (:documentation "Used to inform the UI whether or not we want
to display complext numbers.  If 'how' is non-nil, complex
display is requested.  The engine will attempt not to call this
function unless the complex mode has changed."))

(defgeneric ui-set-error-text (ui error-text)
  (:documentation "Used to inform the UI when an error has
occured.  The engine will not call this function unless an error
has just been set, or just been cleared."))

(defgeneric ui-has-error-text (ui)
  (:documentation "Used by the UI to establish whether there is
an error message to display."))

(defgeneric ui-clear-stack-contents (ui max-depth)
  (:documentation "Erase the UI's knowledge of the stack
contents, and allocate space for up values with depth up to
max-depth.  The engine will call this when setting up for a
paint."))

(defgeneric ui-add-stack-value (ui stack-depth
                                real-part-string
                                &optional imag-part-string)
  (:documentation "Inform the UI of the contents of the stack.  X
is at depth=0.  The engine will call this at least once when
setting up for a paint."))

(defgeneric ui-clear-memory-contents (ui)
  (:documentation "Erase the UI's knowledge of the memory
contents.  The engine will call this when setting up for a
paint."))

(defgeneric ui-add-memory-value (ui precedence label
                                 real-part-string
                                 &optional imag-part-string)
  (:documentation "Inform the UI of the contents of memory.  When
not enough space is available to display all memory contents,
those with higher precedence should be displayed ahead of those
with lower.  The engine may call this one or more times when
setting up for a paint."))

(defgeneric ui-clear-program-contents (ui)
  (:documentation "Erase the UI's knowledge of program memory.
The engine will normally not call this unless program steps have
been deleted."))

(defgeneric ui-add-program-step (ui step-num display-string)
  (:documentation "Inform the UI of the contents of a single step
of memory.  Step-num will be unique.  The engine will call
whenever new program steps are present."))

(defgeneric ui-get-program-step-string (ui step-num)
  (:documentation "Used by the UI to retrieve a particular step
number from the program memory."))

(defgeneric ui-set-program-counter (ui)
  (:documentation "Inform the UI of what program step would next
be executed if program execution were to begin.  The engine will
attempt to call this only when it has changed."))

(defgeneric ui-paint (ui)
  (:documentation "Ask the UI to repaint itself based on its new
settings."))

The current code is checked into the git repository under the tag v2014-11-30.

The HP-67 emulator, some more error-related cleanups

With this latest set of changes, we have the curses-based version of the emulator more or less complete for interactive use.  We’ve put in more error handling and fixed some bugs.

Soon, we’ll put in the programming mode, which will be a bit more complex than these minor patches.   After that, we’ll start extending the calculator beyond the abilities of the physical calculator.  That means allowing complex numbers, multi-character strings as labels and memory registers, perhaps hexadecimal output for integers.

This change also adds memory registers to the display.

Here’s what happens when you try to take the square root of -1 right now, before complex numbers are permitted:

cli-error-state2

Here is the display with some memory registers loaded, and display digits set to 5:

with-memory

The current code is in the git repository under the tag v2014-11-28.

The HP-67 emulator, hardening the input

While we’ve got a program that can accept commands and behave like a calculator, it is, at this point, somewhat brittle.  Unexpected input can cause it to enter the Lisp debugger, which isn’t how we’d like to have things behave.  So, it’s time to start making sure that bad input is handled gracefully.

We’ll define an error status region of the screen that will fill in with text when the calculator determines that an error has occurred.  In that case, we’ll mark the stack with an error text and force the user to type the “clx” command before continuing.  This means we have to add a new field to our keypress structure, “can clear errors”, so that we can make sure only that key can be used until the error has been cleared.

Now, in engine.lisp, we cheated a bit on the code that allows an entire number to be input at once.  We used read-from-string on unsanitized input to see if the string contained a number.  This can break in a variety of ways if the string contains, say, unbalanced double-quotes, to name only one of many bad cases.  So, to clean it up, we do a pre-test to verify that the string contains only characters that are legal in a numeric context.  That is, the digits, the dot, the minus sign, and ‘d’ or ‘e’ in either lower- or upper-case, for the exponential notation.

Here, then, is an example of what happens when the user tries to divide 1 by 0:cli-error-state

This code is checked into the git repository under the tag v2014-11-26.

The HP-67 emulator, a working calculator emerges

We are now ready to put together an actual useful calculator.  With an event loop and input handler, we can now perform interactive operations commanded by the user.  By displaying the stack, we can show the user the results of those operations.

One problem we will have is keyboards.  They keyboard has the ability to send control keys, most of which are handled in an implementation-dependent manner by Lisp.  We would like to filter out control characters unless they have some particular meaning to the program.  This is done with two new functions, quit-character and allowed-character, both defined under the reader macro #+sbcl, so only compiled when running on SBCL.  In SBCL, the character codes for the common keyboard characters are just their ASCII values, so we can filter keypresses by their ASCII code.

We also want the arithmetic operation keys to be hot keys.  That means that if they are the first character on a line, they will immediately take effect, as if the enter key had been pressed.

Main now looks like this:
curses-cli.lisp

(defun main()
  (charms:with-curses ()
    (let ((w charms:*standard-window*)
          n-rows n-cols)

      (multiple-value-setq
          (n-cols n-rows)
        (charms:window-dimensions w))

      (macrolet
          ((wsc (ostring)
             `(charms:write-string-at-cursor w ,ostring)))
        
        (charms:enable-echoing)
        (charms:disable-extra-keys w)
        (charms:disable-non-blocking-mode w)
        (charms:enable-raw-input :interpret-control-characters t)

        (let* ((stack (get-new-stack-object 4))
               (mode (get-new-mode-object))
               (all-keys (get-key-abbrevs mode
                                          :sort-fcn #'comp<
                                          :veto-list *excluded-keys*))
               (maxlen (apply 'max
                              (mapcar 'length all-keys)))
               (keys-per-row (floor (/ n-cols (1+ maxlen)))))

          (do (exit-requested)
              (exit-requested)

            (charms:clear-window w)

            (let ((active-keys (get-key-abbrevs mode
                                                :sort-fcn #'comp<
                                                :veto-list *excluded-keys*
                                                :limit-to-mode t))
                  (i 0)
                  (accumulator (make-string-output-stream)))

              (dolist (candidate all-keys)
                (when (member candidate active-keys
                              :test 'string=)
                  (multiple-value-bind (r c)
                      (floor i keys-per-row)
                    (charms:move-cursor w (* c (1+ maxlen)) r))
                  (wsc candidate))
                (incf i))

              (dotimes (j 4)
                (let ((entry (nth j (stack-registers stack))))
                  (when entry
                    (charms:move-cursor w 0 (- n-rows j 4))
                    (wsc (format-for-printing mode entry)))))

              (charms:move-cursor w 0 (- n-rows 2))

              (charms:refresh-window w)

              (do ((pos 0)
                   (c (charms:get-char w) (charms:get-char w)))
                  ((char= c #\Newline))

                (cond
                  ((quit-character c)
                   (return-from main))
                  ((allowed-character c)
                   (format accumulator "~C" c)
                   (incf pos)))

                (when (and (= pos 1)
                           (member c *hot-keys* :test 'char=))
                  (return)))

              (let ((result (get-output-stream-string accumulator)))
                (handle-one-keypress result nil nil stack mode
                                     :arg-is-num t))))

              )))))

It can be loaded into SBCL (but not SLIME) with the command:
(asdf:oos 'asdf:load-op 'curses-cli)

Here’s what this first version of the calculator looks like:

Initial version of the CLI
Initial version of the CLI

This code is in the git repository under the tag v2014-11-24.

The HP-67 emulator, a command-line interface begins

Now that the internals for interactive mode are mainly done, it’s time to put a front-end on it, so that we can have something to test on.

I’m not an interface person, I write computational engines, and have little experience interacting with the user of the program.  If a reader could recommend a good GUI from among those on cliki.net that I should use for the graphical interface, that would be helpful.

In the mean time, I’m starting a text-based interface, using NCurses.  The library I’ll be using is cl-charms, as it’s based on the more modern CFFI system for calling external libraries.

The CLI behaves differently from a GUI that replicates the behaviour of the calculator.  For instance, you’re not likely to input numbers one digit at a time, you can simply type the entire number into the prompt.  Also, some keys are unnecessary in this context.  For instance, the specific gosub keys for labels A, B, C, D, E, and their lower-case equivalents.  From the CLI you would just enter the label on the line, so having those keys there only clutters the display.

In the CLI, we will want all of the keys to have natural positions in the table, but only display those keys which have meaning in the current mode.  This means that keys that only have effect in programming mode shouldn’t be displayed in interactive mode.  We’ll leave those spaces blank, to avoid confusion with key names moving around based on the mode.

One minor annoyance, cl-charms doesn’t run within a SLIME context, so we’ll have to test outside of SLIME.

Here is the beginning of the CLI function.  This just displays the keys, waits 4 seconds, then exits:
curses-cli.lisp

(defun main()
  (charms:with-curses ()
    (charms:disable-echoing)
    (charms:disable-extra-keys charms:*standard-window*)
    (charms:enable-non-blocking-mode charms:*standard-window*)
    (charms:enable-raw-input :interpret-control-characters t)

    (multiple-value-bind (n-cols n-rows)
        (charms:window-dimensions charms:*standard-window*)

      (let* ((stack (get-new-stack-object 4))
             (mode (get-new-mode-object))
             (all-keys (get-key-abbrevs mode
                                        :sort-fcn #'comp<
                                        :veto-list *excluded-keys*))
             (maxlen (apply 'max
                            (mapcar 'length all-keys)))
             (keys-per-row (floor (/ n-cols (1+ maxlen)))))

        (do (exit-requested)
            (exit-requested)

          (charms:clear-window charms:*standard-window*)

          (let ((active-keys (get-key-abbrevs mode
                                              :sort-fcn #'comp<
                                              :veto-list *excluded-keys*
                                              :limit-to-mode t))
                (i 0))

            (dolist (candidate all-keys)
              (when (member candidate active-keys
                            :test 'string=)
                (multiple-value-bind (r c)
                    (floor i keys-per-row)
                  (charms:move-cursor charms:*standard-window*
                                      (* c (1+ maxlen))
                                      r))
                (charms:write-string-at-cursor charms:*standard-window*
                                               candidate))
              (incf i))

            (charms:refresh-window charms:*standard-window*)

            (sleep 4)

            (setf exit-requested t)

            ))))))

This code is checked into the git repository under the tag v2014-11-22.