├── doc ├── 72dpi.png ├── 96dpi.png ├── hdpi.png ├── mdpi.png ├── console.png ├── plotter.png ├── plotters.png ├── printer.png ├── xxxhdpi.png ├── hdpi-cyan.png ├── hq-plotter.png ├── graft-drawing.png ├── hdpi-violet.png ├── plotter-wrong.png ├── printer-cyan.png ├── xxxhdpi-cyan.png ├── 72dpi-96dpi-sbs.png ├── plotter-wrong-sbs.png ├── screens-different-kinds.png ├── sheets-different-units.png ├── grafts.md └── drawing.lisp ├── crash-course ├── cc-border.png ├── cc-final.png ├── cc-input.png ├── cc-background.png ├── cc-text-and-buttons.png ├── crash-course.lisp └── cc-asciinema.json ├── src ├── medium.lisp ├── package.lisp ├── charming-clim.lisp~ ├── charming-clim.lisp ├── frame-manager.lisp ├── various.lisp ├── port.lisp └── classes.lisp ├── charming-clim.asd └── README.md /doc/72dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/72dpi.png -------------------------------------------------------------------------------- /doc/96dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/96dpi.png -------------------------------------------------------------------------------- /doc/hdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/hdpi.png -------------------------------------------------------------------------------- /doc/mdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/mdpi.png -------------------------------------------------------------------------------- /doc/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/console.png -------------------------------------------------------------------------------- /doc/plotter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/plotter.png -------------------------------------------------------------------------------- /doc/plotters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/plotters.png -------------------------------------------------------------------------------- /doc/printer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/printer.png -------------------------------------------------------------------------------- /doc/xxxhdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/xxxhdpi.png -------------------------------------------------------------------------------- /doc/hdpi-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/hdpi-cyan.png -------------------------------------------------------------------------------- /doc/hq-plotter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/hq-plotter.png -------------------------------------------------------------------------------- /doc/graft-drawing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/graft-drawing.png -------------------------------------------------------------------------------- /doc/hdpi-violet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/hdpi-violet.png -------------------------------------------------------------------------------- /doc/plotter-wrong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/plotter-wrong.png -------------------------------------------------------------------------------- /doc/printer-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/printer-cyan.png -------------------------------------------------------------------------------- /doc/xxxhdpi-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/xxxhdpi-cyan.png -------------------------------------------------------------------------------- /doc/72dpi-96dpi-sbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/72dpi-96dpi-sbs.png -------------------------------------------------------------------------------- /crash-course/cc-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/crash-course/cc-border.png -------------------------------------------------------------------------------- /crash-course/cc-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/crash-course/cc-final.png -------------------------------------------------------------------------------- /crash-course/cc-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/crash-course/cc-input.png -------------------------------------------------------------------------------- /doc/plotter-wrong-sbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/plotter-wrong-sbs.png -------------------------------------------------------------------------------- /crash-course/cc-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/crash-course/cc-background.png -------------------------------------------------------------------------------- /doc/screens-different-kinds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/screens-different-kinds.png -------------------------------------------------------------------------------- /doc/sheets-different-units.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/doc/sheets-different-units.png -------------------------------------------------------------------------------- /crash-course/cc-text-and-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkochmanski/charming-clim/HEAD/crash-course/cc-text-and-buttons.png -------------------------------------------------------------------------------- /src/medium.lisp: -------------------------------------------------------------------------------- 1 | 2 | ;;;; Copyright (c) 2018, Daniel Kochmański (daniel@turtleware.eu) 3 | ;;;; 4 | ;;;; Implementation of the charming medium. 5 | 6 | (in-package #:charming-clim) 7 | -------------------------------------------------------------------------------- /src/package.lisp: -------------------------------------------------------------------------------- 1 | (cl:in-package #:common-lisp-user) 2 | 3 | (defpackage #:charming-clim 4 | (:use #:clim-lisp) 5 | (:export #:test-charming-clim 6 | #:start-swank-and-hang)) 7 | -------------------------------------------------------------------------------- /src/charming-clim.lisp~: -------------------------------------------------------------------------------- 1 | ;;;; charming-clim.lisp 2 | ;;;; 3 | ;;;; Copyright (c) 2018 Daniel Kochmański 4 | 5 | (in-package #:charming-clim) 6 | 7 | ;;; "charming-clim" goes here. Hacks and glory await! 8 | 9 | -------------------------------------------------------------------------------- /src/charming-clim.lisp: -------------------------------------------------------------------------------- 1 | 2 | ;;;; Copyright (c) 2018, Daniel Kochmański (daniel@turtleware.eu) 3 | ;;;; 4 | ;;;; Attaching backend to McCLIM core machinery. 5 | 6 | (in-package #:charming-clim) 7 | 8 | ;;; This trick allows us to attach new backends without the hassle of having 9 | ;;; central backend registry. 10 | (setf (get :charming :port-type) 'charming-port) 11 | (setf (get :charming :server-path-parser) 'identity) 12 | -------------------------------------------------------------------------------- /src/frame-manager.lisp: -------------------------------------------------------------------------------- 1 | 2 | ;;;; Copyright (c) 2018, Daniel Kochmański (daniel@turtleware.eu) 3 | ;;;; 4 | ;;;; Implementation of the charming frame manager. 5 | 6 | (in-package #:charming-clim) 7 | 8 | (defmethod clim:make-pane-1 ((fm charming-frame-manager) (frame clim:application-frame) 9 | type &rest initargs) 10 | (apply #'make-instance type :frame frame :manager fm :port (clim:port frame) initargs)) 11 | -------------------------------------------------------------------------------- /charming-clim.asd: -------------------------------------------------------------------------------- 1 | ;;;; charming-clim.asd 2 | ;;;; 3 | ;;;; Copyright (c) 2018 Daniel Kochmański 4 | 5 | (asdf:defsystem #:charming-clim 6 | :description "Charming CLIM is cl-charms backend for McCLIM." 7 | :author "Daniel Kochmański" 8 | :license "BSD-2-Clause" 9 | :depends-on (#:mcclim #:cl-charms) 10 | :serial t 11 | :pathname "src" 12 | :components ((:file "package") 13 | (:file "charming-clim") 14 | (:file "classes") 15 | (:file "port") 16 | (:file "frame-manager") 17 | (:file "medium") 18 | (:file "various"))) 19 | 20 | -------------------------------------------------------------------------------- /src/various.lisp: -------------------------------------------------------------------------------- 1 | 2 | ;;;; Copyright (c) 2018, Daniel Kochmański (daniel@turtleware.eu) 3 | ;;;; 4 | ;;;; Test application and other handy stuff simplifying development. 5 | 6 | (in-package #:charming-clim) 7 | 8 | ;;; Test application. 9 | (clim:define-application-frame cc-test () 10 | () 11 | (:pane :application-pane :display-function #'display) 12 | (:geometry :height 100 :width 100)) 13 | 14 | (defgeneric display (frame pane) 15 | (:method ((frame cc-test) (pane clim:application-pane)) 16 | (format pane "Hello world!"))) 17 | 18 | (defun test-charming-clim (&optional (port :charming)) 19 | (let ((clim:*default-server-path* (list port))) 20 | (clim:run-frame-top-level 21 | (clim:make-application-frame 'cc-test)))) 22 | 23 | (defvar *console-io* *terminal-io*) 24 | 25 | ;;; Swank server and wait loop. 26 | (defun start-swank-and-hang () 27 | (asdf:load-system :swank) 28 | (funcall (find-symbol "CREATE-SERVER" "SWANK") :port 5555 :dont-close t) 29 | (loop (sleep 1))) 30 | 31 | (defun %start-color () 32 | (when (eql (charms/ll:has-colors) charms/ll:FALSE) 33 | (error "Your terminal does not support color.")) 34 | (let ((ret-code (charms/ll:start-color))) 35 | (if (= ret-code 0) 36 | T 37 | (error "start-color error ~s." ret-code)))) 38 | -------------------------------------------------------------------------------- /src/port.lisp: -------------------------------------------------------------------------------- 1 | 2 | ;;;; Copyright (c) 2018, Daniel Kochmański (daniel@turtleware.eu) 3 | ;;;; 4 | ;;;; Implementation of the charming port. 5 | 6 | (in-package #:charming-clim) 7 | 8 | ;;; initialize-instance :after method plays role of the port constructor. 9 | (defmethod initialize-instance :after ((port charming-port) &rest initargs) 10 | (declare (ignore initargs)) 11 | ;; Fix parents. 12 | (setf (slot-value (clim:port-pointer port) 'clim:port) port 13 | (slot-value (clim:frame-manager port) 'clim:port) port) 14 | ;; Initialize terminal. 15 | (clim:restart-port port)) 16 | 17 | ;;; In principle port may have many frame managers and each frame manager is 18 | ;;; associated with one port. McCLIM implements this by having `frame-managers' 19 | ;;; reader in `basic-port', but we have only one fm. 20 | (defmethod climi::frame-managers ((port charming-port)) 21 | (list (clim:frame-manager port))) 22 | 23 | ;;; This method should restart event processing loop and discard all pending 24 | ;;; events. It may (but doesn't have to) restart connection to the server. 25 | (defmethod clim:restart-port :after ((port charming-port)) 26 | ;; If port was not initialized `charms/ll:endwin' will return error. 27 | (ignore-errors (clim:destroy-port port)) 28 | ;; Initialize charms using the high-level interface. 29 | (let ((win (charms:initialize))) 30 | (charms:enable-raw-input) 31 | (charms:disable-echoing) 32 | (charms:enable-extra-keys win) 33 | (charms:disable-non-blocking-mode win) 34 | ;; This part could be ommited, but we want some visual effect of our WIP. 35 | (multiple-value-bind (w h) (charms:window-dimensions win) 36 | (let* ((str "Charming CLIM port initialized.") 37 | (x (truncate (- w (length str)) 2)) 38 | (y (truncate h 2))) 39 | (charms:clear-window win) 40 | (charms:write-string-at-point win str x y) 41 | (charms:refresh-window win))))) 42 | 43 | ;;; Finalize connection to the port and release all resources if any. 44 | (defmethod clim:destroy-port :after ((port charming-port)) 45 | (charms:finalize)) 46 | 47 | (defmethod clim:make-medium ((port charming-port) sheet) 48 | (make-instance 'charming-medium :sheet sheet)) 49 | 50 | ;;; Note to myself: find the purpose of the following functions, add generic 51 | ;;; function definitions and document them: 52 | ;;; port-set-mirror-region 53 | ;;; port-set-mirror-transformation 54 | -------------------------------------------------------------------------------- /src/classes.lisp: -------------------------------------------------------------------------------- 1 | 2 | ;;;; Copyright (c) 2018, Daniel Kochmański (daniel@turtleware.eu) 3 | ;;;; 4 | ;;;; Class definitions with brief commentary for a CLIM backend based on 5 | ;;;; cl-charms (ncurses wrapper written in Common Lisp). 6 | 7 | (in-package #:charming-clim) 8 | 9 | ;;; Port is a logical connection to a display server. It abstracts managament of 10 | ;;; windows, resources, incoming events etc. 11 | ;;; 12 | ;;; Usually we want to subclass `basic-port' which has some functionality 13 | ;;; implemented out of the box and provides a reasonable framework which is 14 | ;;; suitable as a starting point for client-server communication model. 15 | ;;; 16 | ;;; With port we have associated a few objects: its primary pointer, the frame 17 | ;;; manager and the keyboard-focus. `keyboard-input-focus' holds the client to 18 | ;;; whom keyboard events are dispatched. Called by `stream-set-input-focus'. 19 | ;;; 20 | ;;; Note to myself: pointer should be member of basic-port with a reader 21 | ;;; `port-pointer'. This method is mentioned in the spec in context of other 22 | ;;; operators but it is not documented (ommision probably). This should be 23 | ;;; documented too. 24 | ;;; 25 | ;;; Note to myself: we define method `frame-manager' specialized on port in CLIM 26 | ;;; package. This method is usually specialized on frame. 27 | (defclass charming-port (clim:basic-port) 28 | ((pointer :accessor clim:port-pointer 29 | :initform (make-instance 'charming-pointer)) 30 | (frame-manager :accessor clim:frame-manager 31 | :initform (make-instance 'charming-frame-manager)) 32 | ;; See annotation in http://bauhh.dyndns.org:8000/clim-spec/8-1.html#_304 33 | (keyboard-input-focus :accessor clim:port-keyboard-input-focus 34 | :initform nil))) 35 | 36 | ;;; Pointer is a class representing the pointing device. It is worth noting that 37 | ;;; CLIM doesn't mention possibility of multiple pointers associated with the 38 | ;;; port, however `tracking-pointer' documentation uses phrasing "primary 39 | ;;; pointer" for the sheet being `port-pointer' of its port. That means that 40 | ;;; "secondary pointer" is an option too. See method `pointer-event-pointer'. 41 | ;;; 42 | ;;; In curses pointer may be in one of three states: invisible, normal and very 43 | ;;; visible (usually that means blinking). 44 | ;;; 45 | ;;; Note to myself: `standard-pointer' should implement some of the common 46 | ;;; methods that are currently entirely provided by backends. Same goes for the 47 | ;;; coordinates and `pointer-cursor' accessor. Documentation should be provided 48 | ;;; too for the `pointer-cursor' which is underdocumented. 49 | (defclass charming-pointer (clim:standard-pointer) 50 | ((cursor :accessor clim:pointer-cursor 51 | :initform charms/ll:cursor_normal 52 | :type (member charms/ll:cursor_invisible 53 | charms/ll:cursor_normal 54 | charms/ll:cursor_very_visible)) 55 | (x :initform 0) 56 | (y :initform 0))) 57 | 58 | ;;; Frame manager is responsible for the actual realization of look-and-feel of 59 | ;;; the frame and for selecting pane types for abstract gadgets. When a frame is 60 | ;;; adopted by the frame manager, protocol for generating pane hierarchy should 61 | ;;; be invoked. 62 | ;;; 63 | ;;; Note to myself: `basic-port' has slot `frame-managers' (plural) and we 64 | ;;; manage a list of such. From my initial investigation there is always one 65 | ;;; frame manager associated with the port and we call it in 66 | ;;; `Core/clim-core/frames.lisp'. This should be simplified and burden of 67 | ;;; implementing multiple frame managers should be put on the backend 68 | ;;; implementer. We should add method `frame-managers' which returns all backend 69 | ;;; frame managers defaulting to list made of the only frame-manager. 70 | ;;; 71 | ;;; Note to myself: there seems to be some hackery in port initialize-instance 72 | ;;; for each backend to actually set a frame-manager - couldn't it be 73 | ;;; implemented with `default-initarg' of `basic-port' subclass? 74 | ;;; 75 | ;;; Note to myself: all backends provide a hackish mapping between generic 76 | ;;; gadget name and a concrete pane class. This involves some symbol manging 77 | ;;; etc. It may be worth to investigate whenever some more intelligible internal 78 | ;;; protocol could be pulled off for that which will be common for frame 79 | ;;; managers. 80 | (defclass charming-frame-manager (clim:frame-manager) ()) 81 | 82 | ;;; Graft is a root window in the display server being represented as a CLIM 83 | ;;; sheet. Many grafts may be connected to the same display server (port). 84 | ;;; 85 | ;;; Note to myself: grafts are pretty versatile but we don't seem to implement 86 | ;;; neither units nor orientation. When fixed add appropriate annotation to 87 | ;;; `http://bauhh.dyndns.org:8000/clim-spec/edit/apropos?q=find-graft'. 88 | (defclass charming-graft (clim:graft) ()) 89 | 90 | ;;; Medium is an abstraction for graphics state of a sheet like bounding 91 | ;;; rectangle, ink, text-style or line-style. Medium also contains a sheet 92 | ;;; transformation. 93 | (defclass charming-medium (clim:basic-medium) ()) 94 | -------------------------------------------------------------------------------- /doc/grafts.md: -------------------------------------------------------------------------------- 1 | 2 | # Sheets as ideal forms 3 | 4 | CLIM operates on various kinds of objects. Some are connected by an inheritance, 5 | other by a composition and some are similar in a different sense. 6 | 7 | As programmers, we often deal with the inheritance and the composition, 8 | especially since OOP is a dominating paradigm of programming (no matter if the 9 | central concept is the object or the function). Not so often we deal with the 10 | third type of connection, that is the Form and the phenomena which are merely a 11 | shadow mimicking it[^1]. 12 | 13 | Let us talk about sheets. The sheet is a region[^2] with an infinite resolution 14 | and potentially infinite extent on which we can draw. Sheets may be arranged 15 | into hierarchies creating a windowing system with a child-parent relation. Sheet 16 | is the Form with no visual appearance. What we observe is an approximation of 17 | the sheet which may be hard to recognize when compared to other approximations. 18 | 19 | [^1]: [Theory of forms](https://en.wikipedia.org/wiki/Theory_of_forms). 20 | 21 | [^2]: And many other things. 22 | 23 | ## Physical devices 24 | 25 | Sheet hierarchies may be manipulated without a physical medium but to make them 26 | visible we need to draw them. CLIM defines `ports` and `grafts` to allow rooting 27 | sheets to a display server. `medium` is defined to draw sheet approximation on 28 | the screen[^3]. 29 | 30 | How should look a square with side length 10 when drawn on a sheet? Since it 31 | doesn't have a unit we can't tell. Before we decide on a unit we choose we need 32 | to take into consideration at least the following scenarios: 33 | 34 | 1. If we assume device-specific unit results may drastically differ (density may 35 | vary from say 1200dpi on a printer down to 160dpi on a desktop[^4]!. The very 36 | same square could have 1cm on a paper sheet, 7cm and 10cm on different displays 37 | and 200cm on a terminal. From the perspective of a person who uses the 38 | application, this may be confusing because physical objects don't change size 39 | without a reason. 40 | 41 | 2. Another possible approach is to assume the physical world distances measured 42 | in millimeters (or inches if you must). Thanks to that object will have a 43 | similar size on different mediums. This is better but still not good. We have to 44 | acknowledge that most computer displays are pixel based. Specifying distances in 45 | millimeters will inevitably lead to worse drawing quality[^5] (compared to the 46 | situation when we use pixels as the base unit). Moreover, conversion from 47 | millimeter values to the pixel will most inevitably lead to work on floats. 48 | 49 | 3. Some applications may have specific demands. For instance, application is 50 | meant to run on a bus stop showing arrival times. Space of the display is very 51 | limited and we can't afford approximation from the high-density specification 52 | (in pixels or millimeters) to 80x40 screen (5 lines of 8 characters with 5x8 53 | dots each). We need to be very precise and the ability to request a particular 54 | unit size for a sheet is essential. Of course, we want to test such application 55 | on a computer screen. 56 | 57 | I will try to answer this question in a moment. First, we have to talk about the 58 | `CLIM` specification and limitations imposed by McCLIM's implementation of 59 | grafts. 60 | 61 | [^3]: See some [general recommendations](http://bauhh.dyndns.org:8000/clim-spec/12-4.html). 62 | 63 | [^4]: Technically it should be [PPI, not DPI](https://99designs.com/blog/tips/ppi-vs-dpi-whats-the-difference/) (pixels per inch). 64 | 65 | [^5]: Given programmer specifies sheet size in integers (like 100x100). 66 | 67 | ## Ports, grafts, and McCLIM limitations 68 | 69 | If a port is a physical connection to a display server then graft is its screen 70 | representation. The following picture illustrates how the same physical screen 71 | may be perceived depending on its settings and the way we look at it. 72 | 73 | ![Graft drawing](graft-drawing.png) 74 | 75 | As we can see graft has an orientation (`:default` starts at the top-left corner 76 | like a paper sheet and `:graphics` should start at the bottom left corner like 77 | on the chart). Moreover, it has units. Currently, McCLIM will recognize 78 | `:device`, `:inches`, `:millimeters` and `:screen-sized`[^11]. 79 | 80 | That said McCLIM doesn't implement graft in a useful way. Everything is measured 81 | in pixels (which `:device` units are assumed to be) and only the `:default` 82 | orientation is implemented. By now we should already know that pixels are a not 83 | a good choice for the default unit. Also, programmer doesn't have means to 84 | request unit for a sheet (this is the API problem). 85 | 86 | [^11]: Screen size unit means that the coordinate [1/2, 1/2] is exactly in the 87 | middle of the screens. 88 | 89 | ## Physical size and pixel size compromise 90 | 91 | We will skip the third situation, for now, to decide what unit should we default 92 | to. There are cognitive arguments for units based on a real-world distance but 93 | there are also and practical reasons against using millimeters – poor mapping to 94 | pixels and the fact that CLIM software which is already written is defined with 95 | the assumption that we operate on something comparable to a pixel. 96 | 97 | Having all that in mind the default unit should 98 | be 99 | [device-independent pixel](https://en.wikipedia.org/wiki/Device-independent_pixel). 100 | One of McCLIM long-term goals is to adhere 101 | to [Material Design](https://material.io/) guidelines – that's why we will use 102 | dip[^6] unit. 100dp has absolute value 15.875mm. On 160dpi display it is 100px, 103 | on 96dpi display it is 60px, on 240dpi display it is 150px etc. This unit gives 104 | us some nice characteristics: we are rather compatible with the existing 105 | applications and we preserving absolute distances across different screens. 106 | 107 | [^6]: [Density-independent pixel](https://material.io/guidelines/layout/units-measurements.html#units-measurements-density-independent-pixels-dp). 108 | 109 | ## How to draw a rectangle on the medium 110 | 111 | Application medium may be a pixel-based screen, paper sheet or even a text 112 | terminal. When the programmer writes the application he operates on dip units 113 | which have absolute value 0.15875mm. It is the McCLIM responsibility to map 114 | these units onto the device. To be precise each graft needs to hold an extra 115 | transformation which is applied before sending content to the display server. 116 | 117 | Now we will go through a few example mappings of two rectangle borders[^7] drawn 118 | on a sheet. The violet rectangle coordinates are `[5,5], [22,35]` and the cyan 119 | rectangle coordinates are `[25,10], [30,15]`. 120 | 121 | * MDPI display device units are dip and they match native units of our 122 | choosing. No transformation is required. 123 | 124 | ![Graft drawing](mdpi.png) 125 | 126 | * Some old displays have density 72PPI. Not all coordinates map exactly to 127 | pixels - we need to round them[^3]. Notice that border is thicker and that 128 | proportions are a little distorted. On the other hand despite a big change in 129 | resolution size of the object is similar in real-world values. 130 | 131 | Windows Presentation Foundation declared 96PPI screen's pixel being the 132 | device-independent pixel because such displays were pretty common on 133 | desktops. Almost all coordinates map perfectly on this screen. Notice the 134 | approximation of the right side of the violet rectangle. 135 | 136 | ![Lower DPI](72dpi-96dpi-sbs.png) 137 | 138 | * Fact that the screen has higher density doesn't mean that coordinates mapping 139 | perfectly on a lower density screen will map well to a higher density 140 | one. Take this HDPI screen. Almost all coordinates are floats while on the 141 | MDPI display they had all integer values. 142 | 143 | ![HDPI](hdpi.png) 144 | 145 | * Higher resolution makes rectangles look better (borderline is thinner and 146 | distortions are less visible to the eye). Here is XXXHDPI: 147 | 148 | ![XXXHDPI](xxxhdpi.png) 149 | 150 | * Some printers have a really high DPI, here is imaginary 2560 DPI printer. 151 | Funnily enough its accuracy exceeds our screen density so the red border which 152 | is meant to show the "ideal" rectangle is a little off (it's fine if we scale 153 | the image though). 154 | 155 | ![HQ Printer](printer.png) 156 | 157 | * Until now we've seen some screens with square pixels (or dots). Let's take a 158 | look at something with a really low density - a character terminal. To make 159 | the illustration better we assume an absurd terminal which has 5x8 DP per 160 | character (too small to be seen by a human eye). Notice that the real size of 161 | the rectangles is still similar. 162 | 163 | ![Character terminal](console.png) 164 | 165 | It is time to deal with `graphics` orientation (Y-axis grows towards the 166 | top). An imaginary plotter with 80DPI resolution will be used to illustrate two 167 | solutions (the first one is wrong!). Knowing the plotter screen height is 168 | important to know where we start the drawing. 169 | 170 | * Graft reverts Y axis and sends the image to the plotter. Do you see what is 171 | wrong with this picture? We have defined both rectangles in default 172 | orientation, so our drawing should look similar disregarding the medium we 173 | print on. We do preserve the real size but we don't preserve the image 174 | orientation – cyan rectangle should be higher on the plot. 175 | 176 | ![Plotter (bad transformation)](plotter-wrong-sbs.png) 177 | 178 | * Correct transformation involves reverting Y axis and translating objects by 179 | the screen height. See the correct transformation (on 80DPI and on MDPI 180 | plotter). 181 | 182 | ![80DPI and MDPI plotters](plotters.png) 183 | 184 | [^7]: the Ideal border is composed of lines which are 1-dimensional objects 185 | which doesn't occupy any space. Red border in drawings marks "ideal" object 186 | boundaries. Points are labeled in device units (with possible fractions). 187 | 188 | ## Sheets written with a special device in mind 189 | 190 | There is still an unanswered question - how can we program applications with a 191 | specific device limitations in mind? As we have discussed earlier default sheet 192 | unit should be dip and default sheet orientation is the same as a paper 193 | sheet's[^8]. 194 | 195 | Writing an application for a terminal is different than writing an application 196 | for a web browser. The number of characters which fit on the screen is limited, 197 | drawing shapes is not practical etc. To ensure that the application is rendered 198 | correctly we need a special kind of sheet which will operate on units being 199 | characters. Take a look at the following example. 200 | 201 | ![Sheets with different units.](sheets-different-units.png) 202 | 203 | The first sheet base unit is a character of a certain physical size. We print 204 | some information on it in various colors. Nothing prevents us from drawing a 205 | circle[^9] but that would miss the point. We use a character as a unit because 206 | we don't want rounding and approximation. Background and foreground colors are 207 | inherited. 208 | 209 | The second sheet base unit is dip. It has two circles, solid grey background and 210 | a few strings (one is written in italic). 211 | 212 | Ideally, we want to be able to render both sheets on the same physical 213 | screen. The graft and the medium will take care of the sheet approximation. The 214 | effect should look something like this. 215 | 216 | ![Different kinds of screens.](screens-different-kinds.png) 217 | 218 | The first screen is a terminal. Circles from the second sheet are approximated 219 | with characters and look ugly but the overall effect resembles the original 220 | application. Proportions are preserved (to some degree). We see also the 221 | terminal-specialized sheet which looks exactly as we have defined it. 222 | 223 | The second screen is mDPI display. The second sheet looks very much like 224 | something we have defined. What is more interesting though is our first sheet – 225 | it looks exactly the same as on the terminal. 226 | 227 | [^8]: Providing means to change defaults requires additional thought and is a 228 | material for a separate chapter. McCLIM doesn't allow it yet. 229 | 230 | [^9]: As we have noted sheets are ideal planar spaces where line thickness is 0 231 | and there is nothing preventing us from using fractions as coordinates. 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | # Port and graft protocols 254 | 255 | Now we know *what* we want. Time to think about *how* to achieve it. Let me 256 | remind you what kind of objects we are dealing with: 257 | 258 | * Port is a logical connection to a display server. For instance, it may contain 259 | a foreign handler which is passed to the external system API. It is 260 | responsible for the communication – configuring, writing to and reading[^10] 261 | from a device, we are connected to. 262 | 263 | * Graft is a logical screen representation on which we draw. It is responsible 264 | for all transformations necessary to achieve the desired effect on the 265 | physical screen. The same port may have many associated grafts for 266 | applications with different units and orientations. 267 | 268 | * Medium is a representation of the sheet on a physical device. Sheet is the 269 | Form which is a region and may be drawn – it doesn't concern itself with 270 | physical limitations. 271 | 272 | In the next post I will show how to implement the port and the graft (and a bit 273 | of the medium) for the charming backend. I will cover only bare minimum for 274 | mediums important to verify that graft works as expected. Complete medium 275 | implementation is the material for a separate post. 276 | 277 | [^10]: Sometimes we deal with devices which we can't take input from – for 278 | instance a printer, a PDF render or a display without other peripherals. 279 | -------------------------------------------------------------------------------- /doc/drawing.lisp: -------------------------------------------------------------------------------- 1 | 2 | (in-package #:clim-user) 3 | 4 | (defun draw-grid (sheet &optional (xstep 1) (ystep 1)) 5 | (let ((ink +gray+)) 6 | (loop for coord from 0 to 40 by xstep 7 | do (draw-line* sheet coord 0 coord 40 :ink ink)) 8 | (loop for coord from 0 to 40 by ystep 9 | do (draw-line* sheet 0 coord 40 coord :ink ink)))) 10 | 11 | (defun draw-coords (sheet unit &optional (label "1mm")) 12 | (let ((ink +red+)) 13 | ;; orientation 14 | (draw-arrow* sheet 0 0 44 0 :ink ink :head-width .5 :head-length 1) 15 | (draw-arrow* sheet 0 0 0 43 :ink ink :head-width .5 :head-length 1) 16 | ;; units 17 | (surrounding-output-with-border (sheet :background +grey+) 18 | (draw-text* sheet unit 40 0 :align-x :right :align-y :center :text-size :small)) 19 | (surrounding-output-with-border (sheet :background +grey+) 20 | (draw-text* sheet unit 0 40 :align-x :center :align-y :bottom :text-size :small)) 21 | ;; show where is 1mm 22 | (let ((1mm (/ 100 15.875))) 23 | (draw-line* sheet -0.5 1mm 0.5 1mm :line-thickness 1) 24 | (draw-line* sheet 1mm -0.5 1mm 0.5 :line-thickness 1) 25 | (draw-text* sheet label -1 1mm :align-x :right :align-y :center :text-size :small) 26 | (draw-text* sheet label 1mm -1 :align-x :center :align-y :bottom :text-size :small)))) 27 | 28 | ;; http://bauhh.dyndns.org:8000/clim-spec/12-4.html 29 | 30 | (defun round1* (p dx dy) 31 | (make-point (multiple-value-bind (x r) (round (/ (point-x p) dx)) 32 | (if (= r -0.5) 33 | (* (1- x) dx) 34 | (* x dx))) 35 | (multiple-value-bind (y r) (round (/ (point-y p) dy)) 36 | (if (= r -0.5) 37 | (* (1- y) dy) 38 | (* y dy))))) 39 | 40 | (defun round2* (p dx dy) 41 | (make-point (multiple-value-bind (x r) (round (/ (point-x p) dx)) 42 | (if (= r -0.5) 43 | (* (1- x) dx) 44 | (* x dx))) 45 | (multiple-value-bind (y r) (round (/ (point-y p) dy)) 46 | (if (= r -0.5) 47 | (* (1- y) dy) 48 | (* y dy))))) 49 | 50 | (defun rectangle-frame (p1 p2 &optional (xwidth 1) (ywidth 1)) 51 | (let* ((p1 (round1* p1 xwidth ywidth)) 52 | (p2 (round2* p2 xwidth ywidth)) 53 | (diff (region-difference (make-rectangle p1 p2) 54 | (make-rectangle* (+ (point-x p1) xwidth) 55 | (+ (point-y p1) ywidth) 56 | (- (point-x p2) xwidth) 57 | (- (point-y p2) ywidth))))) 58 | (if (eql diff +nowhere+) 59 | +everywhere+ 60 | diff))) 61 | 62 | (defun slap-rectangle (sheet p1 p2 ink &optional (xwidth 1) (ywidth 1)) 63 | (draw-rectangle sheet (round1* p1 xwidth ywidth) (round2* p2 xwidth ywidth) 64 | :ink ink :clipping-region (rectangle-frame p1 p2 xwidth ywidth)) 65 | (draw-rectangle sheet p1 p2 :ink +red+ :filled nil)) 66 | 67 | (defun relative-position (p0 p1 dx dy) 68 | (make-point (/ (- (point-x p1) (point-x p0)) dx) 69 | (/ (- (point-y p1) (point-y p0)) dy))) 70 | 71 | (defun label-rectangle (sheet p0 p1 p2 xwidth ywidth) 72 | (draw-text sheet (format nil "(~,2f, ~,2f)" 73 | (abs (point-x (relative-position p0 p1 xwidth ywidth))) 74 | (abs (point-y (relative-position p0 p1 xwidth ywidth)))) 75 | p1 76 | :align-x :center :align-y :center :text-size :small) 77 | (draw-text sheet (format nil "(~,2f, ~,2f)" 78 | (abs (point-x (relative-position p0 p2 xwidth ywidth))) 79 | (abs (point-y (relative-position p0 p2 xwidth ywidth)))) 80 | p2 81 | :align-x :center :align-y :center :text-size :small)) 82 | 83 | (defparameter *p0* (make-point 0 0)) 84 | (defparameter *p0-graphics* (make-point 0 40)) 85 | (defparameter *r1p1* (make-point 5 5)) 86 | (defparameter *r1p2* (make-point 22 35)) 87 | (defparameter *r2p1* (make-point 25 10)) 88 | (defparameter *r2p2* (make-point 30 15)) 89 | 90 | (defun draw-objects (sheet dx dy) 91 | (slap-rectangle sheet *r1p1* *r1p2* +violet+ dx dy) 92 | (slap-rectangle sheet *r2p1* *r2p2* +cyan+ dx dy)) 93 | 94 | (defun draw-screen (sheet unit &key (dx 1) (dy dx) (scale 10) (orientation :default)) 95 | (window-clear sheet) 96 | (ecase orientation 97 | (:default (with-translation (sheet 50 50) 98 | (with-scaling (sheet scale) 99 | (draw-grid sheet dx dy) 100 | (draw-coords sheet unit) 101 | (draw-objects sheet dx dy) 102 | (label-rectangle sheet *p0* *r1p1* *r1p2* dx dy) 103 | (label-rectangle sheet *p0* *r2p1* *r2p2* dx dy)))) 104 | (:graphics (%draw-graphics-screen sheet unit dx dy scale)) 105 | (:graphics-wrong (%draw-gr-screen-wrong sheet unit dx dy scale)))) 106 | 107 | (defun %draw-gr-screen-wrong (sheet unit dx dy scale 108 | &aux (tr (compose-transformations 109 | ;; we must know screen size! 110 | (make-translation-transformation 0 500) 111 | (make-scaling-transformation 1 -1)))) 112 | (window-clear sheet) 113 | (with-drawing-options (sheet :transformation tr) 114 | (with-translation (sheet 50 50) 115 | (with-scaling (sheet scale) 116 | (draw-grid sheet dx dy) 117 | (draw-coords sheet unit) 118 | (draw-objects sheet dx dy) 119 | (label-rectangle sheet *p0* *r1p1* *r1p2* dx dy) 120 | (label-rectangle sheet *p0* *r2p1* *r2p2* dx dy))))) 121 | 122 | (defun %draw-graphics-screen (sheet unit dx dy scale 123 | &aux (tr (compose-transformations 124 | ;; we must know screen size! 125 | (make-translation-transformation 0 500) 126 | (make-scaling-transformation 1 -1)))) 127 | (with-drawing-options (sheet :transformation tr) 128 | (with-translation (sheet 50 50) 129 | (with-scaling (sheet scale) 130 | (draw-grid sheet dx dy) 131 | (draw-coords sheet unit)))) 132 | (with-translation (sheet 50 50) 133 | (with-scaling (sheet scale) 134 | (draw-objects sheet dx dy))) 135 | ;; gross hack to show "device-native" coordinates 136 | (with-translation (sheet 50 50) 137 | (with-scaling (sheet scale) 138 | (let ((r1p1 (make-point (point-x *r1p1*) (point-y *r1p2*))) 139 | (r1p2 (make-point (point-x *r1p2*) (point-y *r1p1*))) 140 | (r2p1 (make-point (point-x *r2p1*) (point-y *r2p2*))) 141 | (r2p2 (make-point (point-x *r2p2*) (point-y *r2p1*)))) 142 | (label-rectangle sheet *p0-graphics* r1p1 r1p2 dx dy) 143 | (label-rectangle sheet *p0-graphics* r2p1 r2p2 dx dy))))) 144 | 145 | 146 | 147 | (eval-when (:compile-toplevel :load-toplevel :execute) 148 | (defvar *screens* (make-hash-table)) 149 | (defvar *scale* 10)) 150 | 151 | (defun show-screen (sheet name &optional (scale *scale*)) 152 | ;; Try (show-screen *standard-output* :console) in the Listener. 153 | (funcall (gethash name *screens*) sheet scale)) 154 | 155 | (defmacro dse (name description unit &key (dx 1) (dy dx) (orientation :default)) 156 | `(setf (gethash ,name *screens*) 157 | (lambda (sheet &optional (scale *scale*)) 158 | ,description 159 | (draw-screen sheet ,unit :dx ,dx :dy ,dy :scale scale :orientation ,orientation)))) 160 | 161 | (dse :console "Terminal with ridiculously small characters." "ch (5x8)" :dx 5 :dy 8) 162 | (dse :72dpi "Very old graphical display." "px (72 ppi)" :dx (/ 160 72)) 163 | (dse :96dpi "Old graphical display (WPF's DIP)." "px (96 ppi)" :dx (/ 160 96)) 164 | (dse :mdpi "MDPI (Material Design's density-independent pixel)" "dp (160 ppi)") 165 | (dse :hdpi "HDPI" "px (240 ppi)" :dx (/ 160 240)) 166 | (dse :xxxhdpi "XXXHDPI" "px (640 ppi)" :dx (/ 160 640)) 167 | (dse :printer "HQ printer" "dot (2560 dpi)" :dx (/ 160 2560)) 168 | (dse :plotter "LQ plotter" "dot (80 dpi)" :orientation :graphics :dx (/ 160 80)) 169 | (dse :drawer "MDPI drawer" "dot (160 dpi)" :orientation :graphics) 170 | (dse :plotter* "LQ plotter (malfunctioning" "dot (80 dpi)" :orientation :graphics-wrong :dx (/ 160 80)) 171 | (dse :drawer* "MDPI drawer (malfunctioning)" "dot (160 dpi)" :orientation :graphics-wrong) 172 | 173 | (defun test (&rest keys) 174 | (maphash (lambda (k v) 175 | (when (or (null keys) 176 | (member k keys)) 177 | (let ((sheet (open-window-stream :label (format nil "~a" k) 178 | :width 500 179 | :height 500 180 | :scroll-bars nil))) 181 | (sleep 0.1) 182 | (funcall v sheet)))) 183 | *screens*)) 184 | 185 | 186 | (defun draw-screen-app (sheet &optional (grid t)) 187 | (with-translation (sheet 40 40) 188 | (with-scaling (sheet 10) 189 | (when grid 190 | (draw-grid sheet 10 10)) 191 | (draw-coords sheet "dp" "1cm") 192 | (draw-rectangle* sheet 2 2 28 28 :ink +grey42+) 193 | (draw-circle* sheet 15 8 5 :ink +dark-green+) 194 | (draw-circle* sheet 15 22 5 :ink +dark-red+) 195 | (draw-text* sheet "Remove?" 15 15 196 | :align-x :center 197 | :align-y :center 198 | :text-size :huge 199 | :text-family :fix 200 | :text-face :italic) 201 | (draw-text* sheet "YES" 15 8 202 | :align-x :center 203 | :align-y :center 204 | :text-size :huge 205 | :text-family :fixed 206 | :text-face :bold 207 | :ink +white+) 208 | (draw-text* sheet "NO" 15 22 209 | :align-x :center 210 | :align-y :center 211 | :text-size :huge 212 | :text-family :fixed 213 | :text-face :bold 214 | :ink +white+) 215 | (repaint-sheet sheet +everywhere+)))) 216 | 217 | (defun draw-console-app (sheet &optional (grid t) (fg +black+) (bg +white+)) 218 | (with-translation (sheet 40 40) 219 | (with-scaling (sheet 10) 220 | (when grid 221 | (draw-grid sheet 5/2 8/2) 222 | (draw-coords sheet "char" "1cm")) 223 | (with-translation (sheet (* 7 5/2) (* 4 8/2)) 224 | (flet ((draw-string** (str y &optional (ink fg)) 225 | (dotimes (v (length str)) 226 | (draw-rectangle* sheet 227 | (* v 5/2) 228 | (* y 8/2) 229 | (* (1+ v) 5/2) 230 | (* (1+ y) 8/2) 231 | :ink bg) 232 | (draw-text* sheet (elt str v) (* v 5/2) (* y 8/2) 233 | :align-x :left 234 | :align-y :top 235 | :text-size 36 236 | :ink ink 237 | :text-family :fixed)))) 238 | (draw-string** "Bus Time" 0) 239 | (draw-string** "--------" 1) 240 | (draw-string** "16 now" 2 +red+) 241 | (draw-string** "10 >1m" 3 +red+) 242 | (draw-string** "03z 5m" 4 +green+))) 243 | (repaint-sheet sheet +everywhere+)))) 244 | 245 | (defun draw-two-apps (sheet) 246 | (window-clear sheet) 247 | (draw-console-app sheet) 248 | (with-translation (sheet 460 0) 249 | (draw-screen-app sheet))) 250 | 251 | (defun draw-two-screens (sheet) 252 | (window-clear sheet) 253 | (draw-on-console sheet) 254 | (with-translation (sheet 460 0) 255 | (draw-on-screen sheet))) 256 | 257 | (defun draw-on-screen (sheet) 258 | (draw-screen-app sheet nil) 259 | (draw-console-app sheet nil +white+ +black+)) 260 | 261 | (defun draw-on-console (sheet) 262 | (with-translation (sheet 40 40) 263 | (with-scaling (sheet 10) 264 | (flet ((draw-cell (x y &key show-grid ch (fc +white+) (bc +black+)) 265 | (setf x (* x 5/2) y (* y 8/2)) 266 | (draw-rectangle* sheet x y (+ x 5/2) (+ y 8/2) :ink bc) 267 | (when show-grid 268 | (with-scaling (sheet 1/10) 269 | (draw-rectangle* sheet 270 | (+ (* 10 x) 2) 271 | (+ (* 10 y) 2) 272 | (- (* 10 (+ x 5/2)) 2) 273 | (- (* 10 (+ y 8/2)) 2) 274 | :ink +gray22+ 275 | :filled nil))) 276 | (when ch 277 | (draw-text* sheet ch x y 278 | :align-x :left 279 | :align-y :top 280 | :text-size 34 281 | :ink fc 282 | :text-family :fixed)))) 283 | (dotimes (x 16) 284 | (dotimes (y 10) 285 | (draw-cell x y :show-grid t))) 286 | (draw-coords sheet "char" "1cm") 287 | (let ((y 1)) 288 | (loop for x from 1 to 3 do (draw-cell x y :bc +grey42+)) 289 | (draw-cell 4 y :bc +dark-green+ :ch #\Y) 290 | (draw-cell 5 y :bc +dark-green+ :ch #\E) 291 | (draw-cell 6 y :bc +dark-green+ :ch #\S) 292 | (draw-cell 7 y :bc +dark-green+) 293 | (loop for x from 8 to 10 do (draw-cell x y :bc +grey42+))) 294 | (let ((y 2)) 295 | (loop for x from 1 to 3 do (draw-cell x y :bc +grey42+)) 296 | (draw-cell 4 y :bc +dark-green+) 297 | (draw-cell 5 y :bc +dark-green+) 298 | (draw-cell 6 y :bc +dark-green+) 299 | (draw-cell 7 y :bc +dark-green+) 300 | (loop for x from 8 to 10 do (draw-cell x y :bc +grey42+))) 301 | (let ((y 3) 302 | (str " _Remove?_")) 303 | (dotimes (v (length str)) 304 | (draw-cell (+ v 1) y :ch (elt str v) :bc +grey42+))) 305 | (let ((y 4)) 306 | (loop for x from 1 to 4 do (draw-cell x y :bc +grey42+)) 307 | (draw-cell 5 y :bc +dark-red+) 308 | (draw-cell 6 y :bc +dark-red+) 309 | (loop for x from 7 to 10 do (draw-cell x y :bc +grey42+))) 310 | (let ((y 5)) 311 | (loop for x from 1 to 3 do (draw-cell x y :bc +grey42+)) 312 | (draw-cell 4 y :bc +dark-red+) 313 | (draw-cell 5 y :bc +dark-red+ :ch #\N) 314 | (draw-cell 6 y :bc +dark-red+ :ch #\O) 315 | (draw-cell 7 y :bc +dark-red+) 316 | (loop for x from 8 to 10 do (draw-cell x y :bc +grey42+))) 317 | (let ((y 6)) 318 | (loop for x from 1 to 4 do (draw-cell x y :bc +grey42+)) 319 | (draw-cell 5 y :bc +dark-red+) 320 | (draw-cell 6 y :bc +dark-red+) 321 | (loop for x from 7 to 10 do (draw-cell x y :bc +grey42+)))))) 322 | (draw-console-app sheet nil +white+ +grey20+) 323 | (repaint-sheet sheet +everywhere+)) 324 | -------------------------------------------------------------------------------- /crash-course/crash-course.lisp: -------------------------------------------------------------------------------- 1 | 2 | (defpackage #:charming-clim/charms-crash-course 3 | (:use #:cl) 4 | (:export #:hello-world) 5 | (:nicknames #:ccc)) 6 | (in-package #:charming-clim/charms-crash-course) 7 | 8 | ;;; Higher level interface additions 9 | (defun draw-window-border (window 10 | &optional 11 | (ls #\|) (rs #\|) (ts #\-) (bs #\-) 12 | (tl #\+) (tr #\+) (bl #\+) (br #\+)) 13 | (apply #'charms/ll:wborder (charms::window-pointer window) 14 | (mapcar #'char-code (list ls rs ts bs tl tr bl br)))) 15 | 16 | (defun draw-window-box (window &optional (verch #\|) (horch #\-)) 17 | (charms/ll:box (charms::window-pointer window) (char-code verch) (char-code horch))) 18 | 19 | (eval-when (:load-toplevel :compile-toplevel :execute) 20 | (defun start-color () 21 | (when (eql (charms/ll:has-colors) charms/ll:FALSE) 22 | (error "Your terminal does not support color.")) 23 | (let ((ret-code (charms/ll:start-color))) 24 | (if (= ret-code 0) 25 | T 26 | (error "start-color error ~s." ret-code)))) 27 | 28 | (defconstant +black+ charms/ll:COLOR_BLACK) 29 | (defconstant +red+ charms/ll:COLOR_RED) 30 | (defconstant +green+ charms/ll:COLOR_GREEN) 31 | (defconstant +yellow+ charms/ll:COLOR_YELLOW) 32 | (defconstant +blue+ charms/ll:COLOR_BLUE) 33 | (defconstant +magenta+ charms/ll:COLOR_MAGENTA) 34 | (defconstant +cyan+ charms/ll:COLOR_CYAN) 35 | (defconstant +white+ charms/ll:COLOR_WHITE) 36 | (charms:with-curses () (start-color))) 37 | 38 | (defmacro define-color-pair ((name pair) foreground background) 39 | `(defparameter ,name (progn (charms/ll:init-pair ,pair ,foreground ,background) 40 | (charms/ll:color-pair ,pair)))) 41 | 42 | (define-color-pair (+white/blue+ 1) +white+ +blue+) 43 | (define-color-pair (+black/red+ 2) +black+ +red+) 44 | 45 | (defun draw-window-background (window color-pair) 46 | (charms/ll:wbkgd (charms::window-pointer window) color-pair)) 47 | 48 | (defmacro with-colors ((window color-pair) &body body) 49 | (let ((winptr (gensym))) 50 | (alexandria:once-only (color-pair) 51 | `(let ((,winptr (charms::window-pointer ,window))) 52 | (charms/ll:wattron ,winptr ,color-pair) 53 | ,@body 54 | (charms/ll:wattroff ,winptr ,color-pair))))) 55 | 56 | 57 | ;;; first program (with border!) 58 | (defun hello-world () 59 | (charms:with-curses () 60 | (charms:disable-echoing) 61 | (charms:enable-raw-input) 62 | (loop named hello-world 63 | with window = (charms:make-window 50 15 10 10) 64 | do (progn 65 | (charms:clear-window window) 66 | (draw-window-border window) 67 | (charms:write-string-at-point window "Hello world!" 0 0) 68 | (charms:refresh-window window) 69 | 70 | ;; Process input 71 | (when (eql (charms:get-char window) #\q) 72 | (return-from hello-world)))))) 73 | 74 | 75 | ;;; Second program (colors!) 76 | (defun pretty-hello-world () 77 | (charms:with-curses () 78 | (charms:disable-echoing) 79 | (charms:enable-raw-input) 80 | (start-color) 81 | (loop named hello-world 82 | with window = (charms:make-window 50 15 10 10) 83 | do (progn 84 | (charms:clear-window window) 85 | (draw-window-background window +white/blue+) 86 | (with-colors (window +white/blue+) 87 | (charms:write-string-at-point window "Hello world!" 0 0)) 88 | (with-colors (window +black/red+) 89 | (charms:write-string-at-point window "Hello world!" 0 1)) 90 | (charms:refresh-window window) 91 | 92 | ;; Process input 93 | (when (eql (charms:get-char window :ignore-error t) #\q) 94 | (return-from hello-world)))))) 95 | 96 | 97 | ;;; third program, amazing computation (asynchronous input) 98 | (defun amazing-hello-world () 99 | (charms:with-curses () 100 | (charms:disable-echoing) 101 | (charms:enable-raw-input) 102 | (start-color) 103 | (loop named hello-world 104 | with window = (let ((win (charms:make-window 50 15 10 10))) 105 | (charms:enable-non-blocking-mode win) 106 | win) 107 | for flip-flop = (not flip-flop) 108 | do (progn 109 | (charms:clear-window window) 110 | (draw-window-background window +white/blue+) 111 | (with-colors (window (if flip-flop 112 | +white/blue+ 113 | +black/red+)) 114 | (charms:write-string-at-point window "Hello world!" 0 0)) 115 | (charms:refresh-window window) 116 | ;; Process input 117 | (when (eql (charms:get-char window :ignore-error t) #\q) 118 | (return-from hello-world)) 119 | (sleep 1))))) 120 | 121 | ;;; asynchronous input hack (should be a mailbox!) 122 | (defparameter *recompute-flag* nil "ugly and unsafe hack for communication") 123 | (defvar *recompute-thread* nil) 124 | 125 | (defun start-recompute-thread () 126 | (when *recompute-thread* 127 | (bt:destroy-thread *recompute-thread*)) 128 | (setf *recompute-thread* 129 | (bt:make-thread 130 | #'(lambda () 131 | (loop 132 | (sleep 1) 133 | (setf *recompute-flag* t)))))) 134 | 135 | (defun stop-recompute-thread () 136 | (when *recompute-thread* 137 | (bt:destroy-thread *recompute-thread*) 138 | (setf *recompute-thread* nil))) 139 | 140 | (defun display-amazing-hello-world (window flip-flop) 141 | (charms:clear-window window) 142 | (draw-window-background window +white/blue+) 143 | (with-colors (window (if flip-flop 144 | +white/blue+ 145 | +black/red+)) 146 | (charms:write-string-at-point window "Hello world!" 0 0)) 147 | (charms:refresh-window window)) 148 | 149 | (defun get-amazing-hello-world-input (window) 150 | (when *recompute-flag* 151 | (setf *recompute-flag* nil) 152 | (return-from get-amazing-hello-world-input :compute)) 153 | (charms:get-char window :ignore-error t)) 154 | 155 | (defun improved-amazing-hello-world () 156 | (charms:with-curses () 157 | (charms:disable-echoing) 158 | (charms:enable-raw-input) 159 | (start-color) 160 | (let ((window (charms:make-window 50 15 10 10)) 161 | (flip-flop nil)) 162 | (charms:enable-non-blocking-mode window) 163 | (display-amazing-hello-world window flip-flop) 164 | (loop named hello-world 165 | do (case (get-amazing-hello-world-input window) 166 | ((#\q #\Q) (return-from hello-world)) 167 | (:compute (setf flip-flop (not flip-flop)) 168 | (display-amazing-hello-world window flip-flop)) 169 | ;; don't be a pig to a processor 170 | (otherwise (sleep 1/60))))))) 171 | 172 | 173 | ;;; fourth program, greedy desires (gadgets and keyboard input) 174 | 175 | (define-color-pair (+black/white+ 3) +black+ +white+) ; color for text-input (inactive) 176 | (define-color-pair (+black/cyan+ 4) +black+ +cyan+) ; color for text-input (active) 177 | (define-color-pair (+yellow/black+ 5) +yellow+ +black+) ; color for button (inactive) 178 | (define-color-pair (+red/black+ 6) +red+ +black+) ; color for button (active) 179 | 180 | (defparameter *active-gadget* nil) 181 | (defparameter *computation-name* "Hello world!") 182 | 183 | ;;; gadget should be type of `window' or `panel' – we are simplistic 184 | (defclass gadget () 185 | ((position :initarg :position :accessor gadget-position))) 186 | 187 | (defgeneric display-gadget (window gadget &key &allow-other-keys) 188 | (:method ((window charms:window) (gadget gadget) &key) 189 | (declare (ignore window gadget)))) 190 | 191 | (defgeneric handle-input (gadget input &key &allow-other-keys) 192 | (:method (gadget input &key) 193 | (declare (ignore gadget input)) 194 | NIL)) 195 | 196 | (defclass text-input-gadget (gadget) 197 | ((buffer :initarg :buffer :accessor gadget-buffer) 198 | (width :initarg :width :reader gadget-width))) 199 | 200 | (defun make-text-input-gadget (width x y) 201 | (make-instance 'text-input-gadget 202 | :width width 203 | :position (cons x y) 204 | :buffer (make-array width 205 | :element-type 'character 206 | :initial-element #\space 207 | :fill-pointer 0))) 208 | 209 | (defmethod display-gadget ((window charms:window) (gadget text-input-gadget) &key) 210 | (with-colors (window (if (eql gadget *active-gadget*) 211 | +black/cyan+ 212 | +black/white+)) 213 | (let ((background (make-string (gadget-width gadget) :initial-element #\space))) 214 | (destructuring-bind (x . y) (gadget-position gadget) 215 | (charms:write-string-at-point window background x y) 216 | (charms:write-string-at-point window (gadget-buffer gadget) x y))))) 217 | 218 | (defmethod handle-input ((gadget text-input-gadget) input &key) 219 | (let ((buffer (gadget-buffer gadget))) 220 | (case input 221 | ((#\Backspace #\Rubout) 222 | (unless (zerop (fill-pointer buffer)) 223 | (vector-pop buffer))) 224 | ((#\Return #\Newline) 225 | (unless (zerop (fill-pointer buffer)) 226 | (setf *computation-name* (copy-seq buffer) 227 | (fill-pointer buffer) 0))) 228 | ((#\ESC #\ETX) ;; etx is C-c 229 | (setf (fill-pointer buffer) 0)) 230 | (otherwise 231 | (when (ignore-errors (graphic-char-p input)) 232 | (vector-push input buffer)))))) 233 | 234 | (defclass button-gadget (gadget) 235 | ((label :initarg :label :reader gadget-label) 236 | (action :initarg :action :reader gadget-action))) 237 | 238 | (defun make-button-gadget (text callback x y) 239 | (make-instance 'button-gadget :label text :action callback :position (cons x y))) 240 | 241 | (defmethod display-gadget ((window charms:window) (gadget button-gadget) &key) 242 | (with-colors (window (if (eql gadget *active-gadget*) 243 | +red/black+ 244 | +yellow/black+)) 245 | (destructuring-bind (x . y) (gadget-position gadget) 246 | (charms:write-string-at-point window (gadget-label gadget) x y)))) 247 | 248 | (defmethod handle-input ((gadget button-gadget) input &key) 249 | (when (member input '(#\return #\newline)) 250 | (funcall (gadget-action gadget)))) 251 | 252 | (defun toggle-recompute-thread () 253 | (if *recompute-thread* 254 | (stop-recompute-thread) 255 | (start-recompute-thread))) 256 | 257 | (defparameter *gadgets* 258 | (list (make-text-input-gadget 26 2 13) 259 | (make-button-gadget " Toggle " 'toggle-recompute-thread 30 11) 260 | (make-button-gadget " Exit " 'exit-application 40 11) 261 | (make-button-gadget " Accept " 'accept-input-box 30 13) 262 | (make-button-gadget " Cancel " 'cancel-input-box 40 13))) 263 | 264 | (defun accept-input-box () 265 | (handle-input (car *gadgets*) #\return)) 266 | 267 | (defun cancel-input-box () 268 | (handle-input (car *gadgets*) #\esc)) 269 | 270 | (defun exit-application () 271 | (throw :exit :exit-button)) 272 | 273 | (defun display-greedy-hello-world (window flip-flop) 274 | (charms:clear-window window) 275 | (draw-window-background window +white/blue+) 276 | (with-colors (window (if flip-flop 277 | +white/blue+ 278 | +black/red+)) 279 | (charms:write-string-at-point window *computation-name* 2 1)) 280 | (dolist (g *gadgets*) 281 | (if (eql g *active-gadget*) 282 | (display-gadget window g) 283 | (charms:with-restored-cursor window 284 | (display-gadget window g)))) 285 | (charms:refresh-window window)) 286 | 287 | (defun get-greedy-hello-world-input (window) 288 | (when *recompute-flag* 289 | (setf *recompute-flag* nil) 290 | (return-from get-greedy-hello-world-input :compute)) 291 | (charms:get-char window :ignore-error t)) 292 | 293 | (defun greedy-hello-world () 294 | (charms:with-curses () 295 | (charms:disable-echoing) 296 | (charms:enable-raw-input) 297 | (start-color) 298 | (let ((window (charms:make-window 50 15 10 10)) 299 | (flip-flop nil)) 300 | (charms:enable-non-blocking-mode window) 301 | (display-greedy-hello-world window flip-flop) 302 | (catch :exit 303 | (loop 304 | do (let ((input (get-greedy-hello-world-input window))) 305 | ;; (when input 306 | ;; (format *xxx* "have ~s~%" input)) 307 | (case input 308 | (#\Dc1 ;; this is C-q 309 | (throw :exit :c-q)) 310 | (#\Dc2 ;; this is C-r 311 | (charms:clear-window charms:*standard-window*) 312 | (charms:refresh-window charms:*standard-window*) 313 | (display-greedy-hello-world window flip-flop)) 314 | (#\tab 315 | (alexandria:if-let ((remaining (cdr (member *active-gadget* *gadgets*)))) 316 | (setf *active-gadget* (car remaining)) 317 | (setf *active-gadget* (car *gadgets*))) 318 | (display-greedy-hello-world window flip-flop)) 319 | (:compute 320 | (setf flip-flop (not flip-flop)) 321 | (display-greedy-hello-world window flip-flop)) 322 | (otherwise 323 | (if (handle-input *active-gadget* input) 324 | ;; redisplay only if handle-input returns non-NIL 325 | (display-greedy-hello-world window flip-flop) 326 | ;; don't be a pig to a processor 327 | (sleep 1/60)))))))))) 328 | 329 | 330 | ;;; fifth program, enterprise software (mouse integration) 331 | 332 | (defgeneric bounding-rectangle (gadget) 333 | (:method ((gadget text-input-gadget)) 334 | (destructuring-bind (x . y) (gadget-position gadget) 335 | (values x 336 | y 337 | (+ x -1 (gadget-width gadget)) 338 | y))) 339 | (:method ((gadget button-gadget)) 340 | (destructuring-bind (x . y) (gadget-position gadget) 341 | (values x 342 | y 343 | (+ x -1 (length (gadget-label gadget))) 344 | y)))) 345 | 346 | (defun region-contains-position-p (gadget x y) 347 | (multiple-value-bind (x-min y-min x-max y-max) 348 | (bounding-rectangle gadget) 349 | (and (<= x-min x x-max) 350 | (<= y-min y y-max)))) 351 | 352 | (defun distribute-mouse-event (bstate x y) 353 | (dolist (g *gadgets*) 354 | (when (region-contains-position-p g x y) 355 | (setf *active-gadget* g) 356 | (when (eql bstate charms/ll:button1_clicked) 357 | (handle-input g #\return)) 358 | (return)))) 359 | 360 | (defun display-enterprise-hello-world (window flip-flop) 361 | (charms:with-restored-cursor window 362 | (charms:clear-window window) 363 | (draw-window-background window +white/blue+) 364 | (if flip-flop 365 | (with-colors (window +white/blue+) 366 | (charms:write-string-at-point window *computation-name* 2 1)) 367 | (with-colors (window +black/red+) 368 | (charms:write-string-at-point window *computation-name* 2 1))) 369 | (dolist (g *gadgets*) (display-gadget window g)) 370 | (charms:refresh-window window))) 371 | 372 | (defun get-enterprise-hello-world-input (window) 373 | (when *recompute-flag* 374 | (setf *recompute-flag* nil) 375 | (return-from get-enterprise-hello-world-input :compute)) 376 | (let ((c (charms/ll:wgetch (charms::window-pointer window)))) 377 | (when (not (eql c charms/ll:ERR)) 378 | (alexandria:switch (c) 379 | (charms/ll:KEY_BACKSPACE #\Backspace) 380 | (charms/ll:KEY_MOUSE :KEY-MOUSE) 381 | (otherwise (charms::c-char-to-character c)))))) 382 | 383 | (defun enterprise-process-event (window flip-flop) 384 | (loop 385 | (let ((input (get-enterprise-hello-world-input window))) 386 | (case input 387 | (#\Dc1 ;; this is C-q 388 | (throw :exit :c-q)) 389 | (#\Dc2 ;; this is C-r 390 | (display-enterprise-hello-world window flip-flop)) 391 | (#\tab 392 | (alexandria:if-let ((remaining (cdr (member *active-gadget* *gadgets*)))) 393 | (setf *active-gadget* (car remaining)) 394 | (setf *active-gadget* (car *gadgets*))) 395 | (display-enterprise-hello-world window flip-flop)) 396 | (#\Latin_Small_Letter_S_With_Caron ;; this is S-[tab] 397 | (if (eql *active-gadget* (car *gadgets*)) 398 | (setf *active-gadget* (alexandria:lastcar *gadgets*)) 399 | (do ((g *gadgets* (cdr g))) 400 | ((eql *active-gadget* (cadr g)) 401 | (setf *active-gadget* (car g))))) 402 | (display-enterprise-hello-world window flip-flop)) 403 | (:key-mouse 404 | (handler-case (multiple-value-bind (bstate x y z id) 405 | (charms/ll:getmouse) 406 | (declare (ignore z id)) 407 | ;; window starts at 10,10 408 | (decf x 10) 409 | (decf y 10) 410 | (charms:move-cursor window x y) 411 | (distribute-mouse-event bstate x y) 412 | (display-enterprise-hello-world window flip-flop)) 413 | (error () nil))) 414 | (:compute 415 | (setf flip-flop (not flip-flop)) 416 | (display-enterprise-hello-world window flip-flop)) 417 | (otherwise 418 | (if (handle-input *active-gadget* input) 419 | ;; redisplay only if handle-input returns non-NIL 420 | (display-enterprise-hello-world window flip-flop) 421 | ;; don't be a pig to a processor 422 | (sleep 1/60))))))) 423 | 424 | (defun start-mouse () 425 | (charms/ll:mousemask 426 | (logior charms/ll:all_mouse_events charms/ll:report_mouse_position)) 427 | (format cl-user::*console-io* "~c[?1003h" #\esc)) 428 | 429 | (defun enterprise-hello-world () 430 | (charms:with-curses () 431 | (charms:disable-echoing) 432 | (charms:enable-raw-input) 433 | (start-color) 434 | (let ((window (charms:make-window 50 15 10 10)) 435 | (flip-flop nil)) 436 | ;; full enterprise ay? 437 | (charms:enable-non-blocking-mode window) 438 | (charms:enable-extra-keys window) 439 | (start-mouse) 440 | (display-enterprise-hello-world window flip-flop) 441 | (catch :exit 442 | (loop (funcall 'enterprise-process-event window flip-flop)))))) 443 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Writing a new CLIM backend 3 | 4 | This work is meant as a showcase how to write a new McCLIM backend. To make it 5 | more interesting to me I'm writing it using `cl-charms` library which is a 6 | Common Lisp library for `ncurses` - console manipulation library for UNIX 7 | systems. During development I'm planning to make notes about necessary steps. If 8 | possible I'll also write a test suite for backends which will test the 9 | functionality from most basic parts (like creating windows) to more 10 | sophisticated ones (transformations and drawing). That should simplify 11 | verifying, if new a backend works fine and to what degree it is complete. We 12 | start with a crash course for `cl-charms` library. 13 | 14 | ## Pointers 15 | 16 | * [charming clim](https://github.com/dkochmanski/charming-clim) is a main project repository 17 | * [cl-charms remote branch](https://github.com/dkochmanski/cl-charms) - some 18 | functionality may depend on changes in this repository (pull requests to 19 | upstream are pending) 20 | * [cl-charms upstream repository](https://github.com/HiTECNOLOGYs/cl-charms). 21 | 22 | 23 | # cl-charms crash course 24 | 25 | Ensure you have `ncurses` development package installed on your system. Start 26 | the real terminal (Emacs doesn't start `*inferior-lisp*` in something `ncurses` 27 | can work with) and launch your implementation. I use `CCL` because usually 28 | software is developed with `SBCL` and I want to catch as many problems with 29 | `cl-charms` as possible. After that start swank server and connect from your 30 | Emacs session. 31 | 32 | ``` 33 | ~ ccl 34 | ? (defvar *console-io* *terminal-io*) ; we will need it later 35 | *CONSOLE-IO* 36 | ? (ql:quickload 'swank :silent t) 37 | (SWANK) 38 | ? (swank:create-server :port 4005 :dont-close t) 39 | ;; Swank started at port: 4005. 40 | 4005 41 | ? (loop (sleep 1)) 42 | ``` 43 | 44 | We loop over sleep because we don't want console prompt to read our first line 45 | for the console. If you don't do that you may have somewhat confusing behavior. 46 | 47 | In Emacs: `M-x slime-connect *Host:* localhost *Port:* 4005`. Now we are working 48 | in `*slime-repl ccl*` buffer in Emacs and we have ncurses output in the terminal 49 | we have launched server from. Try some demos bundled with the library: 50 | 51 | ``` 52 | CL-USER> (ql:quickload '(cl-charms bordeaux-threads alexandria)) 53 | (CL-CHARMS BORDEAUX-THREADS ALEXANDRIA) 54 | CL-USER> (ql:quickload '(cl-charms-paint cl-charms-timer) :silent t) 55 | (CL-CHARMS-PAINT CL-CHARMS-TIMER) 56 | CL-USER> (charms-timer:main) ; quit with Q, start/stop/reset with [SPACE] 57 | CL-USER> (charms-paint:main) ; quit with Q, move with WSAD and paint with [SPACE] 58 | ``` 59 | 60 | Now we will go through various `charms` (and `ncurses`) capabilities. Our final 61 | goal is to have a window with four buttons and text input box. Navigation should 62 | be possible with `[TAB]` / `[SHIFT]+[TAB]` and by selecting gadgets with a mouse 63 | pointer. Behold, time for the first application. 64 | 65 | ## First application 66 | 67 | Lets dissect this simple program which prints "Hello world!" on the screen: 68 | 69 | ```common-lisp 70 | (defun hello-world () 71 | (charms:with-curses () 72 | (charms:disable-echoing) 73 | (charms:enable-raw-input) 74 | (loop named hello-world 75 | with window = (charms:make-window 50 15 10 10) 76 | do (progn 77 | (charms:clear-window window) 78 | (charms:write-string-at-point window "Hello world!" 0 0) 79 | (charms:refresh-window window) 80 | 81 | ;; Process input 82 | (when (eql (charms:get-char window) #\q) 83 | (return-from hello-world)) 84 | (sleep 0.1))))) 85 | ``` 86 | 87 | Program must be wrapped in `charms:with-curses` macro which ensures proper 88 | initialization and finalization of the program. In this operator context 89 | `charms` functions which configure the library are available. We use 90 | `charms:disable-echoing` to prevent unnecessary obfuscation of the window (we 91 | interpret characters ourself) and `(charms:enable-raw-input)` to turn off line 92 | buffering. `charms:*standard-window*` is a window covering whole terminal 93 | screen. 94 | 95 | We create a Window for output (its size is 50x15 and offset is 10x10) and then 96 | in a loop we print "Hello world!" (at the top-left corner of it) until user 97 | press the character `q`. 98 | 99 | ## Extending cl-charms API 100 | 101 | All functions used until now come from higher-level interface. `charms` has also 102 | a low-level interface which maps to `libncurses` via `CFFI`. This interface is 103 | defined in the package named `charms/ll`. I highly recommend skimming through 104 | http://www.tldp.org/HOWTO/NCURSES-Programming-HOWTO which is a great overview of 105 | `ncurses` functionality. 106 | 107 | We want borders around the window. CFFI interface is a bit ugly (i.e we would 108 | have to extract a window pointer to call `wborder` on it). We are going to 109 | abstract this with a function which plays nice with the lispy abstraction. 110 | 111 | ```common-lisp 112 | (defun draw-window-border (window 113 | &optional 114 | (ls #\|) (rs #\|) (ts #\-) (bs #\-) 115 | (tl #\+) (tr #\+) (bl #\+) (br #\+)) 116 | (apply #'charms/ll:wborder (charms::window-pointer window) 117 | (mapcar #'char-code (list ls rs ts bs tl tr bl br)))) 118 | 119 | (defun draw-window-box (window &optional (verch #\|) (horch #\-)) 120 | (charms/ll:box (charms::window-pointer window) (char-code verch) (char-code horch))) 121 | ``` 122 | 123 | Now we can freely use `draw-window-border`. Put `(draw-window-box window)` after 124 | `(charms:clear-window window)` in `hello-world` program and see the result. It 125 | is ugly, but what did you expect from a window rendered in the terminal? 126 | 127 | ![](cc-border.png) 128 | 129 | It is worth mentioning that border is drawn inside the window, so when we start 130 | writing string at point [0,0] - it overlaps with the border. If we want to paint 131 | content *inside* the border we should start at least at [1,1] and stop at 132 | [48,13]. 133 | 134 | Somewhat more appealing result may be achieved by having distinct window 135 | background instead of drawing a border with characters. To do that we need to 136 | dive into the low-level interface once more. We define colors API. 137 | 138 | ```common-lisp 139 | (defun start-color () 140 | (when (eql (charms/ll:has-colors) charms/ll:FALSE) 141 | (error "Your terminal does not support color.")) 142 | (let ((ret-code (charms/ll:start-color))) 143 | (if (= ret-code 0) 144 | T 145 | (error "start-color error ~s." ret-code)))) 146 | 147 | (eval-when (:load-toplevel :compile-toplevel :execute) 148 | (defconstant +black+ charms/ll:COLOR_BLACK) 149 | (defconstant +red+ charms/ll:COLOR_RED) 150 | (defconstant +green+ charms/ll:COLOR_GREEN) 151 | (defconstant +yellow+ charms/ll:COLOR_YELLOW) 152 | (defconstant +blue+ charms/ll:COLOR_BLUE) 153 | (defconstant +magenta+ charms/ll:COLOR_MAGENTA) 154 | (defconstant +cyan+ charms/ll:COLOR_CYAN) 155 | (defconstant +white+ charms/ll:COLOR_WHITE)) 156 | 157 | (defmacro define-color-pair ((name pair) foreground background) 158 | `(progn 159 | (start-color) 160 | (defparameter ,name (progn (charms/ll:init-pair ,pair ,foreground ,background) 161 | (charms/ll:color-pair ,pair))))) 162 | 163 | (define-color-pair (+white/blue+ 1) +white+ +blue+) 164 | (define-color-pair (+black/red+ 2) +black+ +red+) 165 | 166 | (defun draw-window-background (window color-pair) 167 | (charms/ll:wbkgd (charms::window-pointer window) color-pair)) 168 | 169 | (defmacro with-colors ((window color-pair) &body body) 170 | (let ((winptr (gensym))) 171 | (alexandria:once-only (color-pair) 172 | `(let ((,winptr (charms::window-pointer ,window))) 173 | (charms/ll:wattron ,winptr ,color-pair) 174 | ,@body 175 | (charms/ll:wattroff ,winptr ,color-pair))))) 176 | ``` 177 | 178 | `start-color` must be called when we configure the library. We map `charm/ll` 179 | constants to lisp constants and create `define-color-pair` macro. This 180 | abstraction could be improved so we are not forced to supply pair numbers and 181 | providing proper association between names and integers. We skip that step for 182 | brevity. Define two color pairs, function for filling a window background and 183 | macro `with-colors` for drawing with a specified palette. Finally lets use the 184 | new abstraction in `pretty-hello-world` function: 185 | 186 | ```common-lisp 187 | (defun pretty-hello-world () 188 | (charms:with-curses () 189 | (charms:disable-echoing) 190 | (charms:enable-raw-input) 191 | (start-color) 192 | (loop named hello-world 193 | with window = (charms:make-window 50 15 10 10) 194 | do (progn 195 | (charms:clear-window window) 196 | (draw-window-background window +white/blue+) 197 | (with-colors (window +white/blue+) 198 | (charms:write-string-at-point window "Hello world!" 0 0)) 199 | (with-colors (window +black/red+) 200 | (charms:write-string-at-point window "Hello world!" 0 1)) 201 | (charms:refresh-window window) 202 | 203 | ;; Process input 204 | (when (eql (charms:get-char window :ignore-error t) #\q) 205 | (return-from hello-world)) 206 | (sleep 0.1))))) 207 | ``` 208 | 209 | Result looks, as promised in the function name, very pretty ;-) 210 | 211 | ![](cc-background.png) 212 | 213 | ## Asynchronous input 214 | 215 | Printing `Hello world!` doesn't satisfy our needs, we want to interact with a 216 | brilliant software we've just made while its running. Even more, we want to do 217 | it without blocking computations going on in the system (which are truly 218 | amazing, believe me). First lets visualise these computations to know that they 219 | are really happening. Modify the program loop to draw `Hello World!` in 220 | different color on each iteration. 221 | 222 | ```common-lisp 223 | (defun amazing-hello-world () 224 | (charms:with-curses () 225 | (charms:disable-echoing) 226 | (charms:enable-raw-input) 227 | (start-color) 228 | (loop named hello-world 229 | with window = (charms:make-window 50 15 10 10) 230 | for flip-flop = (not flip-flop) 231 | do (progn 232 | (charms:clear-window window) 233 | (draw-window-background window +white/blue+) 234 | (with-colors (window (if flip-flop 235 | +white/blue+ 236 | +black/red+)) 237 | (charms:write-string-at-point window "Hello world!" 0 0)) 238 | (charms:refresh-window window) 239 | ;; Process input 240 | (when (eql (charms:get-char window :ignore-error t) #\q) 241 | (return-from hello-world)) 242 | (sleep 1))))) 243 | ``` 244 | 245 | Something is not right. When we run `amazing-hello-world` to see it flipping – 246 | it doesn't. Our program is flawed. It waits for each character to verify that 247 | the user hasn't requested application exit. You press any key (i.e space) to 248 | proceed to the next iteration. Now we must think of how to obtain input from 249 | user without halting the application. 250 | 251 | To do that we can enable non blocking mode for our window. 252 | 253 | with window = (let ((win (charms:make-window 50 15 10 10))) 254 | (charms:enable-non-blocking-mode win) 255 | win) 256 | 257 | This solution is not complete unfortunately. It works reasonably well, but we 258 | have to wait a second (because "computation" is performed every second, we call 259 | `sleep` after each get-char) before the character is handled. It gets even worse 260 | if we notice, that pressing five times character `b` and then `q` will delay 261 | processing by six seconds (characters are processed one after another in 262 | different iterations with one second sleep between them). We need something 263 | better. 264 | 265 | I hear your internal scream: use threads! It is important to keep in mind, that 266 | if you can get without threads you probably should (same applies for cache and 267 | many other clever techniques which introduce even cleverer bugs). Keep also in 268 | mind that `ncurses` is not thread-safe. We are going to listen for events from 269 | all inputs like select does and generate "recompute" event each second. On 270 | implementation which support timers we could use them but we'll use... a thread 271 | to generate "ticks". Note that we use a thread as an asynchronous input rather 272 | than asynchronous charms access. 273 | 274 | ```common-lisp 275 | ;;; asynchronous input hack (should be a mailbox!) 276 | (defparameter *recompute-flag* nil "ugly and unsafe hack for communication") 277 | (defvar *recompute-thread* nil) 278 | 279 | (defun start-recompute-thread () 280 | (when *recompute-thread* 281 | (bt:destroy-thread *recompute-thread*)) 282 | (setf *recompute-thread* 283 | (bt:make-thread 284 | #'(lambda () 285 | (loop 286 | (sleep 1) 287 | (setf *recompute-flag* t)))))) 288 | 289 | (defun stop-recompute-thread () 290 | (when *recompute-thread* 291 | (bt:destroy-thread *recompute-thread*) 292 | (setf *recompute-thread* nil))) 293 | ``` 294 | 295 | In this snippet we create an interface to start a thread which sets a global 296 | flag. General solution should be a mailbox (or a thread-safe stream) where 297 | asynchronous thread writes and event loop reads from. We will settle with this 298 | hack though (it is a crash course not a book after all). Start recompute thread 299 | in the background before you start new application. Note, that this code is not 300 | thread-safe, we concurrently read and write to a global variable. We are also 301 | very drastic with `bt:destroy-thread`, something not recommended in **any** code 302 | which is not a demonstration like this one. 303 | 304 | Time to refactor input and output functions: `display-amazing-hello-world` and 305 | `get-amazing-hello-world-input`. 306 | 307 | ```common-lisp 308 | (defun display-amazing-hello-world (window flip-flop) 309 | (charms:clear-window window) 310 | (draw-window-background window +white/blue+) 311 | (with-colors (window (if flip-flop 312 | +white/blue+ 313 | +black/red+)) 314 | (charms:write-string-at-point window "Hello world!" 0 0)) 315 | (charms:refresh-window window)) 316 | 317 | (defun get-amazing-hello-world-input (window) 318 | (when *recompute-flag* 319 | (setf *recompute-flag* nil) 320 | (return-from get-amazing-hello-world-input :compute)) 321 | (charms:get-char window :ignore-error t)) 322 | ``` 323 | 324 | And finally improved application which takes asynchronous input without blocking. 325 | 326 | ```common-lisp 327 | (defun improved-amazing-hello-world () 328 | (charms:with-curses () 329 | (charms:disable-echoing) 330 | (charms:enable-raw-input) 331 | (start-color) 332 | (let ((window (charms:make-window 50 15 10 10)) 333 | (flip-flop nil)) 334 | (charms:enable-non-blocking-mode window) 335 | (display-amazing-hello-world window flip-flop) 336 | (loop named hello-world 337 | do (case (get-amazing-hello-world-input window) 338 | ((#\q #\Q) (return-from hello-world)) 339 | (:compute (setf flip-flop (not flip-flop)) 340 | (display-amazing-hello-world window flip-flop)) 341 | ;; don't be a pig to a processor 342 | (otherwise (sleep 1/60))))))) 343 | ``` 344 | 345 | When you are done with demo you may call `stop-recompute-thread` to spare your 346 | image unnecessary flipping a global variable. 347 | 348 | ## Gadgets and input handling 349 | 350 | So we have created an amazing piece of software which does the computation and 351 | reacts (instantaneously!) to our input. Greed is an amazing phenomena – we want 352 | more... We want *interactive* application – buttons and input box (allowing us 353 | to influence the amazing computation at run time). 354 | 355 | First we define abstract class `gadget`. 356 | 357 | ```common-lisp 358 | (defparameter *active-gadget* nil) 359 | 360 | ;;; gadget should be type of `window' or `panel' – we are simplistic 361 | (defclass gadget () 362 | ((position :initarg :position :accessor gadget-position))) 363 | 364 | (defgeneric display-gadget (window gadget &key &allow-other-keys) 365 | (:method ((window charms:window) (gadget gadget) &key) 366 | (declare (ignore window gadget)))) 367 | 368 | (defgeneric handle-input (gadget input &key &allow-other-keys) 369 | (:method (gadget input &key) 370 | (declare (ignore gadget input)))) 371 | ``` 372 | 373 | In our model each gadget has at least position, display function and method for 374 | handling input. Both methods are gadget-specific with defaults doing nothing. We 375 | define also a parameter `*active-gadget*` which holds gadget receiving input. 376 | 377 | `handle-input` returns `T` only if this input causes, that gadget has to be 378 | redisplayed. Otherwise it should return `NIL` (this is a small optimization 379 | which we will use later in main application loop). 380 | 381 | Lets define something what we will use in our application. 382 | 383 | ```common-lisp 384 | (define-color-pair (+black/white+ 3) +black+ +white+) ; color for text-input (inactive) 385 | (define-color-pair (+black/cyan+ 4) +black+ +cyan+) ; color for text-input (active) 386 | (defparameter *computation-name* "Hello world!") 387 | 388 | (defclass text-input-gadget (gadget) 389 | ((buffer :initarg :buffer :accessor gadget-buffer) 390 | (width :initarg :width :reader gadget-width))) 391 | 392 | (defun make-text-input-gadget (width x y) 393 | (make-instance 'text-input-gadget 394 | :width width 395 | :position (cons x y) 396 | :buffer (make-array width 397 | :element-type 'character 398 | :initial-element #\space 399 | :fill-pointer 0))) 400 | 401 | (defmethod display-gadget ((window charms:window) (gadget text-input-gadget) &key) 402 | (with-colors (window (if (eql gadget *active-gadget*) 403 | +black/cyan+ 404 | +black/white+)) 405 | (let ((background (make-string (gadget-width gadget) :initial-element #\space))) 406 | (destructuring-bind (x . y) (gadget-position gadget) 407 | (charms:write-string-at-point window background x y) 408 | (charms:write-string-at-point window (gadget-buffer gadget) x y))))) 409 | 410 | (defmethod handle-input ((gadget text-input-gadget) input &key) 411 | (let ((buffer (gadget-buffer gadget))) 412 | (case input 413 | ((#\Backspace #\Rubout) 414 | (unless (zerop (fill-pointer buffer)) 415 | (vector-pop buffer))) 416 | ((#\Return #\Newline) 417 | (unless (zerop (fill-pointer buffer)) 418 | (setf *computation-name* (copy-seq buffer) 419 | (fill-pointer buffer) 0))) 420 | (#\ESC 421 | (setf (fill-pointer buffer) 0)) 422 | (otherwise 423 | (when (ignore-errors (graphic-char-p input)) 424 | (vector-push input buffer)))))) 425 | ``` 426 | 427 | First gadget we define is `text-input-gadget`. What we need as its internal 428 | state is a `buffer` which holds text which is typed in the box. We care also 429 | about the string maximal `width`. 430 | 431 | Moreover define colors for it to use in `display-gadget` (we depend on global 432 | parameter `*active-gadget*` what is a very poor taste). In display function we 433 | create a "background" (that wouldn't be necessary if it were a panel, 434 | abstraction defined in a library accompanying `ncurses`) and then at the gadget 435 | position we draw background and `buffer` contents (text which was already typed 436 | in `text-input-gadget`). 437 | 438 | Second function `handle-input` interprets characters it receives and acts 439 | accordingly. If it is `backspace` (or `rubout` as on my keyboard with this 440 | terminal settings), we remove one element. If it is `return` (or `newline`) we 441 | change the computation name and empty input. `escape` clears the box and 442 | finally, if it is a character which we can print, we add it to the text-input 443 | (`vector-push` won't extend vector length so extra characters are ignored). 444 | 445 | ```common-lisp 446 | (defparameter *gadgets* 447 | (list (make-text-input-gadget 26 2 13))) 448 | 449 | (defun display-greedy-hello-world (window flip-flop) 450 | (charms:clear-window window) 451 | (draw-window-background window +white/blue+) 452 | (with-colors (window (if flip-flop 453 | +white/blue+ 454 | +black/red+)) 455 | (charms:write-string-at-point window *computation-name* 2 1)) 456 | (dolist (g *gadgets*) 457 | (if (eql g *active-gadget*) 458 | (display-gadget window g) 459 | (charms:with-restored-cursor window 460 | (display-gadget window g)))) 461 | (charms:refresh-window window)) 462 | ``` 463 | 464 | We maintain a list of gadgets which are displayed one-by-one in the window 465 | (after signalling, that computation is being performed). Previously we had hard 466 | coded "Hello world!" name but now we depend on variable `*computation-name*` 467 | which may be modified from the input box. 468 | 469 | Each gadget is displayed but only `*active-gadget*` is allowed to modify cursor 470 | position. Other rendering is wrapped in `charms:with-restored-cursor` macro 471 | which does the thing name suggests. That means, that cursor will be position 472 | whenever `*active-gadget*` puts it (or if it doesn't modify its position – 473 | cursor will be at the end of the computation string). 474 | 475 | ```common-lisp 476 | (defun get-greedy-hello-world-input (window) 477 | (when *recompute-flag* 478 | (setf *recompute-flag* nil) 479 | (return-from get-greedy-hello-world-input :compute)) 480 | (charms:get-char window :ignore-error t)) 481 | 482 | (defun greedy-hello-world () 483 | (charms:with-curses () 484 | (charms:disable-echoing) 485 | (charms:enable-raw-input) 486 | (start-color) 487 | (let ((window (charms:make-window 50 15 10 10)) 488 | (flip-flop nil)) 489 | (charms:enable-non-blocking-mode window) 490 | (display-greedy-hello-world window flip-flop) 491 | (catch :exit 492 | (loop 493 | do (let ((input (get-greedy-hello-world-input window))) 494 | (case input 495 | (#\Dc1 ;; this is C-q 496 | (throw :exit :c-q)) 497 | (#\Dc2 ;; this is C-r 498 | (charms:clear-window charms:*standard-window*) 499 | (charms:refresh-window charms:*standard-window*) 500 | (display-greedy-hello-world window flip-flop)) 501 | (#\tab 502 | (alexandria:if-let ((remaining (cdr (member *active-gadget* *gadgets*)))) 503 | (setf *active-gadget* (car remaining)) 504 | (setf *active-gadget* (car *gadgets*))) 505 | (display-greedy-hello-world window flip-flop)) 506 | (:compute 507 | (setf flip-flop (not flip-flop)) 508 | (display-greedy-hello-world window flip-flop)) 509 | (otherwise 510 | (if (handle-input *active-gadget* input) 511 | ;; redisplay only if handle-input returns non-NIL 512 | (display-greedy-hello-world window flip-flop) 513 | ;; don't be a pig to a processor 514 | (sleep 1/60)))))))))) 515 | ``` 516 | 517 | `get-greedy-hello-world-input` is fairly the same as 518 | `get-amazing-hello-world-input` for now. `greedy-hello-world` on the other hand 519 | handles input differently. For `Quit` we reserve `C-q` instead of `q` because we 520 | want to be able to type this character in the input box. We also add `C-r` 521 | sequence to refresh whole screen (just in case if we resize the terminal and 522 | some glitches remain). `:compute` is handled the same way as it was 523 | previously. Finally if input is something else we feed it to the 524 | `*active-gadget*`. If `handle-input` returns something else than NIL we 525 | redisplay the application. 526 | 527 | Note that if it is not redisplayed (i.e input is not handled because 528 | `*active-gadget*` was NIL or it didn't handle the input) we are a good citizen 529 | and instead of hogging the processor we wait 1/60 of a second. On the other hand 530 | if events come one by one we are legitimately busy so we skip this rest time and 531 | continue the event loop. 532 | 533 | We don't have means to change which gadget is active yet. For that we have to 534 | add a new key which will be handled in the main event loop of the application 535 | `#\tab`. At least we wrap whole loop body in `(catch :exit)` to allow dynamic 536 | exit (instead of lexical one with `block`/`return-from` pair). 537 | 538 | Start the application and make `text-input-gadget` active by pressing 539 | `[tab]`. Notice that its background changes. Now we have working input box where 540 | we can type new string and when we confirm it with `[enter]` string at the top 541 | will change. 542 | 543 | ![](cc-input.png) 544 | 545 | We want more gadgets. For now we will settle with buttons. 546 | 547 | ```common-lisp 548 | (defclass button-gadget (gadget) 549 | ((label :initarg :label :reader gadget-label) 550 | (action :initarg :action :reader gadget-action))) 551 | 552 | (defun make-button-gadget (text callback x y) 553 | (make-instance 'button-gadget :label text :action callback :position (cons x y))) 554 | 555 | (define-color-pair (+yellow/black+ 5) +yellow+ +black+) 556 | (define-color-pair (+red/black+ 6) +red+ +black+) 557 | 558 | (defmethod display-gadget ((window charms:window) (gadget button-gadget) &key) 559 | (with-colors (window (if (eql gadget *active-gadget*) 560 | +red/black+ 561 | +yellow/black+)) 562 | (destructuring-bind (x . y) (gadget-position gadget) 563 | (charms:write-string-at-point window (gadget-label gadget) x y)))) 564 | 565 | (defmethod handle-input ((gadget button-gadget) input &key) 566 | (when (member input '(#\return #\newline)) 567 | (funcall (gadget-action gadget)))) 568 | ``` 569 | 570 | Each button is a gadget which (in addition to inherited `position`) has a 571 | `label` and the associated `action`. Active button label is drawn with yellow 572 | and red ink depending on its state. Handling input reacts only to pressing 573 | `[enter]`. When button action is activated gadget's `action` is funcall-ed 574 | (without arguments). 575 | 576 | Modify `*gadgets*` parameter to have four buttons of ours and run the 577 | application (try pressing `[tab]` a few times to see that active widget is 578 | changing). 579 | 580 | ```common-lisp 581 | (defparameter *gadgets* 582 | (list (make-text-input-gadget 26 2 13) 583 | (make-button-gadget " Toggle " 'toggle-recompute-thread 30 11) 584 | (make-button-gadget " Exit " 'exit-application 40 11) 585 | (make-button-gadget " Accept " 'accept-input-box 30 13) 586 | (make-button-gadget " Cancel " 'cancel-input-box 40 13))) 587 | 588 | (defun toggle-recompute-thread () 589 | (if *recompute-thread* 590 | (stop-recompute-thread) 591 | (start-recompute-thread))) 592 | 593 | (defun accept-input-box () 594 | (handle-input (car *gadgets*) #\return)) 595 | 596 | (defun cancel-input-box () 597 | (handle-input (car *gadgets*) #\esc)) 598 | 599 | (defun exit-application () 600 | (throw :exit :exit-button)) 601 | ``` 602 | 603 | We have created four buttons – `Toggle` starts/stops the "computation" thread 604 | which feeds us with input, `Exit` quits the application (that's why we have 605 | changed `block` to `catch` in `greedy-hello-world` main loop) and `Accept` / 606 | `Cancel` pass `return` and `escape` input to the `text-input-gadget`. Once again 607 | we prove that we lack a good taste because we aim at first element of 608 | `*gadgets*` parameter assuming, that first element is text input field. 609 | 610 | ![](cc-text-and-buttons.png) 611 | 612 | The result looks a little like a user interface, doesn't it? Try activating 613 | various buttons and check out if text input works as desired. When you select 614 | `Toggle` and press `[enter]` computation label at the top will start / stop 615 | blinking in one second intervals. 616 | 617 | ## Mouse integration 618 | 619 | Our software has reached the phase where it is production ready. Investors crawl 620 | at our doorbell because they feel it is something what will boost their 621 | income. We are sophisticated and we have proposed a neat idea which will have a 622 | tremendous impact on the market. We have even created buttons and text-input 623 | gadgets which are the key to success. Something is still missing though. After a 624 | brainstorm meeting with the most brilliant minds of our decade we've came to a 625 | conclusion – what we miss is the mouse integration. That's crazy, I know. *Mouse 626 | on a terminal?* It is a heresy! Yet we believe that we have to take the risk if 627 | we want to outpace the competition. Roll up your sleeves – we are going to 628 | change the face of the modern UI design! ;-) 629 | 630 | First we will start with abstracting things to make it easy to distribute events 631 | among gadgets. To do that we need to know where pointer event has happened. Lets 632 | assume that mouse event is composed of three parameters: button, x coordinate 633 | and y coordinate. Each gadget occupies some space on the screen (lets call it a 634 | region) which may be characterized by its bounding rectangle with [min-x, min-y] 635 | and [max-x, max-y] points. 636 | 637 | ```common-lisp 638 | (defgeneric bounding-rectangle (gadget) 639 | (:method ((gadget text-input-gadget)) 640 | (destructuring-bind (x . y) (gadget-position gadget) 641 | (values x 642 | y 643 | (+ x -1 (gadget-width gadget)) 644 | y))) 645 | (:method ((gadget button-gadget)) 646 | (destructuring-bind (x . y) (gadget-position gadget) 647 | (values x 648 | y 649 | (+ x -1 (length (gadget-label gadget))) 650 | y)))) 651 | ``` 652 | 653 | We could refactor button-gadget to have gadget-width method (that would simplify 654 | the implementation), but lets use what we have now. Having such representation 655 | of bounding rectangle we can now easily determine if cursor event occurred 656 | "over" the gadget or somewhere else. 657 | 658 | ```common-lisp 659 | (defun region-contains-position-p (gadget x y) 660 | (multiple-value-bind (x-min y-min x-max y-max) 661 | (bounding-rectangle gadget) 662 | (and (<= x-min x x-max) 663 | (<= y-min y y-max)))) 664 | ``` 665 | 666 | Now distributing mouse event is as easy as iterating over all gadgets and 667 | verifying if event applies to the gadget. If it does we make such gadget 668 | active. Moreover if left mouse button is clicked we simulate `[return]` key to 669 | be handled by the previously defined `handle-input` method. 670 | 671 | ```common-lisp 672 | (defun distribute-mouse-event (bstate x y) 673 | (dolist (g *gadgets*) 674 | (when (region-contains-position-p g x y) 675 | (setf *active-gadget* g) 676 | (when (eql bstate charms/ll:button1_clicked) 677 | (handle-input g #\return)) 678 | (return)))) 679 | ``` 680 | 681 | Notice that we have used low-level charms interface (comparing bstate with 682 | `charms/ll:button1_clicked`). Other events are also defined in the `charms/ll` 683 | package so you may expand mouse handling interface. 684 | 685 | Time to define display function for our enterprise application. `ncurses` cursor 686 | should to follow mouse movement, so displaying gadgets can't affect cursor 687 | position. To achieve that we wrap whole display function in 688 | `charms:with-restored-cursor` macro. 689 | 690 | ```common-lisp 691 | (defun display-enterprise-hello-world (window flip-flop) 692 | (charms:with-restored-cursor window 693 | (charms:clear-window window) 694 | (draw-window-background window +white/blue+) 695 | (if flip-flop 696 | (with-colors (window +white/blue+) 697 | (charms:write-string-at-point window *computation-name* 2 1)) 698 | (with-colors (window +black/red+) 699 | (charms:write-string-at-point window *computation-name* 2 1))) 700 | (dolist (g *gadgets*) (display-gadget window g)) 701 | (charms:refresh-window window))) 702 | ``` 703 | 704 | Usually terminal emulators doesn't report mouse movement (only clicks). To 705 | enable such reports print the following in the terminal output (note, that slime 706 | doesn't bind `*terminal-io*` to the right thing, so we use `*console-io*` which 707 | we have dfined at the beginning): 708 | 709 | (format *console-io* "~c[?1003h" #\esc) 710 | 711 | This escape sequence sets xterm mode for reporting any mouse event including 712 | mouse movement (see 713 | [http://invisible-island.net/xterm/ctlseqs/ctlseqs.html](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)). This 714 | escape sequence is usually honored by other terminal emulators (not only 715 | xterm). Without it we wouldn't be able to track mouse movement (only pointer 716 | press, release, scroll etc). This is important to us because we want to activate 717 | gadgets when mouse moves over them. 718 | 719 | To start mouse and configure its events lets create initialization function. We 720 | configure terminal to report all mouse events including its position. After that 721 | we tell the terminal emulator to report mouse position events. 722 | 723 | ```common-lisp 724 | (defun start-mouse () 725 | (charms/ll:mousemask 726 | (logior charms/ll:all_mouse_events charms/ll:report_mouse_position)) 727 | (format *console-io* "~c[?1003h~%" #\esc)) 728 | ``` 729 | 730 | We need to handle new event type `:key-mouse` (already handled types are `C-q`, 731 | `C-r`, `TAB`, `:compute` and "other" characterrs). Since event handling gets 732 | more complicated we factor it into a separate function 733 | `enterprise-process-event`. Note the `handler-case` – when mouse event queue is 734 | empty (for instance because the mouse event is masked), then function will 735 | return error. We also take into account that mouse events are reported starting 736 | at absolute terminal coordinates while our window starts at point 737 | [10,10]. Additionally we implement shift+tab sequence which on my system is 738 | reported as `#\Latin_Small_Letter_S_With_Caron`. 739 | 740 | ```common-lisp 741 | (defun get-enterprise-hello-world-input (window) 742 | (when *recompute-flag* 743 | (setf *recompute-flag* nil) 744 | (return-from get-enterprise-hello-world-input :compute)) 745 | (let ((c (charms/ll:wgetch (charms::window-pointer window)))) 746 | (when (not (eql c charms/ll:ERR)) 747 | (alexandria:switch (c) 748 | (charms/ll:KEY_BACKSPACE #\Backspace) 749 | (charms/ll:KEY_MOUSE :KEY-MOUSE) 750 | (otherwise (charms::c-char-to-character c)))))) 751 | 752 | (defun enterprise-process-event (window flip-flop) 753 | (loop 754 | (let ((input (get-enterprise-hello-world-input window))) 755 | (case input 756 | (#\Dc1 ;; this is C-q 757 | (throw :exit :c-q)) 758 | (#\Dc2 ;; this is C-r 759 | (display-enterprise-hello-world window flip-flop)) 760 | (#\tab 761 | (alexandria:if-let ((remaining (cdr (member *active-gadget* *gadgets*)))) 762 | (setf *active-gadget* (car remaining)) 763 | (setf *active-gadget* (car *gadgets*))) 764 | (display-enterprise-hello-world window flip-flop)) 765 | (#\Latin_Small_Letter_S_With_Caron ;; this is S-[tab] 766 | (if (eql *active-gadget* (car *gadgets*)) 767 | (setf *active-gadget* (alexandria:lastcar *gadgets*)) 768 | (do ((g *gadgets* (cdr g))) 769 | ((eql *active-gadget* (cadr g)) 770 | (setf *active-gadget* (car g))))) 771 | (display-enterprise-hello-world window flip-flop)) 772 | (:key-mouse 773 | (handler-case (multiple-value-bind (bstate x y z id) 774 | (charms/ll:getmouse) 775 | (declare (ignore z id)) 776 | ;; window starts at 10,10 777 | (decf x 10) 778 | (decf y 10) 779 | (charms:move-cursor window x y) 780 | (distribute-mouse-event bstate x y) 781 | (display-enterprise-hello-world window flip-flop)) 782 | (error () nil))) 783 | (:compute 784 | (setf flip-flop (not flip-flop)) 785 | (display-enterprise-hello-world window flip-flop)) 786 | (otherwise 787 | (if (handle-input *active-gadget* input) 788 | ;; redisplay only if handle-input returns non-NIL 789 | (display-enterprise-hello-world window flip-flop) 790 | ;; don't be a pig to a processor 791 | (sleep 1/60))))))) 792 | ``` 793 | 794 | To make terminal report mouse events in the intelligible way we need to call 795 | function `charms:enable-extra-keys` (thanks to that we don't deal with raw 796 | character sequences). We also call `start-mouse`. 797 | 798 | ```common-lisp 799 | (defun enterprise-hello-world () 800 | (charms:with-curses () 801 | (charms:disable-echoing) 802 | (charms:enable-raw-input) 803 | (start-color) 804 | (let ((window (charms:make-window 50 15 10 10)) 805 | (flip-flop nil)) 806 | ;; full enterprise ay? 807 | (charms:enable-non-blocking-mode window) 808 | (charms:enable-extra-keys window) 809 | (start-mouse) 810 | (display-enterprise-hello-world window flip-flop) 811 | (catch :exit 812 | (loop (funcall 'enterprise-process-event window flip-flop)))))) 813 | ``` 814 | 815 | Our final result is splendid! We got rich :-) 816 | 817 | [![asciicast](cc-final.png)](https://asciinema.org/a/KNDnnycLc2uHMsmi7YvFMUpau) 818 | 819 | To finish our lisp session type in the slime REPL `(quit)`. 820 | 821 | ## Conclusions 822 | 823 | In this crash course we have explored some parts of the `cl-charms` interface 824 | (windows, colors, mouse integration...) and defined an ad-hoc toolkit for the 825 | user interface. There is much more to learn and the library may be expanded 826 | (especially with regard to high level interface, but also to include supplement 827 | `ncurses` libraries like `panel`, `menu` and `forms`). Ability to run `charms` 828 | application in a new terminal started with `run-program` would be beneficial 829 | too. 830 | 831 | Some programmers may have noticed that we have defined an ad-hoc, 832 | informally-specified, bug-ridden, slow implementation of 1/10th of CLIM. No 833 | wonder, it defines a very good abstraction for defining UI in a consistent 834 | manner and it is a standard (with full specification). 835 | 836 | Instead of reinventing the wheel we want to plug into CLIM abstractions and use 837 | `cl-charms` as CLIM backend. This option will be explored in a near-term future 838 | – it will help to document a process of writing new backends. Of course such 839 | backend will be limited in many ways – that will be an opportunity to explore 840 | bugs which got unnoticed when the smallest distance is a pixel (in contrast to a 841 | reasonably big terminal character size). Stay tuned :-) 842 | -------------------------------------------------------------------------------- /crash-course/cc-asciinema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "width": 95, 4 | "height": 50, 5 | "duration": 114.465701, 6 | "command": null, 7 | "title": null, 8 | "env": { 9 | "TERM": "xterm", 10 | "SHELL": "/usr/bin/zsh" 11 | }, 12 | "stdout": [ 13 | [ 14 | 0.137774, 15 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 16 | ], 17 | [ 18 | 5.5e-05, 19 | "\u001b]2;jack@sun-wukong: ~/common-lisp/charming-clim\u0007\u001b]1;..charming-clim\u0007" 20 | ], 21 | [ 22 | 0.009269, 23 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcharming-clim\u001b[00m \u001b[01;34mgit:(\u001b[31mmaster\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K" 24 | ], 25 | [ 26 | 2.6e-05, 27 | "\u001b[?1h\u001b=\u001b[?2004h" 28 | ], 29 | [ 30 | 1.36311, 31 | "~" 32 | ], 33 | [ 34 | 0.318529, 35 | "\b~/" 36 | ], 37 | [ 38 | 0.253347, 39 | "b" 40 | ], 41 | [ 42 | 0.08077, 43 | "i" 44 | ], 45 | [ 46 | 0.099495, 47 | "n" 48 | ], 49 | [ 50 | 0.216868, 51 | "/" 52 | ], 53 | [ 54 | 0.166312, 55 | "c" 56 | ], 57 | [ 58 | 0.166938, 59 | "c" 60 | ], 61 | [ 62 | 0.079716, 63 | "l" 64 | ], 65 | [ 66 | 0.416346, 67 | "\u001b[?1l\u001b>\u001b[?2004l\r\r\n" 68 | ], 69 | [ 70 | 0.002967, 71 | "\u001b]2;~/bin/ccl\u0007\u001b]1;~/bin/ccl\u0007" 72 | ], 73 | [ 74 | 0.298741, 75 | "Welcome to Clozure Common Lisp Version 1.11 (LinuxX8664)!\r\n\r\nCCL is developed and maintained by Clozure Associates. For more information\r\nabout CCL visit http://ccl.clozure.com. To enquire about Clozure's Common Lisp\r\nconsulting services e-mail info@clozure.com or visit http://www.clozure.com.\r\n\r\n? " 76 | ], 77 | [ 78 | 0.577327, 79 | "(defvar *console-io* *terminal-io*)" 80 | ], 81 | [ 82 | 0.403937, 83 | "\r\n" 84 | ], 85 | [ 86 | 0.000844, 87 | "*CONSOLE-IO*\r\n? " 88 | ], 89 | [ 90 | 0.803786, 91 | "(ql:quickload 'swank :silent t)" 92 | ], 93 | [ 94 | 0.618684, 95 | "\r\n" 96 | ], 97 | [ 98 | 0.162822, 99 | "(SWANK)\r\n? " 100 | ], 101 | [ 102 | 0.563759, 103 | "(swank:create-server :port 4005 :dont-close t)" 104 | ], 105 | [ 106 | 0.780836, 107 | "\r\n" 108 | ], 109 | [ 110 | 0.002687, 111 | ";; Swank started at port: 4005.\r\n" 112 | ], 113 | [ 114 | 0.002042, 115 | "4005\r\n? " 116 | ], 117 | [ 118 | 0.484231, 119 | "(loop (sleep 1))" 120 | ], 121 | [ 122 | 0.771931, 123 | "\r\n" 124 | ], 125 | [ 126 | 1.469771, 127 | "\u001b[?1049h\u001b[1;50r\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[H\u001b[2J\u001b[39;49m\u001b[39;49m\u001b[50d\u001b[K\u001b[50;1H\u001b[?1049l\r\u001b[?1l\u001b>" 128 | ], 129 | [ 130 | 0.114009, 131 | "\u001b[?1049h\u001b[1;50r\u001b[39;49m\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[39;49m\u001b[37m\u001b[40m\u001b[H\u001b[2J\u001b[39;49m\u001b[50d\u001b[K\u001b[50;1H\u001b[?1049l\r\u001b[?1l\u001b>" 132 | ], 133 | [ 134 | 0.104105, 135 | "\u001b[?1049h\u001b[1;50r\u001b[39;49m\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[39;49m\u001b[37m\u001b[40m\u001b[H\u001b[2J" 136 | ], 137 | [ 138 | 2.1e-05, 139 | "\u001b[?1h\u001b=" 140 | ], 141 | [ 142 | 0.000139, 143 | "\u001b[?1000h" 144 | ], 145 | [ 146 | 0.002523, 147 | "\u001b[11;11H\u001b[37m\u001b[44m\u001b[50X\n \u001b[30m\u001b[41mHello world!\u001b[37m\u001b[44m\u001b[36X\u001b[13;11H\u001b[50X\n\u001b[50X\n\u001b[50X\n\u001b[50X\n\u001b[50X\n\u001b[50X\n\u001b[50X\n\u001b[50X\n\u001b[50X\n\u001b[30X\u001b[22;41H\u001b[33m\u001b[40m Toggle \u001b[37m\u001b[44m \u001b[33m\u001b[40m Exit \u001b[37m\u001b[44m \u001b[23;11H\u001b[50X\n \u001b[30m\u001b[47m\u001b[26X\u001b[24;39H\u001b[37m\u001b[44m \u001b[33m\u001b[40m Accept \u001b[37m\u001b[44m \u001b[33m\u001b[40m Cancel \u001b[37m\u001b[44m \u001b[25;11H\u001b[50X\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[11;11H" 148 | ], 149 | [ 150 | 0.183553, 151 | "\u001b[?1003h" 152 | ], 153 | [ 154 | 1.164587, 155 | "\u001b[24;59H\u001b[21;11H" 156 | ], 157 | [ 158 | 0.034392, 159 | "\u001b[24;59H\u001b[19;14H" 160 | ], 161 | [ 162 | 0.017542, 163 | "\u001b[24;59H\u001b[15;21H" 164 | ], 165 | [ 166 | 0.017662, 167 | "\u001b[24;59H\u001b[13;25H" 168 | ], 169 | [ 170 | 0.017741, 171 | "\u001b[24;59H\u001b[12;26H" 172 | ], 173 | [ 174 | 0.371377, 175 | "\u001b[24;59H\u001b[13;26H" 176 | ], 177 | [ 178 | 0.102094, 179 | "\u001b[24;59H" 180 | ], 181 | [ 182 | 6.4e-05, 183 | "\u001b[14;27H" 184 | ], 185 | [ 186 | 0.034518, 187 | "\u001b[24;59H" 188 | ], 189 | [ 190 | 7.5e-05, 191 | "\u001b[14;28H" 192 | ], 193 | [ 194 | 0.017496, 195 | "\u001b[24;59H\u001b[15;29H" 196 | ], 197 | [ 198 | 0.034531, 199 | "\u001b[24;59H\u001b[16;30H" 200 | ], 201 | [ 202 | 0.033883, 203 | "\u001b[24;59H\u001b[16;31H" 204 | ], 205 | [ 206 | 0.119169, 207 | "\u001b[24;59H\u001b[17;31H" 208 | ], 209 | [ 210 | 0.034456, 211 | "\u001b[24;59H\u001b[18;31H" 212 | ], 213 | [ 214 | 0.034421, 215 | "\u001b[24;59H\u001b[19;30H" 216 | ], 217 | [ 218 | 0.017733, 219 | "\u001b[24;59H\u001b[19;29H" 220 | ], 221 | [ 222 | 0.017745, 223 | "\u001b[24;59H\u001b[19;28H" 224 | ], 225 | [ 226 | 0.017583, 227 | "\u001b[24;59H\u001b[20;27H" 228 | ], 229 | [ 230 | 0.017854, 231 | "\u001b[24;59H\u001b[20;26H" 232 | ], 233 | [ 234 | 0.017639, 235 | "\u001b[24;59H\u001b[20;25H" 236 | ], 237 | [ 238 | 0.017962, 239 | "\u001b[24;59H\u001b[21;24H" 240 | ], 241 | [ 242 | 0.017538, 243 | "\u001b[24;59H" 244 | ], 245 | [ 246 | 0.000119, 247 | "\u001b[21;23H" 248 | ], 249 | [ 250 | 0.084906, 251 | "\u001b[24;59H\u001b[21;22H" 252 | ], 253 | [ 254 | 0.304164, 255 | "\u001b[24;59H\u001b[22;23H" 256 | ], 257 | [ 258 | 0.017657, 259 | "\u001b[24;59H\u001b[22;24H" 260 | ], 261 | [ 262 | 0.017678, 263 | "\u001b[24;59H\u001b[22;25H" 264 | ], 265 | [ 266 | 0.034343, 267 | "\u001b[24;59H\u001b[22;26H" 268 | ], 269 | [ 270 | 0.017898, 271 | "\u001b[24;59H\u001b[22;27H" 272 | ], 273 | [ 274 | 0.018013, 275 | "\u001b[24;59H\u001b[22;28H" 276 | ], 277 | [ 278 | 0.017624, 279 | "\u001b[24;59H\u001b[22;29H" 280 | ], 281 | [ 282 | 0.017642, 283 | "\u001b[24;59H\u001b[22;30H" 284 | ], 285 | [ 286 | 0.017845, 287 | "\u001b[24;59H" 288 | ], 289 | [ 290 | 0.00013, 291 | "\u001b[22;32H" 292 | ], 293 | [ 294 | 0.000773, 295 | "\u001b[24;59H" 296 | ], 297 | [ 298 | 0.000302, 299 | "\u001b[22;33H" 300 | ], 301 | [ 302 | 0.01755, 303 | "\u001b[24;59H\u001b[22;34H" 304 | ], 305 | [ 306 | 0.034425, 307 | "\u001b[24;59H" 308 | ], 309 | [ 310 | 7.1e-05, 311 | "\u001b[22;35H" 312 | ], 313 | [ 314 | 0.304153, 315 | "\u001b[24;59H\u001b[22;36H" 316 | ], 317 | [ 318 | 0.051337, 319 | "\u001b[24;59H\u001b[22;37H" 320 | ], 321 | [ 322 | 0.034561, 323 | "\u001b[24;59H\u001b[22;38H" 324 | ], 325 | [ 326 | 0.068145, 327 | "\u001b[24;59H\u001b[22;39H" 328 | ], 329 | [ 330 | 0.101783, 331 | "\u001b[24;59H\u001b[22;40H" 332 | ], 333 | [ 334 | 0.152495, 335 | "\u001b[C\u001b[31m\u001b[40m Toggle \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[22;41H" 336 | ], 337 | [ 338 | 0.051471, 339 | "\u001b[24;59H\u001b[22;42H" 340 | ], 341 | [ 342 | 0.034405, 343 | "\u001b[24;59H\u001b[22;43H" 344 | ], 345 | [ 346 | 0.034452, 347 | "\u001b[24;59H\u001b[22;44H" 348 | ], 349 | [ 350 | 0.337451, 351 | "\u001b[24;59H" 352 | ], 353 | [ 354 | 0.000694, 355 | "\u001b[22;45H" 356 | ], 357 | [ 358 | 0.117999, 359 | "\u001b[24;59H\u001b[23;45H" 360 | ], 361 | [ 362 | 0.085126, 363 | "\u001b[A\b\b\b\b\u001b[33m\u001b[40m Toggle \u001b[24;41H\u001b[31m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 364 | ], 365 | [ 366 | 0.000108, 367 | "\u001b[24;45H" 368 | ], 369 | [ 370 | 0.102044, 371 | "\u001b[24;59H\u001b[25;45H" 372 | ], 373 | [ 374 | 0.152017, 375 | "\u001b[24;59H" 376 | ], 377 | [ 378 | 5.3e-05, 379 | "\u001b[25;44H" 380 | ], 381 | [ 382 | 0.371082, 383 | "\u001b[24;59H\u001b[24;44H" 384 | ], 385 | [ 386 | 0.017483, 387 | "\u001b[24;59H\u001b[24;43H" 388 | ], 389 | [ 390 | 0.034531, 391 | "\u001b[24;59H\u001b[24;42H" 392 | ], 393 | [ 394 | 0.034722, 395 | "\u001b[24;59H" 396 | ], 397 | [ 398 | 0.000157, 399 | "\u001b[24;41H" 400 | ], 401 | [ 402 | 0.017875, 403 | "\u001b[24;59H" 404 | ], 405 | [ 406 | 0.000166, 407 | "\u001b[24;40H" 408 | ], 409 | [ 410 | 0.018008, 411 | "\u001b[24;59H\u001b[24;39H" 412 | ], 413 | [ 414 | 0.017761, 415 | "\u001b[24;13H\u001b[30m\u001b[46m\u001b[26X\u001b[24;39H\u001b[37m\u001b[44m \u001b[33m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;37H" 416 | ], 417 | [ 418 | 0.017707, 419 | "\u001b[24;59H\u001b[24;36H" 420 | ], 421 | [ 422 | 0.017101, 423 | "\u001b[24;59H\u001b[24;34H" 424 | ], 425 | [ 426 | 0.017023, 427 | "\u001b[24;59H\u001b[24;33H" 428 | ], 429 | [ 430 | 0.017585, 431 | "\u001b[24;59H" 432 | ], 433 | [ 434 | 9.2e-05, 435 | "\u001b[24;32H" 436 | ], 437 | [ 438 | 0.018152, 439 | "\u001b[24;59H\u001b[24;31H" 440 | ], 441 | [ 442 | 0.034253, 443 | "\u001b[24;59H\u001b[24;30H" 444 | ], 445 | [ 446 | 0.10298, 447 | "\u001b[24;59H\u001b[24;29H" 448 | ], 449 | [ 450 | 0.613532, 451 | "\u001b[24;59H\u001b[24;29H" 452 | ], 453 | [ 454 | 0.407351, 455 | "\u001b[24;13H\u001b[30m\u001b[46mh\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 456 | ], 457 | [ 458 | 0.068572, 459 | "\u001b[24;14H\u001b[30m\u001b[46me\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 460 | ], 461 | [ 462 | 0.152281, 463 | "\u001b[24;15H\u001b[30m\u001b[46ml\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 464 | ], 465 | [ 466 | 0.169274, 467 | "\u001b[24;16H\u001b[30m\u001b[46ml\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 468 | ], 469 | [ 470 | 0.170983, 471 | "\u001b[24;17H\u001b[30m\u001b[46mo\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 472 | ], 473 | [ 474 | 0.085276, 475 | "\u001b[24;59H\u001b[24;29H" 476 | ], 477 | [ 478 | 0.068874, 479 | "\u001b[24;19H\u001b[30m\u001b[46mw\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 480 | ], 481 | [ 482 | 0.084948, 483 | "\u001b[24;20H\u001b[30m\u001b[46mo\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 484 | ], 485 | [ 486 | 0.136412, 487 | "\u001b[24;21H\u001b[30m\u001b[46mr\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 488 | ], 489 | [ 490 | 0.120095, 491 | "\u001b[22G\u001b[30m\u001b[46ml\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 492 | ], 493 | [ 494 | 0.000228, 495 | "\u001b[24;29H" 496 | ], 497 | [ 498 | 0.136625, 499 | "\u001b[23G\u001b[30m\u001b[46md\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 500 | ], 501 | [ 502 | 0.291323, 503 | "\u001b[24G\u001b[30m\u001b[46m!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;29H" 504 | ], 505 | [ 506 | 0.799473, 507 | "\u001b[24;59H\u001b[24;31H" 508 | ], 509 | [ 510 | 0.017046, 511 | "\u001b[24;59H\u001b[23;33H" 512 | ], 513 | [ 514 | 0.017221, 515 | "\u001b[24;59H\u001b[22;38H" 516 | ], 517 | [ 518 | 0.017134, 519 | "\u001b[24;59H" 520 | ], 521 | [ 522 | 5.8e-05, 523 | "\u001b[22;40H" 524 | ], 525 | [ 526 | 0.018178, 527 | "\u001b[24;59H\u001b[21;43H" 528 | ], 529 | [ 530 | 0.017031, 531 | "\u001b[24;59H" 532 | ], 533 | [ 534 | 6.4e-05, 535 | "\u001b[21;45H" 536 | ], 537 | [ 538 | 0.018098, 539 | "\u001b[24;59H\u001b[21;46H" 540 | ], 541 | [ 542 | 0.017857, 543 | "\u001b[24;59H\u001b[21;47H" 544 | ], 545 | [ 546 | 0.018161, 547 | "\u001b[24;59H\u001b[21;48H" 548 | ], 549 | [ 550 | 0.034854, 551 | "\u001b[24;59H" 552 | ], 553 | [ 554 | 8.8e-05, 555 | "\u001b[21;50H" 556 | ], 557 | [ 558 | 0.034731, 559 | "\u001b[24;59H\u001b[21;51H" 560 | ], 561 | [ 562 | 0.035127, 563 | "\u001b[24;59H" 564 | ], 565 | [ 566 | 7.3e-05, 567 | "\u001b[21;52H" 568 | ], 569 | [ 570 | 0.050872, 571 | "\n\b\u001b[31m\u001b[40m Exit \u001b[24;13H\u001b[30m\u001b[47mhello world!\u001b[14X\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[22;53H" 572 | ], 573 | [ 574 | 0.101599, 575 | "\u001b[24;59H\u001b[23;53H" 576 | ], 577 | [ 578 | 0.034885, 579 | "\n\u001b[59G\u001b[23;54H" 580 | ], 581 | [ 582 | 0.185798, 583 | "\n\u001b[59G\u001b[A\b\b\b\b" 584 | ], 585 | [ 586 | 0.135794, 587 | "\n\u001b[59G\u001b[23;54H" 588 | ], 589 | [ 590 | 0.017573, 591 | "\n\u001b[59G\u001b[23;53H" 592 | ], 593 | [ 594 | 0.000991, 595 | "\n\u001b[59G" 596 | ], 597 | [ 598 | 6.9e-05, 599 | "\u001b[23;52H" 600 | ], 601 | [ 602 | 0.017377, 603 | "\u001b[24;59H\u001b[23;49H" 604 | ], 605 | [ 606 | 0.017883, 607 | "\u001b[24;59H\u001b[23;48H" 608 | ], 609 | [ 610 | 0.017716, 611 | "\u001b[24;59H\u001b[23;47H" 612 | ], 613 | [ 614 | 0.017897, 615 | "\u001b[24;59H\u001b[23;46H" 616 | ], 617 | [ 618 | 0.186108, 619 | "\u001b[22;51H\u001b[33m\u001b[40m Exit \u001b[24;41H\u001b[31m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;46H" 620 | ], 621 | [ 622 | 0.472246, 623 | "\u001b[24;59H\u001b[24;45H" 624 | ], 625 | [ 626 | 1.052031, 627 | "\u001b[12;13H\u001b[30m\u001b[41mh\u001b[24;13H\u001b[30m\u001b[47m \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;45H" 628 | ], 629 | [ 630 | 0.523279, 631 | "\u001b[24;59H" 632 | ], 633 | [ 634 | 0.000105, 635 | "\u001b[24;44H" 636 | ], 637 | [ 638 | 0.017635, 639 | "\u001b[24;59H\u001b[25;43H" 640 | ], 641 | [ 642 | 0.018151, 643 | "\u001b[24;59H\u001b[25;42H" 644 | ], 645 | [ 646 | 0.962212, 647 | "\u001b[24;59H\u001b[25;21H" 648 | ], 649 | [ 650 | 0.017007, 651 | "\u001b[24;59H\u001b[23;19H" 652 | ], 653 | [ 654 | 0.017719, 655 | "\u001b[24;59H" 656 | ], 657 | [ 658 | 0.000103, 659 | "\u001b[20;17H" 660 | ], 661 | [ 662 | 0.017553, 663 | "\u001b[24;59H\u001b[19;16H" 664 | ], 665 | [ 666 | 0.01768, 667 | "\u001b[24;59H\u001b[17;16H" 668 | ], 669 | [ 670 | 0.01771, 671 | "\u001b[24;59H\u001b[15;16H" 672 | ], 673 | [ 674 | 0.017809, 675 | "\u001b[24;59H\u001b[14;16H" 676 | ], 677 | [ 678 | 0.202805, 679 | "\u001b[24;59H\u001b[15;16H" 680 | ], 681 | [ 682 | 0.034653, 683 | "\u001b[24;59H" 684 | ], 685 | [ 686 | 9.4e-05, 687 | "\u001b[15;17H" 688 | ], 689 | [ 690 | 0.135046, 691 | "\u001b[24;59H" 692 | ], 693 | [ 694 | 0.000109, 695 | "\u001b[16;17H" 696 | ], 697 | [ 698 | 0.01697, 699 | "\u001b[24;59H\u001b[17;17H" 700 | ], 701 | [ 702 | 0.017086, 703 | "\u001b[24;59H\u001b[19;18H" 704 | ], 705 | [ 706 | 0.017641, 707 | "\u001b[24;59H\u001b[20;18H" 708 | ], 709 | [ 710 | 0.017706, 711 | "\u001b[24;59H\u001b[21;18H" 712 | ], 713 | [ 714 | 0.017615, 715 | "\u001b[24;59H\u001b[22;18H" 716 | ], 717 | [ 718 | 0.051384, 719 | "\u001b[24;59H\u001b[23;18H" 720 | ], 721 | [ 722 | 0.304178, 723 | "\n\u001b[13G\u001b[30m\u001b[46m\u001b[26X\u001b[24;39H\u001b[37m\u001b[44m \u001b[33m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 724 | ], 725 | [ 726 | 0.000138, 727 | "\u001b[24;18H" 728 | ], 729 | [ 730 | 0.835226, 731 | "\u001b[24;59H\u001b[24;18H" 732 | ], 733 | [ 734 | 0.424539, 735 | "\u001b[13G\u001b[30m\u001b[46md\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 736 | ], 737 | [ 738 | 0.000478, 739 | "\u001b[24;18H" 740 | ], 741 | [ 742 | 0.102022, 743 | "\b\b\b\b\u001b[30m\u001b[46mi\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 744 | ], 745 | [ 746 | 0.119809, 747 | "\b\b\b\u001b[30m\u001b[46mf\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 748 | ], 749 | [ 750 | 0.153098, 751 | "\b\b\u001b[30m\u001b[46mf\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 752 | ], 753 | [ 754 | 0.153446, 755 | "\b\u001b[30m\u001b[46me\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 756 | ], 757 | [ 758 | 0.068611, 759 | "\u001b[30m\u001b[46mr\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 760 | ], 761 | [ 762 | 0.290034, 763 | "\u001b[C\u001b[30m\u001b[46me\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 764 | ], 765 | [ 766 | 0.15472, 767 | "\u001b[20G\u001b[30m\u001b[46mn\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 768 | ], 769 | [ 770 | 0.119577, 771 | "\u001b[21G\u001b[30m\u001b[46mt\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 772 | ], 773 | [ 774 | 0.086779, 775 | "\u001b[24;59H\u001b[24;18H" 776 | ], 777 | [ 778 | 0.137401, 779 | "\u001b[23G\u001b[30m\u001b[46mh\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 780 | ], 781 | [ 782 | 0.068786, 783 | "\u001b[24G\u001b[30m\u001b[46me\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 784 | ], 785 | [ 786 | 0.119625, 787 | "\u001b[25G\u001b[30m\u001b[46ml\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 788 | ], 789 | [ 790 | 0.187115, 791 | "\u001b[24;26H\u001b[30m\u001b[46ml\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 792 | ], 793 | [ 794 | 0.134806, 795 | "\u001b[24;27H\u001b[30m\u001b[46mo\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 796 | ], 797 | [ 798 | 0.101812, 799 | "\u001b[24;59H\u001b[24;18H" 800 | ], 801 | [ 802 | 0.119462, 803 | "\u001b[24;29H\u001b[30m\u001b[46mw\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 804 | ], 805 | [ 806 | 0.205324, 807 | "\u001b[24;30H\u001b[30m\u001b[46mo\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 808 | ], 809 | [ 810 | 0.101597, 811 | "\u001b[24;31H\u001b[30m\u001b[46mr\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 812 | ], 813 | [ 814 | 0.102549, 815 | "\u001b[24;32H\u001b[30m\u001b[46ml\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 816 | ], 817 | [ 818 | 0.119961, 819 | "\u001b[24;33H\u001b[30m\u001b[46md\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 820 | ], 821 | [ 822 | 0.323896, 823 | "\u001b[24;34H\u001b[30m\u001b[46m!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;18H" 824 | ], 825 | [ 826 | 0.526792, 827 | "\u001b[24;59H\u001b[24;19H" 828 | ], 829 | [ 830 | 0.017763, 831 | "\u001b[24;59H\u001b[24;20H" 832 | ], 833 | [ 834 | 0.017766, 835 | "\u001b[24;59H\u001b[24;21H" 836 | ], 837 | [ 838 | 0.017782, 839 | "\u001b[24;59H\u001b[24;22H" 840 | ], 841 | [ 842 | 0.017976, 843 | "\u001b[24;59H\u001b[24;24H" 844 | ], 845 | [ 846 | 0.018152, 847 | "\u001b[24;59H\u001b[24;25H" 848 | ], 849 | [ 850 | 0.018471, 851 | "\u001b[24;59H\u001b[24;27H" 852 | ], 853 | [ 854 | 0.018065, 855 | "\u001b[24;59H" 856 | ], 857 | [ 858 | 6.5e-05, 859 | "\u001b[24;29H" 860 | ], 861 | [ 862 | 0.001237, 863 | "\u001b[24;59H\u001b[24;32H" 864 | ], 865 | [ 866 | 0.017731, 867 | "\u001b[24;59H" 868 | ], 869 | [ 870 | 0.000108, 871 | "\u001b[23;34H" 872 | ], 873 | [ 874 | 0.017704, 875 | "\u001b[24;59H\u001b[23;37H" 876 | ], 877 | [ 878 | 0.018202, 879 | "\u001b[24;59H\u001b[23;38H" 880 | ], 881 | [ 882 | 0.035091, 883 | "\u001b[24;59H\u001b[23;39H" 884 | ], 885 | [ 886 | 0.169772, 887 | "\u001b[24;59H\u001b[23;40H" 888 | ], 889 | [ 890 | 0.033859, 891 | "\u001b[24;59H\u001b[23;41H" 892 | ], 893 | [ 894 | 0.017076, 895 | "\u001b[24;59H\u001b[23;42H" 896 | ], 897 | [ 898 | 0.017788, 899 | "\u001b[24;13H\u001b[30m\u001b[47mdifferent hello world! \u001b[37m\u001b[44m \u001b[31m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;43H" 900 | ], 901 | [ 902 | 0.102032, 903 | "\u001b[24;59H" 904 | ], 905 | [ 906 | 8.6e-05, 907 | "\u001b[25;43H" 908 | ], 909 | [ 910 | 0.337612, 911 | "\u001b[24;59H\u001b[24;43H" 912 | ], 913 | [ 914 | 0.800387, 915 | "\u001b[12;13H\u001b[30m\u001b[41mdifferent hello world!\u001b[24;13H\u001b[30m\u001b[47m\u001b[22X\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;43H" 916 | ], 917 | [ 918 | 0.522307, 919 | "\u001b[24;59H" 920 | ], 921 | [ 922 | 8.6e-05, 923 | "\u001b[25;42H" 924 | ], 925 | [ 926 | 0.017671, 927 | "\u001b[24;59H" 928 | ], 929 | [ 930 | 0.000149, 931 | "\u001b[25;41H" 932 | ], 933 | [ 934 | 0.017471, 935 | "\u001b[24;59H\u001b[25;39H" 936 | ], 937 | [ 938 | 0.017656, 939 | "\u001b[24;59H" 940 | ], 941 | [ 942 | 8.6e-05, 943 | "\u001b[25;37H" 944 | ], 945 | [ 946 | 0.017214, 947 | "\u001b[24;59H" 948 | ], 949 | [ 950 | 9.5e-05, 951 | "\u001b[25;33H" 952 | ], 953 | [ 954 | 0.018229, 955 | "\u001b[24;59H\u001b[25;30H" 956 | ], 957 | [ 958 | 0.017811, 959 | "\u001b[24;59H\u001b[25;28H" 960 | ], 961 | [ 962 | 0.017927, 963 | "\u001b[24;13H\u001b[30m\u001b[46m\u001b[26X\u001b[24;39H\u001b[37m\u001b[44m \u001b[33m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 964 | ], 965 | [ 966 | 9.4e-05, 967 | "\u001b[24;26H" 968 | ], 969 | [ 970 | 0.000895, 971 | "\u001b[24;59H\u001b[23;25H" 972 | ], 973 | [ 974 | 0.051975, 975 | "\u001b[24;59H\u001b[22;25H" 976 | ], 977 | [ 978 | 0.050779, 979 | "\u001b[24;59H" 980 | ], 981 | [ 982 | 8.7e-05, 983 | "\u001b[22;26H" 984 | ], 985 | [ 986 | 0.017061, 987 | "\u001b[24;59H\u001b[22;27H" 988 | ], 989 | [ 990 | 0.034619, 991 | "\u001b[24;59H\u001b[22;28H" 992 | ], 993 | [ 994 | 0.102455, 995 | "\u001b[24;59H\u001b[22;29H" 996 | ], 997 | [ 998 | 0.086297, 999 | "\u001b[24;59H\u001b[21;29H" 1000 | ], 1001 | [ 1002 | 0.034629, 1003 | "\u001b[24;59H\u001b[20;29H" 1004 | ], 1005 | [ 1006 | 0.034823, 1007 | "\u001b[24;59H" 1008 | ], 1009 | [ 1010 | 0.000148, 1011 | "\u001b[19;29H" 1012 | ], 1013 | [ 1014 | 0.256013, 1015 | "\u001b[24;59H\u001b[18;29H" 1016 | ], 1017 | [ 1018 | 0.034802, 1019 | "\u001b[24;59H" 1020 | ], 1021 | [ 1022 | 0.000173, 1023 | "\u001b[17;29H" 1024 | ], 1025 | [ 1026 | 0.017699, 1027 | "\u001b[24;59H\u001b[16;29H" 1028 | ], 1029 | [ 1030 | 0.018478, 1031 | "\u001b[24;59H" 1032 | ], 1033 | [ 1034 | 0.000118, 1035 | "\u001b[14;30H" 1036 | ], 1037 | [ 1038 | 0.018106, 1039 | "\u001b[24;59H\u001b[13;30H" 1040 | ], 1041 | [ 1042 | 0.017573, 1043 | "\u001b[24;59H\u001b[12;30H" 1044 | ], 1045 | [ 1046 | 0.017868, 1047 | "\u001b[24;59H\u001b[11;30H" 1048 | ], 1049 | [ 1050 | 0.034797, 1051 | "\u001b[24;59H" 1052 | ], 1053 | [ 1054 | 0.000155, 1055 | "\u001b[11;29H" 1056 | ], 1057 | [ 1058 | 0.375729, 1059 | "\u001b[24;59H" 1060 | ], 1061 | [ 1062 | 0.000109, 1063 | "\u001b[12;29H" 1064 | ], 1065 | [ 1066 | 0.61207, 1067 | "\u001b[24;59H\u001b[13;29H" 1068 | ], 1069 | [ 1070 | 0.017817, 1071 | "\u001b[24;59H\u001b[13;30H" 1072 | ], 1073 | [ 1074 | 0.017816, 1075 | "\u001b[24;59H\u001b[14;31H" 1076 | ], 1077 | [ 1078 | 0.017924, 1079 | "\u001b[24;59H\u001b[15;33H" 1080 | ], 1081 | [ 1082 | 0.017703, 1083 | "\u001b[24;59H\u001b[16;34H" 1084 | ], 1085 | [ 1086 | 0.017835, 1087 | "\u001b[24;59H\u001b[16;35H" 1088 | ], 1089 | [ 1090 | 0.01865, 1091 | "\u001b[24;59H\u001b[17;36H" 1092 | ], 1093 | [ 1094 | 0.017342, 1095 | "\u001b[24;59H\u001b[17;37H" 1096 | ], 1097 | [ 1098 | 0.017784, 1099 | "\u001b[24;59H\u001b[18;38H" 1100 | ], 1101 | [ 1102 | 0.052104, 1103 | "\u001b[24;59H\u001b[19;39H" 1104 | ], 1105 | [ 1106 | 0.051048, 1107 | "\u001b[24;59H\u001b[19;40H" 1108 | ], 1109 | [ 1110 | 0.067512, 1111 | "\u001b[24;59H\u001b[20;40H" 1112 | ], 1113 | [ 1114 | 0.084777, 1115 | "\u001b[24;59H\u001b[21;41H" 1116 | ], 1117 | [ 1118 | 0.186923, 1119 | "\u001b[24;59H\u001b[21;42H" 1120 | ], 1121 | [ 1122 | 0.084966, 1123 | "\n\b\u001b[31m\u001b[40m Toggle \u001b[24;13H\u001b[30m\u001b[47m\u001b[26X\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[22;42H" 1124 | ], 1125 | [ 1126 | 0.152387, 1127 | "\u001b[24;59H\u001b[22;43H" 1128 | ], 1129 | [ 1130 | 0.587663, 1131 | "\u001b[24;59H\u001b[22;43H" 1132 | ], 1133 | [ 1134 | 1.012837, 1135 | "\u001b[12;13H\u001b[37m\u001b[44mdifferent hello world!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1136 | ], 1137 | [ 1138 | 0.000122, 1139 | "\u001b[22;43H" 1140 | ], 1141 | [ 1142 | 0.993933, 1143 | "\u001b[12;13H\u001b[30m\u001b[41mdifferent hello world!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[22;43H" 1144 | ], 1145 | [ 1146 | 0.67551, 1147 | "\u001b[24;59H\u001b[22;42H" 1148 | ], 1149 | [ 1150 | 0.017572, 1151 | "\u001b[24;59H" 1152 | ], 1153 | [ 1154 | 7.2e-05, 1155 | "\u001b[23;41H" 1156 | ], 1157 | [ 1158 | 0.01796, 1159 | "\u001b[24;59H" 1160 | ], 1161 | [ 1162 | 8.5e-05, 1163 | "\u001b[23;39H" 1164 | ], 1165 | [ 1166 | 0.017555, 1167 | "\u001b[22;41H\u001b[33m\u001b[40m Toggle \u001b[24;13H\u001b[30m\u001b[46m\u001b[26X\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1168 | ], 1169 | [ 1170 | 0.000138, 1171 | "\u001b[24;37H" 1172 | ], 1173 | [ 1174 | 0.017446, 1175 | "\u001b[24;59H" 1176 | ], 1177 | [ 1178 | 0.000138, 1179 | "\u001b[24;35H" 1180 | ], 1181 | [ 1182 | 0.017487, 1183 | "\u001b[24;59H" 1184 | ], 1185 | [ 1186 | 0.000136, 1187 | "\u001b[24;34H" 1188 | ], 1189 | [ 1190 | 0.017941, 1191 | "\u001b[24;59H\u001b[25;32H" 1192 | ], 1193 | [ 1194 | 0.102185, 1195 | "\u001b[24;59H" 1196 | ], 1197 | [ 1198 | 0.00031, 1199 | "\u001b[25;31H" 1200 | ], 1201 | [ 1202 | 0.119753, 1203 | "\u001b[12;13H\u001b[37m\u001b[44mdifferent hello world!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[25;31H" 1204 | ], 1205 | [ 1206 | 0.103484, 1207 | "\u001b[24;59H\u001b[24;31H" 1208 | ], 1209 | [ 1210 | 0.930786, 1211 | "\u001b[24;59H" 1212 | ], 1213 | [ 1214 | 0.000744, 1215 | "\u001b[12;13H\u001b[30m\u001b[41mdifferent hello world!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1216 | ], 1217 | [ 1218 | 0.184222, 1219 | "\u001b[24;59H\u001b[24;31H" 1220 | ], 1221 | [ 1222 | 0.663035, 1223 | "\u001b[24;13H\u001b[30m\u001b[46mO\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1224 | ], 1225 | [ 1226 | 0.119853, 1227 | "\u001b[12;13H\u001b[37m\u001b[44mdifferent hello world!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1228 | ], 1229 | [ 1230 | 0.205371, 1231 | "\u001b[24;14H\u001b[30m\u001b[46mh\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1232 | ], 1233 | [ 1234 | 0.374513, 1235 | "\u001b[24;15H\u001b[30m\u001b[46m,\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1236 | ], 1237 | [ 1238 | 0.086233, 1239 | "\u001b[24;59H\u001b[24;31H" 1240 | ], 1241 | [ 1242 | 0.171446, 1243 | "\u001b[24;17H\u001b[30m\u001b[46mh\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1244 | ], 1245 | [ 1246 | 0.000323, 1247 | "\u001b[24;31H" 1248 | ], 1249 | [ 1250 | 0.068819, 1251 | "\u001b[24;18H\u001b[30m\u001b[46mi\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1252 | ], 1253 | [ 1254 | 0.085933, 1255 | "\u001b[12;13H\u001b[30m\u001b[41mdifferent hello world!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1256 | ], 1257 | [ 1258 | 0.340987, 1259 | "\u001b[24;19H\u001b[30m\u001b[46m!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1260 | ], 1261 | [ 1262 | 0.66508, 1263 | "\u001b[12;13H\u001b[37m\u001b[44mdifferent hello world!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;31H" 1264 | ], 1265 | [ 1266 | 0.018037, 1267 | "\u001b[24;59H\u001b[24;32H" 1268 | ], 1269 | [ 1270 | 0.018012, 1271 | "\u001b[24;59H\u001b[24;33H" 1272 | ], 1273 | [ 1274 | 0.018059, 1275 | "\u001b[24;59H\u001b[24;34H" 1276 | ], 1277 | [ 1278 | 0.017801, 1279 | "\u001b[24;59H\u001b[24;35H" 1280 | ], 1281 | [ 1282 | 0.017984, 1283 | "\u001b[24;59H\u001b[25;36H" 1284 | ], 1285 | [ 1286 | 0.051968, 1287 | "\u001b[24;59H\u001b[25;37H" 1288 | ], 1289 | [ 1290 | 0.086708, 1291 | "\u001b[24;59H\u001b[25;38H" 1292 | ], 1293 | [ 1294 | 0.018296, 1295 | "\u001b[24;59H\u001b[25;39H" 1296 | ], 1297 | [ 1298 | 0.017725, 1299 | "\u001b[24;59H\u001b[25;40H" 1300 | ], 1301 | [ 1302 | 0.017216, 1303 | "\u001b[24;59H\u001b[25;41H" 1304 | ], 1305 | [ 1306 | 0.000225, 1307 | "\u001b[24;59H" 1308 | ], 1309 | [ 1310 | 1.7e-05, 1311 | "\u001b[25;42H" 1312 | ], 1313 | [ 1314 | 0.017078, 1315 | "\u001b[24;13H\u001b[30m\u001b[47mOh, hi!\u001b[19X\u001b[24;39H\u001b[37m\u001b[44m \u001b[31m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;43H" 1316 | ], 1317 | [ 1318 | 0.018105, 1319 | "\u001b[24;59H\u001b[24;44H" 1320 | ], 1321 | [ 1322 | 0.08488, 1323 | "\u001b[24;59H\u001b[24;45H" 1324 | ], 1325 | [ 1326 | 0.763, 1327 | "\u001b[12;13H\u001b[37m\u001b[44mOh, hi!\u001b[15X\u001b[24;13H\u001b[30m\u001b[47m \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1328 | ], 1329 | [ 1330 | 0.000865, 1331 | "\u001b[12;13H\u001b[30m\u001b[41mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1332 | ], 1333 | [ 1334 | 9.5e-05, 1335 | "\u001b[24;45H" 1336 | ], 1337 | [ 1338 | 0.2029, 1339 | "\u001b[24;59H\u001b[24;44H" 1340 | ], 1341 | [ 1342 | 0.01769, 1343 | "\u001b[24;59H\u001b[24;42H" 1344 | ], 1345 | [ 1346 | 0.018087, 1347 | "\u001b[24;59H\u001b[24;40H" 1348 | ], 1349 | [ 1350 | 0.017121, 1351 | "\u001b[24;13H\u001b[30m\u001b[46m\u001b[26X\u001b[24;39H\u001b[37m\u001b[44m \u001b[33m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;38H" 1352 | ], 1353 | [ 1354 | 0.017655, 1355 | "\u001b[24;59H\u001b[24;37H" 1356 | ], 1357 | [ 1358 | 0.01818, 1359 | "\u001b[24;59H" 1360 | ], 1361 | [ 1362 | 0.000183, 1363 | "\u001b[24;36H" 1364 | ], 1365 | [ 1366 | 0.017757, 1367 | "\u001b[24;59H\u001b[24;35H" 1368 | ], 1369 | [ 1370 | 0.051524, 1371 | "\u001b[24;59H\u001b[24;34H" 1372 | ], 1373 | [ 1374 | 0.085038, 1375 | "\u001b[24;59H\u001b[24;33H" 1376 | ], 1377 | [ 1378 | 0.60345, 1379 | "\u001b[24;59H" 1380 | ], 1381 | [ 1382 | 0.000584, 1383 | "\u001b[12;13H\u001b[37m\u001b[44mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1384 | ], 1385 | [ 1386 | 0.237164, 1387 | "\u001b[24;13H\u001b[30m\u001b[46mv\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1388 | ], 1389 | [ 1390 | 0.205131, 1391 | "\u001b[24;14H\u001b[30m\u001b[46me\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1392 | ], 1393 | [ 1394 | 0.067453, 1395 | "\u001b[24;15H\u001b[30m\u001b[46mr\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1396 | ], 1397 | [ 1398 | 0.102161, 1399 | "\u001b[24;16H\u001b[30m\u001b[46my\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1400 | ], 1401 | [ 1402 | 0.135281, 1403 | "\u001b[24;59H\u001b[24;33H" 1404 | ], 1405 | [ 1406 | 0.033913, 1407 | "\u001b[12;13H\u001b[30m\u001b[41mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1408 | ], 1409 | [ 1410 | 0.085212, 1411 | "\u001b[24;18H\u001b[30m\u001b[46mm\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1412 | ], 1413 | [ 1414 | 0.186992, 1415 | "\u001b[24;19H\u001b[30m\u001b[46mu\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1416 | ], 1417 | [ 1418 | 0.068918, 1419 | "\u001b[24;20H\u001b[30m\u001b[46mc\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1420 | ], 1421 | [ 1422 | 0.086248, 1423 | "\u001b[24;21H\u001b[30m\u001b[46mh\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1424 | ], 1425 | [ 1426 | 0.577592, 1427 | "\u001b[12;13H\u001b[37m\u001b[44mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;33H" 1428 | ], 1429 | [ 1430 | 0.051363, 1431 | "\u001b[24;59H\u001b[24;34H" 1432 | ], 1433 | [ 1434 | 0.017729, 1435 | "\u001b[24;59H\u001b[24;35H" 1436 | ], 1437 | [ 1438 | 0.017864, 1439 | "\u001b[24;59H\u001b[24;37H" 1440 | ], 1441 | [ 1442 | 0.017775, 1443 | "\u001b[24;59H\u001b[24;39H" 1444 | ], 1445 | [ 1446 | 0.017919, 1447 | "\u001b[24;13H\u001b[30m\u001b[47mvery much\u001b[17X\u001b[24;39H\u001b[37m\u001b[44m \u001b[31m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;42H" 1448 | ], 1449 | [ 1450 | 0.017731, 1451 | "\u001b[24;59H" 1452 | ], 1453 | [ 1454 | 0.000385, 1455 | "\u001b[24;44H" 1456 | ], 1457 | [ 1458 | 0.017788, 1459 | "\u001b[24;59H\u001b[24;46H" 1460 | ], 1461 | [ 1462 | 0.017776, 1463 | "\u001b[24;59H" 1464 | ], 1465 | [ 1466 | 7.3e-05, 1467 | "\u001b[24;47H" 1468 | ], 1469 | [ 1470 | 0.000909, 1471 | "\u001b[24;59H" 1472 | ], 1473 | [ 1474 | 0.000459, 1475 | "\u001b[24;48H" 1476 | ], 1477 | [ 1478 | 0.01754, 1479 | "\u001b[24;59H" 1480 | ], 1481 | [ 1482 | 8.7e-05, 1483 | "\u001b[24;49H" 1484 | ], 1485 | [ 1486 | 0.034896, 1487 | "\u001b[24;59H\u001b[24;50H" 1488 | ], 1489 | [ 1490 | 0.033786, 1491 | "\u001b[24;41H\u001b[33m\u001b[40m Accept \u001b[37m\u001b[44m \u001b[31m\u001b[40m Cancel \u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;51H" 1492 | ], 1493 | [ 1494 | 0.016947, 1495 | "\u001b[24;59H\u001b[52G" 1496 | ], 1497 | [ 1498 | 0.017635, 1499 | "\u001b[59G\u001b[53G" 1500 | ], 1501 | [ 1502 | 0.034912, 1503 | "\u001b[59G" 1504 | ], 1505 | [ 1506 | 8.3e-05, 1507 | "\u001b[54G" 1508 | ], 1509 | [ 1510 | 0.786371, 1511 | "\u001b[24;13H\u001b[30m\u001b[47m \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1512 | ], 1513 | [ 1514 | 0.000413, 1515 | "\u001b[12;13H\u001b[30m\u001b[41mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1516 | ], 1517 | [ 1518 | 0.000247, 1519 | "\u001b[54G" 1520 | ], 1521 | [ 1522 | 0.538857, 1523 | "\u001b[59G" 1524 | ], 1525 | [ 1526 | 9.8e-05, 1527 | "\u001b[53G" 1528 | ], 1529 | [ 1530 | 0.017789, 1531 | "\u001b[59G" 1532 | ], 1533 | [ 1534 | 0.000105, 1535 | "\u001b[52G" 1536 | ], 1537 | [ 1538 | 0.017814, 1539 | "\u001b[24;41H\u001b[31m\u001b[40m Accept \u001b[37m\u001b[44m \u001b[33m\u001b[40m Cancel \u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1540 | ], 1541 | [ 1542 | 0.000108, 1543 | "\u001b[24;48H" 1544 | ], 1545 | [ 1546 | 0.017037, 1547 | "\u001b[24;59H\u001b[24;45H" 1548 | ], 1549 | [ 1550 | 0.01798, 1551 | "\u001b[24;59H" 1552 | ], 1553 | [ 1554 | 0.000153, 1555 | "\u001b[24;39H" 1556 | ], 1557 | [ 1558 | 0.017572, 1559 | "\u001b[24;13H\u001b[30m\u001b[46m\u001b[26X\u001b[24;39H\u001b[37m\u001b[44m \u001b[33m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1560 | ], 1561 | [ 1562 | 0.000178, 1563 | "\u001b[24;36H" 1564 | ], 1565 | [ 1566 | 0.017239, 1567 | "\u001b[24;59H\u001b[25;31H" 1568 | ], 1569 | [ 1570 | 0.017687, 1571 | "\u001b[24;59H\u001b[25;29H" 1572 | ], 1573 | [ 1574 | 0.017969, 1575 | "\u001b[24;59H\u001b[25;28H" 1576 | ], 1577 | [ 1578 | 0.018077, 1579 | "\u001b[24;59H\u001b[25;27H" 1580 | ], 1581 | [ 1582 | 0.034758, 1583 | "\u001b[24;59H\u001b[25;26H" 1584 | ], 1585 | [ 1586 | 0.135972, 1587 | "\u001b[12;13H\u001b[37m\u001b[44mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[25;26H" 1588 | ], 1589 | [ 1590 | 0.170844, 1591 | "\u001b[24;59H\u001b[24;26H" 1592 | ], 1593 | [ 1594 | 0.069047, 1595 | "\u001b[24;59H\u001b[24;25H" 1596 | ], 1597 | [ 1598 | 0.561057, 1599 | "\u001b[24;59H\u001b[24;25H" 1600 | ], 1601 | [ 1602 | 0.137485, 1603 | "\u001b[24;13H\u001b[30m\u001b[46mB\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1604 | ], 1605 | [ 1606 | 0.000122, 1607 | "\u001b[24;25H" 1608 | ], 1609 | [ 1610 | 0.068869, 1611 | "\u001b[12;13H\u001b[30m\u001b[41mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;25H" 1612 | ], 1613 | [ 1614 | 0.154607, 1615 | "\u001b[24;14H\u001b[30m\u001b[46my\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;25H" 1616 | ], 1617 | [ 1618 | 0.204733, 1619 | "\u001b[24;15H\u001b[30m\u001b[46me\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;25H" 1620 | ], 1621 | [ 1622 | 0.630527, 1623 | "\u001b[12;13H\u001b[37m\u001b[44mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;25H" 1624 | ], 1625 | [ 1626 | 0.288705, 1627 | "\u001b[24;16H\u001b[30m\u001b[46m.\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;25H" 1628 | ], 1629 | [ 1630 | 0.594378, 1631 | "\u001b[24;59H" 1632 | ], 1633 | [ 1634 | 7.9e-05, 1635 | "\u001b[24;26H" 1636 | ], 1637 | [ 1638 | 0.033987, 1639 | "\u001b[24;59H" 1640 | ], 1641 | [ 1642 | 0.000109, 1643 | "\u001b[24;28H" 1644 | ], 1645 | [ 1646 | 0.017086, 1647 | "\u001b[24;59H\u001b[24;30H" 1648 | ], 1649 | [ 1650 | 0.018502, 1651 | "\u001b[24;59H\u001b[23;34H" 1652 | ], 1653 | [ 1654 | 0.017866, 1655 | "\u001b[24;59H\u001b[23;35H" 1656 | ], 1657 | [ 1658 | 0.017996, 1659 | "\u001b[24;59H\u001b[23;37H" 1660 | ], 1661 | [ 1662 | 0.017977, 1663 | "\u001b[12;13H\u001b[30m\u001b[41mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[23;37H" 1664 | ], 1665 | [ 1666 | 0.373497, 1667 | "\u001b[24;59H\u001b[23;38H" 1668 | ], 1669 | [ 1670 | 0.034733, 1671 | "\u001b[24;59H\u001b[23;39H" 1672 | ], 1673 | [ 1674 | 0.017482, 1675 | "\u001b[24;59H\u001b[23;40H" 1676 | ], 1677 | [ 1678 | 0.018289, 1679 | "\u001b[24;59H\u001b[23;41H" 1680 | ], 1681 | [ 1682 | 0.017995, 1683 | "\u001b[24;59H\u001b[23;42H" 1684 | ], 1685 | [ 1686 | 0.001041, 1687 | "\u001b[24;59H" 1688 | ], 1689 | [ 1690 | 8.9e-05, 1691 | "\u001b[23;43H" 1692 | ], 1693 | [ 1694 | 0.017885, 1695 | "\u001b[24;59H" 1696 | ], 1697 | [ 1698 | 0.000101, 1699 | "\u001b[23;44H" 1700 | ], 1701 | [ 1702 | 0.017097, 1703 | "\u001b[24;59H\u001b[23;45H" 1704 | ], 1705 | [ 1706 | 0.290505, 1707 | "\u001b[24;13H\u001b[30m\u001b[47mBye.\u001b[22X\u001b[24;39H\u001b[37m\u001b[44m \u001b[31m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;45H" 1708 | ], 1709 | [ 1710 | 0.218583, 1711 | "\u001b[12;13H\u001b[37m\u001b[44mOh, hi!\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;45H" 1712 | ], 1713 | [ 1714 | 0.537387, 1715 | "\u001b[12;13H\u001b[37m\u001b[44mBye. \u001b[24;13H\u001b[30m\u001b[47m \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[24;45H" 1716 | ], 1717 | [ 1718 | 0.202506, 1719 | "\u001b[24;59H\u001b[23;45H" 1720 | ], 1721 | [ 1722 | 0.253068, 1723 | "\u001b[12;13H\u001b[30m\u001b[41mBye.\u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[23;45H" 1724 | ], 1725 | [ 1726 | 0.236654, 1727 | "\u001b[A\b\b\b\b\u001b[31m\u001b[40m Toggle \u001b[24;41H\u001b[33m\u001b[40m Accept \u001b[24;59H\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m" 1728 | ], 1729 | [ 1730 | 0.000158, 1731 | "\u001b[22;45H" 1732 | ], 1733 | [ 1734 | 0.722401, 1735 | "\u001b[24;59H" 1736 | ], 1737 | [ 1738 | 0.000227, 1739 | "\u001b[22;45H" 1740 | ], 1741 | [ 1742 | 0.387631, 1743 | "\u001b[24;59H\u001b[22;47H" 1744 | ], 1745 | [ 1746 | 0.017308, 1747 | "\u001b[24;59H\u001b[22;49H" 1748 | ], 1749 | [ 1750 | 0.017033, 1751 | "\u001b[24;59H\u001b[22;50H" 1752 | ], 1753 | [ 1754 | 0.017629, 1755 | "\u001b[22;41H\u001b[33m\u001b[40m Toggle \u001b[37m\u001b[44m \u001b[31m\u001b[40m Exit \n\n\u001b(B\u001b[m\u001b[39;49m\u001b[37m\u001b[40m\u001b[22;51H" 1756 | ], 1757 | [ 1758 | 0.017931, 1759 | "\u001b[24;59H\u001b[22;52H" 1760 | ], 1761 | [ 1762 | 0.033839, 1763 | "\u001b[24;59H\u001b[22;53H" 1764 | ], 1765 | [ 1766 | 0.134552, 1767 | "\u001b[24;59H\u001b[22;54H" 1768 | ], 1769 | [ 1770 | 0.77588, 1771 | "\u001b[?1000l" 1772 | ], 1773 | [ 1774 | 0.000558, 1775 | "\u001b[39;49m\r\u001b[50d\u001b[K\u001b[50;1H\u001b[?1049l\r\u001b[?1l\u001b>" 1776 | ], 1777 | [ 1778 | 4.252162, 1779 | "\r\n" 1780 | ], 1781 | [ 1782 | 0.004734, 1783 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1784 | ], 1785 | [ 1786 | 0.000175, 1787 | "\u001b]2;jack@sun-wukong: ~/common-lisp/charming-clim\u0007" 1788 | ], 1789 | [ 1790 | 0.000541, 1791 | "\u001b]1;..charming-clim\u0007" 1792 | ], 1793 | [ 1794 | 0.019374, 1795 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcharming-clim\u001b[00m \u001b[01;34mgit:(\u001b[31mmaster\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K" 1796 | ], 1797 | [ 1798 | 9.3e-05, 1799 | "\u001b[?1h\u001b=\u001b[?2004h" 1800 | ], 1801 | [ 1802 | 0.892005, 1803 | "\u001b[?2004l\r" 1804 | ], 1805 | [ 1806 | 0.001576, 1807 | "\r\n" 1808 | ] 1809 | ] 1810 | } 1811 | --------------------------------------------------------------------------------