The following article is a rebuttal of the following article: CL-CHARMS Crash Course
This article can also serve as a ``crash course'' in using my ACUTE-TERMINAL-CONTROL.
I recommend reading that article first, as the structure here mimicks the structure there. This is an article made with the intent of thoroughly defeating the notion that linking to a C library for something so simple and benign as sending control codes to a terminal device is anything resembling an adequate or sensible act. I will do this by showing, step-by-step, how my ACUTE-TERMINAL-CONTROL Common Lisp library, written entirely in Common Lisp, is an entirely better mechanism for this than wrapping Ncurses in a Common Lisp facade. Risking memory leaks shouldn't be necessary to control a terminal.
The only necessary software you need for this is a Common Lisp implementation that supports a seven-bit character set (ASCII is fine and likely supported.), followed by CL-ECMA-48 (This implements the ECMA-48 standard and ACUTE-TERMINAL-CONTROL uses it.), followed lastly by ACUTE-TERMINAL-CONTROL itself. I won't be involving SWANK in this, as that would unnecessarily complicate the matter. If you ever package a piece of software like this that manipulates a terminal device, you likely won't be using SWANK anyway.
To start, this is a basic program which will clear the terminal screen; display ``Hello world!'' beginning at location 10,10; and wait until the ``q'' key is pressed before returning:
(defun hello-world () (disable-system-echoing) (disable-system-buffering) (erase) (setf (cursor) '(10 . 10)) (write-string "Hello world!") (finish-output) (loop (if (char= #\q (read-char)) (return))) (values))
Now, compare this with the HELLO-WORLD in the other article. In order to hide Ncurses initialization and whatnot, since it's a C library, it's generally required to use a CL-CHARMS:WITH-CURSES macro in order to obfuscate this. CL-CHARMS also enforces a window abstraction and it's apparently necessary to repeatedly write the same output in a loop; I'm gladly unfamiliar with Ncurses and so don't know why this would ever be the case. Regardless, there's no continuous computation and so no need to use SLEEP to decrease it; note how standard Common Lisp functions can be used to interact with the terminal, rather than specialized such functions; this program simply communicates with *STANDARD-OUTPUT* and *STANDARD-INPUT*.
Now, before continuing, I also find it necessary to point out my distaste for the functions I've written which control system echoing and system buffering. Not only are these arguably rather malformed concepts, but they are entirely impossible to implement in standard Common Lisp, due to the design of the systems that impose this. I strongly urge every programmer to, instead, manipulate these settings as necessary before the program is even entered. It's likely that a Common Lisp program packaged for use would be begun by a program more amicable to the native system; under POSIX, I use
s=$(stty -g); stty raw -echo for this and later restore the previous settings upon exit; my Common Lisp stays free of these concerns and it is merely the relatively short and unimportant native program that must capitulate to any considerations of the underlying environment.
The lower level interface ACUTE-TERMINAL-CONTROL uses is CL-ECMA-48, which implements the ECMA-48 standard. Rather than reading a ``HOWTO'', you should read the document, which is an actual standard that can be relied upon and with a canonical defining document. Furthermore, by familiarizing yourself with this, you'll understand how simple it truly is to build an interface with a suitable terminal; it's not some Herculean task requiring gargantuan libraries after all.
ACUTE-TERMINAL-CONTROL defines no function for drawing windows; in Common Lisp, it's easy enough to define this by one's self. Follows is a trivial definition; as an optimization, this will be defined in terms of CL-ECMA-48, sans the use of CURSOR to obtain the current coordinates:
(defun draw-window-border (width height &aux (cursor (cursor))) (or cursor (error "The stream doesn't seem to correspond to a terminal device.")) (assert (> width 1) (width)) (assert (> height 1) (height)) (write-char #\+) (if (> width 2) (write-char #\-)) (if (> width 3) (repeat (- width 3))) (write-char #\+) (if (> height 2) (dotimes (h (- height 2)) (declare (ignorable h)) (next-line) (cursor-character-absolute (car cursor)) (write-char #\|) (cursor-character-absolute (+ -1 width (car cursor))) (write-char #\|))) (next-line) (cursor-character-absolute (car cursor)) (write-char #\+) (if (> width 2) (write-char #\-)) (if (> width 3) (repeat (- width 3))) (write-char #\+) (values))
There are clearly ways this could be improved or specialized to the specific program, but this is sufficient to illustrate the concept and how simple it is. It also demonstrates optimization opportunities that are lost at higher levels, such as using the REPEAT control function rather than repeatedly writing a single character or creating a string all of one character to write. This function requires system echoing and system buffering to be disabled in order to work properly.
There's no need to have a START-COLORS function, as this is both an ugly initialization function and a terminal should ignore control functions it can't fulfill; so, we won't be returning C error codes, either. Similarly, color codes are defined by integers, rather than C definitions. The Ncurses abstraction of color pairs is also unnecessary. Colors are handled in ACUTE-TERMINAL-CONTROL with the (SETF BACKGROUND) and (SETF FOREGROUND) functions using named symbols; for colors outside of the ECMA-48 standard, ISO 8613-6 colors may be used.
Here is a function similar to DRAW-WINDOW-BORDER that instead draws a blank colored rectangle; it properly reinstates the previous background color if there's no interruptions; a more practical version would use UNWIND-PROTECT to enforce this:
(defun draw-window-background (width height color &aux (cursor (cursor)) (previous (background))) (or cursor (error "The stream doesn't seem to correspond to a terminal device.")) (assert (> width 0) (width)) (assert (> height 0) (height)) (setf (background) color) (dotimes (h height) (declare (ignorable h)) (write-char #\space) (repeat (1- width)) (next-line) (cursor-character-absolute (car cursor))) (setf (background) previous) (values))
Follows is a ``pretty'' hello world, elaborated on from earlier:
(defun pretty-hello-world () (disable-system-echoing) (disable-system-buffering) (erase) (setf (cursor) '(10 . 10)) (draw-window-background 50 15 :blue) (setf (foreground) :white (background) :blue) (setf (cursor) '(10 . 10)) (write-string "Hello world!") (setf (foreground) :red (background) :black) (setf (cursor) '(11 . 10)) (write-string "Hello world!") (setf (foreground) :default (background) :default) (finish-output) (loop (if (char= #\q (read-char)) (return))) (values))
Now here is an ``amazing'' hello world:
(defun amazing-hello-world (&aux (foreground '#1=(:white :blue . #1#)) (background '#2=(:black :red . #2#))) (disable-system-echoing) (disable-system-buffering) (erase) (setf (cursor) '(10 . 10)) (draw-window-background 50 15 :blue) (setf (cursor) '(10 . 10)) (loop (setf (foreground) (car foreground) (background) (car background) foreground (cdr foreground) background (cdr background)) (write-string "Hello world!") (cursor-character-absolute 10) (force-output) (if (eql #\q (read-char-no-hang)) (return)) (sleep 1)) (setf (foreground) :default (background) :default) (values))
Now, the obvious issue is a lack of responsiveness caused by the SLEEP. The obvious solution is to introduce multiple threads of program execution. Since this doesn't use Ncurses, it doesn't matter that Ncurses isn't ``thread-safe''. It's simple to use BORDEAUX-THREADS here; due to the API description, a special variable will be used:
(defparameter improved-amazing-hello-world-thread (bt:current-thread)) (defun improved-amazing-hello-world (&aux (foreground '#1=(:white :blue . #1#)) (background '#2=(:black :red . #2#))) (bt:make-thread (lambda () (loop (if (eql #\q (read-char)) (bt:interrupt-thread improved-amazing-hello-world-thread (lambda () (throw 'end nil))))))) (disable-system-echoing) (disable-system-buffering) (erase) (setf (cursor) '(10 . 10)) (draw-window-background 50 15 :blue) (setf (cursor) '(10 . 10)) (catch 'end (loop (setf (foreground) (car foreground) (background) (car background) foreground (cdr foreground) background (cdr background)) (write-string "Hello world!") (cursor-character-absolute 10) (force-output) (sleep 1))) (setf (foreground) :default (background) :default) (values))
Now, I will not bother with reconstructing a simpler form of the following code in that other article; part of the function of this article is showing that requiring Ncurses, and immediately making your otherwise nice Common Lisp perfectly unportable and dependent on C, is ridiculous for such basic programming tasks. Now, part of writing a new terminal program is, firstly, understanding that this is technology from the same era as Common Lisp and, like Common Lisp, was designed with some foresight that keeps it nice and useful in modern times; however, it will inevitably shape the interface you present and a suitably complex interface would be better suited by a true graphical interface; you're better off creating a unique interface, rather than shoehorning buttons and whatnot into the model. Secondly and unlike Common Lisp, the terminal standards are enforced and progressed rather solely by the implementors of the terminals and the xterm developers in particular have slowly piled on poorly designed interfaces that must be worked around in order to use certain features, because they had little sense of aesthetics. No terminal I'm aware of properly parses ISO 8613-6 colors, as an example, and require an invalid sequence to be sent instead.
I will now discuss comparable mouse functionality. This isn't hard either; it's simply poor design on the part of the xterm experts that makes it a bother; there's four mouse APIs: two of which are broken and not recommended; one which is still broken, but recommended, and the only option in some cases; and one which is recommended, the least broken, and the only one that should actually behave correctly in all circumstances on modern terminals with higher resolutions.
The relevant enabling function is ENABLE-MOUSE-REPORTING; compare it to START-MOUSE from the other article; this merely uses the SET-MODE and RESET-MODE control functions with parameters dictated by xterm's interfaces:
(defun enable-mouse-reporting (&optional report-hover (stream *standard-output*) &aux (*standard-output* stream)) "Enable mouse reporting events. If REPORT-HOVER is true, those are enabled; if not, those are explicitly disabled." (if report-hover (set-mode '(#\? 9 1000 1003 1006)) (progn (reset-mode 1003) (set-mode '(#\? 9 1000 1006)))))
I put a good effort to make the majority of ACUTE-TERMINAL-CONTROL functions SETF functions, as this is a nice and familiar interface for Common Lisp. For reading in higher level events, it's not necessary to call any initialization function; I merely employ the READ-EVENT and READ-EVENT-NO-HANG functions. Through these two simple functions, higher level events can be recognized and handled. The ACUTE-TERMINAL-CONTROL documentation is much more comprehensive.
Now, in conclusion, I've demonstrated that defining terminal interfaces is a rather simple task. The only reason one would employ an entire C library for something so simple is pure laziness. I consider myself to be a lazy programmer and, being too lazy to read the Ncurses documentation and deal with CFFI errors, I defined my own simple interface for much of the same thing. The CL-ECMA-48 and ACUTE-TERMINAL-CONTROL source code, together, is less than one thousand lines of Common Lisp and is well documented. So ends my rebuttal of that CL-CHARMS Crash Course.